diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-01-30 04:37:25 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-01-30 04:37:25 +0900 |
| commit | f6154dc0af1a0d65819e87240f4385f9573095cb (patch) | |
| tree | 699a5ca07d6727b7f8497d4769f25d6d62f94b5a /src/client/app | |
| parent | Add Event activity-type support (#5785) (diff) | |
| download | misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.gz misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.bz2 misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.zip | |
v12 (#5712)
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Diffstat (limited to 'src/client/app')
431 files changed, 0 insertions, 56462 deletions
diff --git a/src/client/app/admin/assets/header-icon.svg b/src/client/app/admin/assets/header-icon.svg deleted file mode 100644 index d677d2d163..0000000000 --- a/src/client/app/admin/assets/header-icon.svg +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="512" - height="512" - viewBox="0 0 135.46667 135.46667" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="header-icon.dark.svg" - inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png" - inkscape:export-xdpi="6" - inkscape:export-ydpi="6"> - <defs - id="defs2"> - <inkscape:path-effect - effect="simplify" - id="path-effect5115" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - <inkscape:path-effect - effect="simplify" - id="path-effect5111" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - <inkscape:path-effect - effect="simplify" - id="path-effect5104" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="1.4142136" - inkscape:cx="114.309" - inkscape:cy="251.50613" - inkscape:document-units="px" - inkscape:current-layer="g4502" - showgrid="true" - units="px" - inkscape:snap-bbox="true" - inkscape:bbox-nodes="true" - inkscape:snap-bbox-edge-midpoints="false" - inkscape:snap-smooth-nodes="true" - inkscape:snap-center="true" - inkscape:snap-page="true" - inkscape:window-width="1920" - inkscape:window-height="1027" - inkscape:window-x="-8" - inkscape:window-y="1072" - inkscape:window-maximized="1" - inkscape:snap-object-midpoints="true" - inkscape:snap-midpoints="true" - inkscape:object-paths="true" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" - objecttolerance="1" - guidetolerance="1" - inkscape:snap-nodes="false" - inkscape:snap-others="false"> - <inkscape:grid - type="xygrid" - id="grid4504" - spacingx="4.2333334" - spacingy="4.2333334" - empcolor="#ff3fff" - empopacity="0.25098039" - empspacing="4" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="レイヤー 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-30.809093,-111.78601)"> - <g - id="g4502" - transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)"> - <g - style="fill-opacity:1" - transform="translate(-1.3333333e-6,-1.3439941e-6)" - id="g5125"> - <g - transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" - id="text4489" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - aria-label="Mi"> - <path - sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" - inkscape:connector-curvature="0" - id="path5210" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px" - d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> - <path - inkscape:connector-curvature="0" - id="path5212" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px" - d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> - </g> - </g> - </g> - </g> -</svg> diff --git a/src/client/app/admin/script.ts b/src/client/app/admin/script.ts deleted file mode 100644 index 3f2d6466ac..0000000000 --- a/src/client/app/admin/script.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Admin - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import Index from './views/index.vue'; -import NotFound from '../common/views/pages/not-found.vue'; - -init(launch => { - document.title = 'Admin'; - - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/admin/', - routes: [ - { path: '/:page', component: Index }, - { path: '/', redirect: '/dashboard' }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/admin/style.styl b/src/client/app/admin/style.styl deleted file mode 100644 index ae1a28226a..0000000000 --- a/src/client/app/admin/style.styl +++ /dev/null @@ -1,6 +0,0 @@ -@import "../app" -@import "../reset" - -html - height 100% - background var(--bg) diff --git a/src/client/app/admin/views/abuse.vue b/src/client/app/admin/views/abuse.vue deleted file mode 100644 index afa285debc..0000000000 --- a/src/client/app/admin/views/abuse.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faExclamationCircle"/> {{ $t('title') }}</template> - <section class="fit-top"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div v-for="report in userReports" :key="report.id" class="haexwsjc"> - <ui-horizon-group inputs> - <ui-input :value="report.user | acct" type="text" readonly> - <span>{{ $t('target') }}</span> - </ui-input> - <ui-input :value="report.reporter | acct" type="text" readonly> - <span>{{ $t('reporter') }}</span> - </ui-input> - </ui-horizon-group> - <ui-textarea :value="report.comment" readonly> - <span>{{ $t('details') }}</span> - </ui-textarea> - <ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button> - </div> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/abuse.vue'), - - data() { - return { - limit: 10, - untilId: undefined, - userReports: [], - existMore: false, - faExclamationCircle - }; - }, - - mounted() { - this.fetchUserReports(); - }, - - methods: { - fetchUserReports() { - this.$root.api('admin/abuse-user-reports', { - untilId: this.untilId, - limit: this.limit + 1 - }).then(reports => { - if (reports.length == this.limit + 1) { - reports.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.userReports = this.userReports.concat(reports); - this.untilId = this.userReports[this.userReports.length - 1].id; - }); - }, - - removeReport(report) { - this.$root.api('admin/remove-abuse-user-report', { - reportId: report.id - }).then(() => { - this.userReports = this.userReports.filter(r => r.id != report.id); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.haexwsjc - padding-bottom 16px - border-bottom solid 1px var(--faceDivider) - -</style> diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue deleted file mode 100644 index f6c0540b37..0000000000 --- a/src/client/app/admin/views/announcements.vue +++ /dev/null @@ -1,91 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faBroadcastTower"/> {{ $t('announcements') }}</template> - <section v-for="(announcement, i) in announcements" class="fit-top"> - <ui-input v-model="announcement.title" @change="save"> - <span>{{ $t('title') }}</span> - </ui-input> - <ui-textarea v-model="announcement.text"> - <span>{{ $t('text') }}</span> - </ui-textarea> - <ui-input v-model="announcement.image"> - <span>{{ $t('image-url') }}</span> - </ui-input> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button> - <ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button> - </ui-horizon-group> - </section> - <section> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/announcements.vue'), - data() { - return { - announcements: [], - faBroadcastTower, faPlus - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - }); - }, - - methods: { - add() { - this.announcements.unshift({ - title: '', - text: '', - image: null - }); - }, - - remove(i) { - this.$root.dialog({ - type: 'warning', - text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.announcements = this.announcements.filter((_, j) => j !== i); - this.save(true); - this.$root.dialog({ - type: 'success', - text: this.$t('_remove.removed') - }); - }); - }, - - save(silent) { - this.$root.api('admin/update-meta', { - announcements: this.announcements - }).then(() => { - if (!silent) { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> diff --git a/src/client/app/admin/views/dashboard.ap-log.vue b/src/client/app/admin/views/dashboard.ap-log.vue deleted file mode 100644 index ee48ef15ea..0000000000 --- a/src/client/app/admin/views/dashboard.ap-log.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="hyhctythnmwihguaaapnbrbszsjqxpio"> - <table> - <thead> - <tr> - <th><fa :icon="faExchangeAlt"/> In/Out</th> - <th><fa :icon="faBolt"/> Activity</th> - <th><fa icon="server"/> Host</th> - <th><fa icon="user"/> Actor</th> - </tr> - </thead> - <tbody> - <tr v-for="log in logs" :key="log.id"> - <td :class="log.direction">{{ log.direction == 'in' ? '<' : '>' }} {{ log.direction }}</td> - <td>{{ log.activity }}</td> - <td>{{ log.host }}</td> - <td>@{{ log.actor }}</td> - </tr> - </tbody> - </table> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faExchangeAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - data() { - return { - logs: [], - connection: null, - faBolt, faExchangeAlt - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('apLog'); - this.connection.on('log', this.onLog); - this.connection.on('logs', this.onLogs); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 50 - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onLog(log) { - log.id = Math.random(); - this.logs.unshift(log); - if (this.logs.length > 50) this.logs.pop(); - }, - - onLogs(logs) { - for (const log of logs.reverse()) { - this.onLog(log) - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.hyhctythnmwihguaaapnbrbszsjqxpio - display block - padding 12px 16px 16px 16px - height 250px - overflow auto - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--adminDashboardCardBg) - border-radius 8px - - > table - width 100% - max-width 100% - overflow auto - border-spacing 0 - border-collapse collapse - color var(--adminDashboardCardFg) - font-size 14px - - thead - border-bottom solid 1px var(--adminDashboardCardDivider) - - tr - th - font-weight normal - text-align left - - tbody - tr - &:nth-child(odd) - background rgba(0, 0, 0, 0.025) - - th, td - padding 8px 16px - min-width 128px - - td.in - color #d26755 - - td.out - color #55bb83 - -</style> diff --git a/src/client/app/admin/views/dashboard.charts.vue b/src/client/app/admin/views/dashboard.charts.vue deleted file mode 100644 index b2ac19efff..0000000000 --- a/src/client/app/admin/views/dashboard.charts.vue +++ /dev/null @@ -1,527 +0,0 @@ -<template> -<div class="qvgidhudpqhjttdhxubzuyrhyzgslujw"> - <header> - <b><fa :icon="['far', 'chart-bar']"/> {{ $t('title') }}:</b> - <select v-model="src"> - <optgroup :label="$t('federation')"> - <option value="federation-instances">{{ $t('charts.federation-instances') }}</option> - <option value="federation-instances-total">{{ $t('charts.federation-instances-total') }}</option> - </optgroup> - <optgroup :label="$t('users')"> - <option value="users">{{ $t('charts.users') }}</option> - <option value="users-total">{{ $t('charts.users-total') }}</option> - <option value="active-users">{{ $t('charts.active-users') }}</option> - </optgroup> - <optgroup :label="$t('notes')"> - <option value="notes">{{ $t('charts.notes') }}</option> - <option value="local-notes">{{ $t('charts.local-notes') }}</option> - <option value="remote-notes">{{ $t('charts.remote-notes') }}</option> - <option value="notes-total">{{ $t('charts.notes-total') }}</option> - </optgroup> - <optgroup :label="$t('drive')"> - <option value="drive-files">{{ $t('charts.drive-files') }}</option> - <option value="drive-files-total">{{ $t('charts.drive-files-total') }}</option> - <option value="drive">{{ $t('charts.drive') }}</option> - <option value="drive-total">{{ $t('charts.drive-total') }}</option> - </optgroup> - <optgroup :label="$t('network')"> - <option value="network-requests">{{ $t('charts.network-requests') }}</option> - <option value="network-time">{{ $t('charts.network-time') }}</option> - <option value="network-usage">{{ $t('charts.network-usage') }}</option> - </optgroup> - </select> - <div> - <span @click="span = 'day'" :class="{ active: span == 'day' }">{{ $t('per-day') }}</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">{{ $t('per-hour') }}</span> - </div> - </header> - <div ref="chart"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import * as tinycolor from 'tinycolor2'; -import ApexCharts from 'apexcharts'; - -const limit = 90; - -const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); -const negate = arr => arr.map(x => -x); - -export default Vue.extend({ - i18n: i18n('admin/views/charts.vue'), - data() { - return { - chart: null, - src: 'notes', - span: 'hour', - chartInstance: null - }; - }, - - computed: { - data(): any { - if (this.chart == null) return null; - switch (this.src) { - case 'federation-instances': return this.federationInstancesChart(false); - case 'federation-instances-total': return this.federationInstancesChart(true); - case 'users': return this.usersChart(false); - case 'users-total': return this.usersChart(true); - case 'active-users': return this.activeUsersChart(); - 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(); - case 'network-requests': return this.networkRequestsChart(); - case 'network-time': return this.networkTimeChart(); - case 'network-usage': return this.networkUsageChart(); - } - }, - - stats(): any[] { - const stats = - this.span == 'day' ? this.chart.perDay : - this.span == 'hour' ? this.chart.perHour : - null; - - return stats; - } - }, - - watch: { - src() { - this.render(); - }, - - span() { - this.render(); - } - }, - - async mounted() { - this.now = new Date(); - - const [perHour, perDay] = await Promise.all([Promise.all([ - this.$root.api('charts/federation', { limit: limit, span: 'hour' }), - this.$root.api('charts/users', { limit: limit, span: 'hour' }), - this.$root.api('charts/active-users', { limit: limit, span: 'hour' }), - this.$root.api('charts/notes', { limit: limit, span: 'hour' }), - this.$root.api('charts/drive', { limit: limit, span: 'hour' }), - this.$root.api('charts/network', { limit: limit, span: 'hour' }) - ]), Promise.all([ - this.$root.api('charts/federation', { limit: limit, span: 'day' }), - this.$root.api('charts/users', { limit: limit, span: 'day' }), - this.$root.api('charts/active-users', { limit: limit, span: 'day' }), - this.$root.api('charts/notes', { limit: limit, span: 'day' }), - this.$root.api('charts/drive', { limit: limit, span: 'day' }), - this.$root.api('charts/network', { limit: limit, span: 'day' }) - ])]); - - const chart = { - perHour: { - federation: perHour[0], - users: perHour[1], - activeUsers: perHour[2], - notes: perHour[3], - drive: perHour[4], - network: perHour[5] - }, - perDay: { - federation: perDay[0], - users: perDay[1], - activeUsers: perDay[2], - notes: perDay[3], - drive: perDay[4], - network: perDay[5] - } - }; - - this.chart = chart; - - this.render(); - }, - - beforeDestroy() { - this.chartInstance.destroy(); - }, - - methods: { - setSrc(src) { - this.src = src; - }, - - render() { - if (this.chartInstance) { - this.chartInstance.destroy(); - } - - this.chartInstance = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 300, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, - } - }, - }, - stroke: { - curve: 'straight', - width: 2 - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' - }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - }, - yaxis: { - labels: { - formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - } - }, - series: this.data.series - }); - - this.chartInstance.render(); - }, - - getDate(i: number) { - const y = this.now.getFullYear(); - const m = this.now.getMonth(); - const d = this.now.getDate(); - const h = this.now.getHours(); - - return ( - this.span == 'day' ? new Date(y, m, d - i) : - this.span == 'hour' ? new Date(y, m, d, h - i) : - null - ); - }, - - format(arr) { - return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v })); - }, - - federationInstancesChart(total: boolean): any { - return { - series: [{ - name: 'Instances', - data: this.format(total - ? this.stats.federation.instance.total - : sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)) - ) - }] - }; - }, - - notesChart(type: string): any { - return { - series: [{ - name: 'All', - type: 'line', - data: this.format(type == 'combined' - ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec)) - : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec)) - ) - }, { - name: 'Renotes', - type: 'area', - data: this.format(type == 'combined' - ? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote) - : this.stats.notes[type].diffs.renote - ) - }, { - name: 'Replies', - type: 'area', - data: this.format(type == 'combined' - ? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply) - : this.stats.notes[type].diffs.reply - ) - }, { - name: 'Normal', - type: 'area', - data: this.format(type == 'combined' - ? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal) - : this.stats.notes[type].diffs.normal - ) - }] - }; - }, - - notesTotalChart(): any { - return { - series: [{ - name: 'Combined', - type: 'line', - data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) - }, { - name: 'Local', - type: 'area', - data: this.format(this.stats.notes.local.total) - }, { - name: 'Remote', - type: 'area', - data: this.format(this.stats.notes.remote.total) - }] - }; - }, - - usersChart(total: boolean): any { - return { - series: [{ - name: 'Combined', - type: 'line', - data: this.format(total - ? sum(this.stats.users.local.total, this.stats.users.remote.total) - : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) - ) - }, { - name: 'Local', - type: 'area', - data: this.format(total - ? this.stats.users.local.total - : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) - ) - }, { - name: 'Remote', - type: 'area', - data: this.format(total - ? this.stats.users.remote.total - : sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) - ) - }] - }; - }, - - activeUsersChart(): any { - return { - series: [{ - name: 'Combined', - type: 'line', - data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count)) - }, { - name: 'Local', - type: 'area', - data: this.format(this.stats.activeUsers.local.count) - }, { - name: 'Remote', - type: 'area', - data: this.format(this.stats.activeUsers.remote.count) - }] - }; - }, - - driveChart(): any { - return { - bytes: true, - series: [{ - name: 'All', - type: 'line', - data: this.format( - sum( - this.stats.drive.local.incSize, - negate(this.stats.drive.local.decSize), - this.stats.drive.remote.incSize, - negate(this.stats.drive.remote.decSize) - ) - ) - }, { - name: 'Local +', - type: 'area', - data: this.format(this.stats.drive.local.incSize) - }, { - name: 'Local -', - type: 'area', - data: this.format(negate(this.stats.drive.local.decSize)) - }, { - name: 'Remote +', - type: 'area', - data: this.format(this.stats.drive.remote.incSize) - }, { - name: 'Remote -', - type: 'area', - data: this.format(negate(this.stats.drive.remote.decSize)) - }] - }; - }, - - driveTotalChart(): any { - return { - bytes: true, - series: [{ - name: 'Combined', - type: 'line', - data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) - }, { - name: 'Local', - type: 'area', - data: this.format(this.stats.drive.local.totalSize) - }, { - name: 'Remote', - type: 'area', - data: this.format(this.stats.drive.remote.totalSize) - }] - }; - }, - - driveFilesChart(): any { - return { - series: [{ - name: 'All', - type: 'line', - data: this.format( - sum( - this.stats.drive.local.incCount, - negate(this.stats.drive.local.decCount), - this.stats.drive.remote.incCount, - negate(this.stats.drive.remote.decCount) - ) - ) - }, { - name: 'Local +', - type: 'area', - data: this.format(this.stats.drive.local.incCount) - }, { - name: 'Local -', - type: 'area', - data: this.format(negate(this.stats.drive.local.decCount)) - }, { - name: 'Remote +', - type: 'area', - data: this.format(this.stats.drive.remote.incCount) - }, { - name: 'Remote -', - type: 'area', - data: this.format(negate(this.stats.drive.remote.decCount)) - }] - }; - }, - - driveFilesTotalChart(): any { - return { - series: [{ - name: 'Combined', - type: 'line', - data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) - }, { - name: 'Local', - type: 'area', - data: this.format(this.stats.drive.local.totalCount) - }, { - name: 'Remote', - type: 'area', - data: this.format(this.stats.drive.remote.totalCount) - }] - }; - }, - - networkRequestsChart(): any { - return { - series: [{ - name: 'Incoming', - data: this.format(this.stats.network.incomingRequests) - }] - }; - }, - - networkTimeChart(): any { - const data = []; - - for (let i = 0; i < limit; i++) { - data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0); - } - - return { - series: [{ - name: 'Avg time', - data: this.format(data) - }] - }; - }, - - networkUsageChart(): any { - return { - bytes: true, - series: [{ - name: 'Incoming', - data: this.format(this.stats.network.incomingBytes) - }, { - name: 'Outgoing', - data: this.format(this.stats.network.outgoingBytes) - }] - }; - }, - } -}); -</script> - -<style lang="stylus" scoped> -.qvgidhudpqhjttdhxubzuyrhyzgslujw - display block - flex 1 - padding 32px 24px - padding-bottom 0 - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - > header - display flex - margin 0 8px - padding 0 0 8px 0 - font-size 1em - color var(--adminDashboardCardFg) - border-bottom solid 1px var(--adminDashboardCardDivider) - - > b - margin-right 8px - - > *:last-child - margin-left auto - - * - &:not(.active) - color var(--primary) - cursor pointer - -</style> diff --git a/src/client/app/admin/views/dashboard.cpu-memory.vue b/src/client/app/admin/views/dashboard.cpu-memory.vue deleted file mode 100644 index a3951e7618..0000000000 --- a/src/client/app/admin/views/dashboard.cpu-memory.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<div class="zyknedwtlthezamcjlolyusmipqmjgxz"> - <div> - <header> - <span><fa icon="microchip"/> CPU <span>{{ cpuP }}%</span></span> - <span v-if="meta">{{ meta.cpu.model }}</span> - </header> - <div ref="cpu"></div> - </div> - <div> - <header> - <span><fa icon="memory"/> MEM <span>{{ memP }}%</span></span> - <span v-if="meta"></span> - </header> - <div ref="mem"></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - props: ['connection'], - - data() { - return { - stats: [], - cpuChart: null, - memChart: null, - cpuP: '', - memP: '', - meta: null - }; - }, - - watch: { - stats(stats) { - this.cpuChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.cpu_usage })) - }]); - this.memChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: (x.mem.used / x.mem.total) })) - }]); - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); - - const chartOpts = { - chart: { - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - series: [{ - data: [] - }], - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - max: 1 - } - }; - - this.cpuChart = new ApexCharts(this.$refs.cpu, chartOpts); - this.memChart = new ApexCharts(this.$refs.mem, chartOpts); - - this.cpuChart.render(); - this.memChart.render(); - }, - - beforeDestroy() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - - this.cpuChart.destroy(); - this.memChart.destroy(); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 200) this.stats.shift(); - - this.cpuP = (stats.cpu_usage * 100).toFixed(0); - this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.zyknedwtlthezamcjlolyusmipqmjgxz - display flex - - > div - display block - flex 1 - padding 20px 12px 0 12px - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - &:first-child - margin-right 16px - - > header - display flex - padding 0 8px - margin-bottom -16px - color var(--adminDashboardCardFg) - font-size 14px - - > span - &:last-child - margin-left auto - opacity 0.7 - - > span - opacity 0.7 - - > div - margin-bottom -10px - - @media (max-width 1000px) - display block - margin-bottom 26px - - > div - &:first-child - margin-right 0 - margin-bottom 26px - -</style> diff --git a/src/client/app/admin/views/dashboard.queue-charts.vue b/src/client/app/admin/views/dashboard.queue-charts.vue deleted file mode 100644 index d2d7811bff..0000000000 --- a/src/client/app/admin/views/dashboard.queue-charts.vue +++ /dev/null @@ -1,196 +0,0 @@ -<template> -<div class="mzxlfysy"> - <div> - <header> - <span><fa :icon="faInbox"/> In</span> - <span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span> - </header> - <div ref="in"></div> - </div> - <div> - <header> - <span><fa :icon="faPaperPlane"/> Out</span> - <span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span> - </header> - <div ref="out"></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faInbox } from '@fortawesome/free-solid-svg-icons'; -import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import ApexCharts from 'apexcharts'; - -const limit = 150; - -export default Vue.extend({ - data() { - return { - stats: [], - inChart: null, - outChart: null, - faInbox, faPaperPlane - }; - }, - - computed: { - latestStats(): any { - return this.stats[this.stats.length - 1]; - } - }, - - watch: { - stats(stats) { - this.inChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.active })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) - }]); - this.outChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.active })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) - }]); - } - }, - - mounted() { - const chartOpts = { - chart: { - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - legend: { - show: false - }, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any, - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - } - }; - - this.inChart = new ApexCharts(this.$refs.in, chartOpts); - this.outChart = new ApexCharts(this.$refs.out, chartOpts); - - this.inChart.render(); - this.outChart.render(); - - const connection = this.$root.stream.useSharedConnection('queueStats'); - connection.on('stats', this.onStats); - connection.on('statsLog', this.onStatsLog); - connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: limit - }); - - this.$once('hook:beforeDestroy', () => { - connection.dispose(); - this.inChart.destroy(); - this.outChart.destroy(); - }); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > limit) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mzxlfysy - display flex - - > div - display block - flex 1 - padding 20px 12px 0 12px - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - &:first-child - margin-right 16px - - > header - display flex - padding 0 8px - margin-bottom -16px - color var(--adminDashboardCardFg) - font-size 14px - - > span - &:last-child - margin-left auto - opacity 0.7 - - > span - opacity 0.7 - - > div - margin-bottom -10px - - @media (max-width 1000px) - display block - margin-bottom 26px - - > div - &:first-child - margin-right 0 - margin-bottom 26px - -</style> diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue deleted file mode 100644 index 5ccfaa06ca..0000000000 --- a/src/client/app/admin/views/dashboard.vue +++ /dev/null @@ -1,286 +0,0 @@ -<template> -<div class="obdskegsannmntldydackcpzezagxqfy"> - <header v-if="meta"> - <p><b>Misskey</b><span>{{ meta.version }}</span></p> - <p><b>Machine</b><span>{{ meta.machine }}</span></p> - <p><b>OS</b><span>{{ meta.os }}</span></p> - <p><b>Node</b><span>{{ meta.node }}</span></p> - <p>{{ $t('@.ai-chan-kawaii') }}</p> - </header> - - <marquee-text v-if="instances.length > 0" class="instances" :repeat="10" :duration="60"> - <span v-for="instance in instances" class="instance"> - <b :style="{ background: instance.bg }">{{ instance.host }}</b>{{ instance.notesCount | number }} / {{ instance.usersCount | number }} - </span> - </marquee-text> - - <div v-if="stats" class="stats"> - <div> - <div> - <div><fa icon="user"/></div> - <div> - <span>{{ $t('accounts') }}</span> - <b>{{ stats.originalUsersCount | number }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('users')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa icon="pencil-alt"/></div> - <div> - <span>{{ $t('notes') }}</span> - <b>{{ stats.originalNotesCount | number }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('notes')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa :icon="faDatabase"/></div> - <div> - <span>{{ $t('drive') }}</span> - <b>{{ stats.driveUsageLocal | bytes }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('drive')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa :icon="['far', 'hdd']"/></div> - <div> - <span>{{ $t('instances') }}</span> - <b>{{ stats.instances | number }}</b> - </div> - </div> - <div> - <span><fa icon="globe"/> {{ $t('federated') }}</span> - <span @click="setChartSrc('federation-instances-total')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - </div> - - <div class="charts"> - <x-charts ref="charts"/> - </div> - - <div class="queue"> - <x-queue/> - </div> - - <div class="cpu-memory"> - <x-cpu-memory :connection="connection"/> - </div> - - <div class="ap"> - <x-ap-log/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import XCpuMemory from "./dashboard.cpu-memory.vue"; -import XQueue from "./dashboard.queue-charts.vue"; -import XCharts from "./dashboard.charts.vue"; -import XApLog from "./dashboard.ap-log.vue"; -import { faDatabase } from '@fortawesome/free-solid-svg-icons'; -import MarqueeText from 'vue-marquee-text-component'; -import randomColor from 'randomcolor'; - -export default Vue.extend({ - i18n: i18n('admin/views/dashboard.vue'), - - components: { - XCpuMemory, - XQueue, - XCharts, - XApLog, - MarqueeText - }, - - data() { - return { - stats: null, - connection: null, - meta: null, - instances: [], - faDatabase - }; - }, - - created() { - this.connection = this.$root.stream.useSharedConnection('serverStats'); - - this.updateStats(); - - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - - this.$root.api('federation/instances', { - sort: '+notes' - }).then(instances => { - for (const i of instances) { - i.bg = randomColor({ - seed: i.host, - luminosity: 'dark' - }); - } - this.instances = instances; - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - setChartSrc(src) { - this.$refs.charts.setSrc(src); - }, - - updateStats() { - this.$root.api('stats', {}, true).then(stats => { - this.stats = stats; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.obdskegsannmntldydackcpzezagxqfy - padding 16px - - @media (min-width 500px) - padding 16px - - > header - display flex - padding-bottom 16px - border-bottom solid 1px var(--adminDashboardHeaderBorder) - color var(--adminDashboardHeaderFg) - font-size 14px - white-space nowrap - - @media (max-width 1000px) - display none - - > p - display block - margin 0 32px 0 0 - overflow hidden - text-overflow ellipsis - - > b - &:after - content ':' - margin-right 8px - - &:last-child - margin-left auto - margin-right 0 - - > .instances - padding 16px - color var(--adminDashboardHeaderFg) - font-size 13px - - >>> .instance - margin 0 10px - - > b - padding 2px 6px - margin-right 4px - border-radius 4px - color #fff - - > .stats - display flex - justify-content space-between - margin-bottom 16px - - > div - flex 1 - margin-right 16px - color var(--adminDashboardCardFg) - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--adminDashboardCardBg) - border-radius 8px - - &:last-child - margin-right 0 - - > div:first-child - display flex - align-items center - text-align center - - &:last-child - margin-right 0 - - > div:first-child - padding 16px 24px - font-size 28px - - > div:last-child - flex 1 - padding 16px 32px 16px 0 - text-align right - - > span - font-size 70% - opacity 0.7 - - > b - display block - - > div:last-child - display flex - padding 6px 16px - border-top solid 1px var(--adminDashboardCardDivider) - - > span - font-size 70% - opacity 0.7 - - &:last-child - margin-left auto - cursor pointer - - @media (max-width 900px) - display grid - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr - gap 16px - - > div - margin-right 0 - - @media (max-width 500px) - display block - - > div:not(:last-child) - margin-bottom 16px - - > .charts - margin-bottom 16px - - > .queue - margin-bottom 16px - - > .cpu-memory - margin-bottom 16px - -</style> diff --git a/src/client/app/admin/views/db.vue b/src/client/app/admin/views/db.vue deleted file mode 100644 index 9f87a749b6..0000000000 --- a/src/client/app/admin/views/db.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faDatabase"/> {{ $t('tables') }}</template> - <section v-if="tables"> - <div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div> - </section> - <section> - <header><fa :icon="faBroom"/> {{ $t('vacuum') }}</header> - <ui-info>{{ $t('vacuum-info') }}</ui-info> - <ui-switch v-model="fullVacuum">FULL</ui-switch> - <ui-switch v-model="analyzeVacuum">ANALYZE</ui-switch> - <ui-button @click="vacuum()"><fa :icon="faBroom"/> {{ $t('vacuum') }}</ui-button> - <ui-info warn>{{ $t('vacuum-exclamation') }}</ui-info> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faDatabase, faBroom } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/db.vue'), - - data() { - return { - tables: null, - fullVacuum: true, - analyzeVacuum: true, - faDatabase, faBroom - }; - }, - - mounted() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('admin/get-table-stats').then(tables => { - this.tables = tables; - }); - }, - - vacuum() { - this.$root.api('admin/vacuum', { - full: this.fullVacuum, - analyze: this.analyzeVacuum, - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - } -}); -</script> diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue deleted file mode 100644 index 1152db2b91..0000000000 --- a/src/client/app/admin/views/drive.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template> - <section class="fit-top"> - <ui-input v-model="target" type="text"> - <span>{{ $t('fileid-or-url') }}</span> - </ui-input> - <ui-horizon-group> - <ui-button @click="findAndToggleSensitive(true)"><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="findAndToggleSensitive(false)"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button> - </ui-horizon-group> - <ui-button @click="findAndDel()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - <ui-button @click="show()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - <ui-textarea v-if="file" :value="file | json5" readonly tall style="margin-top:16px;"></ui-textarea> - </section> - <section> - <ui-button @click="cleanUp()"><fa :icon="faTrashAlt"/> {{ $t('clean-up') }}</ui-button> - <ui-button @click="cleanRemoteFiles()"><fa :icon="faTrashAlt"/> {{ $t('clean-remote-files') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faCloud"/> {{ $t('@.drive') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('sort.title') }}</template> - <option value="-createdAt">{{ $t('sort.createdAtAsc') }}</option> - <option value="+createdAt">{{ $t('sort.createdAtDesc') }}</option> - <option value="-size">{{ $t('sort.sizeAsc') }}</option> - <option value="+size">{{ $t('sort.sizeDesc') }}</option> - </ui-select> - <ui-select v-model="origin"> - <template #label>{{ $t('origin.title') }}</template> - <option value="combined">{{ $t('origin.combined') }}</option> - <option value="local">{{ $t('origin.local') }}</option> - <option value="remote">{{ $t('origin.remote') }}</option> - </ui-select> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="kidvdlkg" v-for="file in files"> - <div @click="file._open = !file._open"> - <div> - <x-file-thumbnail class="thumbnail" :file="file" fit="contain" @click="showFileMenu(file)"/> - </div> - <div> - <header> - <b>{{ file.name }}</b> - <span class="username">@{{ file.user | acct }}</span> - </header> - <div> - <div> - <span style="margin-right:16px;">{{ file.type }}</span> - <span>{{ file.size | bytes }}</span> - </div> - <div><mk-time :time="file.createdAt" mode="detail"/></div> - </div> - </div> - </div> - <div v-show="file._open"> - <ui-input readonly :value="file.url"></ui-input> - <ui-horizon-group> - <ui-button @click="toggleSensitive(file)" v-if="file.isSensitive"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button> - <ui-button @click="toggleSensitive(file)" v-else><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - </ui-horizon-group> - </div> - </div> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetch">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import XFileThumbnail from '../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/drive.vue'), - - components: { - XFileThumbnail - }, - - data() { - return { - file: null, - target: null, - sort: '+createdAt', - origin: 'combined', - limit: 10, - offset: 0, - files: [], - existMore: false, - faCloud, faTrashAlt, faEye, faEyeSlash, faTerminal, faSearch - }; - }, - - watch: { - sort() { - this.files = []; - this.offset = 0; - this.fetch(); - }, - - origin() { - this.files = []; - this.offset = 0; - this.fetch(); - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - async fetchFile() { - try { - return await this.$root.api('drive/files/show', this.target.startsWith('http') ? { url: this.target } : { fileId: this.target }); - } catch (e) { - if (e == 'file-not-found') { - this.$root.dialog({ - type: 'error', - text: this.$t('file-not-found') - }); - } else { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - } - } - }, - - fetch() { - this.$root.api('admin/drive/files', { - origin: this.origin, - sort: this.sort, - offset: this.offset, - limit: this.limit + 1 - }).then(files => { - if (files.length == this.limit + 1) { - files.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - for (const x of files) { - x._open = false; - } - this.files = this.files.concat(files); - this.offset += this.limit; - }); - }, - - async del(file: any) { - const process = async () => { - await this.$root.api('drive/files/delete', { fileId: file.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - toggleSensitive(file: any) { - this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: !file.isSensitive - }).then(() => { - file.isSensitive = !file.isSensitive; - }); - }, - - async show() { - const file = await this.fetchFile(); - this.$root.api('admin/drive/show-file', { fileId: file.id }).then(info => { - this.file = info; - }); - }, - - async findAndToggleSensitive(sensitive) { - const process = async () => { - const file = await this.fetchFile(); - await this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: sensitive - }); - this.$root.dialog({ - type: 'success', - text: sensitive ? this.$t('marked-as-sensitive') : this.$t('unmarked-as-sensitive') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - async findAndDel() { - const process = async () => { - const file = await this.fetchFile(); - await this.$root.api('drive/files/delete', { fileId: file.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - cleanRemoteFiles() { - this.$root.dialog({ - type: 'warning', - text: this.$t('clean-remote-files-are-you-sure'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('admin/drive/clean-remote-files'); - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - cleanUp() { - this.$root.api('admin/drive/cleanup'); - this.$root.dialog({ - type: 'success', - splash: true - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kidvdlkg - padding 16px 0 - border-top solid 1px var(--faceDivider) - - > div:first-child - display flex - cursor pointer - - > div:nth-child(1) - > .thumbnail - display flex - width 64px - height 64px - background-size cover - background-position center center - - > div:nth-child(2) - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - word-break break-word - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue deleted file mode 100644 index 2925fcab57..0000000000 --- a/src/client/app/admin/views/emoji.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="plus"/> {{ $t('add-emoji.title') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-input v-model="name"> - <span>{{ $t('add-emoji.name') }}</span> - <template #desc>{{ $t('add-emoji.name-desc') }}</template> - </ui-input> - <ui-input v-model="category" :datalist="categoryList"> - <span>{{ $t('add-emoji.category') }}</span> - </ui-input> - <ui-input v-model="aliases"> - <span>{{ $t('add-emoji.aliases') }}</span> - <template #desc>{{ $t('add-emoji.aliases-desc') }}</template> - </ui-input> - </ui-horizon-group> - <ui-input v-model="url"> - <template #icon><fa icon="link"/></template> - <span>{{ $t('add-emoji.url') }}</span> - </ui-input> - <ui-info>{{ $t('add-emoji.info') }}</ui-info> - <ui-button @click="add">{{ $t('add-emoji.add') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faGrin"/> {{ $t('emojis.title') }}</template> - <section v-for="emoji in emojis" :key="emoji.name" class="oryfrbft"> - <div> - <img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/> - </div> - <div> - <ui-horizon-group> - <ui-input v-model="emoji.name"> - <span>{{ $t('add-emoji.name') }}</span> - </ui-input> - <ui-input v-model="emoji.category" :datalist="categoryList"> - <span>{{ $t('add-emoji.category') }}</span> - </ui-input> - <ui-input v-model="emoji.aliases"> - <span>{{ $t('add-emoji.aliases') }}</span> - </ui-input> - </ui-horizon-group> - <ui-input v-model="emoji.url"> - <template #icon><fa icon="link"/></template> - <span>{{ $t('add-emoji.url') }}</span> - </ui-input> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button> - <ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button> - </ui-horizon-group> - </div> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faGrin } from '@fortawesome/free-regular-svg-icons'; -import { unique } from '../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('admin/views/emoji.vue'), - data() { - return { - name: '', - category: '', - url: '', - aliases: '', - emojis: [], - faGrin - }; - }, - - mounted() { - this.fetchEmojis(); - }, - - computed: { - categoryList() { - return unique(this.emojis.map((x: any) => x.category || '').filter((x: string) => x !== '')); - } - }, - - methods: { - add() { - this.$root.api('admin/emoji/add', { - name: this.name, - category: this.category, - url: this.url, - aliases: this.aliases.split(' ').filter(x => x.length > 0) - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('add-emoji.added') - }); - this.fetchEmojis(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - fetchEmojis() { - this.$root.api('admin/emoji/list').then(emojis => { - for (const e of emojis) { - e.aliases = (e.aliases || []).join(' '); - } - this.emojis = emojis; - }); - }, - - updateEmoji(emoji) { - this.$root.api('admin/emoji/update', { - id: emoji.id, - name: emoji.name, - category: emoji.category, - url: emoji.url, - aliases: emoji.aliases.split(' ').filter(x => x.length > 0) - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('updated') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - removeEmoji(emoji) { - this.$root.dialog({ - type: 'warning', - text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('admin/emoji/remove', { - id: emoji.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('remove-emoji.removed') - }); - this.fetchEmojis(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.oryfrbft - @media (min-width 500px) - display flex - - > div:first-child - @media (max-width 500px) - padding-bottom 16px - - > img - vertical-align bottom - - > div:last-child - flex 1 - - @media (min-width 500px) - padding-left 16px - -</style> diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue deleted file mode 100644 index b419cca1d7..0000000000 --- a/src/client/app/admin/views/federation.vue +++ /dev/null @@ -1,553 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template> - <section class="fit-top"> - <ui-input class="target" v-model="target" type="text" @enter="showInstance()"> - <span>{{ $t('host') }}</span> - <template #prefix><fa :icon="faServer"/></template> - </ui-input> - <ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - - <div class="instance" v-if="instance"> - <ui-horizon-group inputs> - <ui-input :value="instance.host" type="text" readonly> - <span>{{ $t('host') }}</span> - <template #prefix><fa :icon="faServer"/></template> - </ui-input> - <ui-input :value="instance.caughtAt | date" type="text" readonly> - <span>{{ $t('caught-at') }}</span> - <template #prefix><fa :icon="faCrosshairs"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.notesCount | number" type="text" readonly> - <span>{{ $t('notes') }}</span> - <template #prefix><fa :icon="faEnvelopeOpenText"/></template> - </ui-input> - <ui-input :value="instance.usersCount | number" type="text" readonly> - <span>{{ $t('users') }}</span> - <template #prefix><fa :icon="faUsers"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.followingCount | number" type="text" readonly> - <span>{{ $t('following') }}</span> - <template #prefix><fa :icon="faCaretDown"/></template> - </ui-input> - <ui-input :value="instance.followersCount | number" type="text" readonly> - <span>{{ $t('followers') }}</span> - <template #prefix><fa :icon="faCaretUp"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.latestRequestSentAt | date" type="text" readonly> - <span>{{ $t('latest-request-sent-at') }}</span> - <template #prefix><fa :icon="faPaperPlane"/></template> - </ui-input> - <ui-input :value="instance.latestStatus" type="text" readonly> - <span>{{ $t('status') }}</span> - <template #prefix><fa :icon="faTrafficLight"/></template> - </ui-input> - </ui-horizon-group> - <ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly> - <span>{{ $t('latest-request-received-at') }}</span> - <template #prefix><fa :icon="faInbox"/></template> - </ui-input> - <ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch> - <details> - <summary>{{ $t('charts') }}</summary> - <ui-horizon-group inputs> - <ui-select v-model="chartSrc"> - <option value="requests">{{ $t('chart-srcs.requests') }}</option> - <option value="users">{{ $t('chart-srcs.users') }}</option> - <option value="users-total">{{ $t('chart-srcs.users-total') }}</option> - <option value="notes">{{ $t('chart-srcs.notes') }}</option> - <option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option> - <option value="ff">{{ $t('chart-srcs.ff') }}</option> - <option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option> - <option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option> - <option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option> - <option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option> - <option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option> - </ui-select> - <ui-select v-model="chartSpan"> - <option value="hour">{{ $t('chart-spans.hour') }}</option> - <option value="day">{{ $t('chart-spans.day') }}</option> - </ui-select> - </ui-horizon-group> - <div ref="chart"></div> - </details> - <details> - <summary>{{ $t('delete-all-files') }}</summary> - <ui-button @click="deleteAllFiles()" style="margin-top: 16px;"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button> - </details> - <details> - <summary>{{ $t('remove-all-following') }}</summary> - <ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button> - <ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info> - </details> - </div> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faServer"/> {{ $t('instances') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('sort') }}</template> - <option value="-caughtAt">{{ $t('sorts.caughtAtAsc') }}</option> - <option value="+caughtAt">{{ $t('sorts.caughtAtDesc') }}</option> - <option value="-lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtAsc') }}</option> - <option value="+lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtDesc') }}</option> - <option value="-notes">{{ $t('sorts.notesAsc') }}</option> - <option value="+notes">{{ $t('sorts.notesDesc') }}</option> - <option value="-users">{{ $t('sorts.usersAsc') }}</option> - <option value="+users">{{ $t('sorts.usersDesc') }}</option> - <option value="-following">{{ $t('sorts.followingAsc') }}</option> - <option value="+following">{{ $t('sorts.followingDesc') }}</option> - <option value="-followers">{{ $t('sorts.followersAsc') }}</option> - <option value="+followers">{{ $t('sorts.followersDesc') }}</option> - <option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option> - <option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option> - <option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option> - <option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('state') }}</template> - <option value="all">{{ $t('states.all') }}</option> - <option value="blocked">{{ $t('states.blocked') }}</option> - <option value="notResponding">{{ $t('states.not-responding') }}</option> - <option value="markedAsClosed">{{ $t('states.marked-as-closed') }}</option> - </ui-select> - </ui-horizon-group> - - <div class="instances"> - <header> - <span>{{ $t('host') }}</span> - <span>{{ $t('notes') }}</span> - <span>{{ $t('users') }}</span> - <span>{{ $t('following') }}</span> - <span>{{ $t('followers') }}</span> - <span>{{ $t('status') }}</span> - </header> - <div v-for="instance in instances" :style="{ opacity: instance.isNotResponding ? 0.5 : 1 }"> - <a @click.prevent="showInstance(instance.host)" rel="nofollow noopener" target="_blank" :href="`https://${instance.host}`" :style="{ textDecoration: instance.isMarkedAsClosed ? 'line-through' : 'none' }">{{ instance.host }}</a> - <span>{{ instance.notesCount | number }}</span> - <span>{{ instance.usersCount | number }}</span> - <span>{{ instance.followingCount | number }}</span> - <span>{{ instance.followersCount | number }}</span> - <span>{{ instance.latestStatus }}</span> - </div> - </div> - - <ui-info v-if="instances.length == limit">{{ $t('result-is-truncated', { n: limit }) }}</ui-info> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faBan"/> {{ $t('blocked-hosts') }}</template> - <section class="fit-top"> - <ui-textarea v-model="blockedHosts"> - <template #desc>{{ $t('blocked-hosts-info') }}</template> - </ui-textarea> - <ui-button @click="saveBlockedHosts">{{ $t('save') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import { faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; -import ApexCharts from 'apexcharts'; -import * as tinycolor from 'tinycolor2'; - -const chartLimit = 90; -const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); -const negate = arr => arr.map(x => -x); - -export default Vue.extend({ - i18n: i18n('admin/views/federation.vue'), - - filters: { - date: v => v ? new Date(v).toLocaleString() : 'N/A' - }, - - data() { - return { - instance: null, - target: null, - sort: '+lastCommunicatedAt', - state: 'all', - limit: 100, - instances: [], - chart: null, - chartSrc: 'requests', - chartSpan: 'hour', - chartInstance: null, - blockedHosts: '', - faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox - }; - }, - - computed: { - data(): any { - if (this.chart == null) return null; - switch (this.chartSrc) { - case 'requests': return this.requestsChart(); - case 'users': return this.usersChart(false); - case 'users-total': return this.usersChart(true); - case 'notes': return this.notesChart(false); - case 'notes-total': return this.notesChart(true); - case 'ff': return this.ffChart(false); - case 'ff-total': return this.ffChart(true); - case 'drive-usage': return this.driveUsageChart(false); - case 'drive-usage-total': return this.driveUsageChart(true); - case 'drive-files': return this.driveFilesChart(false); - case 'drive-files-total': return this.driveFilesChart(true); - } - }, - - stats(): any[] { - const stats = - this.chartSpan == 'day' ? this.chart.perDay : - this.chartSpan == 'hour' ? this.chart.perHour : - null; - - return stats; - } - }, - - watch: { - sort() { - this.fetchInstances(); - }, - - state() { - this.fetchInstances(); - }, - - async instance() { - this.now = new Date(); - - const [perHour, perDay] = await Promise.all([ - this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), - this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), - ]); - - const chart = { - perHour: perHour, - perDay: perDay - }; - - this.chart = chart; - - this.renderChart(); - }, - - chartSrc() { - this.renderChart(); - }, - - chartSpan() { - this.renderChart(); - } - }, - - mounted() { - this.fetchInstances(); - - this.$root.getMeta().then(meta => { - this.blockedHosts = meta.blockedHosts.join('\n'); - }); - }, - - beforeDestroy() { - this.chartInstance.destroy(); - }, - - methods: { - showInstance(target?: string) { - this.$root.api('federation/show-instance', { - host: target || this.target - }).then(instance => { - if (instance == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('instance-not-registered') - }); - } else { - this.instance = instance; - this.target = ''; - } - }); - }, - - fetchInstances() { - this.instances = []; - this.$root.api('federation/instances', { - blocked: this.state === 'blocked' ? true : null, - notResponding: this.state === 'notResponding' ? true : null, - markedAsClosed: this.state === 'markedAsClosed' ? true : null, - sort: this.sort, - limit: this.limit - }).then(instances => { - this.instances = instances; - }); - }, - - removeAllFollowing() { - this.$root.api('admin/federation/remove-all-following', { - host: this.instance.host - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - deleteAllFiles() { - this.$root.api('admin/federation/delete-all-files', { - host: this.instance.host - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - updateInstance() { - this.$root.api('admin/federation/update-instance', { - host: this.instance.host, - isBlocked: this.instance.isBlocked || false, - isClosed: this.instance.isMarkedAsClosed || false - }); - }, - - setSrc(src) { - this.chartSrc = src; - }, - - renderChart() { - if (this.chartInstance) { - this.chartInstance.destroy(); - } - - this.chartInstance = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 300, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - theme: this.$store.state.device.darkmode ? 'dark' : 'light' - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' - }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - }, - yaxis: { - labels: { - formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - } - }, - series: this.data.series - }); - - this.chartInstance.render(); - }, - - getDate(i: number) { - const y = this.now.getFullYear(); - const m = this.now.getMonth(); - const d = this.now.getDate(); - const h = this.now.getHours(); - - return ( - this.chartSpan == 'day' ? new Date(y, m, d - i) : - this.chartSpan == 'hour' ? new Date(y, m, d, h - i) : - null - ); - }, - - format(arr) { - return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v })); - }, - - requestsChart(): any { - return { - series: [{ - name: 'Incoming', - data: this.format(this.stats.requests.received) - }, { - name: 'Outgoing (succeeded)', - data: this.format(this.stats.requests.succeeded) - }, { - name: 'Outgoing (failed)', - data: this.format(this.stats.requests.failed) - }] - }; - }, - - usersChart(total: boolean): any { - return { - series: [{ - name: 'Users', - type: 'area', - data: this.format(total - ? this.stats.users.total - : sum(this.stats.users.inc, negate(this.stats.users.dec)) - ) - }] - }; - }, - - notesChart(total: boolean): any { - return { - series: [{ - name: 'Notes', - type: 'area', - data: this.format(total - ? this.stats.notes.total - : sum(this.stats.notes.inc, negate(this.stats.notes.dec)) - ) - }] - }; - }, - - ffChart(total: boolean): any { - return { - series: [{ - name: 'Following', - type: 'area', - data: this.format(total - ? this.stats.following.total - : sum(this.stats.following.inc, negate(this.stats.following.dec)) - ) - }, { - name: 'Followers', - type: 'area', - data: this.format(total - ? this.stats.followers.total - : sum(this.stats.followers.inc, negate(this.stats.followers.dec)) - ) - }] - }; - }, - - driveUsageChart(total: boolean): any { - return { - bytes: true, - series: [{ - name: 'Drive usage', - type: 'area', - data: this.format(total - ? this.stats.drive.totalUsage - : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) - ) - }] - }; - }, - - driveFilesChart(total: boolean): any { - return { - series: [{ - name: 'Drive files', - type: 'area', - data: this.format(total - ? this.stats.drive.totalFiles - : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) - ) - }] - }; - }, - - saveBlockedHosts() { - this.$root.api('admin/update-meta', { - blockedHosts: this.blockedHosts ? this.blockedHosts.split('\n') : [] - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.target - margin-bottom 16px !important - -.instances - width 100% - - > header - display flex - - > * - color var(--text) - font-weight bold - - > div - display flex - - > * > * - flex 1 - overflow auto - - &:first-child - min-width 200px - -</style> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue deleted file mode 100644 index 1b81185749..0000000000 --- a/src/client/app/admin/views/index.vue +++ /dev/null @@ -1,297 +0,0 @@ -<template> -<div class="mk-admin" :class="{ isMobile }"> - <header v-show="isMobile"> - <button class="nav" @click="navOpend = true"><fa icon="bars"/></button> - <span>MisskeyMyAdmin</span> - </header> - <div class="nav-backdrop" - v-if="navOpend && isMobile" - @click="navOpend = false" - @touchstart="navOpend = false" - ></div> - <nav v-show="navOpend"> - <div class="mi"> - <img svg-inline src="../assets/header-icon.svg"/> - </div> - <div class="me"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </div> - <ul> - <li><router-link to="/dashboard" active-class="active"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</router-link></li> - <li><router-link to="/instance" active-class="active"><fa icon="cog" fixed-width/>{{ $t('instance') }}</router-link></li> - <li><router-link to="/queue" active-class="active"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</router-link></li> - <li><router-link to="/logs" active-class="active"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</router-link></li> - <li><router-link to="/db" active-class="active"><fa :icon="faDatabase" fixed-width/>{{ $t('db') }}</router-link></li> - <li><router-link to="/moderators" active-class="active"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</router-link></li> - <li><router-link to="/users" active-class="active"><fa icon="users" fixed-width/>{{ $t('users') }}</router-link></li> - <li><router-link to="/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link></li> - <li><router-link to="/federation" active-class="active"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</router-link></li> - <li><router-link to="/emoji" active-class="active"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</router-link></li> - <li><router-link to="/announcements" active-class="active"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</router-link></li> - <li><router-link to="/abuse" active-class="active"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</router-link></li> - </ul> - <div class="back-to-misskey"> - <a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a> - </div> - <div class="version"> - <small>Misskey {{ version }}</small> - </div> - </nav> - <main> - <div class="page"> - <div v-if="page == 'dashboard'"><x-dashboard/></div> - <div v-if="page == 'instance'"><x-instance/></div> - <div v-if="page == 'queue'"><x-queue/></div> - <div v-if="page == 'logs'"><x-logs/></div> - <div v-if="page == 'db'"><x-db/></div> - <div v-if="page == 'moderators'"><x-moderators/></div> - <div v-if="page == 'users'"><x-users/></div> - <div v-if="page == 'emoji'"><x-emoji/></div> - <div v-if="page == 'announcements'"><x-announcements/></div> - <div v-if="page == 'drive'"><x-drive/></div> - <div v-if="page == 'federation'"><x-federation/></div> - <div v-if="page == 'abuse'"><x-abuse/></div> - </div> - </main> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { version } from '../../config'; -import XDashboard from './dashboard.vue'; -import XInstance from './instance.vue'; -import XQueue from './queue.vue'; -import XLogs from './logs.vue'; -import XDb from './db.vue'; -import XModerators from './moderators.vue'; -import XEmoji from './emoji.vue'; -import XAnnouncements from './announcements.vue'; -import XUsers from './users.vue'; -import XDrive from './drive.vue'; -import XAbuse from './abuse.vue'; -import XFederation from './federation.vue'; - -import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream, faDatabase } from '@fortawesome/free-solid-svg-icons'; -import { faGrin } from '@fortawesome/free-regular-svg-icons'; - -// Detect the user agent -const ua = navigator.userAgent.toLowerCase(); -const isMobile = /mobile|iphone|ipad|android/.test(ua); - -export default Vue.extend({ - i18n: i18n('admin/views/index.vue'), - components: { - XDashboard, - XInstance, - XQueue, - XLogs, - XDb, - XModerators, - XEmoji, - XAnnouncements, - XUsers, - XDrive, - XAbuse, - XFederation, - }, - provide: { - isMobile - }, - data() { - return { - version, - isMobile, - navOpend: !isMobile, - faGrin, - faArrowLeft, - faHeadset, - faGlobe, - faExclamationCircle, - faTasks, - faStream, - faDatabase, - }; - }, - computed: { - page() { - return this.$route.params.page; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-admin - $headerHeight = 48px - - display flex - height 100% - - > header - position fixed - top 0 - z-index 10000 - width 100% - color var(--mobileHeaderFg) - background-color var(--mobileHeaderBg) - box-shadow 0 1px 0 rgba(#000, 0.075) - - &, * - user-select none - - > span - display block - line-height $headerHeight - text-align center - - > .nav - display block - position absolute - top 0 - left 0 - z-index 10001 - padding 0 - width $headerHeight - font-size 1.4em - line-height $headerHeight - border-right solid 1px rgba(#000, 0.1) - - > [data-icon] - transition all 0.2s ease - - > nav - position fixed - z-index 20001 - top 0 - left 0 - width 250px - height 100vh - overflow auto - background #333 - color #fff - - > .mi - text-align center - - > svg - width 24px - height 82px - vertical-align top - fill #fff - opacity 0.7 - - > .me - display flex - margin 0 16px 16px 16px - padding 16px 0 - align-items center - border-top solid 1px #555 - border-bottom solid 1px #555 - - > .avatar - height 48px - border-radius 100% - vertical-align middle - - > .name - margin 0 16px - padding 0 - color #fff - overflow hidden - text-overflow ellipsis - white-space nowrap - font-size 15px - - > .back-to-misskey - margin 16px 16px 0 16px - padding 0 - border-top solid 1px #555 - - > a - display block - padding 16px 4px - color inherit - text-decoration none - color #eee - font-size 15px - - &:hover - color #fff - - > [data-icon] - margin-right 6px - - > .version - margin 0 16px 16px 16px - padding-top 16px - border-top solid 1px #555 - text-align center - - > small - opacity 0.7 - - > ul - margin 0 - padding 0 - list-style none - font-size 15px - - > li > a - display block - padding 10px 16px - margin 0 - user-select none - color #eee - transition margin-left 0.2s ease - - &:hover - color #fff - - > [data-icon] - margin-right 6px - - &.active - margin-left 8px - color var(--primary) !important - - &:after - content "" - display block - position absolute - top 0 - right 0 - bottom 0 - margin auto 0 - height 0 - border-top solid 16px transparent - border-right solid 16px var(--bg) - border-bottom solid 16px transparent - border-left solid 16px transparent - - > .nav-backdrop - position fixed - top 0 - left 0 - z-index 20000 - width 100% - height 100% - background var(--mobileNavBackdrop) - - > main - width 100% - padding 0 0 0 250px - - > .page - max-width 1150px - - @media (min-width 500px) - padding 16px - - &.isMobile - > main - padding $headerHeight 0 0 0 - -</style> diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue deleted file mode 100644 index ebc554f955..0000000000 --- a/src/client/app/admin/views/instance.vue +++ /dev/null @@ -1,523 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="cog"/> {{ $t('instance') }}</template> - <section class="fit-top"> - <ui-input :value="host" readonly>{{ $t('host') }}</ui-input> - <ui-input v-model="name">{{ $t('instance-name') }}</ui-input> - <ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea> - <ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input> - <ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input> - <ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input> - <ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input> - <details> - <summary>{{ $t('advanced-config') }}</summary> - <ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input> - <ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input> - <ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input> - <ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input> - </details> - </section> - <section class="fit-bottom"> - <header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header> - <ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input> - <ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input> - </section> - <section> - <ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch> - <ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template> - <section class="fit-top fit-bottom"> - <ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input> - </section> - <section> - <ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> - <ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch> - <ui-info>{{ $t('disabling-timelines-info') }}</ui-info> - </section> - <section> - <ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch> - <ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template> - <section> - <ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch> - <template v-if="useObjectStorage"> - <ui-info> - <i18n path="object-storage-s3-info"> - <a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a> - </i18n> - </ui-info> - <ui-info>{{ $t('object-storage-gcs-info') }}</ui-info> - <ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input> - <ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input> - </ui-horizon-group> - <ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input> - <ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input> - <ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch> - </template> - </section> - <section> - <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> - <ui-switch v-model="proxyRemoteFiles">{{ $t('proxy-remote-files') }}<template #desc>{{ $t('proxy-remote-files-desc') }}</template></ui-switch> - </section> - <section class="fit-top fit-bottom"> - <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - <ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faThumbtack"/> {{ $t('pinned-users') }}</template> - <section class="fit-top"> - <ui-textarea v-model="pinnedUsers"> - <template #desc>{{ $t('pinned-users-info') }}</template> - </ui-textarea> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template> - <section> - <ui-info>{{ $t('proxy-account-info') }}</ui-info> - <ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input> - <ui-info warn>{{ $t('proxy-account-warn') }}</ui-info> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template> - <section> - <ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch> - <template v-if="enableEmail"> - <ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input> - <ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch> - <ui-horizon-group inputs> - <ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input> - <ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch> - <ui-button @click="testEmail()">{{ $t('test-email') }}</ui-button> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template> - <section> - <ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch> - <template v-if="enableServiceWorker"> - <ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info> - <ui-horizon-group inputs class="fit-bottom"> - <ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input> - <ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input> - </ui-horizon-group> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template> - <section :class="enableRecaptcha ? 'fit-bottom' : ''"> - <ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch> - <template v-if="enableRecaptcha"> - <ui-info>{{ $t('recaptcha-info') }}</ui-info> - <ui-info warn>{{ $t('recaptcha-info2') }}</ui-info> - <ui-horizon-group inputs> - <ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input> - <ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input> - </ui-horizon-group> - </template> - </section> - <section v-if="enableRecaptcha && recaptchaSiteKey"> - <header>{{ $t('recaptcha-preview') }}</header> - <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template> - <section> - <header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header> - <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> - <template v-if="enableTwitterIntegration"> - <ui-horizon-group> - <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input> - <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info> - </template> - </section> - <section> - <header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header> - <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> - <template v-if="enableGithubIntegration"> - <ui-horizon-group> - <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input> - <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info> - </template> - </section> - <section> - <header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header> - <ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> - <template v-if="enableDiscordIntegration"> - <ui-horizon-group> - <ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input> - <ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <details> - <summary style="color:var(--text);">{{ $t('advanced-config') }}</summary> - - <ui-card> - <template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template> - <section class="fit-top"> - <ui-textarea v-model="hiddenTags"> - <template #desc>{{ $t('hidden-tags-info') }}</template> - </ui-textarea> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title>summaly Proxy</template> - <section class="fit-top fit-bottom"> - <ui-input v-model="summalyProxy">URL</ui-input> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { url, host } from '../../config'; -import { toUnicode } from 'punycode'; -import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons'; -import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/instance.vue'), - - data() { - return { - url, - host: toUnicode(host), - maintainerName: null, - maintainerEmail: null, - ToSUrl: null, - repositoryUrl: "https://github.com/syuilo/misskey", - feedbackUrl: null, - disableRegistration: false, - disableLocalTimeline: false, - disableGlobalTimeline: false, - enableEmojiReaction: true, - useStarForReactionFallback: false, - mascotImageUrl: null, - bannerUrl: null, - errorImageUrl: null, - iconUrl: null, - name: null, - description: null, - languages: null, - cacheRemoteFiles: false, - proxyRemoteFiles: false, - localDriveCapacityMb: null, - remoteDriveCapacityMb: null, - maxNoteTextLength: null, - enableRecaptcha: false, - recaptchaSiteKey: null, - recaptchaSecretKey: null, - enableTwitterIntegration: false, - twitterConsumerKey: null, - twitterConsumerSecret: null, - enableGithubIntegration: false, - githubClientId: null, - githubClientSecret: null, - enableDiscordIntegration: false, - discordClientId: null, - discordClientSecret: null, - proxyAccount: null, - summalyProxy: null, - enableEmail: false, - email: null, - smtpSecure: false, - smtpHost: null, - smtpPort: null, - smtpUser: null, - smtpPass: null, - smtpAuth: false, - enableServiceWorker: false, - swPublicKey: null, - swPrivateKey: null, - pinnedUsers: '', - hiddenTags: '', - useObjectStorage: false, - objectStorageBaseUrl: null, - objectStorageBucket: null, - objectStoragePrefix: null, - objectStorageEndpoint: null, - objectStorageRegion: null, - objectStoragePort: null, - objectStorageAccessKey: null, - objectStorageSecretKey: null, - objectStorageUseSSL: false, - faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag - }; - }, - - created() { - this.$root.getMeta(true).then(meta => { - this.maintainerName = meta.maintainerName; - this.maintainerEmail = meta.maintainerEmail; - this.ToSUrl = meta.ToSUrl; - this.repositoryUrl = meta.repositoryUrl; - this.feedbackUrl = meta.feedbackUrl; - this.disableRegistration = meta.disableRegistration; - this.disableLocalTimeline = meta.disableLocalTimeline; - this.disableGlobalTimeline = meta.disableGlobalTimeline; - this.enableEmojiReaction = meta.enableEmojiReaction; - this.useStarForReactionFallback = meta.useStarForReactionFallback; - this.mascotImageUrl = meta.mascotImageUrl; - this.bannerUrl = meta.bannerUrl; - this.errorImageUrl = meta.errorImageUrl; - this.iconUrl = meta.iconUrl; - this.name = meta.name; - this.description = meta.description; - this.languages = meta.langs.join(' '); - this.cacheRemoteFiles = meta.cacheRemoteFiles; - this.proxyRemoteFiles = meta.proxyRemoteFiles; - this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; - this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; - this.maxNoteTextLength = meta.maxNoteTextLength; - this.enableRecaptcha = meta.enableRecaptcha; - this.recaptchaSiteKey = meta.recaptchaSiteKey; - this.recaptchaSecretKey = meta.recaptchaSecretKey; - this.proxyAccount = meta.proxyAccount; - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.twitterConsumerKey = meta.twitterConsumerKey; - this.twitterConsumerSecret = meta.twitterConsumerSecret; - this.enableGithubIntegration = meta.enableGithubIntegration; - this.githubClientId = meta.githubClientId; - this.githubClientSecret = meta.githubClientSecret; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.discordClientId = meta.discordClientId; - this.discordClientSecret = meta.discordClientSecret; - this.summalyProxy = meta.summalyProxy; - this.enableEmail = meta.enableEmail; - this.email = meta.email; - this.smtpSecure = meta.smtpSecure; - this.smtpHost = meta.smtpHost; - this.smtpPort = meta.smtpPort; - this.smtpUser = meta.smtpUser; - this.smtpPass = meta.smtpPass; - this.smtpAuth = meta.smtpUser != null && meta.smtpUser !== ''; - this.enableServiceWorker = meta.enableServiceWorker; - this.swPublicKey = meta.swPublickey; - this.swPrivateKey = meta.swPrivateKey; - this.pinnedUsers = meta.pinnedUsers.join('\n'); - this.hiddenTags = meta.hiddenTags.join('\n'); - this.useObjectStorage = meta.useObjectStorage; - this.objectStorageBaseUrl = meta.objectStorageBaseUrl; - this.objectStorageBucket = meta.objectStorageBucket; - this.objectStoragePrefix = meta.objectStoragePrefix; - this.objectStorageEndpoint = meta.objectStorageEndpoint; - this.objectStorageRegion = meta.objectStorageRegion; - this.objectStoragePort = meta.objectStoragePort; - this.objectStorageAccessKey = meta.objectStorageAccessKey; - this.objectStorageSecretKey = meta.objectStorageSecretKey; - this.objectStorageUseSSL = meta.objectStorageUseSSL; - }); - }, - - mounted() { - const renderRecaptchaPreview = () => { - if (!(window as any).grecaptcha) return; - if (!this.$refs.recaptcha) return; - if (!this.recaptchaSiteKey) return; - (window as any).grecaptcha.render(this.$refs.recaptcha, { - sitekey: this.recaptchaSiteKey - }); - }; - - window.onRecaotchaLoad = () => { - renderRecaptchaPreview(); - }; - - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); - head.appendChild(script); - - this.$watch('enableRecaptcha', () => { - renderRecaptchaPreview(); - }); - - this.$watch('recaptchaSiteKey', () => { - renderRecaptchaPreview(); - }); - }, - - methods: { - invite() { - this.$root.api('admin/invite').then(x => { - this.$root.dialog({ - type: 'info', - text: x.code - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async testEmail() { - this.$root.api('admin/send-email', { - to: this.maintainerEmail, - subject: 'Test email', - text: 'Yo' - }).then(x => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - updateMeta() { - this.$root.api('admin/update-meta', { - maintainerName: this.maintainerName, - maintainerEmail: this.maintainerEmail, - ToSUrl: this.ToSUrl, - repositoryUrl: this.repositoryUrl, - feedbackUrl: this.feedbackUrl, - disableRegistration: this.disableRegistration, - disableLocalTimeline: this.disableLocalTimeline, - disableGlobalTimeline: this.disableGlobalTimeline, - enableEmojiReaction: this.enableEmojiReaction, - useStarForReactionFallback: this.useStarForReactionFallback, - mascotImageUrl: this.mascotImageUrl, - bannerUrl: this.bannerUrl, - errorImageUrl: this.errorImageUrl, - iconUrl: this.iconUrl, - name: this.name, - description: this.description, - langs: this.languages ? this.languages.split(' ') : [], - cacheRemoteFiles: this.cacheRemoteFiles, - proxyRemoteFiles: this.proxyRemoteFiles, - localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), - remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), - maxNoteTextLength: parseInt(this.maxNoteTextLength, 10), - enableRecaptcha: this.enableRecaptcha, - recaptchaSiteKey: this.recaptchaSiteKey, - recaptchaSecretKey: this.recaptchaSecretKey, - proxyAccount: this.proxyAccount, - enableTwitterIntegration: this.enableTwitterIntegration, - twitterConsumerKey: this.twitterConsumerKey, - twitterConsumerSecret: this.twitterConsumerSecret, - enableGithubIntegration: this.enableGithubIntegration, - githubClientId: this.githubClientId, - githubClientSecret: this.githubClientSecret, - enableDiscordIntegration: this.enableDiscordIntegration, - discordClientId: this.discordClientId, - discordClientSecret: this.discordClientSecret, - summalyProxy: this.summalyProxy, - enableEmail: this.enableEmail, - email: this.email, - smtpSecure: this.smtpSecure, - smtpHost: this.smtpHost, - smtpPort: parseInt(this.smtpPort, 10), - smtpUser: this.smtpAuth ? this.smtpUser : '', - smtpPass: this.smtpAuth ? this.smtpPass : '', - enableServiceWorker: this.enableServiceWorker, - swPublicKey: this.swPublicKey, - swPrivateKey: this.swPrivateKey, - pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [], - hiddenTags: this.hiddenTags ? this.hiddenTags.split('\n') : [], - useObjectStorage: this.useObjectStorage, - objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, - objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, - objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, - objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, - objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, - objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, - objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, - objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, - objectStorageUseSSL: this.objectStorageUseSSL, - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue deleted file mode 100644 index cb54318187..0000000000 --- a/src/client/app/admin/views/logs.vue +++ /dev/null @@ -1,119 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faStream"/> {{ $t('logs') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-input v-model="domain" :debounce="true"> - <span>{{ $t('domain') }}</span> - </ui-input> - <ui-select v-model="level"> - <template #label>{{ $t('level') }}</template> - <option value="all">{{ $t('levels.all') }}</option> - <option value="info">{{ $t('levels.info') }}</option> - <option value="success">{{ $t('levels.success') }}</option> - <option value="warning">{{ $t('levels.warning') }}</option> - <option value="error">{{ $t('levels.error') }}</option> - <option value="debug">{{ $t('levels.debug') }}</option> - </ui-select> - </ui-horizon-group> - - <div class="nqjzuvev"> - <code v-for="log in logs" :key="log.id" :class="log.level"> - <details> - <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> - <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> - </details> - </code> - </div> - - <ui-button @click="deleteAll()">{{ $t('delete-all') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faStream } from '@fortawesome/free-solid-svg-icons'; -import VueJsonPretty from 'vue-json-pretty'; - -export default Vue.extend({ - i18n: i18n('admin/views/logs.vue'), - - components: { - VueJsonPretty - }, - - data() { - return { - logs: [], - level: 'all', - domain: '', - faStream - }; - }, - - watch: { - level() { - this.logs = []; - this.fetch(); - }, - - domain() { - this.logs = []; - this.fetch(); - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('admin/logs', { - level: this.level === 'all' ? null : this.level, - domain: this.domain === '' ? null : this.domain, - limit: 100 - }).then(logs => { - this.logs = logs.reverse(); - }); - }, - - deleteAll() { - this.$root.api('admin/delete-logs').then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nqjzuvev - padding 8px - background #000 - color #fff - font-size 14px - - > code - display block - - &.error - color #f00 - - &.warning - color #ff0 - - &.success - color #0f0 - - &.debug - opacity 0.7 - -</style> diff --git a/src/client/app/admin/views/moderators.vue b/src/client/app/admin/views/moderators.vue deleted file mode 100644 index 8ceab02d97..0000000000 --- a/src/client/app/admin/views/moderators.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="plus"/> {{ $t('add-moderator.title') }}</template> - <section class="fit-top"> - <ui-input v-model="username" type="text"> - <template #prefix>@</template> - </ui-input> - <ui-horizon-group> - <ui-button @click="add" :disabled="changing">{{ $t('add-moderator.add') }}</ui-button> - <ui-button @click="remove" :disabled="changing">{{ $t('add-moderator.remove') }}</ui-button> - </ui-horizon-group> - </section> - </ui-card> - - <ui-card> - <template #title>{{ $t('logs.title') }}</template> - <section class="fit-top"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div v-for="log in logs" :key="log.id" class=""> - <ui-horizon-group inputs> - <ui-input :value="log.user | acct" type="text" readonly> - <span>{{ $t('logs.moderator') }}</span> - </ui-input> - <ui-input :value="log.type" type="text" readonly> - <span>{{ $t('logs.type') }}</span> - </ui-input> - <ui-input :value="log.createdAt | date" type="text" readonly> - <span>{{ $t('logs.at') }}</span> - </ui-input> - </ui-horizon-group> - <ui-textarea :value="JSON.stringify(log.info, null, 4)" readonly> - <span>{{ $t('logs.info') }}</span> - </ui-textarea> - </div> - </sequential-entrance> - <ui-button v-if="existMoreLogs" @click="fetchLogs">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import parseAcct from "../../../../misc/acct/parse"; - -export default Vue.extend({ - i18n: i18n('admin/views/moderators.vue'), - - data() { - return { - username: '', - changing: false, - logs: [], - untilLogId: null, - existMoreLogs: false - }; - }, - - created() { - this.fetchLogs(); - }, - - methods: { - async add() { - this.changing = true; - - const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.username)); - await this.$root.api('admin/moderators/add', { userId: user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('add-moderator.added') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.changing = false; - }, - - async remove() { - this.changing = true; - - const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.username)); - await this.$root.api('admin/moderators/remove', { userId: user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('add-moderator.removed') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.changing = false; - }, - - fetchLogs() { - this.$root.api('admin/show-moderation-logs', { - untilId: this.untilId, - limit: 10 + 1 - }).then(logs => { - if (logs.length == 10 + 1) { - logs.pop(); - this.existMoreLogs = true; - } else { - this.existMoreLogs = false; - } - this.logs = this.logs.concat(logs); - this.untilLogId = this.logs[this.logs.length - 1].id; - }); - }, - } -}); -</script> diff --git a/src/client/app/admin/views/queue.chart.vue b/src/client/app/admin/views/queue.chart.vue deleted file mode 100644 index ff29aa8392..0000000000 --- a/src/client/app/admin/views/queue.chart.vue +++ /dev/null @@ -1,181 +0,0 @@ -<template> -<div> - <ui-info warn v-if="latestStats && latestStats.waiting > 0">The queue is jammed.</ui-info> - <ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> - <ui-input :value="latestStats.activeSincePrevTick | number" type="text" readonly> - <span>Process</span> - <template #prefix><fa :icon="fasPlayCircle"/></template> - <template #suffix>jobs/tick</template> - </ui-input> - <ui-input :value="latestStats.active | number" type="text" readonly> - <span>Active</span> - <template #prefix><fa :icon="farPlayCircle"/></template> - <template #suffix>jobs</template> - </ui-input> - <ui-input :value="latestStats.waiting | number" type="text" readonly> - <span>Waiting</span> - <template #prefix><fa :icon="faStopCircle"/></template> - <template #suffix>jobs</template> - </ui-input> - <ui-input :value="latestStats.delayed | number" type="text" readonly> - <span>Delayed</span> - <template #prefix><fa :icon="faStopwatch"/></template> - <template #suffix>jobs</template> - </ui-input> - </ui-horizon-group> - <div ref="chart" class="wptihjuy"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import ApexCharts from 'apexcharts'; -import * as tinycolor from 'tinycolor2'; -import { faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons'; -import { faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/queue.vue'), - - props: { - type: { - type: String, - required: true - }, - connection: { - required: true - }, - limit: { - type: Number, - required: true - } - }, - - data() { - return { - stats: [], - chart: null, - faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle - }; - }, - - computed: { - latestStats(): any { - return this.stats.length > 0 ? this.stats[this.stats.length - 1][this.type] : null; - } - }, - - watch: { - stats(stats) { - this.chart.updateSeries([{ - name: 'Process', - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x[this.type].activeSincePrevTick })) - }, { - name: 'Active', - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x[this.type].active })) - }, { - name: 'Waiting', - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x[this.type].waiting })) - }, { - name: 'Delayed', - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x[this.type].delayed })) - }]); - }, - }, - - mounted() { - this.chart = new ApexCharts(this.$refs.chart, { - chart: { - id: this.type, - group: 'queue', - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, - } - }, - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - series: [] as any, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - } - }); - - this.chart.render(); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - - this.$once('hook:beforeDestroy', () => { - if (this.chart) this.chart.destroy(); - }); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > this.limit) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wptihjuy - min-height 200px !important - margin -8px - -</style> diff --git a/src/client/app/admin/views/queue.vue b/src/client/app/admin/views/queue.vue deleted file mode 100644 index 9aa740c68c..0000000000 --- a/src/client/app/admin/views/queue.vue +++ /dev/null @@ -1,159 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template> - <section> - <header><fa :icon="faPaperPlane"/> {{ $t('domains.deliver') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="deliver"/> - </section> - <section> - <header><fa :icon="faInbox"/> {{ $t('domains.inbox') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="inbox"/> - </section> - <section> - <details> - <summary>{{ $t('other-queues') }}</summary> - <section> - <header><fa :icon="faDatabase"/> {{ $t('domains.db') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="db"/> - </section> - <ui-hr/> - <section> - <header><fa :icon="faCloud"/> {{ $t('domains.objectStorage') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="objectStorage"/> - </section> - </details> - </section> - <section> - <ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="domain"> - <template #label>{{ $t('queue') }}</template> - <option value="deliver">{{ $t('domains.deliver') }}</option> - <option value="inbox">{{ $t('domains.inbox') }}</option> - <option value="db">{{ $t('domains.db') }}</option> - <option value="objectStorage">{{ $t('domains.objectStorage') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('state') }}</template> - <option value="active">{{ $t('states.active') }}</option> - <option value="waiting">{{ $t('states.waiting') }}</option> - <option value="delayed">{{ $t('states.delayed') }}</option> - </ui-select> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="xvvuvgsv" v-for="job in jobs" :key="job.id"> - <b>{{ job.id }}</b> - <template v-if="domain === 'deliver'"> - <span>{{ job.data.to }}</span> - </template> - <template v-if="domain === 'inbox'"> - <span>{{ job.data.activity.id }}</span> - </template> - <span>{{ `(${job.attempts}/${job.maxAttempts}, ${Math.floor((jobsFetched - job.timestamp) / 1000 / 60)}min)` }}</span> - </div> - </sequential-entrance> - <ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faTasks, faInbox, faDatabase, faCloud } from '@fortawesome/free-solid-svg-icons'; -import { faPaperPlane, faChartBar } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../i18n'; -import XChart from './queue.chart.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/queue.vue'), - - components: { - XChart - }, - - data() { - return { - connection: null, - chartLimit: 200, - jobs: [], - jobsLimit: 50, - jobsFetched: Date.now(), - domain: 'deliver', - state: 'delayed', - faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud - }; - }, - - watch: { - domain() { - this.jobs = []; - this.fetchJobs(); - }, - - state() { - this.jobs = []; - this.fetchJobs(); - }, - }, - - mounted() { - this.fetchJobs(); - - this.connection = this.$root.stream.useSharedConnection('queueStats'); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: this.chartLimit - }); - - this.$once('hook:beforeDestroy', () => { - this.connection.dispose(); - }); - }, - - methods: { - async removeAllJobs() { - const process = async () => { - await this.$root.api('admin/queue/clear'); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - fetchJobs() { - this.$root.api('admin/queue/jobs', { - domain: this.domain, - state: this.state, - limit: this.jobsLimit - }).then(jobs => { - this.jobsFetched = Date.now(), - this.jobs = jobs; - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.xvvuvgsv - margin-left -6px - > b, span - margin 0 6px - -</style> diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue deleted file mode 100644 index 9c3db2d6c2..0000000000 --- a/src/client/app/admin/views/users.user.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<div class="kofvwchc"> - <div> - <a :href="user | userPage(null, true)"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div @click="click(user.id)"> - <header> - <b><mk-user-name :user="user"/></b> - <span class="username">@{{ user | acct }}</span> - <span class="is-admin" v-if="user.isAdmin">admin</span> - <span class="is-moderator" v-if="user.isModerator">moderator</span> - <span class="is-silenced" v-if="user.isSilenced" :title="$t('@.silenced-user')"><fa :icon="faMicrophoneSlash"/></span> - <span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span> - </header> - <div> - <span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> - </div> - <div> - <span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/users.vue'), - props: ['user', 'click'], - data() { - return { - faSnowflake, faMicrophoneSlash - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.kofvwchc - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - cursor pointer - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - > .username - margin-left 8px - opacity 0.7 - - > .is-admin - > .is-moderator - flex-shrink 0 - align-self center - margin 0 0 0 .5em - padding 1px 6px - font-size 80% - border-radius 3px - background var(--noteHeaderAdminBg) - color var(--noteHeaderAdminFg) - - > .is-silenced - > .is-suspended - margin 0 0 0 .5em - color #4dabf7 - - &:hover - color var(--primaryForeground) - background var(--primary) - text-decoration none - border-radius 3px - - &:active - color var(--primaryForeground) - background var(--primaryDarken10) - border-radius 3px -</style> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue deleted file mode 100644 index 920bfc381e..0000000000 --- a/src/client/app/admin/views/users.vue +++ /dev/null @@ -1,366 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template> - <section class="fit-top"> - <ui-input class="target" v-model="target" type="text" @enter="showUser"> - <span>{{ $t('username-or-userid') }}</span> - </ui-input> - <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - - <div ref="user" class="user" v-if="user" :key="user.id"> - <x-user :user="user"/> - <div class="actions"> - <ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button> - <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> - <ui-horizon-group> - <ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button> - <ui-button @click="unsilenceUser">{{ $t('unmake-silence') }}</ui-button> - </ui-horizon-group> - <ui-horizon-group> - <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> - <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> - </ui-horizon-group> - <ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button> - <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> - </div> - </div> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faUsers"/> {{ $t('users.title') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('users.sort.title') }}</template> - <option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option> - <option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option> - <option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option> - <option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('users.state.title') }}</template> - <option value="all">{{ $t('users.state.all') }}</option> - <option value="available">{{ $t('users.state.available') }}</option> - <option value="admin">{{ $t('users.state.admin') }}</option> - <option value="moderator">{{ $t('users.state.moderator') }}</option> - <option value="silenced">{{ $t('users.state.silenced') }}</option> - <option value="suspended">{{ $t('users.state.suspended') }}</option> - </ui-select> - <ui-select v-model="origin"> - <template #label>{{ $t('users.origin.title') }}</template> - <option value="combined">{{ $t('users.origin.combined') }}</option> - <option value="local">{{ $t('users.origin.local') }}</option> - <option value="remote">{{ $t('users.origin.remote') }}</option> - </ui-select> - </ui-horizon-group> - <ui-horizon-group searchboxes> - <ui-input v-model="searchUsername" type="text" spellcheck="false" @input="fetchUsers(true)"> - <span>{{ $t('username') }}</span> - </ui-input> - <ui-input v-model="searchHost" type="text" spellcheck="false" @input="fetchUsers(true)" :disabled="origin === 'local'"> - <span>{{ $t('host') }}</span> - </ui-input> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <x-user v-for="user in users" :key="user.id" :user='user' :click="showUserOnClick"/> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import parseAcct from "../../../../misc/acct/parse"; -import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import XUser from './users.user.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/users.vue'), - components: { - XUser - }, - data() { - return { - user: null, - target: null, - suspending: false, - unsuspending: false, - sort: '+createdAt', - state: 'all', - origin: 'local', - searchUsername: '', - searchHost: '', - limit: 10, - offset: 0, - users: [], - existMore: false, - faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt - }; - }, - - watch: { - sort() { - this.users = []; - this.offset = 0; - this.fetchUsers(); - }, - - state() { - this.users = []; - this.offset = 0; - this.fetchUsers(); - }, - - origin() { - if (this.origin === 'local') this.searchHost = ''; - this.users = []; - this.offset = 0; - this.fetchUsers(); - } - }, - - mounted() { - this.fetchUsers(); - }, - - methods: { - /** テキストエリアのユーザーを解決する */ - fetchUser() { - return new Promise((res) => { - const usernamePromise = this.$root.api('users/show', parseAcct(this.target)); - const idPromise = this.$root.api('users/show', { userId: this.target }); - - let _notFound = false; - const notFound = () => { - if (_notFound) { - this.$root.dialog({ - type: 'error', - text: this.$t('user-not-found') - }); - } else { - _notFound = true; - } - }; - - usernamePromise.then(res).catch(e => { - if (e == 'user not found') { - notFound(); - } - }); - idPromise.then(res).catch(e => { - notFound(); - }); - }); - }, - - /** テキストエリアから処理対象ユーザーを設定する */ - async showUser() { - this.user = null; - const user = await this.fetchUser(); - this.$root.api('admin/show-user', { userId: user.id }).then(info => { - this.user = info; - }); - this.target = ''; - }, - - async showUserOnClick(userId: string) { - this.$root.api('admin/show-user', { userId: userId }).then(info => { - this.user = info; - this.$nextTick(() => { - this.$refs.user.scrollIntoView(); - }); - }); - }, - - /** 処理対象ユーザーの情報を更新する */ - async refreshUser() { - this.$root.api('admin/show-user', { userId: this.user.id }).then(info => { - this.user = info; - }); - }, - - async resetPassword() { - if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; - - this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => { - this.$root.dialog({ - type: 'success', - text: this.$t('password-updated', { password: res.password }) - }); - }); - }, - - async silenceUser() { - if (!await this.getConfirmed(this.$t('silence-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/silence-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.refreshUser(); - }, - - async unsilenceUser() { - if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/unsilence-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.refreshUser(); - }, - - async suspendUser() { - if (!await this.getConfirmed(this.$t('suspend-confirm'))) return; - - this.suspending = true; - - const process = async () => { - await this.$root.api('admin/suspend-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('suspended') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.suspending = false; - - this.refreshUser(); - }, - - async unsuspendUser() { - if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return; - - this.unsuspending = true; - - const process = async () => { - await this.$root.api('admin/unsuspend-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('unsuspended') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.unsuspending = false; - - this.refreshUser(); - }, - - async updateRemoteUser() { - this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { - this.$root.dialog({ - type: 'success', - text: this.$t('remote-user-updated') - }); - }); - - this.refreshUser(); - }, - - async deleteAllFiles() { - if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - async getConfirmed(text: string): Promise<Boolean> { - const confirm = await this.$root.dialog({ - type: 'warning', - showCancelButton: true, - title: 'confirm', - text, - }); - - return !confirm.canceled; - }, - - fetchUsers(truncate?: boolean) { - if (truncate) this.offset = 0; - this.$root.api('admin/show-users', { - state: this.state, - origin: this.origin, - sort: this.sort, - offset: this.offset, - limit: this.limit + 1, - username: this.searchUsername, - hostname: this.searchHost - }).then(users => { - if (users.length == this.limit + 1) { - users.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.users = truncate ? users : this.users.concat(users); - this.offset += this.limit; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.target - margin-bottom 16px !important - -.user - margin-top 32px - - > .actions - margin-left 80px -</style> diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl deleted file mode 100644 index 6c4d5b8b6f..0000000000 --- a/src/client/app/animation.styl +++ /dev/null @@ -1,47 +0,0 @@ -.zoom-in-top-enter-active, -.zoom-in-top-leave-active { - opacity: 1; - transform: scaleY(1); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); - transform-origin: center top; -} -.zoom-in-top-enter, -.zoom-in-top-leave-active { - opacity: 0; - transform: scaleY(0); -} - -.entranceFromTop { - animation-duration: 0.5s; - animation-name: entranceFromTop; -} - -@keyframes entranceFromTop { - from { - opacity: 0; - transform: translateY(-64px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes jump { - 0% { transform: translateY(0); } - 25% { transform: translateY(-16px); } - 50% { transform: translateY(0); } - 75% { transform: translateY(-8px); } - 100% { transform: translateY(0); } -} - -@keyframes blink { - 0% { opacity: 1; } - 30% { opacity: 1; } - 90% { opacity: 0; } -} diff --git a/src/client/app/app.styl b/src/client/app/app.styl deleted file mode 100644 index 6389aa0a87..0000000000 --- a/src/client/app/app.styl +++ /dev/null @@ -1,84 +0,0 @@ -@import "../style" -@import "../animation" - -html - &.progress - &, * - cursor progress !important - -html - // iOSのため - overflow auto - -body - overflow-wrap break-word - -#nprogress - pointer-events none - - position absolute - z-index 65536 - - .bar - background var(--primary) - - position fixed - z-index 65537 - top 0 - left 0 - - width 100% - height 2px - - /* Fancy blur effect */ - .peg - display block - position absolute - right 0 - width 100px - height 100% - box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary) - opacity 1 - - transform rotate(3deg) translate(0px, -4px) - -#wait - display block - position fixed - z-index 65537 - top 15px - right 15px - - &:before - content "" - display block - width 18px - height 18px - box-sizing border-box - - border solid 2px transparent - border-top-color var(--primary) - border-left-color var(--primary) - border-radius 50% - - animation progress-spinner 400ms linear infinite - - @keyframes progress-spinner - 0% - transform rotate(0deg) - 100% - transform rotate(360deg) - -code - font-family Consolas, 'Courier New', Courier, Monaco, monospace - -pre - display block - - > code - display block - overflow auto - tab-size 2 - -[data-icon] - display inline-block diff --git a/src/client/app/app.vue b/src/client/app/app.vue deleted file mode 100644 index e639c9f9ac..0000000000 --- a/src/client/app/app.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<router-view id="app" v-hotkey.global="keymap"></router-view> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url, lang } from './config'; - -export default Vue.extend({ - computed: { - keymap(): any { - return { - 'h|slash': this.help, - 'd': this.dark - }; - } - }, - - methods: { - help() { - window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank'); - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - } - } -}); -</script> diff --git a/src/client/app/auth/assets/icon.svg b/src/client/app/auth/assets/icon.svg deleted file mode 100644 index 36f5d3e404..0000000000 --- a/src/client/app/auth/assets/icon.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 512" width="512" height="512"><defs><clipPath id="_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns"><rect width="512" height="512"/></clipPath></defs><g clip-path="url(#_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns)"><clipPath id="_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom"><rect x="0" y="0" width="512" height="512" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom)"><g id="Group"><g id="g4502"><g id="g5125"><g id="text4489"><path d=" M 190.093 359.243 C 167.923 359.32 148.881 345.963 139.9 330.409 C 135.104 323.615 125.617 321.198 125.482 330.409 L 125.482 372.939 C 125.482 390.026 119.253 404.799 106.794 417.258 C 94.69 429.362 79.917 435.413 62.474 435.413 C 45.387 435.413 30.614 429.362 18.155 417.258 C 6.052 404.799 0 390.026 0 372.939 L 0 139.061 C 0 125.89 3.738 113.965 11.213 103.285 C 19.045 92.25 29.012 84.596 41.116 80.325 C 47.879 77.833 54.999 76.587 62.474 76.587 C 81.697 76.587 97.716 84.062 110.531 99.013 C 117.295 106.489 121.211 110.405 122.279 110.761 C 122.279 110.761 173.043 172.145 174.467 173.213 C 175.891 174.281 180.073 182.446 190.093 182.446 C 200.112 182.446 204.829 174.281 206.253 173.213 C 207.676 172.145 258.44 110.761 258.44 110.761 C 258.796 111.117 262.534 107.201 269.654 99.013 C 282.825 84.062 299.022 76.587 318.245 76.587 C 325.364 76.587 332.484 77.833 339.603 80.325 C 351.707 84.596 361.496 92.25 368.972 103.285 C 376.803 113.965 380.719 125.89 380.719 139.061 L 380.719 372.939 C 380.719 390.026 374.489 404.799 362.03 417.258 C 349.927 429.362 335.154 435.413 317.711 435.413 C 300.624 435.413 285.851 429.362 273.391 417.258 C 261.288 404.799 255.237 390.026 255.237 372.939 L 255.237 330.409 C 254.184 318.802 243.925 326.116 240.285 330.409 C 230.674 348.208 212.262 359.167 190.093 359.243 Z M 457.535 184.448 Q 435.109 184.448 419.09 168.963 Q 403.605 152.944 403.605 130.518 Q 403.605 108.091 419.09 92.606 Q 435.109 76.587 457.535 76.587 Q 479.962 76.587 495.981 92.606 Q 512 108.091 512 130.518 Q 512 152.944 495.981 168.963 Q 479.962 184.448 457.535 184.448 Z M 458.069 195.128 Q 480.496 195.128 495.981 211.147 Q 512 227.166 512 249.592 L 512 381.482 Q 512 403.909 495.981 419.928 Q 480.496 435.413 458.069 435.413 Q 435.643 435.413 419.624 419.928 Q 403.605 403.909 403.605 381.482 L 403.605 249.592 Q 403.605 227.166 419.624 211.147 Q 435.643 195.128 458.069 195.128 Z " fill-rule="evenodd" fill="rgb(157,157,157)"/></g></g></g></g></g></g></svg>
\ No newline at end of file diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts deleted file mode 100644 index 91bb24b108..0000000000 --- a/src/client/app/auth/script.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Authorize Form - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import Index from './views/index.vue'; -import NotFound from '../common/views/pages/not-found.vue'; - -/** - * init - */ -init(launch => { - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/auth/', - routes: [ - { path: '/:token', component: Index }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/auth/style.styl b/src/client/app/auth/style.styl deleted file mode 100644 index bd25e1b572..0000000000 --- a/src/client/app/auth/style.styl +++ /dev/null @@ -1,15 +0,0 @@ -@import "../app" -@import "../reset" - -html - background #eee - - @media (max-width 600px) - background #fff - -body - margin 0 - padding 32px 0 - - @media (max-width 600px) - padding 0 diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue deleted file mode 100644 index 064dbf3887..0000000000 --- a/src/client/app/auth/views/form.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="form"> - <header> - <h1 v-html="$t('share-access', { name })"></h1> - <img :src="app.iconUrl"/> - </header> - <div class="app"> - <section> - <h2>{{ app.name }}</h2> - <p class="id">{{ app.id }}</p> - <p class="description">{{ app.description }}</p> - </section> - <section> - <h2>{{ $t('permission-ask') }}</h2> - <ul> - <template v-for="p in app.permission"> - <li :key="p">{{ $t(`@.permissions.${p}`) }}</li> - </template> - </ul> - </section> - </div> - <div class="action"> - <button @click="cancel">{{ $t('cancel') }}</button> - <button @click="accept">{{ $t('accept') }}</button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; - -export default Vue.extend({ - i18n: i18n('auth/views/form.vue'), - props: ['session'], - computed: { - name(): string { - const el = document.createElement('div'); - el.textContent = this.app.name - return el.innerHTML; - }, - app(): any { - return this.session.app; - } - }, - methods: { - cancel() { - this.$root.api('auth/deny', { - token: this.session.token - }).then(() => { - this.$emit('denied'); - }); - }, - - accept() { - this.$root.api('auth/accept', { - token: this.session.token - }).then(() => { - this.$emit('accepted'); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.form - - > header - > h1 - margin 0 - padding 32px 32px 20px 32px - font-size 24px - font-weight normal - color #777 - - i - color #77aeca - - &:before - content '「' - - &:after - content '」' - - b - color #666 - - > img - display block - z-index 1 - width 84px - height 84px - margin 0 auto -38px auto - border solid 5px #fff - border-radius 100% - box-shadow 0 2px 2px rgba(#000, 0.1) - - > .app - padding 44px 16px 0 16px - color #555 - background #eee - box-shadow 0 2px 2px rgba(#000, 0.1) inset - - &:after - content '' - display block - clear both - - > section - float left - width 50% - padding 8px - text-align left - - > h2 - margin 0 - font-size 16px - color #777 - - > .action - padding 16px - - > button - margin 0 8px - padding 0 - - @media (max-width 600px) - > header - > img - box-shadow none - - > .app - box-shadow none - - @media (max-width 500px) - > header - > h1 - font-size 16px - -</style> diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue deleted file mode 100644 index ad9b1e4e35..0000000000 --- a/src/client/app/auth/views/index.vue +++ /dev/null @@ -1,153 +0,0 @@ -<template> -<div class="index"> - <main v-if="$store.getters.isSignedIn"> - <p class="fetching" v-if="fetching">{{ $t('loading') }}<mk-ellipsis/></p> - <x-form - class="form" - ref="form" - v-if="state == 'waiting'" - :session="session" - @denied="state = 'denied'" - @accepted="accepted" - /> - <div class="denied" v-if="state == 'denied'"> - <h1>{{ $t('denied') }}</h1> - <p>{{ $t('denied-paragraph') }}</p> - </div> - <div class="accepted" v-if="state == 'accepted'"> - <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> - <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p> - <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> - </div> - <div class="error" v-if="state == 'fetch-session-error'"> - <p>{{ $t('error') }}</p> - </div> - </main> - <main class="signin" v-if="!$store.getters.isSignedIn"> - <h1>{{ $t('sign-in') }}</h1> - <mk-signin/> - </main> - <footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import XForm from './form.vue'; - -export default Vue.extend({ - i18n: i18n('auth/views/index.vue'), - components: { - XForm - }, - data() { - return { - state: null, - session: null, - fetching: true - }; - }, - computed: { - token(): string { - return this.$route.params.token; - } - }, - mounted() { - if (!this.$store.getters.isSignedIn) return; - - // Fetch session - this.$root.api('auth/session/show', { - token: this.token - }).then(session => { - this.session = session; - this.fetching = false; - - // 既に連携していた場合 - if (this.session.app.isAuthorized) { - this.$root.api('auth/accept', { - token: this.session.token - }).then(() => { - this.accepted(); - }); - } else { - this.state = 'waiting'; - } - }).catch(error => { - this.state = 'fetch-session-error'; - this.fetching = false; - }); - }, - methods: { - accepted() { - this.state = 'accepted'; - if (this.session.app.callbackUrl) { - location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.index - - > main - width 100% - max-width 500px - margin 0 auto - text-align center - background #fff - box-shadow 0 4px 16px rgba(#000, 0.2) - - > .fetching - margin 0 - padding 32px - color #555 - - > div:not(.form) - padding 64px - - > h1 - margin 0 0 8px 0 - padding 0 - font-size 20px - font-weight normal - - > p - margin 0 - color #555 - - &.denied > h1 - color #e65050 - - &.accepted > h1 - color #54af7c - - &.signin - padding 32px 32px 16px 32px - - > h1 - margin 0 0 22px 0 - padding 0 - font-size 20px - font-weight normal - color #555 - - @media (max-width 600px) - max-width none - box-shadow none - - @media (max-width 500px) - > div - > h1 - font-size 16px - - > footer - > img - display block - width 32px - height 32px - margin 16px auto - -</style> diff --git a/src/client/app/boot.js b/src/client/app/boot.js deleted file mode 100644 index 64d4629883..0000000000 --- a/src/client/app/boot.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * MISSKEY BOOT LOADER - * (ENTRY POINT) - */ - -'use strict'; - -(async function() { - // キャッシュ削除要求があれば従う - if (localStorage.getItem('shouldFlush') == 'true') { - refresh(); - return; - } - - const langs = LANGS; - - //#region Apply theme - const theme = localStorage.getItem('theme'); - if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - } - //#endregion - - //#region Load settings - let settings = null; - const vuex = localStorage.getItem('vuex'); - if (vuex) { - settings = JSON.parse(vuex); - } - //#endregion - - // Get the current url information - const url = new URL(location.href); - - //#region Detect app name - let app = null; - - if (`${url.pathname}/`.startsWith('/docs/')) app = 'docs'; - if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev'; - if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth'; - if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin'; - //#endregion - - // Script version - const ver = localStorage.getItem('v') || VERSION; - - //#region Detect the user language - let lang = null; - - if (langs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = langs.find(x => x.split('-')[0] == navigator.language); - - if (lang == null) { - // Fallback - lang = 'en-US'; - } - } - - if (settings && settings.device.lang && - langs.includes(settings.device.lang)) - { - lang = settings.device.lang; - } - - localStorage.setItem('lang', lang); - //#endregion - - //#region Fetch locale data - const cachedLocale = localStorage.getItem('locale'); - const localeKey = localStorage.getItem('localeKey'); - let localeData = null; - - if (cachedLocale == null || localeKey != `${ver}.${lang}`) { - const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`) - .then(response => response.json()); - localeData = locale; - - localStorage.setItem('locale', JSON.stringify(locale)); - localStorage.setItem('localeKey', `${ver}.${lang}`); - } else { - localeData = JSON.parse(cachedLocale); - } - //#endregion - - // Detect the user agent - const ua = navigator.userAgent.toLowerCase(); - let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576; - if (settings && settings.device.appTypeForce) { - if (settings.device.appTypeForce === 'mobile') { - isMobile = true; - } else if (settings.device.appTypeForce === 'desktop') { - isMobile = false; - } - } - - // Get the <head> element - const head = document.getElementsByTagName('head')[0]; - - // If mobile, insert the viewport meta tag - if (isMobile) { - const viewport = document.getElementsByName("viewport").item(0); - viewport.content = `${viewport.content},minimum-scale=1,maximum-scale=1,user-scalable=no`; - head.appendChild(viewport); - } - - // Switch desktop or mobile version - if (app == null) { - app = isMobile ? 'mobile' : 'desktop'; - } - - // Load an app script - // Note: 'async' make it possible to load the script asyncly. - // 'defer' make it possible to run the script when the dom loaded. - const script = document.createElement('script'); - script.src = `/assets/${app}.${ver}.js`; - script.async = true; - script.defer = true; - head.appendChild(script); - - // 3秒経ってもスクリプトがロードされない場合はバージョンが古くて - // 404になっているせいかもしれないので、バージョンを確認して古ければ更新する - // - // 読み込まれたスクリプトからこのタイマーを解除できるように、 - // グローバルにタイマーIDを代入しておく - window.mkBootTimer = window.setTimeout(async () => { - // Fetch meta - const res = await fetch('/api/meta', { - method: 'POST', - cache: 'no-cache' - }); - - // Parse - const meta = await res.json(); - - // Compare versions - if (meta.version != ver) { - localStorage.setItem('v', meta.version); - - alert( - localeData.common._settings["update-available"] + - '\n' + - localeData.common._settings["update-available-desc"] - ); - refresh(); - } - }, 3000); - - function refresh() { - localStorage.setItem('shouldFlush', 'false'); - - localStorage.removeItem('locale'); - - // Clear cache (service worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - for (const registration of registrations) registration.unregister(); - }); - } catch (e) { - console.error(e); - } - - // Force reload - location.reload(true); - } -})(); diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts deleted file mode 100644 index ba4deafe3a..0000000000 --- a/src/client/app/common/define-widget.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Vue from 'vue'; - -export default function <T extends object>(data: { - name: string; - props?: () => T; -}) { - return Vue.extend({ - props: { - widget: { - type: Object - }, - column: { - type: Object, - default: null - }, - platform: { - type: String, - required: true - }, - 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() { - if (this.platform == 'deck') { - this.$store.commit('updateDeckColumn', this.column); - } else { - this.$store.commit('updateWidget', this.widget); - } - } - } - }); -} diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts deleted file mode 100644 index a53d3f479e..0000000000 --- a/src/client/app/common/hotkey.ts +++ /dev/null @@ -1,106 +0,0 @@ -import keyCode from './keycode'; -import { concat } from '../../../prelude/array'; - -type pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type action = { - patterns: pattern[]; - - callback: Function; -}; - -const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => { - const result = { - patterns: [], - callback: callback - } as action; - - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false - } as pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); - } - } - - return pattern; - }); - - return result; -}); - -const ignoreElemens = ['input', 'textarea']; - -function match(e: KeyboardEvent, patterns: action['patterns']): boolean { - const key = e.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl == e.ctrlKey && - pattern.shift == e.shiftKey && - pattern.alt == e.altKey && - !e.metaKey - ); -} - -export default { - install(Vue) { - Vue.directive('hotkey', { - bind(el, binding) { - el._hotkey_global = binding.modifiers.global === true; - - const actions = getKeyMap(binding.value); - - // flatten - const reservedKeys = concat(actions.map(a => a.patterns)); - - el._misskey_reservedKeys = reservedKeys; - - el._keyHandler = (e: KeyboardEvent) => { - const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; - if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; - - for (const action of actions) { - const matched = match(e, action.patterns); - - if (matched) { - if (el._hotkey_global && match(e, targetReservedKeys)) return; - - e.preventDefault(); - e.stopPropagation(); - action.callback(e); - break; - } - } - }; - - if (el._hotkey_global) { - document.addEventListener('keydown', el._keyHandler); - } else { - el.addEventListener('keydown', el._keyHandler); - } - }, - - unbind(el) { - if (el._hotkey_global) { - document.removeEventListener('keydown', el._keyHandler); - } else { - el.removeEventListener('keydown', el._keyHandler); - } - } - }); - } -}; diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts deleted file mode 100644 index 5786c1dc0a..0000000000 --- a/src/client/app/common/keycode.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default (input: string): string[] => { - if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) { - const codes = aliases[input]; - return Array.isArray(codes) ? codes : [codes]; - } else { - return [input]; - } -}; - -export const aliases = { - 'esc': 'Escape', - 'enter': ['Enter', 'NumpadEnter'], - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['NumpadAdd', 'Semicolon'], -}; - -/*! -* Programatically add the following -*/ - -// lower case chars -for (let i = 97; i < 123; i++) { - const char = String.fromCharCode(i); - aliases[char] = `Key${char.toUpperCase()}`; -} - -// numbers -for (let i = 0; i < 10; i++) { - aliases[i] = [`Numpad${i}`, `Digit${i}`]; -} diff --git a/src/client/app/common/scripts/2fa.ts b/src/client/app/common/scripts/2fa.ts deleted file mode 100644 index f638cce156..0000000000 --- a/src/client/app/common/scripts/2fa.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function hexifyAB(buffer) { - return Array.from(new Uint8Array(buffer)) - .map(item => item.toString(16).padStart(2, 0)) - .join(''); -} diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts deleted file mode 100644 index d487915766..0000000000 --- a/src/client/app/common/scripts/check-for-update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { version as current } from '../../config'; - -export default async function($root: any, force = false, silent = false) { - const meta = await $root.getMeta(force); - const newer = meta.version; - - if (newer != current) { - localStorage.setItem('should-refresh', 'true'); - localStorage.setItem('v', newer); - - // Clear cache (service worker) - try { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - } - - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - registration.unregister(); - } - } catch (e) { - console.error(e); - } - - /*if (!silent) { - $root.dialog({ - title: $root.$t('@.update-available-title'), - text: $root.$t('@.update-available', { newer, current }) - }); - }*/ - - return newer; - } else { - return null; - } -} diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/app/common/scripts/collect-page-vars.ts deleted file mode 100644 index a4096fb2c2..0000000000 --- a/src/client/app/common/scripts/collect-page-vars.ts +++ /dev/null @@ -1,48 +0,0 @@ -export function collectPageVars(content) { - const pageVars = []; - const collect = (xs: any[]) => { - for (const x of xs) { - if (x.type === 'textInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.type === 'textareaInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.type === 'numberInput') { - pageVars.push({ - name: x.name, - type: 'number', - value: x.default || 0 - }); - } else if (x.type === 'switch') { - pageVars.push({ - name: x.name, - type: 'boolean', - value: x.default || false - }); - } else if (x.type === 'counter') { - pageVars.push({ - name: x.name, - type: 'number', - value: 0 - }); - } else if (x.type === 'radioButton') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.children) { - collect(x.children); - } - } - }; - collect(content); - return pageVars; -} diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts deleted file mode 100644 index ec854f2f4d..0000000000 --- a/src/client/app/common/scripts/compose-notification.ts +++ /dev/null @@ -1,74 +0,0 @@ -import getNoteSummary from '../../../../misc/get-note-summary'; -import getReactionEmoji from '../../../../misc/get-reaction-emoji'; -import getUserName from '../../../../misc/get-user-name'; - -type Notification = { - title: string; - body: string; - icon: string; - onclick?: any; -}; - -// TODO: i18n - -export default function(type, data): Notification { - switch (type) { - case 'driveFileCreated': - return { - title: 'File uploaded', - body: data.name, - icon: data.url - }; - - case 'unreadMessagingMessage': - return { - title: `New message from ${getUserName(data.user)}`, - body: data.text, // TODO: getMessagingMessageSummary(data), - icon: data.user.avatarUrl - }; - - case 'reversiInvited': - return { - title: 'Play reversi with me', - body: `You got reversi invitation from ${getUserName(data.parent)}`, - icon: data.parent.avatarUrl - }; - - case 'notification': - switch (data.type) { - case 'mention': - return { - title: `${getUserName(data.user)}:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl - }; - - case 'reply': - return { - title: `You got reply from ${getUserName(data.user)}:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl - }; - - case 'quote': - return { - title: `${getUserName(data.user)}:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl - }; - - case 'reaction': - return { - title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, - body: getNoteSummary(data.note), - icon: data.user.avatarUrl - }; - - default: - return null; - } - - default: - return null; - } -} diff --git a/src/client/app/common/scripts/contains.ts b/src/client/app/common/scripts/contains.ts deleted file mode 100644 index a5071b3f25..0000000000 --- a/src/client/app/common/scripts/contains.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default (parent, child) => { - let node = child.parentNode; - while (node) { - if (node == parent) return true; - node = node.parentNode; - } - return false; -}; diff --git a/src/client/app/common/scripts/copy-to-clipboard.ts b/src/client/app/common/scripts/copy-to-clipboard.ts deleted file mode 100644 index ab13cab970..0000000000 --- a/src/client/app/common/scripts/copy-to-clipboard.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Clipboardに値をコピー(TODO: 文字列以外も対応) - */ -export default val => { - // 空div 生成 - const tmp = document.createElement('div'); - // 選択用のタグ生成 - const pre = document.createElement('pre'); - - // 親要素のCSSで user-select: none だとコピーできないので書き換える - pre.style.webkitUserSelect = 'auto'; - pre.style.userSelect = 'auto'; - - tmp.appendChild(pre).textContent = val; - - // 要素を画面外へ - const s = tmp.style; - s.position = 'fixed'; - s.right = '200%'; - - // body に追加 - document.body.appendChild(tmp); - // 要素を選択 - document.getSelection().selectAllChildren(tmp); - - // クリップボードにコピー - const result = document.execCommand('copy'); - - // 要素削除 - document.body.removeChild(tmp); - - return result; -}; diff --git a/src/client/app/common/scripts/format-uptime.ts b/src/client/app/common/scripts/format-uptime.ts deleted file mode 100644 index 6550e4cc39..0000000000 --- a/src/client/app/common/scripts/format-uptime.ts +++ /dev/null @@ -1,25 +0,0 @@ - -/** - * Format like the uptime command - */ -export default function(sec) { - if (!sec) return sec; - - const day = Math.floor(sec / 86400); - const tod = sec % 86400; - - // Days part in string: 2 days, 1 day, null - const d = day >= 2 ? `${day} days` : day >= 1 ? `${day} day` : null; - - // Time part in string: 1 sec, 1 min, 1:01 - const t - = tod < 60 ? `${Math.floor(tod)} sec` - : tod < 3600 ? `${Math.floor(tod / 60)} min` - : `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, '0')}`; - - let str = ''; - if (d) str += `${d}, `; - str += t; - - return str; -} diff --git a/src/client/app/common/scripts/gen-search-query.ts b/src/client/app/common/scripts/gen-search-query.ts deleted file mode 100644 index fc26cb7f78..0000000000 --- a/src/client/app/common/scripts/gen-search-query.ts +++ /dev/null @@ -1,31 +0,0 @@ -import parseAcct from '../../../../misc/acct/parse'; -import { host as localHost } from '../../config'; - -export async function genSearchQuery(v: any, q: string) { - let host: string; - let userId: string; - if (q.split(' ').some(x => x.startsWith('@'))) { - for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { - if (at.includes('.')) { - if (at === localHost || at === '.') { - host = null; - } else { - host = at; - } - } else { - const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null); - if (user) { - userId = user.id; - } else { - // todo: show error - } - } - } - - } - return { - query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), - host: host, - userId: userId - }; -} diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts deleted file mode 100644 index 19f2bdb064..0000000000 --- a/src/client/app/common/scripts/get-face.ts +++ /dev/null @@ -1,11 +0,0 @@ -const faces = [ - '(=^・・^=)', - 'v(\'ω\')v', - '🐡( \'-\' 🐡 )フグパンチ!!!!', - '✌️(´・_・`)✌️', - '(。>﹏<。)', - '(Δ・x・Δ)', - '(コ`・ヘ・´ケ)' -]; - -export default () => faces[Math.floor(Math.random() * faces.length)]; diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/app/common/scripts/get-md5.ts deleted file mode 100644 index b002d762b1..0000000000 --- a/src/client/app/common/scripts/get-md5.ts +++ /dev/null @@ -1,10 +0,0 @@ -// スクリプトサイズがデカい -//import * as crypto from 'crypto'; - -export default (data: ArrayBuffer) => { - //const buf = new Buffer(data); - //const hash = crypto.createHash('md5'); - //hash.update(buf); - //return hash.digest('hex'); - return ''; -}; diff --git a/src/client/app/common/scripts/get-static-image-url.ts b/src/client/app/common/scripts/get-static-image-url.ts deleted file mode 100644 index 7460ca38f2..0000000000 --- a/src/client/app/common/scripts/get-static-image-url.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { url as instanceUrl } from '../../config'; -import * as url from '../../../../prelude/url'; - -export function getStaticImageUrl(baseUrl: string): string { - const u = new URL(baseUrl); - const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので - return `${instanceUrl}/proxy/${dummy}?${url.query({ - url: u.href, - static: '1' - })}`; -} diff --git a/src/client/app/common/scripts/loading.ts b/src/client/app/common/scripts/loading.ts deleted file mode 100644 index 70a3a4c85e..0000000000 --- a/src/client/app/common/scripts/loading.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as NProgress from 'nprogress'; -NProgress.configure({ - trickleSpeed: 500, - showSpinner: false -}); - -const root = document.getElementsByTagName('html')[0]; - -export default { - start: () => { - root.classList.add('progress'); - NProgress.start(); - }, - done: () => { - root.classList.remove('progress'); - NProgress.done(); - }, - set: val => { - NProgress.set(val); - } -}; diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts deleted file mode 100644 index 84e134cc32..0000000000 --- a/src/client/app/common/scripts/note-mixin.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { parse } from '../../../../mfm/parse'; -import { sum, unique } from '../../../../prelude/array'; -import shouldMuteNote from './should-mute-note'; -import MkNoteMenu from '../views/components/note-menu.vue'; -import MkReactionPicker from '../views/components/reaction-picker.vue'; -import pleaseLogin from './please-login'; -import i18n from '../../i18n'; - -function focus(el, fn) { - const target = fn(el); - if (target) { - if (target.hasAttribute('tabindex')) { - target.focus(); - } else { - focus(target, fn); - } - } -} - -type Opts = { - mobile?: boolean; -}; - -export default (opts: Opts = {}) => ({ - i18n: i18n(), - - data() { - return { - showContent: false, - hideThisNote: false, - openingMenu: false - }; - }, - - computed: { - keymap(): any { - return { - 'r': () => this.reply(true), - 'e|a|plus': () => this.react(true), - 'q': () => this.renote(true), - 'f|b': this.favorite, - 'delete|ctrl+d': this.del, - 'ctrl+q': this.renoteDirectly, - 'up|k|shift+tab': this.focusBefore, - 'down|j|tab': this.focusAfter, - //'esc': this.blur, - 'm|o': () => this.menu(true), - 's': this.toggleShowContent, - '1': () => this.reactDirectly('like'), - '2': () => this.reactDirectly('love'), - '3': () => this.reactDirectly('laugh'), - '4': () => this.reactDirectly('hmm'), - '5': () => this.reactDirectly('surprise'), - '6': () => this.reactDirectly('congrats'), - '7': () => this.reactDirectly('angry'), - '8': () => this.reactDirectly('confused'), - '9': () => this.reactDirectly('rip'), - '0': () => this.reactDirectly('pudding'), - }; - }, - - isRenote(): boolean { - return (this.note.renote && - this.note.text == null && - this.note.fileIds.length == 0 && - this.note.poll == null); - }, - - appearNote(): any { - return this.isRenote ? this.note.renote : this.note; - }, - - isMyNote(): boolean { - return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); - }, - - reactionsCount(): number { - return this.appearNote.reactions - ? sum(Object.values(this.appearNote.reactions)) - : 0; - }, - - title(): string { - return ''; - }, - - urls(): string[] { - if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); - } else { - return null; - } - } - }, - - created() { - this.hideThisNote = shouldMuteNote(this.$store.state.i, this.$store.state.settings, this.appearNote); - }, - - methods: { - reply(viaKeyboard = false) { - pleaseLogin(this.$root); - this.$root.$post({ - reply: this.appearNote, - animation: !viaKeyboard, - cb: () => { - this.focus(); - } - }); - }, - - renote(viaKeyboard = false) { - pleaseLogin(this.$root); - this.$root.$post({ - renote: this.appearNote, - animation: !viaKeyboard, - cb: () => { - this.focus(); - } - }); - }, - - renoteDirectly() { - (this as any).api('notes/create', { - renoteId: this.appearNote.id - }); - }, - - react(viaKeyboard = false) { - pleaseLogin(this.$root); - this.blur(); - const w = this.$root.new(MkReactionPicker, { - source: this.$refs.reactButton, - showFocus: viaKeyboard, - animation: !viaKeyboard - }); - w.$once('chosen', reaction => { - this.$root.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }).then(() => { - w.close(); - }); - }); - w.$once('closed', this.focus); - }, - - reactDirectly(reaction) { - this.$root.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, - - undoReact(note) { - const oldReaction = note.myReaction; - if (!oldReaction) return; - this.$root.api('notes/reactions/delete', { - noteId: note.id - }); - }, - - favorite() { - pleaseLogin(this.$root); - this.$root.api('notes/favorites/create', { - noteId: this.appearNote.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.delete-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('notes/delete', { - noteId: this.appearNote.id - }); - }); - }, - - menu(viaKeyboard = false) { - if (this.openingMenu) return; - this.openingMenu = true; - const w = this.$root.new(MkNoteMenu, { - source: this.$refs.menuButton, - note: this.appearNote, - animation: !viaKeyboard - }).$once('closed', () => { - this.openingMenu = false; - this.focus(); - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - }, - - toggleShowContent() { - this.showContent = !this.showContent; - }, - - focus() { - this.$el.focus(); - }, - - blur() { - this.$el.blur(); - }, - - focusBefore() { - focus(this.$el, e => e.previousElementSibling); - }, - - focusAfter() { - focus(this.$el, e => e.nextElementSibling); - } - } -}); diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts deleted file mode 100644 index 5b31a9f9d0..0000000000 --- a/src/client/app/common/scripts/note-subscriber.ts +++ /dev/null @@ -1,149 +0,0 @@ -import Vue from 'vue'; - -export default prop => ({ - data() { - return { - connection: null - }; - }, - - computed: { - $_ns_note_(): any { - return this[prop]; - }, - - $_ns_isRenote(): boolean { - return (this.$_ns_note_.renote != null && - this.$_ns_note_.text == null && - this.$_ns_note_.fileIds.length == 0 && - this.$_ns_note_.poll == null); - }, - - $_ns_target(): any { - return this.$_ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_; - }, - }, - - created() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream; - } - }, - - mounted() { - this.capture(true); - - if (this.$store.getters.isSignedIn) { - this.connection.on('_connected_', this.onStreamConnected); - } - }, - - beforeDestroy() { - this.decapture(true); - - if (this.$store.getters.isSignedIn) { - this.connection.off('_connected_', this.onStreamConnected); - } - }, - - methods: { - capture(withHandler = false) { - if (this.$store.getters.isSignedIn) { - const data = { - id: this.$_ns_target.id - } as any; - - if ( - (this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) || - (this.$_ns_target.mentions || []).includes(this.$store.state.i.id) - ) { - data.read = true; - } - - this.connection.send('sn', data); - if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); - } - }, - - decapture(withHandler = false) { - if (this.$store.getters.isSignedIn) { - this.connection.send('un', { - id: this.$_ns_target.id - }); - if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); - } - }, - - onStreamConnected() { - this.capture(); - }, - - onStreamNoteUpdated(data) { - const { type, id, body } = data; - - if (id !== this.$_ns_target.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - if (this.$_ns_target.reactions == null) { - Vue.set(this.$_ns_target, 'reactions', {}); - } - - if (this.$_ns_target.reactions[reaction] == null) { - Vue.set(this.$_ns_target.reactions, reaction, 0); - } - - // Increment the count - this.$_ns_target.reactions[reaction]++; - - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target, 'myReaction', reaction); - } - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - if (this.$_ns_target.reactions == null) { - return; - } - - if (this.$_ns_target.reactions[reaction] == null) { - return; - } - - // Decrement the count - if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--; - - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target, 'myReaction', null); - } - break; - } - - case 'pollVoted': { - const choice = body.choice; - this.$_ns_target.poll.choices[choice].votes++; - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true); - } - break; - } - - case 'deleted': { - Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt); - Vue.set(this.$_ns_target, 'renote', null); - this.$_ns_target.text = null; - this.$_ns_target.fileIds = []; - this.$_ns_target.poll = null; - this.$_ns_target.geo = null; - this.$_ns_target.cw = null; - break; - } - } - }, - } -}); diff --git a/src/client/app/common/scripts/paging.ts b/src/client/app/common/scripts/paging.ts deleted file mode 100644 index b4f2ec1ae1..0000000000 --- a/src/client/app/common/scripts/paging.ts +++ /dev/null @@ -1,189 +0,0 @@ -import Vue from 'vue'; - -export default (opts) => ({ - data() { - return { - items: [], - queue: [], - offset: 0, - fetching: true, - moreFetching: false, - inited: false, - more: false - }; - }, - - computed: { - empty(): boolean { - return this.items.length == 0 && !this.fetching && this.inited; - }, - - error(): boolean { - return !this.fetching && !this.inited; - } - }, - - watch: { - queue(x) { - if (opts.onQueueChanged) opts.onQueueChanged(this, x); - }, - - pagination() { - this.init(); - } - }, - - created() { - opts.displayLimit = opts.displayLimit || 30; - this.init(); - }, - - mounted() { - if (opts.captureWindowScroll) { - this.isScrollTop = () => { - return window.scrollY <= 8; - }; - - window.addEventListener('scroll', this.onScroll, { passive: true }); - } else if (opts.isContainer) { - this.isScrollTop = () => { - return this.$el.scrollTop <= 8; - }; - - this.$el.addEventListener('scroll', this.onScroll, { passive: true }); - } - }, - - beforeDestroy() { - if (opts.captureWindowScroll) { - window.removeEventListener('scroll', this.onScroll); - } else if (opts.isContainer) { - this.$el.removeEventListener('scroll', this.onScroll); - } - }, - - methods: { - updateItem(i, item) { - Vue.set((this as any).items, i, item); - }, - - reload() { - this.queue = []; - this.items = []; - this.init(); - }, - - async init() { - this.fetching = true; - if (opts.beforeInit) opts.beforeInit(this); - let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; - if (params && params.then) params = await params; - await this.$root.api(this.pagination.endpoint, { - limit: (this.pagination.limit || 10) + 1, - ...params - }).then(x => { - if (x.length == (this.pagination.limit || 10) + 1) { - x.pop(); - this.items = x; - this.more = true; - } else { - this.items = x; - this.more = false; - } - this.offset = x.length; - this.inited = true; - this.fetching = false; - if (opts.onInited) opts.onInited(this); - }, e => { - this.fetching = false; - if (opts.onInited) opts.onInited(this); - }); - }, - - async fetchMore() { - if (!this.more || this.moreFetching || this.items.length === 0) return; - this.moreFetching = true; - let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; - if (params && params.then) params = await params; - await this.$root.api(this.pagination.endpoint, { - limit: (this.pagination.limit || 10) + 1, - ...(this.pagination.endpoint === 'notes/search' ? { - offset: this.offset, - } : { - untilId: this.items[this.items.length - 1].id, - }), - ...params - }).then(x => { - if (x.length == (this.pagination.limit || 10) + 1) { - x.pop(); - this.items = this.items.concat(x); - this.more = true; - } else { - this.items = this.items.concat(x); - this.more = false; - } - this.offset += x.length; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, - - prepend(item, silent = false) { - if (opts.onPrepend) { - const cancel = opts.onPrepend(this, item, silent); - if (cancel) return; - } - - if (this.isScrollTop == null || this.isScrollTop()) { - // Prepend the item - this.items.unshift(item); - - // オーバーフローしたら古い投稿は捨てる - if (this.items.length >= opts.displayLimit) { - this.items = this.items.slice(0, opts.displayLimit); - this.more = true; - } - } else { - this.queue.push(item); - } - }, - - append(item) { - this.items.push(item); - }, - - releaseQueue() { - for (const n of this.queue) { - this.prepend(n, true); - } - this.queue = []; - }, - - onScroll() { - if (this.isScrollTop()) { - this.onTop(); - } - - if (this.$store.state.settings.fetchOnScroll) { - // 親要素が display none だったら弾く - // https://github.com/syuilo/misskey/issues/1569 - // http://d.hatena.ne.jp/favril/20091105/1257403319 - if (this.$el.offsetHeight == 0) return; - - const bottomPosition = opts.isContainer ? this.$el.scrollHeight : document.body.offsetHeight; - - const currentBottomPosition = opts.isContainer ? this.$el.scrollTop + this.$el.clientHeight : window.scrollY + window.innerHeight; - if (currentBottomPosition > (bottomPosition - 8)) this.onBottom(); - } - }, - - onTop() { - this.releaseQueue(); - }, - - onBottom() { - this.fetchMore(); - } - } -}); diff --git a/src/client/app/common/scripts/please-login.ts b/src/client/app/common/scripts/please-login.ts deleted file mode 100644 index 7125541bb1..0000000000 --- a/src/client/app/common/scripts/please-login.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default ($root: any) => { - if ($root.$store.getters.isSignedIn) return; - - $root.dialog({ - title: $root.$t('@.signin-required'), - text: null - }); - - throw new Error('signin required'); -}; diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/app/common/scripts/post-form.ts deleted file mode 100644 index 496782fd30..0000000000 --- a/src/client/app/common/scripts/post-form.ts +++ /dev/null @@ -1,537 +0,0 @@ -import insertTextAtCursor from 'insert-text-at-cursor'; -import { length } from 'stringz'; -import { toASCII } from 'punycode'; -import MkVisibilityChooser from '../views/components/visibility-chooser.vue'; -import getFace from './get-face'; -import { parse } from '../../../../mfm/parse'; -import { host, url } from '../../config'; -import i18n from '../../i18n'; -import { erase, unique } from '../../../../prelude/array'; -import extractMentions from '../../../../misc/extract-mentions'; -import { formatTimeString } from '../../../../misc/format-time-string'; - -export default (opts) => ({ - i18n: i18n(), - - components: { - XPostFormAttaches: () => import('../views/components/post-form-attaches.vue').then(m => m.default), - XPollEditor: () => import('../views/components/poll-editor.vue').then(m => m.default) - }, - - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - instant: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - posting: false, - text: '', - files: [], - uploadings: [], - poll: false, - pollChoices: [], - pollMultiple: false, - pollExpiration: [], - useCw: false, - cw: null, - geo: null, - visibility: 'public', - visibleUsers: [], - localOnly: false, - autocomplete: null, - draghover: false, - quoteId: null, - recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), - maxNoteTextLength: 1000 - }; - }, - - computed: { - draftId(): string { - return this.renote - ? `renote:${this.renote.id}` - : this.reply - ? `reply:${this.reply.id}` - : 'note'; - }, - - placeholder(): string { - const xs = [ - this.$t('@.note-placeholders.a'), - this.$t('@.note-placeholders.b'), - this.$t('@.note-placeholders.c'), - this.$t('@.note-placeholders.d'), - this.$t('@.note-placeholders.e'), - this.$t('@.note-placeholders.f') - ]; - const x = xs[Math.floor(Math.random() * xs.length)]; - - return this.renote - ? opts.mobile ? this.$t('@.post-form.option-quote-placeholder') : this.$t('@.post-form.quote-placeholder') - : this.reply - ? this.$t('@.post-form.reply-placeholder') - : x; - }, - - submitText(): string { - return this.renote - ? this.$t('@.post-form.renote') - : this.reply - ? this.$t('@.post-form.reply') - : this.$t('@.post-form.submit'); - }, - - canPost(): boolean { - return !this.posting && - (1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && - (length(this.text.trim()) <= this.maxNoteTextLength) && - (!this.poll || this.pollChoices.length >= 2); - } - }, - - created() { - this.$root.getMeta().then(meta => { - this.maxNoteTextLength = meta.maxNoteTextLength; - }); - }, - - mounted() { - if (this.initialText) { - this.text = this.initialText; - } - - if (this.mention) { - this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; - this.text += ' '; - } - - if (this.reply && this.reply.user.host != null) { - this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `; - } - - if (this.reply && this.reply.text != null) { - const ast = parse(this.reply.text); - - for (const x of extractMentions(ast)) { - const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; - - // 自分は除外 - if (this.$store.state.i.username == x.username && x.host == null) continue; - if (this.$store.state.i.username == x.username && x.host == host) continue; - - // 重複は除外 - if (this.text.indexOf(`${mention} `) != -1) continue; - - this.text += `${mention} `; - } - } - - // デフォルト公開範囲 - this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility); - - if (this.reply && this.reply.localOnly) { - this.localOnly = true; - } - - // 公開以外へのリプライ時は元の公開範囲を引き継ぐ - if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { - this.visibility = this.reply.visibility; - if (this.reply.visibility === 'specified') { - this.$root.api('users/show', { - userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId) - }).then(users => { - this.visibleUsers.push(...users); - }); - - if (this.reply.userId !== this.$store.state.i.id) { - this.$root.api('users/show', { userId: this.reply.userId }).then(user => { - this.visibleUsers.push(user); - }); - } - } - } - - // keep cw when reply - if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) { - this.useCw = true; - this.cw = this.reply.cw; - } - - this.focus(); - - this.$nextTick(() => { - this.focus(); - }); - - this.$nextTick(() => { - // 書きかけの投稿を復元 - if (!this.instant && !this.mention) { - const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; - if (draft) { - this.text = draft.data.text; - this.files = (draft.data.files || []).filter(e => e); - if (draft.data.poll) { - this.poll = true; - this.$nextTick(() => { - (this.$refs.poll as any).set(draft.data.poll); - }); - } - this.$emit('change-attached-files', this.files); - } - } - - // 削除して編集 - if (this.initialNote) { - const init = this.initialNote; - this.text = init.text ? init.text : ''; - this.files = init.files; - this.cw = init.cw; - this.useCw = init.cw != null; - if (init.poll) { - this.poll = true; - this.$nextTick(() => { - (this.$refs.poll as any).set({ - choices: init.poll.choices.map(c => c.text), - multiple: init.poll.multiple - }); - }); - } - // hack 位置情報投稿が動くようになったら適用する - this.geo = null; - this.visibility = init.visibility; - this.localOnly = init.localOnly; - this.quoteId = init.renote ? init.renote.id : null; - } - - this.$nextTick(() => this.watch()); - }); - }, - - methods: { - watch() { - this.$watch('text', () => this.saveDraft()); - this.$watch('poll', () => this.saveDraft()); - this.$watch('files', () => this.saveDraft()); - }, - - trimmedLength(text: string) { - return length(text.trim()); - }, - - addTag(tag: string) { - insertTextAtCursor(this.$refs.text, ` #${tag} `); - }, - - focus() { - (this.$refs.text as any).focus(); - }, - - chooseFile() { - (this.$refs.file as any).click(); - }, - - chooseFileFromDrive() { - this.$chooseDriveFile({ - multiple: true - }).then(files => { - for (const x of files) this.attachMedia(x); - }); - }, - - attachMedia(driveFile) { - this.files.push(driveFile); - this.$emit('change-attached-files', this.files); - }, - - detachMedia(id) { - this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-files', this.files); - }, - - onChangeFile() { - for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); - }, - - upload(file: File, name?: string) { - (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name); - }, - - onChangeUploadings(uploads) { - this.$emit('change-uploadings', uploads); - }, - - onPollUpdate() { - const got = this.$refs.poll.get(); - this.pollChoices = got.choices; - this.pollMultiple = got.multiple; - this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter]; - this.saveDraft(); - }, - - setGeo() { - if (navigator.geolocation == null) { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.post-form.geolocation-alert') - }); - return; - } - - navigator.geolocation.getCurrentPosition(pos => { - this.geo = pos.coords; - this.$emit('geo-attached', this.geo); - }, err => { - this.$root.dialog({ - type: 'error', - title: this.$t('@.post-form.error'), - text: err.message - }); - }, { - enableHighAccuracy: true - }); - }, - - removeGeo() { - this.geo = null; - this.$emit('geo-dettached'); - }, - - setVisibility() { - const w = this.$root.new(MkVisibilityChooser, { - source: this.$refs.visibilityButton, - currentVisibility: this.localOnly ? `local-${this.visibility}` : this.visibility - }); - w.$once('chosen', v => { - this.applyVisibility(v); - }); - this.$once('hook:beforeDestroy', () => { - w.close(); - }); - }, - - applyVisibility(v: string) { - const m = v.match(/^local-(.+)/); - if (m) { - this.localOnly = true; - this.visibility = m[1]; - } else { - this.localOnly = false; - this.visibility = v; - } - }, - - addVisibleUser() { - this.$root.dialog({ - title: this.$t('@.post-form.enter-username'), - user: true - }).then(({ canceled, result: user }) => { - if (canceled) return; - this.visibleUsers.push(user); - }); - }, - - removeVisibleUser(user) { - this.visibleUsers = erase(user, this.visibleUsers); - }, - - clear() { - this.text = ''; - this.files = []; - this.poll = false; - this.quoteId = null; - this.$emit('change-attached-files', this.files); - }, - - onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); - }, - - async onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { - const file = item.getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - const name = this.$store.state.settings.pasteDialog - ? await this.$root.dialog({ - title: this.$t('@.post-form.enter-file-name'), - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); - } - } - - const paste = e.clipboardData.getData('text'); - - if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) { - e.preventDefault(); - - this.$root.dialog({ - type: 'info', - text: this.$t('@.post-form.quote-question'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) { - insertTextAtCursor(this.$refs.text, paste); - return; - } - - this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; - }); - } - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - if (isFile || isDriveFile) { - e.preventDefault(); - this.draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDragenter(e) { - this.draghover = true; - }, - - onDragleave(e) { - this.draghover = false; - }, - - onDrop(e): void { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) this.upload(x); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - this.$emit('change-attached-files', this.files); - e.preventDefault(); - } - //#endregion - }, - - async emoji() { - const Picker = await import('../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default); - const button = this.$refs.emoji; - const rect = button.getBoundingClientRect(); - const vm = this.$root.new(Picker, { - x: button.offsetWidth + rect.left + window.pageXOffset, - y: rect.top + window.pageYOffset - }); - vm.$once('chosen', emoji => { - insertTextAtCursor(this.$refs.text, emoji); - }); - this.$once('hook:beforeDestroy', () => { - vm.close(); - }); - }, - - saveDraft() { - if (this.instant) return; - - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - data[this.draftId] = { - updatedAt: new Date(), - data: { - text: this.text, - files: this.files, - poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined - } - }; - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - - deleteDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - delete data[this.draftId]; - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - - kao() { - this.text += getFace(); - }, - - post() { - this.posting = true; - const viaMobile = opts.mobile && !this.$store.state.settings.disableViaMobile; - this.$root.api('notes/create', { - text: this.text == '' ? undefined : this.text, - fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, - replyId: this.reply ? this.reply.id : undefined, - renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined, - poll: this.poll ? (this.$refs.poll as any).get() : undefined, - cw: this.useCw ? this.cw || '' : undefined, - visibility: this.visibility, - visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, - localOnly: this.localOnly, - geo: this.geo ? { - coordinates: [this.geo.longitude, this.geo.latitude], - altitude: this.geo.altitude, - accuracy: this.geo.accuracy, - altitudeAccuracy: this.geo.altitudeAccuracy, - heading: isNaN(this.geo.heading) ? null : this.geo.heading, - speed: this.geo.speed, - } : null, - viaMobile: viaMobile - }).then(data => { - this.clear(); - this.deleteDraft(); - this.$emit('posted'); - if (opts.onSuccess) opts.onSuccess(this); - }).catch(err => { - if (opts.onSuccess) opts.onFailure(this); - }).then(() => { - this.posting = false; - }); - - if (this.text && this.text != '') { - const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag); - const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; - localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); - } - }, - } -}); diff --git a/src/client/app/common/scripts/room/furniture.ts b/src/client/app/common/scripts/room/furniture.ts deleted file mode 100644 index 7734e32668..0000000000 --- a/src/client/app/common/scripts/room/furniture.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type RoomInfo = { - roomType: string; - carpetColor: string; - furnitures: Furniture[]; -}; - -export type Furniture = { - id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない - type: string; // こっちが家具ID(chairとか) - position: { - x: number; - y: number; - z: number; - }; - rotation: { - x: number; - y: number; - z: number; - }; - props?: Record<string, any>; -}; diff --git a/src/client/app/common/scripts/room/furnitures.json5 b/src/client/app/common/scripts/room/furnitures.json5 deleted file mode 100644 index 7c1a90a3f9..0000000000 --- a/src/client/app/common/scripts/room/furnitures.json5 +++ /dev/null @@ -1,397 +0,0 @@ -// 家具メタデータ - -// 家具にはユーザーが設定できるプロパティを設定可能です: -// -// props: { -// <propname>: <proptype> -// } -// -// proptype一覧: -// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます -// * color ... 色選択コントロールを出し、選択された色が格納されます - -// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます: -// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。 -// UVは1024*1024だと仮定します。 -// -// <key>: { -// prop: <プロパティ名>, -// uv: { -// x: <テクスチャエリアX座標>, -// y: <テクスチャエリアY座標>, -// width: <テクスチャエリアの幅>, -// height: <テクスチャエリアの高さ>, -// }, -// } -// -// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します -// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します - -// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます: -// -// <key>: <プロパティ名> -// -// <key>には、カスタムカラーを適用したいマテリアル名を指定します -// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します - -[ - { - id: "milk", - place: "floor" - }, - { - id: "bed", - place: "floor" - }, - { - id: "low-table", - place: "floor", - props: { - color: 'color' - }, - color: { - Table: 'color' - } - }, - { - id: "desk", - place: "floor", - props: { - color: 'color' - }, - color: { - Board: 'color' - } - }, - { - id: "chair", - place: "floor", - props: { - color: 'color' - }, - color: { - Chair: 'color' - } - }, - { - id: "chair2", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - Cushion: 'color1', - Leg: 'color2' - } - }, - { - id: "fan", - place: "wall" - }, - { - id: "pc", - place: "floor" - }, - { - id: "plant", - place: "floor" - }, - { - id: "plant2", - place: "floor" - }, - { - id: "eraser", - place: "floor" - }, - { - id: "pencil", - place: "floor" - }, - { - id: "pudding", - place: "floor" - }, - { - id: "cardboard-box", - place: "floor" - }, - { - id: "cardboard-box2", - place: "floor" - }, - { - id: "cardboard-box3", - place: "floor" - }, - { - id: "book", - place: "floor", - props: { - color: 'color' - }, - color: { - Cover: 'color' - } - }, - { - id: "book2", - place: "floor" - }, - { - id: "piano", - place: "floor" - }, - { - id: "facial-tissue", - place: "floor" - }, - { - id: "server", - place: "floor" - }, - { - id: "moon", - place: "floor" - }, - { - id: "corkboard", - place: "wall" - }, - { - id: "mousepad", - place: "floor", - props: { - color: 'color' - }, - color: { - Pad: 'color' - } - }, - { - id: "monitor", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "tv", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "keyboard", - place: "floor" - }, - { - id: "carpet-stripe", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - CarpetAreaA: 'color1', - CarpetAreaB: 'color2' - }, - }, - { - id: "mat", - place: "floor", - props: { - color: 'color' - }, - color: { - Mat: 'color' - } - }, - { - id: "color-box", - place: "floor", - props: { - color: 'color' - }, - color: { - main: 'color' - } - }, - { - id: "wall-clock", - place: "wall" - }, - { - id: "cube", - place: "floor", - props: { - color: 'color' - }, - color: { - Cube: 'color' - } - }, - { - id: "photoframe", - place: "wall", - props: { - photo: 'image', - color: 'color' - }, - texture: { - Photo: { - prop: 'photo', - uv: { - x: 0, - y: 342, - width: 1024, - height: 683, - }, - }, - }, - color: { - Frame: 'color' - } - }, - { - id: "pinguin", - place: "floor", - props: { - body: 'color', - belly: 'color' - }, - color: { - Body: 'body', - Belly: 'belly', - } - }, - { - id: "rubik-cube", - place: "floor", - }, - { - id: "poster-h", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 277, - width: 1024, - height: 745, - }, - }, - }, - }, - { - id: "poster-v", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 0, - width: 745, - height: 1024, - }, - }, - }, - }, - { - id: "sofa", - place: "floor", - props: { - color: 'color' - }, - color: { - Sofa: 'color' - } - }, - { - id: "spiral", - place: "floor", - props: { - color: 'color' - }, - color: { - Step: 'color' - } - }, - { - id: "bin", - place: "floor", - props: { - color: 'color' - }, - color: { - Bin: 'color' - } - }, - { - id: "cup-noodle", - place: "floor" - }, - { - id: "holo-display", - place: "floor", - props: { - image: 'image' - }, - texture: { - Image_Front: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - Image_Back: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - }, - }, - { - id: 'energy-drink', - place: "floor", - } -] diff --git a/src/client/app/common/scripts/room/room.ts b/src/client/app/common/scripts/room/room.ts deleted file mode 100644 index c2a989c784..0000000000 --- a/src/client/app/common/scripts/room/room.ts +++ /dev/null @@ -1,776 +0,0 @@ -import autobind from 'autobind-decorator'; -import { v4 as uuid } from 'uuid'; -import * as THREE from 'three'; -import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; -import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; -import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; -import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'; -import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js'; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; -import { Furniture, RoomInfo } from './furniture'; -import { query as urlQuery } from '../../../../../prelude/url'; -const furnitureDefs = require('./furnitures.json5'); - -THREE.ImageUtils.crossOrigin = ''; - -type Options = { - graphicsQuality: Room['graphicsQuality']; - onChangeSelect: Room['onChangeSelect']; - useOrthographicCamera: boolean; -}; - -/** - * MisskeyRoom Core Engine - */ -export class Room { - private clock: THREE.Clock; - private scene: THREE.Scene; - private renderer: THREE.WebGLRenderer; - private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera; - private controls: OrbitControls; - private composer: EffectComposer; - private mixers: THREE.AnimationMixer[] = []; - private furnitureControl: TransformControls; - private roomInfo: RoomInfo; - private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra'; - private roomObj: THREE.Object3D; - private objects: THREE.Object3D[] = []; - private selectedObject: THREE.Object3D = null; - private onChangeSelect: Function; - private isTransformMode = false; - private renderFrameRequestId: number; - - private get canvas(): HTMLCanvasElement { - return this.renderer.domElement; - } - - private get furnitures(): Furniture[] { - return this.roomInfo.furnitures; - } - - private set furnitures(furnitures: Furniture[]) { - this.roomInfo.furnitures = furnitures; - } - - private get enableShadow() { - return this.graphicsQuality != 'cheep'; - } - - private get usePostFXs() { - return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low'; - } - - private get shadowQuality() { - return ( - this.graphicsQuality === 'ultra' ? 16384 : - this.graphicsQuality === 'high' ? 8192 : - this.graphicsQuality === 'medium' ? 4096 : - this.graphicsQuality === 'low' ? 1024 : - 0); // cheep - } - - constructor(user, isMyRoom, roomInfo: RoomInfo, container, options: Options) { - this.roomInfo = roomInfo; - this.graphicsQuality = options.graphicsQuality; - this.onChangeSelect = options.onChangeSelect; - - this.clock = new THREE.Clock(true); - - //#region Init a scene - this.scene = new THREE.Scene(); - - const width = window.innerWidth; - const height = window.innerHeight; - - //#region Init a renderer - this.renderer = new THREE.WebGLRenderer({ - antialias: false, - stencil: false, - alpha: false, - powerPreference: - this.graphicsQuality === 'ultra' ? 'high-performance' : - this.graphicsQuality === 'high' ? 'high-performance' : - this.graphicsQuality === 'medium' ? 'default' : - this.graphicsQuality === 'low' ? 'low-power' : - 'low-power' // cheep - }); - - this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderer.setSize(width, height); - this.renderer.autoClear = false; - this.renderer.setClearColor(new THREE.Color(0x051f2d)); - this.renderer.shadowMap.enabled = this.enableShadow; - this.renderer.shadowMap.type = - this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'medium' ? THREE.PCFShadowMap : - this.graphicsQuality === 'low' ? THREE.BasicShadowMap : - THREE.BasicShadowMap; // cheep - - container.appendChild(this.canvas); - //#endregion - - //#region Init a camera - this.camera = options.useOrthographicCamera - ? new THREE.OrthographicCamera( - width / - 2, width / 2, height / 2, height / - 2, -10, 10) - : new THREE.PerspectiveCamera(45, width / height); - - if (options.useOrthographicCamera) { - this.camera.position.x = 2; - this.camera.position.y = 2; - this.camera.position.z = 2; - this.camera.zoom = 100; - this.camera.updateProjectionMatrix(); - } else { - this.camera.position.x = 5; - this.camera.position.y = 2; - this.camera.position.z = 5; - } - - this.scene.add(this.camera); - //#endregion - - //#region AmbientLight - const ambientLight = new THREE.AmbientLight(0xffffff, 1); - this.scene.add(ambientLight); - //#endregion - - if (this.graphicsQuality !== 'cheep') { - //#region Room light - const roomLight = new THREE.SpotLight(0xffffff, 0.1); - - roomLight.position.set(0, 8, 0); - roomLight.castShadow = this.enableShadow; - roomLight.shadow.bias = -0.0001; - roomLight.shadow.mapSize.width = this.shadowQuality; - roomLight.shadow.mapSize.height = this.shadowQuality; - roomLight.shadow.camera.near = 0.1; - roomLight.shadow.camera.far = 9; - roomLight.shadow.camera.fov = 45; - - this.scene.add(roomLight); - //#endregion - } - - //#region Out light - const outLight1 = new THREE.SpotLight(0xffffff, 0.4); - outLight1.position.set(9, 3, -2); - outLight1.castShadow = this.enableShadow; - outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある - outLight1.shadow.mapSize.width = this.shadowQuality; - outLight1.shadow.mapSize.height = this.shadowQuality; - outLight1.shadow.camera.near = 6; - outLight1.shadow.camera.far = 15; - outLight1.shadow.camera.fov = 45; - this.scene.add(outLight1); - - const outLight2 = new THREE.SpotLight(0xffffff, 0.2); - outLight2.position.set(-2, 3, 9); - outLight2.castShadow = false; - outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある - outLight2.shadow.camera.near = 6; - outLight2.shadow.camera.far = 15; - outLight2.shadow.camera.fov = 45; - this.scene.add(outLight2); - //#endregion - - //#region Init a controller - this.controls = new OrbitControls(this.camera, this.canvas); - - this.controls.target.set(0, 1, 0); - this.controls.enableZoom = true; - this.controls.enablePan = isMyRoom; - this.controls.minPolarAngle = 0; - this.controls.maxPolarAngle = Math.PI / 2; - this.controls.minAzimuthAngle = 0; - this.controls.maxAzimuthAngle = Math.PI / 2; - this.controls.enableDamping = true; - this.controls.dampingFactor = 0.2; - this.controls.mouseButtons.LEFT = 1; - this.controls.mouseButtons.MIDDLE = 2; - this.controls.mouseButtons.RIGHT = 0; - //#endregion - - //#region POST FXs - if (!this.usePostFXs) { - this.composer = null; - } else { - const renderTarget = new THREE.WebGLRenderTarget(width, height, { - minFilter: THREE.LinearFilter, - magFilter: THREE.LinearFilter, - format: THREE.RGBFormat, - stencilBuffer: false, - }); - - const fxaa = new ShaderPass(FXAAShader); - fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height); - fxaa.renderToScreen = true; - - this.composer = new EffectComposer(this.renderer, renderTarget); - this.composer.addPass(new RenderPass(this.scene, this.camera)); - if (this.graphicsQuality === 'ultra') { - this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512)); - } - this.composer.addPass(fxaa); - } - //#endregion - //#endregion - - //#region Label - //#region Avatar - const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`; - - const textureLoader = new THREE.TextureLoader(); - textureLoader.crossOrigin = 'anonymous'; - - const iconTexture = textureLoader.load(avatarUrl); - iconTexture.wrapS = THREE.RepeatWrapping; - iconTexture.wrapT = THREE.RepeatWrapping; - iconTexture.anisotropy = 16; - - const avatarMaterial = new THREE.MeshBasicMaterial({ - map: iconTexture, - side: THREE.DoubleSide, - alphaTest: 0.5 - }); - - const iconGeometry = new THREE.PlaneGeometry(1, 1); - - const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial); - avatarObject.position.set(-3, 2.5, 2); - avatarObject.rotation.y = Math.PI / 2; - avatarObject.castShadow = false; - - this.scene.add(avatarObject); - //#endregion - - //#region Username - const name = user.username; - - new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => { - const nameGeometry = new THREE.TextGeometry(name, { - size: 0.5, - height: 0, - curveSegments: 8, - font: font, - bevelThickness: 0, - bevelSize: 0, - bevelEnabled: false - }); - - const nameMaterial = new THREE.MeshLambertMaterial({ - color: 0xffffff - }); - - const nameObject = new THREE.Mesh(nameGeometry, nameMaterial); - nameObject.position.set(-3, 2.25, 1.25); - nameObject.rotation.y = Math.PI / 2; - nameObject.castShadow = false; - - this.scene.add(nameObject); - }); - //#endregion - //#endregion - - //#region Interaction - if (isMyRoom) { - this.furnitureControl = new TransformControls(this.camera, this.canvas); - this.scene.add(this.furnitureControl); - - // Hover highlight - this.canvas.onmousemove = this.onmousemove; - - // Click - this.canvas.onmousedown = this.onmousedown; - } - //#endregion - - //#region Init room - this.loadRoom(); - //#endregion - - //#region Load furnitures - for (const furniture of this.furnitures) { - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - //#endregion - - // Start render - if (this.usePostFXs) { - this.renderWithPostFXs(); - } else { - this.renderWithoutPostFXs(); - } - } - - @autobind - private renderWithoutPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithoutPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.render(this.scene, this.camera); - } - - @autobind - private renderWithPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.clear(); - this.composer.render(); - } - - @autobind - private loadRoom() { - const type = this.roomInfo.roomType; - new GLTFLoader().load(`/assets/room/rooms/${type}/${type}.glb`, gltf => { - gltf.scene.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - - child.receiveShadow = this.enableShadow; - - child.material = new THREE.MeshLambertMaterial({ - color: (child.material as THREE.MeshStandardMaterial).color, - map: (child.material as THREE.MeshStandardMaterial).map, - name: (child.material as THREE.MeshStandardMaterial).name, - }); - - // 異方性フィルタリング - if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.anisotropy = 8; - } - }); - - gltf.scene.position.set(0, 0, 0); - - this.scene.add(gltf.scene); - this.roomObj = gltf.scene; - if (this.roomInfo.roomType === 'default') { - this.applyCarpetColor(); - } - }); - } - - @autobind - private loadFurniture(furniture: Furniture) { - const def = furnitureDefs.find(d => d.id === furniture.type); - return new Promise<GLTF>((res, rej) => { - const loader = new GLTFLoader(); - loader.load(`/assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => { - const model = gltf.scene; - - // Load animation - if (gltf.animations.length > 0) { - const mixer = new THREE.AnimationMixer(model); - this.mixers.push(mixer); - for (const clip of gltf.animations) { - mixer.clipAction(clip).play(); - } - } - - model.name = furniture.id; - model.position.x = furniture.position.x; - model.position.y = furniture.position.y; - model.position.z = furniture.position.z; - model.rotation.x = furniture.rotation.x; - model.rotation.y = furniture.rotation.y; - model.rotation.z = furniture.rotation.z; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - child.castShadow = this.enableShadow; - child.receiveShadow = this.enableShadow; - (child.material as THREE.MeshStandardMaterial).metalness = 0; - - // 異方性フィルタリング - if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.anisotropy = 8; - } - }); - - if (def.color) { // カスタムカラー - this.applyCustomColor(model); - } - - if (def.texture) { // カスタムテクスチャ - this.applyCustomTexture(model); - } - - res(gltf); - }, null, rej); - }); - } - - @autobind - private applyCarpetColor() { - this.roomObj.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - if (child.material && - (child.material as THREE.MeshStandardMaterial).name && - (child.material as THREE.MeshStandardMaterial).name === 'Carpet' - ) { - const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomColor(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.color == null) return; - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.color)) { - if (!child.material || - !(child.material as THREE.MeshStandardMaterial).name || - (child.material as THREE.MeshStandardMaterial).name !== t - ) continue; - - const prop = def.color[t]; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const colorHex = parseInt(val.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomTexture(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.texture == null) return; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.texture)) { - if (child.name !== t) continue; - - const prop = def.texture[t].prop; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const canvas = document.createElement('canvas'); - canvas.height = 1024; - canvas.width = 1024; - - child.material = new THREE.MeshLambertMaterial({ - emissive: 0x111111, - side: THREE.DoubleSide, - alphaTest: 0.5, - }); - - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.onload = () => { - const uvInfo = def.texture[t].uv; - - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, - 0, 0, img.width, img.height, - uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height); - - const texture = new THREE.Texture(canvas); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.anisotropy = 16; - texture.flipY = false; - - (child.material as THREE.MeshLambertMaterial).map = texture; - (child.material as THREE.MeshLambertMaterial).needsUpdate = true; - (child.material as THREE.MeshLambertMaterial).map.needsUpdate = true; - }; - img.src = val; - } - }); - } - - @autobind - private onmousemove(ev: MouseEvent) { - if (this.isTransformMode) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1; - const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - if (this.isSelectedObject(object)) continue; - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const intersected = this.getRoot(intersects[0].object); - if (this.isSelectedObject(intersected)) return; - intersected.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919); - } - }); - } - } - - @autobind - private onmousedown(ev: MouseEvent) { - if (this.isTransformMode) return; - if (ev.target !== this.canvas || ev.button !== 0) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1; - const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const selectedObj = this.getRoot(intersects[0].object); - this.selectFurniture(selectedObj); - } else { - this.selectedObject = null; - this.onChangeSelect(null); - } - } - - @autobind - private getRoot(obj: THREE.Object3D): THREE.Object3D { - let found = false; - let x = obj.parent; - while (!found) { - if (x.parent.parent == null) { - found = true; - } else { - x = x.parent; - } - } - return x; - } - - @autobind - private isSelectedObject(obj: THREE.Object3D): boolean { - if (this.selectedObject == null) { - return false; - } else { - return obj.name === this.selectedObject.name; - } - } - - @autobind - private selectFurniture(obj: THREE.Object3D) { - this.selectedObject = obj; - this.onChangeSelect(obj); - obj.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000); - } - }); - } - - /** - * 家具の移動/回転モードにします - * @param type 移動か回転か - */ - @autobind - public enterTransformMode(type: 'translate' | 'rotate') { - this.isTransformMode = true; - this.furnitureControl.setMode(type); - this.furnitureControl.attach(this.selectedObject); - } - - /** - * 家具の移動/回転モードを終了します - */ - @autobind - public exitTransformMode() { - this.isTransformMode = false; - this.furnitureControl.detach(); - } - - /** - * 家具プロパティを更新します - * @param key プロパティ名 - * @param value 値 - */ - @autobind - public updateProp(key: string, value: any) { - const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name); - if (furniture.props == null) furniture.props = {}; - furniture.props[key] = value; - this.applyCustomColor(this.selectedObject); - this.applyCustomTexture(this.selectedObject); - } - - /** - * 部屋に家具を追加します - * @param type 家具の種類 - */ - @autobind - public addFurniture(type: string) { - const furniture = { - id: uuid(), - type: type, - position: { - x: 0, - y: 0, - z: 0, - }, - rotation: { - x: 0, - y: 0, - z: 0, - }, - }; - - this.furnitures.push(furniture); - - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - - /** - * 現在選択されている家具を部屋から削除します - */ - @autobind - public removeFurniture() { - this.exitTransformMode(); - const obj = this.selectedObject; - this.scene.remove(obj); - this.objects = this.objects.filter(object => object.name !== obj.name); - this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name); - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * 全ての家具を部屋から削除します - */ - @autobind - public removeAllFurnitures() { - this.exitTransformMode(); - for (const obj of this.objects) { - this.scene.remove(obj); - } - this.objects = []; - this.furnitures = []; - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * 部屋の床の色を変更します - * @param color 色 - */ - @autobind - public updateCarpetColor(color: string) { - this.roomInfo.carpetColor = color; - this.applyCarpetColor(); - } - - /** - * 部屋の種類を変更します - * @param type 種類 - */ - @autobind - public changeRoomType(type: string) { - this.roomInfo.roomType = type; - this.scene.remove(this.roomObj); - this.loadRoom(); - } - - /** - * 部屋データを取得します - */ - @autobind - public getRoomInfo() { - for (const obj of this.objects) { - const furniture = this.furnitures.find(f => f.id === obj.name); - furniture.position.x = obj.position.x; - furniture.position.y = obj.position.y; - furniture.position.z = obj.position.z; - furniture.rotation.x = obj.rotation.x; - furniture.rotation.y = obj.rotation.y; - furniture.rotation.z = obj.rotation.z; - } - - return this.roomInfo; - } - - /** - * 選択されている家具を取得します - */ - @autobind - public getSelectedObject() { - return this.selectedObject; - } - - @autobind - public findFurnitureById(id: string) { - return this.furnitures.find(furniture => furniture.id === id); - } - - /** - * レンダリングを終了します - */ - @autobind - public destroy() { - // Stop render loop - window.cancelAnimationFrame(this.renderFrameRequestId); - - this.controls.dispose(); - this.scene.dispose(); - } -} diff --git a/src/client/app/common/scripts/search.ts b/src/client/app/common/scripts/search.ts deleted file mode 100644 index 2897ed6318..0000000000 --- a/src/client/app/common/scripts/search.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { faHistory } from '@fortawesome/free-solid-svg-icons'; - -export async function search(v: any, q: string) { - q = q.trim(); - - if (q.startsWith('@') && !q.includes(' ')) { - v.$router.push(`/${q}`); - return; - } - - if (q.startsWith('#')) { - v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`); - return; - } - - // like 2018/03/12 - if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) { - const date = new Date(q.replace(/-/g, '/')); - - // 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは - // 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので - // 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の - // 結果になってしまい、2018/03/12 のコンテンツは含まれない) - if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { - date.setHours(23, 59, 59, 999); - } - - v.$root.$emit('warp', date); - v.$root.dialog({ - icon: faHistory, - splash: true, - }); - return; - } - - if (q.startsWith('https://')) { - const dialog = v.$root.dialog({ - type: 'waiting', - text: v.$t('@.fetching-as-ap-object'), - showOkButton: false, - showCancelButton: false, - cancelableByBgClick: false - }); - - try { - const res = await v.$root.api('ap/show', { - uri: q - }); - dialog.close(); - if (res.type == 'User') { - v.$router.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type == 'Note') { - v.$router.push(`/notes/${res.object.id}`); - } - } catch (e) { - dialog.close(); - // TODO: Show error - } - - return; - } - - v.$router.push(`/search?q=${encodeURIComponent(q)}`); -} diff --git a/src/client/app/common/scripts/should-mute-note.ts b/src/client/app/common/scripts/should-mute-note.ts deleted file mode 100644 index 8fd7888628..0000000000 --- a/src/client/app/common/scripts/should-mute-note.ts +++ /dev/null @@ -1,19 +0,0 @@ -export default function(me, settings, note) { - const isMyNote = me && (note.userId == me.id); - const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const includesMutedWords = (text: string) => - text - ? settings.mutedWords.some(q => q.length > 0 && !q.some(word => - word.startsWith('/') && word.endsWith('/') ? !(new RegExp(word.substr(1, word.length - 2)).test(text)) : !text.includes(word))) - : false; - - return ( - (!isMyNote && note.reply && includesMutedWords(note.reply.text)) || - (!isMyNote && note.renote && includesMutedWords(note.renote.text)) || - (!settings.showMyRenotes && isMyNote && isPureRenote) || - (!settings.showRenotedMyNotes && isPureRenote && note.renote.userId == me.id) || - (!settings.showLocalRenotes && isPureRenote && note.renote.user.host == null) || - (!isMyNote && includesMutedWords(note.text)) - ); -} diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts deleted file mode 100644 index a1b4223b55..0000000000 --- a/src/client/app/common/scripts/stream.ts +++ /dev/null @@ -1,301 +0,0 @@ -import autobind from 'autobind-decorator'; -import { EventEmitter } from 'eventemitter3'; -import ReconnectingWebsocket from 'reconnecting-websocket'; -import { wsUrl } from '../../config'; -import MiOS from '../../mios'; - -/** - * Misskey stream connection - */ -export default class Stream extends EventEmitter { - private stream: ReconnectingWebsocket; - public state: string; - private sharedConnectionPools: Pool[] = []; - private sharedConnections: SharedConnection[] = []; - private nonSharedConnections: NonSharedConnection[] = []; - - constructor(os: MiOS) { - super(); - - this.state = 'initializing'; - - const user = os.store.state.i; - - this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : ''), '', { minReconnectionDelay: 1 }); // https://github.com/pladaria/reconnecting-websocket/issues/91 - this.stream.addEventListener('open', this.onOpen); - this.stream.addEventListener('close', this.onClose); - this.stream.addEventListener('message', this.onMessage); - } - - @autobind - public useSharedConnection(channel: string): SharedConnection { - let pool = this.sharedConnectionPools.find(p => p.channel === channel); - - if (pool == null) { - pool = new Pool(this, channel); - this.sharedConnectionPools.push(pool); - } - - const connection = new SharedConnection(this, channel, pool); - this.sharedConnections.push(connection); - return connection; - } - - @autobind - public removeSharedConnection(connection: SharedConnection) { - this.sharedConnections = this.sharedConnections.filter(c => c !== connection); - } - - @autobind - public removeSharedConnectionPool(pool: Pool) { - this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool); - } - - @autobind - public connectToChannel(channel: string, params?: any): NonSharedConnection { - const connection = new NonSharedConnection(this, channel, params); - this.nonSharedConnections.push(connection); - return connection; - } - - @autobind - public disconnectToChannel(connection: NonSharedConnection) { - this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection); - } - - /** - * Callback of when open connection - */ - @autobind - private onOpen() { - const isReconnect = this.state == 'reconnecting'; - - this.state = 'connected'; - this.emit('_connected_'); - - // チャンネル再接続 - if (isReconnect) { - for (const p of this.sharedConnectionPools) - p.connect(); - for (const c of this.nonSharedConnections) - c.connect(); - } - } - - /** - * Callback of when close connection - */ - @autobind - private onClose() { - if (this.state == 'connected') { - this.state = 'reconnecting'; - this.emit('_disconnected_'); - } - } - - /** - * Callback of when received a message from connection - */ - @autobind - private onMessage(message) { - const { type, body } = JSON.parse(message.data); - - if (type == 'channel') { - const id = body.id; - - let connections: Connection[]; - - connections = this.sharedConnections.filter(c => c.id === id); - - if (connections.length === 0) { - connections = [this.nonSharedConnections.find(c => c.id === id)]; - } - - for (const c of connections.filter(c => c != null)) { - c.emit(body.type, body.body); - } - } else { - this.emit(type, body); - } - } - - /** - * Send a message to connection - */ - @autobind - public send(typeOrPayload, payload?) { - const data = payload === undefined ? typeOrPayload : { - type: typeOrPayload, - body: payload - }; - - this.stream.send(JSON.stringify(data)); - } - - /** - * Close this connection - */ - @autobind - public close() { - this.stream.removeEventListener('open', this.onOpen); - this.stream.removeEventListener('message', this.onMessage); - } -} - -class Pool { - public channel: string; - public id: string; - protected stream: Stream; - public users = 0; - private disposeTimerId: any; - private isConnected = false; - - constructor(stream: Stream, channel: string) { - this.channel = channel; - this.stream = stream; - - this.id = Math.random().toString().substr(2, 8); - - this.stream.on('_disconnected_', this.onStreamDisconnected); - } - - @autobind - private onStreamDisconnected() { - this.isConnected = false; - } - - @autobind - public inc() { - if (this.users === 0 && !this.isConnected) { - this.connect(); - } - - this.users++; - - // タイマー解除 - if (this.disposeTimerId) { - clearTimeout(this.disposeTimerId); - this.disposeTimerId = null; - } - } - - @autobind - public dec() { - this.users--; - - // そのコネクションの利用者が誰もいなくなったら - if (this.users === 0) { - // また直ぐに再利用される可能性があるので、一定時間待ち、 - // 新たな利用者が現れなければコネクションを切断する - this.disposeTimerId = setTimeout(() => { - this.disconnect(); - }, 3000); - } - } - - @autobind - public connect() { - if (this.isConnected) return; - this.isConnected = true; - this.stream.send('connect', { - channel: this.channel, - id: this.id - }); - } - - @autobind - private disconnect() { - this.stream.off('_disconnected_', this.onStreamDisconnected); - this.stream.send('disconnect', { id: this.id }); - this.stream.removeSharedConnectionPool(this); - } -} - -abstract class Connection extends EventEmitter { - public channel: string; - protected stream: Stream; - public abstract id: string; - - constructor(stream: Stream, channel: string) { - super(); - - this.stream = stream; - this.channel = channel; - } - - @autobind - public send(id: string, typeOrPayload, payload?) { - const type = payload === undefined ? typeOrPayload.type : typeOrPayload; - const body = payload === undefined ? typeOrPayload.body : payload; - - this.stream.send('ch', { - id: id, - type: type, - body: body - }); - } - - public abstract dispose(): void; -} - -class SharedConnection extends Connection { - private pool: Pool; - - public get id(): string { - return this.pool.id; - } - - constructor(stream: Stream, channel: string, pool: Pool) { - super(stream, channel); - - this.pool = pool; - this.pool.inc(); - } - - @autobind - public send(typeOrPayload, payload?) { - super.send(this.pool.id, typeOrPayload, payload); - } - - @autobind - public dispose() { - this.pool.dec(); - this.removeAllListeners(); - this.stream.removeSharedConnection(this); - } -} - -class NonSharedConnection extends Connection { - public id: string; - protected params: any; - - constructor(stream: Stream, channel: string, params?: any) { - super(stream, channel); - - this.params = params; - this.id = Math.random().toString().substr(2, 8); - - this.connect(); - } - - @autobind - public connect() { - this.stream.send('connect', { - channel: this.channel, - id: this.id, - params: this.params - }); - } - - @autobind - public send(typeOrPayload, payload?) { - super.send(this.id, typeOrPayload, payload); - } - - @autobind - public dispose() { - this.removeAllListeners(); - this.stream.send('disconnect', { id: this.id }); - this.stream.disconnectToChannel(this); - } -} diff --git a/src/client/app/common/size.ts b/src/client/app/common/size.ts deleted file mode 100644 index 6abb305747..0000000000 --- a/src/client/app/common/size.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default { - install(Vue) { - Vue.directive('size', { - inserted(el, binding) { - const query = binding.value; - const width = el.clientWidth; - for (const q of query) { - if (q.lt && (width <= q.lt)) { - el.classList.add(q.class); - } - if (q.gt && (width >= q.gt)) { - el.classList.add(q.class); - } - } - } - }); - } -}; diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue deleted file mode 100644 index e802000833..0000000000 --- a/src/client/app/common/views/components/acct.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<span class="mk-acct" v-once> - <span class="name">@{{ user.username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span> - <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { host } from '../../../config'; -import { toUnicode } from 'punycode'; -export default Vue.extend({ - props: ['user', 'detail'], - data() { - return { - host: toUnicode(host) - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-acct - > .host.fade - opacity 0.5 - - > .locked - opacity 0.8 - margin-left 0.5em -</style> diff --git a/src/client/app/common/views/components/activity.vue b/src/client/app/common/views/components/activity.vue deleted file mode 100644 index a958616943..0000000000 --- a/src/client/app/common/views/components/activity.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<div> - <div ref="chart"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - }, - limit: { - type: Number, - required: false, - default: 21 - } - }, - data() { - return { - fetching: true, - data: [], - peak: null - }; - }, - mounted() { - this.$root.api('charts/user/notes', { - userId: this.user.id, - span: 'day', - limit: this.limit - }).then(stats => { - const normal = []; - const reply = []; - const renote = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - for (let i = 0; i < this.limit; i++) { - const x = new Date(y, m, d - i); - normal.push([ - x, - stats.diffs.normal[i] - ]); - reply.push([ - x, - stats.diffs.reply[i] - ]); - renote.push([ - x, - stats.diffs.renote[i] - ]); - } - - const chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'bar', - stacked: true, - height: 100, - sparkline: { - enabled: true - }, - }, - plotOptions: { - bar: { - columnWidth: '80%' - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - padding: { - top: 0, - right: 8, - bottom: 0, - left: 8 - } - }, - tooltip: { - shared: true, - intersect: false - }, - series: [{ - name: 'Normal', - data: normal - }, { - name: 'Reply', - data: reply - }, { - name: 'Renote', - data: renote - }], - xaxis: { - type: 'datetime', - crosshairs: { - width: 1, - opacity: 1 - } - } - }); - - chart.render(); - }); - } -}); -</script> diff --git a/src/client/app/common/views/components/analog-clock.vue b/src/client/app/common/views/components/analog-clock.vue deleted file mode 100644 index 5eb7ffd153..0000000000 --- a/src/client/app/common/views/components/analog-clock.vue +++ /dev/null @@ -1,140 +0,0 @@ -<template> -<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none"> - <circle v-for="angle, i in graduations" - :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" - :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" - :r="i % 5 == 0 ? 0.125 : 0.05" - :fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/> - - <line - :x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" - :stroke="sHandColor" - stroke-width="0.05"/> - <line - :x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" - :stroke="mHandColor" - stroke-width="0.1"/> - <line - :x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" - :stroke="hHandColor" - stroke-width="0.1"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as tinycolor from 'tinycolor2'; - -export default Vue.extend({ - props: { - dark: { - type: Boolean, - default: false - }, - smooth: { - type: Boolean, - default: false - } - }, - - data() { - return { - now: new Date(), - enabled: true, - - graduationsPadding: 0.5, - handsPadding: 1, - handsTailLength: 0.7, - hHandLengthRatio: 0.75, - mHandLengthRatio: 1, - sHandLengthRatio: 1 - }; - }, - - computed: { - majorGraduationColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; - }, - minorGraduationColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - }, - - sHandColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; - }, - mHandColor(): string { - return this.dark ? '#fff' : '#777'; - }, - hHandColor(): string { - return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString(); - }, - - ms(): number { - return this.now.getMilliseconds() * this.smooth; - }, - s(): number { - return this.now.getSeconds(); - }, - m(): number { - return this.now.getMinutes(); - }, - h(): number { - return this.now.getHours(); - }, - - hAngle(): number { - return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6; - }, - mAngle(): number { - return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30; - }, - sAngle(): number { - return Math.PI * (this.s + this.ms / 1000) / 30; - }, - - graduations(): any { - const angles = []; - for (let i = 0; i < 60; i++) { - const angle = Math.PI * i / 30; - angles.push(angle); - } - - return angles; - } - }, - - mounted() { - const update = () => { - if (this.enabled) { - this.tick(); - requestAnimationFrame(update); - } - }; - update(); - }, - - beforeDestroy() { - this.enabled = false; - }, - - methods: { - tick() { - this.now = new Date(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-analog-clock - display block -</style> diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue deleted file mode 100644 index bbfb7896ae..0000000000 --- a/src/client/app/common/views/components/autocomplete.vue +++ /dev/null @@ -1,458 +0,0 @@ -<template> -<div class="mk-autocomplete" @contextmenu.prevent="() => {}"> - <ol class="users" ref="suggests" v-if="users.length > 0"> - <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> - <img class="avatar" :src="user.avatarUrl" alt=""/> - <span class="name"> - <mk-user-name :user="user" :key="user.id"/> - </span> - <span class="username">@{{ user | acct }}</span> - </li> - </ol> - <ol class="hashtags" ref="suggests" v-if="hashtags.length > 0"> - <li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1"> - <span class="name">{{ hashtag }}</span> - </li> - </ol> - <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> - <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> - <span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span> - <span class="emoji" v-else-if="!useOsDefaultEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span> - <span class="emoji" v-else>{{ emoji.emoji }}</span> - <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> - <span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span> - </li> - </ol> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { emojilist } from '../../../../../misc/emojilist'; -import contains from '../../../common/scripts/contains'; -import { twemojiSvgBase } from '../../../../../misc/twemoji-base'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -type EmojiDef = { - emoji: string; - name: string; - aliasOf?: string; - url?: string; - isCustomEmoji?: boolean; -}; - -const lib = emojilist.filter(x => x.category !== 'flags'); - -const char2file = (char: string) => { - let codes = Array.from(char).map(x => x.codePointAt(0).toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); - codes = codes.filter(x => x && x.length); - return codes.join('-'); -}; - -const emjdb: EmojiDef[] = lib.map(x => ({ - emoji: x.char, - name: x.name, - aliasOf: null, - url: `${twemojiSvgBase}/${char2file(x.char)}.svg` -})); - -for (const x of lib) { - if (x.keywords) { - for (const k of x.keywords) { - emjdb.push({ - emoji: x.char, - name: k, - aliasOf: x.name, - url: `${twemojiSvgBase}/${char2file(x.char)}.svg` - }); - } - } -} - -emjdb.sort((a, b) => a.name.length - b.name.length); - -export default Vue.extend({ - props: { - type: { - type: String, - required: true, - }, - - q: { - type: String, - required: true, - }, - - textarea: { - type: Object, - required: true, - }, - - complete: { - type: Function, - required: true, - }, - - close: { - type: Function, - required: true, - }, - - x: { - type: Number, - required: true, - }, - - y: { - type: Number, - required: true, - }, - }, - - data() { - return { - getStaticImageUrl, - fetching: true, - users: [], - hashtags: [], - emojis: [], - select: -1, - emojilist, - emojiDb: [] as EmojiDef[] - } - }, - - computed: { - items(): HTMLCollection { - return (this.$refs.suggests as Element).children; - }, - - useOsDefaultEmojis(): boolean { - return this.$store.state.device.useOsDefaultEmojis; - } - }, - - updated() { - //#region 位置調整 - if (this.x + this.$el.offsetWidth > window.innerWidth) { - this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px'; - } else { - this.$el.style.left = this.x + 'px'; - } - - if (this.y + this.$el.offsetHeight > window.innerHeight) { - this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px'; - this.$el.style.marginTop = '0'; - } else { - this.$el.style.top = this.y + 'px'; - this.$el.style.marginTop = 'calc(1em + 8px)'; - } - //#endregion - }, - - mounted() { - //#region Construct Emoji DB - const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; - const emojiDefinitions: EmojiDef[] = []; - - for (const x of customEmojis) { - emojiDefinitions.push({ - name: x.name, - emoji: `:${x.name}:`, - url: x.url, - isCustomEmoji: true - }); - - if (x.aliases) { - for (const alias of x.aliases) { - emojiDefinitions.push({ - name: alias, - aliasOf: x.name, - emoji: `:${x.name}:`, - url: x.url, - isCustomEmoji: true - }); - } - } - } - - emojiDefinitions.sort((a, b) => a.name.length - b.name.length); - - this.emojiDb = emojiDefinitions.concat(emjdb); - //#endregion - - this.textarea.addEventListener('keydown', this.onKeydown); - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - - this.$nextTick(() => { - this.exec(); - - this.$watch('q', () => { - this.$nextTick(() => { - this.exec(); - }); - }); - }); - }, - - beforeDestroy() { - this.textarea.removeEventListener('keydown', this.onKeydown); - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - - methods: { - exec() { - this.select = -1; - if (this.$refs.suggests) { - for (const el of Array.from(this.items)) { - el.removeAttribute('data-selected'); - } - } - - if (this.type == 'user') { - const cacheKey = `autocomplete:user:${this.q}`; - const cache = sessionStorage.getItem(cacheKey); - if (cache) { - const users = JSON.parse(cache); - this.users = users; - this.fetching = false; - } else { - this.$root.api('users/search', { - query: this.q, - limit: 10, - detail: false - }).then(users => { - this.users = users; - this.fetching = false; - - // キャッシュ - sessionStorage.setItem(cacheKey, JSON.stringify(users)); - }); - } - } else if (this.type == 'hashtag') { - if (this.q == null || this.q == '') { - this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); - this.fetching = false; - } else { - const cacheKey = `autocomplete:hashtag:${this.q}`; - const cache = sessionStorage.getItem(cacheKey); - if (cache) { - const hashtags = JSON.parse(cache); - this.hashtags = hashtags; - this.fetching = false; - } else { - this.$root.api('hashtags/search', { - query: this.q, - limit: 30 - }).then(hashtags => { - this.hashtags = hashtags; - this.fetching = false; - - // キャッシュ - sessionStorage.setItem(cacheKey, JSON.stringify(hashtags)); - }); - } - } - } else if (this.type == 'emoji') { - if (this.q == null || this.q == '') { - this.emojis = this.emojiDb.filter(x => x.isCustomEmoji && !x.aliasOf).sort((a, b) => { - var textA = a.name.toUpperCase(); - var textB = b.name.toUpperCase(); - return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; - }); - return; - } - - const matched = []; - const max = 30; - - this.emojiDb.some(x => { - if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == max; - }); - if (matched.length < max) { - this.emojiDb.some(x => { - if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == max; - }); - } - if (matched.length < max) { - this.emojiDb.some(x => { - if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == max; - }); - } - - this.emojis = matched; - } - }, - - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - }, - - onKeydown(e) { - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (e.which) { - case 10: // [ENTER] - case 13: // [ENTER] - if (this.select !== -1) { - cancel(); - (this.items[this.select] as any).click(); - } else { - this.close(); - } - break; - - case 27: // [ESC] - cancel(); - this.close(); - break; - - case 38: // [↑] - if (this.select !== -1) { - cancel(); - this.selectPrev(); - } else { - this.close(); - } - break; - - case 9: // [TAB] - case 40: // [↓] - cancel(); - this.selectNext(); - break; - - default: - e.stopPropagation(); - this.textarea.focus(); - } - }, - - selectNext() { - if (++this.select >= this.items.length) this.select = 0; - this.applySelect(); - }, - - selectPrev() { - if (--this.select < 0) this.select = this.items.length - 1; - this.applySelect(); - }, - - applySelect() { - for (const el of Array.from(this.items)) { - el.removeAttribute('data-selected'); - } - - this.items[this.select].setAttribute('data-selected', 'true'); - (this.items[this.select] as any).focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-autocomplete - position fixed - z-index 65535 - max-width 100% - margin-top calc(1em + 8px) - overflow hidden - background var(--faceHeader) - border solid 1px rgba(#000, 0.1) - border-radius 4px - transition top 0.1s ease, left 0.1s ease - - > ol - display block - margin 0 - padding 4px 0 - max-height 190px - max-width 500px - overflow auto - list-style none - - > li - display flex - align-items center - padding 4px 12px - white-space nowrap - overflow hidden - font-size 0.9em - color rgba(#000, 0.8) - cursor default - - &, * - user-select none - - * - overflow hidden - text-overflow ellipsis - - &:hover - background var(--autocompleteItemHoverBg) - - &[data-selected='true'] - background var(--primary) - - &, * - color #fff !important - - &:active - background var(--primaryDarken10) - - &, * - color #fff !important - - > .users > li - - .avatar - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 100% - - .name - margin 0 8px 0 0 - color var(--autocompleteItemText) - - .username - color var(--autocompleteItemTextSub) - - > .hashtags > li - - .name - color var(--autocompleteItemText) - - > .emojis > li - - .emoji - display inline-block - margin 0 4px 0 0 - width 24px - - > img - width 24px - vertical-align bottom - - .name - color var(--autocompleteItemText) - - .alias - margin 0 0 0 8px - color var(--autocompleteItemTextSub) -</style> diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue deleted file mode 100644 index cd02c6957d..0000000000 --- a/src/client/app/common/views/components/avatar.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> - <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick" v-once> - <span class="inner" :style="icon"></span> - </span> - <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick" v-once> - <span class="inner" :style="icon"></span> - </span> - <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id" v-once> - <span class="inner" :style="icon"></span> - </router-link> - <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview" v-once> - <span class="inner" :style="icon"></span> - </router-link> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - }, - target: { - required: false, - default: null - }, - disableLink: { - required: false, - default: false - }, - disablePreview: { - required: false, - default: false - } - }, - computed: { - lightmode(): boolean { - return this.$store.state.device.lightmode; - }, - cat(): boolean { - return this.user.isCat && this.$store.state.settings.circleIcons; - }, - style(): any { - return { - borderRadius: this.$store.state.settings.circleIcons ? '100%' : null - }; - }, - url(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - icon(): any { - return { - backgroundColor: this.user.avatarColor, - backgroundImage: this.lightmode ? null : `url(${this.url})`, - borderRadius: this.$store.state.settings.circleIcons ? '100%' : null - }; - } - }, - mounted() { - if (this.user.avatarColor) { - this.$el.style.color = this.user.avatarColor; - } - }, - methods: { - onClick(e) { - this.$emit('click', e); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-avatar - display inline-block - vertical-align bottom - flex-shrink 0 - - &:not(.cat) - overflow hidden - border-radius 8px - - &.cat::before, - &.cat::after - background #df548f - border solid 4px currentColor - box-sizing border-box - content '' - display inline-block - height 50% - width 50% - - &.cat::before - border-radius 0 75% 75% - transform rotate(37.5deg) skew(30deg) - - &.cat::after - border-radius 75% 0 75% 75% - transform rotate(-37.5deg) skew(-30deg) - - .inner - background-position center center - background-size cover - bottom 0 - left 0 - position absolute - right 0 - top 0 - transition border-radius 1s ease - z-index 1 - -</style> diff --git a/src/client/app/common/views/components/avatars.vue b/src/client/app/common/views/components/avatars.vue deleted file mode 100644 index 0dc1ece3bf..0000000000 --- a/src/client/app/common/views/components/avatars.vue +++ /dev/null @@ -1,27 +0,0 @@ -<template> -<div> - <mk-avatar v-for="user in us" :user="user" :key="user.id" style="width:32px;height:32px;"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - userIds: { - required: true - }, - }, - data() { - return { - us: [] - }; - }, - async created() { - this.us = await this.$root.api('users/show', { - userIds: this.userIds - }); - } -}); -</script> diff --git a/src/client/app/common/views/components/code-core.vue b/src/client/app/common/views/components/code-core.vue deleted file mode 100644 index 219ed1d80a..0000000000 --- a/src/client/app/common/views/components/code-core.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<x-prism :inline="inline" :language="prismLang">{{ code }}</x-prism> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import 'prismjs'; -import 'prismjs/themes/prism-okaidia.css'; -import XPrism from 'vue-prism-component'; - -export default Vue.extend({ - components: { - XPrism - }, - props: { - code: { - type: String, - required: true - }, - lang: { - type: String, - required: false - }, - inline: { - type: Boolean, - required: false - } - }, - - computed: { - prismLang() { - return Prism.languages[this.lang] ? this.lang : 'js'; - } - } -}); -</script> diff --git a/src/client/app/common/views/components/code.vue b/src/client/app/common/views/components/code.vue deleted file mode 100644 index d52c9f7bc2..0000000000 --- a/src/client/app/common/views/components/code.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<x-code :code="code" :lang="lang" :inline="inline"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XCode: () => import('./code-core.vue').then(m => m.default) - }, - - props: { - code: { - type: String, - required: true - }, - lang: { - type: String, - required: false - }, - inline: { - type: Boolean, - required: false - } - } -}); -</script> diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue deleted file mode 100644 index 19b8c3e974..0000000000 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="troubleshooter"> - <div class="body"> - <h1><fa icon="wrench"/>{{ $t('title') }}</h1> - <div> - <p :data-wip="network == null"> - <template v-if="network != null"> - <template v-if="network"><fa icon="check"/></template> - <template v-if="!network"><fa icon="times"/></template> - </template> - {{ network == null ? this.$t('checking-network') : this.$t('network') }}<mk-ellipsis v-if="network == null"/> - </p> - <p v-if="network == true" :data-wip="internet == null"> - <template v-if="internet != null"> - <template v-if="internet"><fa icon="check"/></template> - <template v-if="!internet"><fa icon="times"/></template> - </template> - {{ internet == null ? this.$t('checking-internet') : this.$t('internet') }}<mk-ellipsis v-if="internet == null"/> - </p> - <p v-if="internet == true" :data-wip="server == null"> - <template v-if="server != null"> - <template v-if="server"><fa icon="check"/></template> - <template v-if="!server"><fa icon="times"/></template> - </template> - {{ server == null ? this.$t('checking-server') : this.$t('server') }}<mk-ellipsis v-if="server == null"/> - </p> - </div> - <p v-if="!end">{{ $t('finding') }}<mk-ellipsis/></p> - <p v-if="network === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-network') }}</b><br>{{ $t('no-network-desc') }}</p> - <p v-if="internet === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-internet') }}</b><br>{{ $t('no-internet-desc') }}</p> - <p v-if="server === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-server') }}</b><br>{{ $t('no-server-desc') }}</p> - <p v-if="server === true" class="success"><b><fa icon="info-circle"/>{{ $t('success') }}</b><br>{{ $t('success-desc') }}</p> - </div> - <footer> - <a href="/assets/flush.html">{{ $t('flush') }}</a> | <a href="/assets/version.html">{{ $t('set-version') }}</a> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { apiUrl } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/connect-failed.troubleshooter.vue'), - data() { - return { - network: navigator.onLine, - end: false, - internet: null, - server: null - }; - }, - mounted() { - if (!this.network) { - this.end = true; - return; - } - - // Check internet connection - fetch(`https://google.com?rand=${Math.random()}`, { - mode: 'no-cors' - }).then(() => { - this.internet = true; - - // Check misskey server is available - fetch(`${apiUrl}/meta`).then(() => { - this.end = true; - this.server = true; - }) - .catch(() => { - this.end = true; - this.server = false; - }); - }) - .catch(() => { - this.end = true; - this.internet = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.troubleshooter - margin-top 1em - - > .body - width 100% - max-width 500px - margin 0 auto - text-align left - background #fff - border-radius 8px - border solid 1px #ddd - - > h1 - margin 0 - padding 0.6em 1.2em - font-size 1em - color #444 - border-bottom solid 1px #eee - - > [data-icon] - margin-right 0.25em - - > div - overflow hidden - padding 0.6em 1.2em - - > p - margin 0.5em 0 - font-size 0.9em - color #444 - - &[data-wip] - color #888 - - > [data-icon] - margin-right 0.25em - - &.times - color #e03524 - - &.check - color #84c32f - - > p - margin 0 - padding 0.7em 1.2em - font-size 1em - color #444 - border-top solid 1px #eee - - > b - > [data-icon] - margin-right 0.25em - - &.success - > b - color #39adad - - &:not(.success) - > b - color #ad4339 - -</style> diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue deleted file mode 100644 index a364304a63..0000000000 --- a/src/client/app/common/views/components/connect-failed.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<div class="mk-connect-failed"> - <img src="/assets/error.jpg" onerror="this.src='https://raw.githubusercontent.com/syuilo/misskey/develop/src/client/assets/error.jpg';" alt=""/> - <h1>{{ $t('title') }}</h1> - <p class="text"> - <span>{{ this.$t('description').substr(0, this.$t('description').indexOf('{')) }}</span> - <a @click="reload">{{ this.$t('description').match(/\{(.+?)\}/)[1] }}</a> - <span>{{ this.$t('description').substr(this.$t('description').indexOf('}') + 1) }}</span> - </p> - <button v-if="!troubleshooting" @click="troubleshooting = true">{{ $t('troubleshoot') }}</button> - <x-troubleshooter v-if="troubleshooting"/> - <p class="thanks">{{ $t('thanks') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XTroubleshooter from './connect-failed.troubleshooter.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/connect-failed.vue'), - components: { - XTroubleshooter - }, - data() { - return { - troubleshooting: false - }; - }, - mounted() { - document.title = 'Oops!'; - document.documentElement.style.setProperty('background', '#f8f8f8', 'important'); - }, - methods: { - reload() { - location.reload(true); - } - } -}); -</script> - -<style lang="stylus" scoped> - - -.mk-connect-failed - width 100% - padding 32px 18px - text-align center - - > img - display block - height 200px - margin 0 auto - pointer-events none - user-select none - - > h1 - display block - margin 1.25em auto 0.65em auto - font-size 1.5em - color #555 - - > .text - display block - margin 0 auto - max-width 600px - font-size 1em - color #666 - - > button - display block - margin 1em auto 0 auto - padding 8px 10px - color var(--primaryForeground) - background var(--primary) - - &:focus - outline solid 3px var(--primaryAlpha03) - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > .thanks - display block - margin 2em auto 0 auto - padding 2em 0 0 0 - max-width 600px - font-size 0.9em - font-style oblique - color #aaa - border-top solid 1px #eee - - @media (max-width 500px) - padding 24px 18px - font-size 80% - - > img - height 150px - -</style> - diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue deleted file mode 100644 index 098aa021d1..0000000000 --- a/src/client/app/common/views/components/cw-button.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle"> - <b>{{ value ? this.$t('hide') : this.$t('show') }}</b> - <span v-if="!value">{{ this.label }}</span> -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { length } from 'stringz'; -import { concat } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/cw-button.vue'), - - props: { - value: { - type: Boolean, - required: true - }, - note: { - type: Object, - required: true - } - }, - - computed: { - label(): string { - return concat([ - this.note.text ? [this.$t('chars', { count: length(this.note.text) })] : [], - this.note.files && this.note.files.length !== 0 ? [this.$t('files', { count: this.note.files.length }) ] : [], - this.note.poll != null ? [this.$t('poll')] : [] - ] as string[][]).join(' / '); - } - }, - - methods: { - length, - - toggle() { - this.$emit('input', !this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nrvgflfuaxwgkxoynpnumyookecqrrvh - display inline-block - padding 4px 8px - font-size 0.7em - color var(--cwButtonFg) - background var(--cwButtonBg) - border-radius 2px - cursor pointer - user-select none - - &:hover - background var(--cwButtonHoverBg) - - > span - margin-left 4px - - &:before - content '(' - &:after - content ')' - -</style> diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue deleted file mode 100644 index 2744903007..0000000000 --- a/src/client/app/common/views/components/dialog.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<ui-modal - ref="modal" - class="modal" - :class="{ splash }" - :close-anime-duration="300" - :close-on-bg-click="false" - @bg-click="onBgClick" - @before-close="onBeforeClose"> - <div class="main" ref="main" :class="{ round: $store.state.device.roundedCorners }"> - <template v-if="type == 'signin'"> - <mk-signin/> - </template> - <template v-else> - <div class="icon" v-if="icon"> - <fa :icon="icon"/> - </div> - <div class="icon" v-else-if="!input && !select && !user" :class="type"> - <fa icon="check" v-if="type === 'success'"/> - <fa :icon="faTimesCircle" v-if="type === 'error'"/> - <fa icon="exclamation-triangle" v-if="type === 'warning'"/> - <fa icon="info-circle" v-if="type === 'info'"/> - <fa :icon="faQuestionCircle" v-if="type === 'question'"/> - <fa icon="spinner" pulse v-if="type === 'waiting'"/> - </div> - <header v-if="title" v-html="title"></header> - <header v-if="title == null && user">{{ $t('@.enter-username') }}</header> - <div class="body" v-if="text" v-html="text"></div> - <ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input> - <ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input> - <ui-select v-if="select" v-model="selectedValue" autofocus> - <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> - </template> - <template v-else> - <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> - <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> - </optgroup> - </template> - </ui-select> - <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)"> - <ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button> - <ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button> - </ui-horizon-group> - </template> - </div> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; -import parseAcct from "../../../../../misc/acct/parse"; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - type: { - type: String, - required: false, - default: 'info' - }, - title: { - type: String, - required: false - }, - text: { - type: String, - required: false - }, - input: { - required: false - }, - select: { - required: false - }, - user: { - required: false - }, - icon: { - required: false - }, - showOkButton: { - type: Boolean, - default: true - }, - showCancelButton: { - type: Boolean, - default: false - }, - cancelableByBgClick: { - type: Boolean, - default: true - }, - splash: { - type: Boolean, - default: false - } - }, - - data() { - return { - inputValue: this.input && this.input.default ? this.input.default : null, - userInputValue: null, - selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, - canOk: true, - faTimesCircle, faQuestionCircle - }; - }, - - watch: { - userInputValue() { - if (this.user) { - this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => { - this.canOk = u != null; - }).catch(() => { - this.canOk = false; - }); - } - } - }, - - mounted() { - if (this.user) this.canOk = false; - - this.$nextTick(() => { - anime({ - targets: this.$refs.main, - opacity: 1, - scale: [1.2, 1], - duration: 300, - easing: 'cubicBezier(0, 0.5, 0.5, 1)' - }); - - if (this.splash) { - setTimeout(() => { - this.close(); - }, 1000); - } - }); - }, - - methods: { - async ok() { - if (!this.canOk) return; - if (!this.showOkButton) return; - - if (this.user) { - const user = await this.$root.api('users/show', parseAcct(this.userInputValue)); - if (user) { - this.$emit('ok', user); - this.close(); - } - } else { - const result = - this.input ? this.inputValue : - this.select ? this.selectedValue : - true; - this.$emit('ok', result); - this.close(); - } - }, - - cancel() { - this.$emit('cancel'); - this.close(); - }, - - onBgClick() { - if (this.cancelableByBgClick) this.cancel(); - } - - close() { - this.$refs.modal.close(); - }, - - onBeforeClose() { - this.$el.style.pointerEvents = 'none'; - (this.$refs.main as any).style.pointerEvents = 'none'; - - anime({ - targets: this.$refs.main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: 'cubicBezier(0, 0.5, 0.5, 1)', - }); - }, - - onInputKeydown(e) { - if (e.which == 13) { // Enter - e.preventDefault(); - e.stopPropagation(); - this.ok(); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.modal - display flex - align-items center - justify-content center - - &.splash - > .main - min-width 0 - width initial - -.main - display block - position fixed - margin auto - padding 32px - min-width 320px - max-width 480px - width calc(100% - 32px) - text-align center - background var(--face) - color var(--faceText) - opacity 0 - - &.round - border-radius 8px - - > .icon - font-size 32px - - &.success - color #85da5a - - &.error - color #ec4137 - - &.warning - color #ecb637 - - > * - display block - margin 0 auto - - & + header - margin-top 16px - - > header - margin 0 0 8px 0 - font-weight bold - font-size 20px - - & + .body - margin-top 8px - - > .body - margin 16px 0 0 0 - - > .buttons - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/app/common/views/components/drive-file-thumbnail.vue deleted file mode 100644 index f44223ad6f..0000000000 --- a/src/client/app/common/views/components/drive-file-thumbnail.vue +++ /dev/null @@ -1,184 +0,0 @@ -<template> -<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`"> - <img - :src="file.url" - :alt="file.name" - :title="file.name" - @load="onThumbnailLoaded" - v-if="detail && is === 'image'"/> - <video - :src="file.url" - ref="volumectrl" - preload="metadata" - controls - v-else-if="detail && is === 'video'"/> - <img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/> - <fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> - <fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> - - <audio - :src="file.url" - ref="volumectrl" - preload="metadata" - controls - v-else-if="detail && is === 'audio'"/> - <fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/> - - <fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/> - <fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/> - <fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/> - <fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/> - <fa :icon="faFile" class="icon" v-else/> - - <fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import { - faFile, - faFileAlt, - faFileImage, - faMusic, - faFileVideo, - faFileCsv, - faFilePdf, - faFileArchive, - faFilm - } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - props: { - file: { - type: Object, - required: true - }, - fit: { - type: String, - required: false, - default: 'cover' - }, - detail: { - type: Boolean, - required: false, - default: false - } - }, - data() { - return { - isContextmenuShowing: false, - isDragging: false, - - faFile, - faFileAlt, - faFileImage, - faMusic, - faFileVideo, - faFileCsv, - faFilePdf, - faFileArchive, - faFilm - }; - }, - computed: { - is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' { - if (this.file.type.startsWith('image/')) return 'image'; - if (this.file.type.startsWith('video/')) return 'video'; - if (this.file.type === 'audio/midi') return 'midi'; - if (this.file.type.startsWith('audio/')) return 'audio'; - if (this.file.type.endsWith('/csv')) return 'csv'; - if (this.file.type.endsWith('/pdf')) return 'pdf'; - if (this.file.type.startsWith('text/')) return 'textfile'; - if ([ - "application/zip", - "application/x-cpio", - "application/x-bzip", - "application/x-bzip2", - "application/java-archive", - "application/x-rar-compressed", - "application/x-tar", - "application/gzip", - "application/x-7z-compressed" - ].some(e => e === this.file.type)) return 'archive'; - return 'unknown'; - }, - isThumbnailAvailable(): boolean { - return this.file.thumbnailUrl - ? (this.is === 'image' || this.is === 'video') - : false; - }, - background(): string { - return this.file.properties.avgColor || 'transparent'; - } - }, - mounted() { - const audioTag = this.$refs.volumectrl as HTMLAudioElement; - if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume; - }, - methods: { - onThumbnailLoaded() { - if (this.file.properties.avgColor) { - anime({ - targets: this.$refs.thumbnail, - backgroundColor: 'transparent', // TODO fade - duration: 100, - easing: 'linear' - }); - } - }, - volumechange() { - const audioTag = this.$refs.volumectrl as HTMLAudioElement; - this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zdjebgpv - display flex - - > img, - > .icon - pointer-events none - - > .icon-sub - position absolute - width 30% - height auto - margin 0 - right 4% - bottom 4% - - > * - margin auto - - &:not(.detail) - > img - height 100% - width 100% - object-fit cover - - > .icon - height 65% - width 65% - - > video, - > audio - width 100% - - &.detail - > .icon - height 100px - width 100px - margin 16px - - > *:not(.icon) - max-height 300px - max-width 100% - height 100% - object-fit contain - -</style> diff --git a/src/client/app/common/views/components/dummy.vue b/src/client/app/common/views/components/dummy.vue deleted file mode 100644 index 5634efc509..0000000000 --- a/src/client/app/common/views/components/dummy.vue +++ /dev/null @@ -1,11 +0,0 @@ -<template> -<div> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ -}); -</script> diff --git a/src/client/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue deleted file mode 100644 index 07349902de..0000000000 --- a/src/client/app/common/views/components/ellipsis.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> - <span class="mk-ellipsis"> - <span>.</span><span>.</span><span>.</span> - </span> -</template> - -<style lang="stylus" scoped> -.mk-ellipsis - > span - animation ellipsis 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes ellipsis - 0%, 80%, 100% - opacity 1 - 40% - opacity 0 -</style> diff --git a/src/client/app/common/views/components/emoji-picker.vue b/src/client/app/common/views/components/emoji-picker.vue deleted file mode 100644 index abae69e28a..0000000000 --- a/src/client/app/common/views/components/emoji-picker.vue +++ /dev/null @@ -1,243 +0,0 @@ -<template> -<div class="prlncendiewqqkrevzeruhndoakghvtx"> - <header> - <button v-for="category in categories" - :title="category.text" - @click="go(category)" - :class="{ active: category.isActive }" - :key="category.text" - > - <fa :icon="category.icon" fixed-width/> - </button> - </header> - <div class="emojis"> - <template v-if="categories[0].isActive"> - <header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recent-emoji') }}</header> - <div class="list"> - <button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])" - :title="emoji.name" - @click="chosen(emoji)" - :key="i" - > - <mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/> - <img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> - </button> - </div> - </template> - - <header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header> - <template v-if="categories.find(x => x.isActive).name"> - <div class="list"> - <button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)" - :title="emoji.name" - @click="chosen(emoji)" - :key="emoji.name" - > - <mk-emoji :emoji="emoji.char"/> - </button> - </div> - </template> - <template v-else> - <div v-for="(key, i) in Object.keys(customEmojis)" :key="i"> - <header class="sub">{{ key || $t('no-category') }}</header> - <div class="list"> - <button v-for="emoji in customEmojis[key]" - :title="emoji.name" - @click="chosen(emoji)" - :key="emoji.name" - > - <img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> - </button> - </div> - </div> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { emojilist } from '../../../../../misc/emojilist'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; -import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons'; -import { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons'; -import { groupByX } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/emoji-picker.vue'), - - data() { - return { - emojilist, - getStaticImageUrl, - customEmojis: {}, - faGlobe, faHistory, - categories: [{ - text: this.$t('custom-emoji'), - icon: faAsterisk, - isActive: true - }, { - name: 'people', - text: this.$t('people'), - icon: ['far', 'laugh'], - isActive: false - }, { - name: 'animals_and_nature', - text: this.$t('animals-and-nature'), - icon: faLeaf, - isActive: false - }, { - name: 'food_and_drink', - text: this.$t('food-and-drink'), - icon: faUtensils, - isActive: false - }, { - name: 'activity', - text: this.$t('activity'), - icon: faFutbol, - isActive: false - }, { - name: 'travel_and_places', - text: this.$t('travel-and-places'), - icon: faCity, - isActive: false - }, { - name: 'objects', - text: this.$t('objects'), - icon: faDice, - isActive: false - }, { - name: 'symbols', - text: this.$t('symbols'), - icon: faHeart, - isActive: false - }, { - name: 'flags', - text: this.$t('flags'), - icon: faFlag, - isActive: false - }] - } - }, - - created() { - let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; - local = groupByX(local, (x: any) => x.category || ''); - this.customEmojis = local; - - if (this.$store.state.device.activeEmojiCategoryName) { - this.goCategory(this.$store.state.device.activeEmojiCategoryName); - } - }, - - methods: { - go(category: any) { - this.goCategory(category.name); - }, - - goCategory(name: string) { - let matched = false; - for (const c of this.categories) { - c.isActive = c.name === name; - if (c.isActive) { - matched = true; - this.$store.commit('device/set', { key: 'activeEmojiCategoryName', value: c.name }); - } - } - if (!matched) { - this.categories[0].isActive = true; - } - }, - - chosen(emoji: any) { - const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`; - - let recents = this.$store.state.device.recentEmojis || []; - recents = recents.filter((e: any) => getKey(e) !== getKey(emoji)); - recents.unshift(emoji) - this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) }); - - this.$emit('chosen', getKey(emoji)); - } - } -}); -</script> - -<style lang="stylus" scoped> -.prlncendiewqqkrevzeruhndoakghvtx - width 350px - background var(--face) - - > header - display flex - - > button - flex 1 - padding 10px 0 - font-size 16px - color var(--text) - transition color 0.2s ease - - &:hover - color var(--textHighlighted) - transition color 0s - - &.active - color var(--primary) - transition color 0s - - > .emojis - height 300px - overflow-y auto - overflow-x hidden - - > header.category - position sticky - top 0 - left 0 - z-index 1 - padding 8px - background var(--faceHeader) - color var(--text) - font-size 12px - - >>> header.sub - padding 4px 8px - color var(--text) - font-size 12px - - >>> div.list - display grid - grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr - gap 4px - padding 8px - - > button - padding 0 - width 100% - - &:before - content '' - display block - width 1px - height 0 - padding-bottom 100% - - &:hover - > * - transform scale(1.2) - transition transform 0s - - > * - position absolute - top 0 - left 0 - width 100% - height 100% - object-fit contain - font-size 28px - transition transform 0.2s ease - pointer-events none - -</style> diff --git a/src/client/app/common/views/components/emoji.vue b/src/client/app/common/views/components/emoji.vue deleted file mode 100644 index 26992c5f7e..0000000000 --- a/src/client/app/common/views/components/emoji.vue +++ /dev/null @@ -1,119 +0,0 @@ -<template> -<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :class="{ normal: normal }" :src="url" :alt="alt" :title="alt"/> -<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/> -<span v-else-if="char && useOsDefaultEmojis">{{ char }}</span> -<span v-else>:{{ name }}:</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; -import { twemojiSvgBase } from '../../../../../misc/twemoji-base'; - -export default Vue.extend({ - props: { - name: { - type: String, - required: false - }, - emoji: { - type: String, - required: false - }, - normal: { - type: Boolean, - required: false, - default: false - }, - customEmojis: { - required: false, - default: () => [] - }, - isReaction: { - type: Boolean, - default: false - }, - }, - - data() { - return { - url: null, - char: null, - customEmoji: null - } - }, - - computed: { - alt(): string { - return this.customEmoji ? `:${this.customEmoji.name}:` : this.char; - }, - - useOsDefaultEmojis(): boolean { - return this.$store.state.device.useOsDefaultEmojis && !this.isReaction; - } - }, - - watch: { - customEmojis() { - if (this.name) { - const customEmoji = this.customEmojis.find(x => x.name == this.name); - if (customEmoji) { - this.customEmoji = customEmoji; - this.url = this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(customEmoji.url) - : customEmoji.url; - } - } - }, - }, - - created() { - if (this.name) { - const customEmoji = this.customEmojis.find(x => x.name == this.name); - if (customEmoji) { - this.customEmoji = customEmoji; - this.url = this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(customEmoji.url) - : customEmoji.url; - } else { - //const emoji = lib[this.name]; - //if (emoji) { - // this.char = emoji.char; - //} - } - } else { - this.char = this.emoji; - } - - if (this.char) { - let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); - codes = codes.filter(x => x && x.length); - - this.url = `${twemojiSvgBase}/${codes.join('-')}.svg`; - } - }, -}); -</script> - -<style lang="stylus" scoped> -.fvgwvorwhxigeolkkrcderjzcawqrscl - height 1.25em - vertical-align -0.25em - - &.custom - height 2.5em - vertical-align middle - transition transform 0.2s ease - - &:hover - transform scale(1.2) - - &.normal - height 1.25em - vertical-align -0.25em - - &:hover - transform none - -</style> diff --git a/src/client/app/common/views/components/error.vue b/src/client/app/common/views/components/error.vue deleted file mode 100644 index 0462a6efda..0000000000 --- a/src/client/app/common/views/components/error.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj"> - <p><fa icon="exclamation-triangle"/> {{ $t('@.error.title') }}</p> - <ui-button @click="() => $emit('retry')">{{ $t('@.error.retry') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n() -}); -</script> - -<style lang="stylus" scoped> -.wjqjnyhzogztorhrdgcpqlkxhkmuetgj - max-width 350px - margin 0 auto - padding 32px - text-align center - color var(--text) - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue deleted file mode 100644 index 3a9fe768d1..0000000000 --- a/src/client/app/common/views/components/file-type-icon.vue +++ /dev/null @@ -1,17 +0,0 @@ -<template> -<span class="mk-file-type-icon"> - <template v-if="kind == 'image'"><fa icon="file-image"/></template> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['type'], - computed: { - kind(): string { - return this.type.split('/')[0]; - } - } -}); -</script> diff --git a/src/client/app/common/views/components/follow-button.vue b/src/client/app/common/views/components/follow-button.vue deleted file mode 100644 index 074a0c05b6..0000000000 --- a/src/client/app/common/views/components/follow-button.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<button class="wfliddvnhxvyusikowhxozkyxyenqxqr" - :class="{ wait, block, inline, mini, transparent, active: isFollowing || hasPendingFollowRequestFromYou }" - @click="onClick" - :disabled="wait" - :inline="inline" -> - <template v-if="!wait"> - <fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template> - </template> - <template v-else><fa icon="spinner" pulse fixed-width/></template> -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/follow-button.vue'), - - props: { - user: { - type: Object, - required: true - }, - block: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default: false - }, - mini: { - type: Boolean, - required: false, - default: false - }, - transparent: { - type: Boolean, - required: false, - default: true - }, - }, - - data() { - return { - isFollowing: this.user.isFollowing, - hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, - wait: false, - connection: null - }; - }, - - computed: { - iconAndText(): any[] { - return ( - (this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] : - (this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['spinner', this.$t('follow-processing')] : - (this.isFollowing) ? ['minus', this.$t('following')] : - (!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] : - (!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] : - [] - ); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('follow', this.onFollowChange); - this.connection.on('unfollow', this.onFollowChange); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onFollowChange(user) { - if (user.id == this.user.id) { - this.isFollowing = user.isFollowing; - this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; - } - }, - - async onClick() { - this.wait = true; - - try { - if (this.isFollowing) { - const { canceled } = await this.$root.dialog({ - type: 'warning', - text: this.$t('@.unfollow-confirm', { name: this.user.name || this.user.username }), - showCancelButton: true - }); - - if (canceled) return; - - await this.$root.api('following/delete', { - userId: this.user.id - }); - } else { - if (this.hasPendingFollowRequestFromYou) { - await this.$root.api('following/requests/cancel', { - userId: this.user.id - }); - } else if (this.user.isLocked) { - await this.$root.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } else { - await this.$root.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } - } - } catch (e) { - console.error(e); - } finally { - this.wait = false; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.wfliddvnhxvyusikowhxozkyxyenqxqr - display block - user-select none - cursor pointer - padding 0 16px - margin 0 - min-width 100px - line-height 36px - font-size 14px - font-weight bold - color var(--primary) - background transparent - outline none - border solid 1px var(--primary) - border-radius 36px - - &:not(.transparent) - background #fff - - &.inline - display inline-block - - &.mini - padding 0 - min-width 0 - width 32px - height 32px - font-size 16px - border-radius 4px - line-height 32px - - &:focus - &:after - border-radius 8px - - &.block - width 100% - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 36px - - &:hover - background var(--primaryAlpha01) - - &:active - background var(--primaryAlpha02) - - &.active - color var(--primaryForeground) - background var(--primary) - - &:hover - background var(--primaryLighten10) - border-color var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - border-color var(--primaryDarken10) - - &.wait - cursor wait !important - opacity 0.7 - - * - pointer-events none - -</style> diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue deleted file mode 100644 index 328e3ca7b0..0000000000 --- a/src/client/app/common/views/components/forkit.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<a class="a" :href="repositoryUrl" rel="noopener" target="_blank" title="View source on GitHub"> - <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden"> - <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> - <path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path> - <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path> - </svg> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue' -export default Vue.extend({ - data() { - return { - repositoryUrl: 'https://github.com/syuilo/misskey' - }; - } -}); -</script> - -<style lang="stylus" scoped> -.a - display block - - > svg - display block - //fill #151513 - //color #fff - fill var(--primary) - color var(--primaryForeground) - - .octo-arm - transform-origin 130px 106px - - &:hover - .octo-arm - animation octocat-wave 560ms ease-in-out - - @keyframes octocat-wave - 0%, 100% - transform rotate(0) - 20%, 60% - transform rotate(-25deg) - 40%, 80% - transform rotate(10deg) - -</style> diff --git a/src/client/app/common/views/components/formula-core.vue b/src/client/app/common/views/components/formula-core.vue deleted file mode 100644 index 69697d6df0..0000000000 --- a/src/client/app/common/views/components/formula-core.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<div v-if="block" v-html="compiledFormula"></div> -<span v-else v-html="compiledFormula"></span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as katex from 'katex'; - -export default Vue.extend({ - props: { - formula: { - type: String, - required: true - }, - block: { - type: Boolean, - required: true - } - }, - computed: { - compiledFormula(): any { - return katex.renderToString(this.formula, { - throwOnError: false - } as any); - } - } -}); -</script> - -<style> -@import "../../../../../../node_modules/katex/dist/katex.min.css"; -</style> diff --git a/src/client/app/common/views/components/formula.vue b/src/client/app/common/views/components/formula.vue deleted file mode 100644 index 73572b72c6..0000000000 --- a/src/client/app/common/views/components/formula.vue +++ /dev/null @@ -1,24 +0,0 @@ -<template> -<x-formula :formula="formula" :block="block" /> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XFormula: () => import('./formula-core.vue').then(m => m.default) - }, - - props: { - formula: { - type: String, - required: true - }, - block: { - type: Boolean, - required: true - } - } -}); -</script> diff --git a/src/client/app/common/views/components/frac.vue b/src/client/app/common/views/components/frac.vue deleted file mode 100644 index 1840bd28fe..0000000000 --- a/src/client/app/common/views/components/frac.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<span class="mk-frac"><span>{{ pad }}</span><span>{{ value }} / {{ total }}</span></span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - value: { - type: Number, - required: true, - }, - total: { - type: Number, - required: true, - }, - }, - computed: { - pad(this: { - value: number; - total: number; - length(value: number): number; - }) { - return '0'.repeat(this.length(this.total) - this.length(this.value)); - }, - }, - methods: { - length(value: number) { - const string = value.toString(); - - return string.includes('e') ? -~string.substr(string.indexOf('e')) : string.length; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-frac - -webkit-font-feature-settings 'tnum' - -moz-font-feature-settings 'tnum' - font-feature-settings 'tnum' - font-variant-numeric tabular-nums - - > :first-child - visibility hidden -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue deleted file mode 100644 index a7c918aa71..0000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ /dev/null @@ -1,473 +0,0 @@ -<template> -<div class="xqnhankfuuilcwvhgsopeqncafzsquya"> - <button class="go-index" v-if="selfNav" @click="goIndex"><fa icon="arrow-left"/></button> - <header><b><router-link :to="blackUser | userPage"><mk-user-name :user="blackUser"/></router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage"><mk-user-name :user="whiteUser"/></router-link></b>({{ $t('@.reversi.white') }})</header> - - <div style="overflow: hidden; line-height: 28px;"> - <p class="turn" v-if="!iAmPlayer && !game.isEnded"> - <mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/> - <mk-ellipsis/> - </p> - <p class="turn" v-if="logPos != logs.length"> - <mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/> - </p> - <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p> - <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p> - <p class="result" v-if="game.isEnded && logPos == logs.length"> - <template v-if="game.winner"> - <mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :plain="true" :custom-emojis="game.winner.emojis"/> - <span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span> - </template> - <template v-else>{{ $t('@.reversi.drawn') }}</template> - </p> - </div> - - <div class="board"> - <div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - <div class="flex"> - <div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - <div class="cells" :style="cellsStyle"> - <div v-for="(stone, i) in o.board" - :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" - @click="set(i)" - :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> - <template v-if="$store.state.settings.gamesReversiUseAvatarStones"> - <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black"> - <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white"> - </template> - <template v-else> - <fa v-if="stone === true" :icon="fasCircle"/> - <fa v-if="stone === false" :icon="farCircle"/> - </template> - </div> - </div> - <div class="labels-y" v-if="this.$store.state.settings.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - </div> - <div class="labels-x" v-if="this.$store.state.settings.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - </div> - - <p class="status"><b>{{ $t('@.reversi.this-turn', { count: logPos }) }}</b> {{ $t('@.reversi.black') }}:{{ o.blackCount }} {{ $t('@.reversi.white') }}:{{ o.whiteCount }} {{ $t('@.reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p> - - <div class="actions" v-if="!game.isEnded && iAmPlayer"> - <form-button @click="surrender">{{ $t('surrender') }}</form-button> - </div> - - <div class="player" v-if="game.isEnded"> - <span>{{ logPos }} / {{ logs.length }}</span> - <ui-horizon-group> - <ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button> - <ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button> - <ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button> - <ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button> - </ui-horizon-group> - </div> - - <div class="info"> - <p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p> - <p v-if="game.loopedBoard">{{ $t('looped-map') }}</p> - <p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import * as CRC32 from 'crc-32'; -import Reversi, { Color } from '../../../../../../../games/reversi/core'; -import { url } from '../../../../../config'; -import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.game.vue'), - props: { - initGame: { - type: Object, - require: true - }, - connection: { - type: Object, - require: true - }, - selfNav: { - type: Boolean, - require: true - } - }, - - data() { - return { - game: null, - o: null as Reversi, - logs: [], - logPos: 0, - pollingClock: null, - faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle - }; - }, - - computed: { - iAmPlayer(): boolean { - if (!this.$store.getters.isSignedIn) return false; - return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id; - }, - - myColor(): Color { - if (!this.iAmPlayer) return null; - if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true; - if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true; - return false; - }, - - opColor(): Color { - if (!this.iAmPlayer) return null; - return this.myColor === true ? false : true; - }, - - blackUser(): any { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - }, - - whiteUser(): any { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - }, - - turnUser(): any { - if (this.o.turn === true) { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.turn === false) { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - return null; - } - }, - - isMyTurn(): boolean { - if (!this.iAmPlayer) return false; - if (this.turnUser == null) return false; - return this.turnUser.id == this.$store.state.i.id; - }, - - cellsStyle(): any { - return { - 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, - 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` - }; - } - }, - - watch: { - logPos(v) { - if (!this.game.isEnded) return; - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - for (const log of this.logs.slice(0, v)) { - this.o.put(log.color, log.pos); - } - this.$forceUpdate(); - } - }, - - created() { - this.game = this.initGame; - - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - this.o.put(log.color, log.pos); - } - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - // 通信を取りこぼしてもいいように定期的にポーリングさせる - if (this.game.isStarted && !this.game.isEnded) { - this.pollingClock = setInterval(() => { - if (this.game.isEnded) return; - const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); - this.connection.send('check', { - crc32: crc32 - }); - }, 3000); - } - }, - - mounted() { - this.connection.on('set', this.onSet); - this.connection.on('rescue', this.onRescue); - this.connection.on('ended', this.onEnded); - }, - - beforeDestroy() { - this.connection.off('set', this.onSet); - this.connection.off('rescue', this.onRescue); - this.connection.off('ended', this.onEnded); - - clearInterval(this.pollingClock); - }, - - methods: { - set(pos) { - if (this.game.isEnded) return; - if (!this.iAmPlayer) return; - if (!this.isMyTurn) return; - if (!this.o.canPut(this.myColor, pos)) return; - - this.o.put(this.myColor, pos); - - // サウンドを再生する - if (this.$store.state.device.enableSounds) { - const sound = new Audio(`${url}/assets/reversi-put-me.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - - this.connection.send('set', { - pos: pos - }); - - this.checkEnd(); - - this.$forceUpdate(); - }, - - onSet(x) { - this.logs.push(x); - this.logPos++; - this.o.put(x.color, x.pos); - this.checkEnd(); - this.$forceUpdate(); - - // サウンドを再生する - if (this.$store.state.device.enableSounds && x.color != this.myColor) { - const sound = new Audio(`${url}/assets/reversi-put-you.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - }, - - onEnded(x) { - this.game = x.game; - }, - - checkEnd() { - this.game.isEnded = this.o.isEnded; - if (this.game.isEnded) { - if (this.o.winner === true) { - this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id; - this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.winner === false) { - this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id; - this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - this.game.winnerId = null; - this.game.winner = null; - } - } - }, - - // 正しいゲーム情報が送られてきたとき - onRescue(game) { - this.game = game; - - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - this.o.put(log.color, log.pos, true); - } - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - this.checkEnd(); - this.$forceUpdate(); - }, - - surrender() { - this.$root.api('games/reversi/games/surrender', { - gameId: this.game.id - }); - }, - - goIndex() { - this.$emit('go-index'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.xqnhankfuuilcwvhgsopeqncafzsquya - text-align center - - > .go-index - position absolute - top 0 - left 0 - z-index 1 - width 42px - height 42px - - > header - padding 8px - border-bottom dashed 1px var(--reversiGameHeaderLine) - - a - color inherit - - > .board - width calc(100% - 16px) - max-width 500px - margin 0 auto - - $label-size = 16px - $gap = 4px - - > .labels-x - height $label-size - padding 0 $label-size - display flex - - > * - flex 1 - display flex - align-items center - justify-content center - font-size 12px - - &:first-child - margin-left -($gap / 2) - - &:last-child - margin-right -($gap / 2) - - > .flex - display flex - - > .labels-y - width $label-size - display flex - flex-direction column - - > * - flex 1 - display flex - align-items center - justify-content center - font-size 12px - - &:first-child - margin-top -($gap / 2) - - &:last-child - margin-bottom -($gap / 2) - - > .cells - flex 1 - display grid - grid-gap $gap - - > div - background transparent - border-radius 6px - overflow hidden - - * - pointer-events none - user-select none - - &.empty - border solid 2px var(--reversiGameEmptyCell) - - &.empty.can - background var(--reversiGameEmptyCell) - - &.empty.myTurn - border-color var(--reversiGameEmptyCellMyTurn) - - &.can - background var(--reversiGameEmptyCellCanPut) - cursor pointer - - &:hover - border-color var(--primaryDarken10) - background var(--primary) - - &:active - background var(--primaryDarken10) - - &.prev - box-shadow 0 0 0 4px var(--primaryAlpha07) - - &.isEnded - border-color var(--reversiGameEmptyCellMyTurn) - - &.none - border-color transparent !important - - > svg - display block - width 100% - height 100% - - > img - display block - width 100% - height 100% - - > .graph - display grid - grid-template-columns repeat(61, 1fr) - width 300px - height 38px - margin 0 auto 16px auto - - > div - &:not(:empty) - background #ccc - - > div:first-child - background #333 - - > div:last-child - background #ccc - - > .status - margin 0 - padding 16px 0 - - > .actions - padding-bottom 16px - - > .player - padding 0 16px 32px 16px - margin 0 auto - max-width 500px - - > span - display inline-block - margin 0 8px - min-width 70px - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue deleted file mode 100644 index 4099389502..0000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div> - <x-room v-if="!g.isStarted" :game="g" :connection="connection"/> - <x-game v-else :init-game="g" :connection="connection" :self-nav="selfNav" @go-index="goIndex"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import XGame from './reversi.game.vue'; -import XRoom from './reversi.room.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.gameroom.vue'), - components: { - XGame, - XRoom - }, - props: { - game: { - type: Object, - required: true - }, - selfNav: { - type: Boolean, - require: true - } - }, - data() { - return { - connection: null, - g: null - }; - }, - created() { - this.g = this.game; - this.connection = this.$root.stream.connectToChannel('gamesReversiGame', { - gameId: this.game.id - }); - this.connection.on('started', this.onStarted); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - onStarted(game) { - Object.assign(this.g, game); - this.$forceUpdate(); - }, - goIndex() { - this.$emit('go-index'); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue deleted file mode 100644 index 94e1d9a7e3..0000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ /dev/null @@ -1,245 +0,0 @@ -<template> -<div class="phgnkghfpyvkrvwiajkiuoxyrdaqpzcx"> - <h1>{{ $t('title') }}</h1> - <p>{{ $t('sub-title') }}</p> - <div class="play"> - <form-button primary round @click="match">{{ $t('invite') }}</form-button> - <details> - <summary>{{ $t('rule') }}</summary> - <div> - <p>{{ $t('rule-desc') }}</p> - <dl> - <dt><b>{{ $t('mode-invite') }}</b></dt> - <dd>{{ $t('mode-invite-desc') }}</dd> - </dl> - </div> - </details> - </div> - <section v-if="invitations.length > 0"> - <h2>{{ $t('invitations') }}</h2> - <div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)"> - <mk-avatar class="avatar" :user="i.parent"/> - <span class="name"><b><mk-user-name :user="i.parent"/></b></span> - <span class="username">@{{ i.parent.username }}</span> - <mk-time :time="i.createdAt"/> - </div> - </section> - <section v-if="myGames.length > 0"> - <h2>{{ $t('my-games') }}</h2> - <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`"> - <mk-avatar class="avatar" :user="g.user1"/> - <mk-avatar class="avatar" :user="g.user2"/> - <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span> - <span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span> - <mk-time :time="g.createdAt" /> - </a> - </section> - <section v-if="games.length > 0"> - <h2>{{ $t('all-games') }}</h2> - <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`"> - <mk-avatar class="avatar" :user="g.user1"/> - <mk-avatar class="avatar" :user="g.user2"/> - <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span> - <span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span> - <mk-time :time="g.createdAt" /> - </a> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.index.vue'), - data() { - return { - games: [], - gamesFetching: true, - gamesMoreFetching: false, - myGames: [], - matching: null, - invitations: [], - connection: null - }; - }, - - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('gamesReversi'); - - this.connection.on('invited', this.onInvited); - - this.$root.api('games/reversi/games', { - my: true - }).then(games => { - this.myGames = games; - }); - - this.$root.api('games/reversi/invitations').then(invitations => { - this.invitations = this.invitations.concat(invitations); - }); - } - - this.$root.api('games/reversi/games').then(games => { - this.games = games; - this.gamesFetching = false; - }); - }, - - beforeDestroy() { - if (this.connection) { - this.connection.dispose(); - } - }, - - methods: { - go(game) { - this.$emit('go', game); - }, - - async match() { - const { result: user } = await this.$root.dialog({ - title: this.$t('enter-username'), - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('games/reversi/match', { - userId: user.id - }).then(res => { - if (res == null) { - this.$emit('matching', user); - } else { - this.$emit('go', res); - } - }); - }, - - accept(invitation) { - this.$root.api('games/reversi/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.$emit('go', game); - } - }); - }, - - onInvited(invite) { - this.invitations.unshift(invite); - } - } -}); -</script> - -<style lang="stylus" scoped> -.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx - > h1 - margin 0 - padding 24px - font-size 24px - text-align center - font-weight normal - color #fff - background linear-gradient(to bottom, var(--reversiBannerGradientStart), var(--reversiBannerGradientEnd)) - - & + p - margin 0 - padding 12px - margin-bottom 12px - text-align center - font-size 14px - border-bottom solid 1px var(--faceDivider) - - > .play - margin 0 auto - padding 0 16px - max-width 500px - text-align center - - > details - margin 8px 0 - - > div - padding 16px - font-size 14px - text-align left - background var(--reversiDescBg) - border-radius 8px - - > section - margin 0 auto - padding 0 16px 16px 16px - max-width 500px - border-top solid 1px var(--faceDivider) - - > h2 - margin 0 - padding 16px 0 8px 0 - font-size 16px - font-weight bold - - .invitation - margin 8px 0 - padding 8px - color var(--text) - background var(--face) - box-shadow 0 2px 16px var(--reversiListItemShadow) - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:focus - border-color var(--primary) - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .avatar - width 32px - height 32px - border-radius 100% - - > span - margin 0 8px - line-height 32px - - .game - display block - margin 8px 0 - padding 8px - color var(--text) - background var(--face) - box-shadow 0 2px 16px var(--reversiListItemShadow) - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .avatar - width 32px - height 32px - border-radius 100% - - > span - margin 0 8px - line-height 32px - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue deleted file mode 100644 index c1657f49e5..0000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ /dev/null @@ -1,355 +0,0 @@ -<template> -<div class="urbixznjwwuukfsckrwzwsqzsxornqij"> - <header><b><mk-user-name :user="game.user1"/></b> vs <b><mk-user-name :user="game.user2"/></b></header> - - <div> - <p>{{ $t('settings-of-the-game') }}</p> - - <div class="card map"> - <header> - <select v-model="mapName" :placeholder="$t('choose-map')" @change="onMapChange"> - <option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/> - <option :label="$t('random')" :value="null"/> - <optgroup v-for="c in mapCategories" :key="c" :label="c"> - <option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option> - </optgroup> - </select> - </header> - - <div> - <div class="random" v-if="game.map == null"><fa icon="dice"/></div> - <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.map.join('')" - :data-none="x == ' '" - @click="onPixelClick(i, x)"> - <fa v-if="x == 'b'" :icon="fasCircle"/> - <fa v-if="x == 'w'" :icon="farCircle"/> - </div> - </div> - </div> - </div> - - <div class="card"> - <header> - <span>{{ $t('black-or-white') }}</span> - </header> - - <div> - <form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio> - <form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - <form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - </div> - </div> - - <div class="card"> - <header> - <span>{{ $t('rules') }}</span> - </header> - - <div> - <ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch> - <ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch> - <ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch> - </div> - </div> - - <div class="card form" v-if="form"> - <header> - <span>{{ $t('settings-of-the-bot') }}</span> - </header> - - <div> - <template v-for="item in form"> - <ui-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</ui-switch> - - <div class="card" v-if="item.type == 'radio'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <form-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @change="onChangeForm(item)">{{ r.label }}</form-radio> - </div> - </div> - - <div class="card" v-if="item.type == 'slider'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - - <div class="card" v-if="item.type == 'textbox'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - </template> - </div> - </div> - </div> - - <footer> - <p class="status"> - <template v-if="isAccepted && isOpAccepted">{{ $t('this-game-is-started-soon') }}<mk-ellipsis/></template> - <template v-if="isAccepted && !isOpAccepted">{{ $t('waiting-for-other') }}<mk-ellipsis/></template> - <template v-if="!isAccepted && isOpAccepted">{{ $t('waiting-for-me') }}</template> - <template v-if="!isAccepted && !isOpAccepted">{{ $t('waiting-for-both') }}<mk-ellipsis/></template> - </p> - - <div class="actions"> - <form-button @click="exit">{{ $t('cancel') }}</form-button> - <form-button primary @click="accept" v-if="!isAccepted">{{ $t('ready') }}</form-button> - <form-button primary @click="cancel" v-if="isAccepted">{{ $t('cancel-ready') }}</form-button> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import * as maps from '../../../../../../../games/reversi/maps'; -import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.room.vue'), - props: ['game', 'connection'], - - data() { - return { - o: null, - isLlotheo: false, - mapName: maps.eighteight.name, - maps: maps, - form: null, - messages: [], - fasCircle, farCircle - }; - }, - - computed: { - mapCategories(): string[] { - const categories = Object.values(maps).map(x => x.category); - return categories.filter((item, pos) => categories.indexOf(item) == pos); - }, - isAccepted(): boolean { - if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true; - return false; - }, - isOpAccepted(): boolean { - if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true; - return false; - } - }, - - created() { - this.connection.on('changeAccepts', this.onChangeAccepts); - this.connection.on('updateSettings', this.onUpdateSettings); - this.connection.on('initForm', this.onInitForm); - this.connection.on('message', this.onMessage); - - if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1; - if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2; - }, - - beforeDestroy() { - this.connection.off('changeAccepts', this.onChangeAccepts); - this.connection.off('updateSettings', this.onUpdateSettings); - this.connection.off('initForm', this.onInitForm); - this.connection.off('message', this.onMessage); - }, - - methods: { - exit() { - - }, - - accept() { - this.connection.send('accept', {}); - }, - - cancel() { - this.connection.send('cancelAccept', {}); - }, - - onChangeAccepts(accepts) { - this.game.user1Accepted = accepts.user1; - this.game.user2Accepted = accepts.user2; - this.$forceUpdate(); - }, - - updateSettings(key: string) { - this.connection.send('updateSettings', { - key: key, - value: this.game[key] - }); - }, - - onUpdateSettings({ key, value }) { - this.game[key] = value; - if (this.game.map == null) { - this.mapName = null; - } else { - const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); - this.mapName = found ? found.name : '-Custom-'; - } - }, - - onInitForm(x) { - if (x.userId == this.$store.state.i.id) return; - this.form = x.form; - }, - - onMessage(x) { - if (x.userId == this.$store.state.i.id) return; - this.messages.unshift(x.message); - }, - - onChangeForm(item) { - this.connection.send('updateForm', { - id: item.id, - value: item.value - }); - }, - - onMapChange() { - if (this.mapName == null) { - this.game.map = null; - } else { - this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; - } - this.$forceUpdate(); - this.updateSettings('map'); - }, - - onPixelClick(pos, pixel) { - const x = pos % this.game.map[0].length; - const y = Math.floor(pos / this.game.map[0].length); - const newPixel = - pixel == ' ' ? '-' : - pixel == '-' ? 'b' : - pixel == 'b' ? 'w' : - ' '; - const line = this.game.map[y].split(''); - line[x] = newPixel; - this.$set(this.game.map, y, line.join('')); - this.$forceUpdate(); - this.updateSettings('map'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.urbixznjwwuukfsckrwzwsqzsxornqij - text-align center - background var(--bg) - - > header - padding 8px - border-bottom dashed 1px #c4cdd4 - - > div - padding 0 16px - - > .card - margin 0 auto 16px auto - - &.map - > header - > select - width 100% - padding 12px 14px - background var(--face) - border 1px solid var(--reversiMapSelectBorder) - border-radius 4px - color var(--text) - cursor pointer - transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) - -webkit-appearance none - -moz-appearance none - appearance none - - &:hover - border-color var(--reversiMapSelectHoverBorder) - - &:focus - &:active - border-color var(--primary) - - > div - > .random - padding 32px 0 - font-size 64px - color var(--text) - opacity 0.7 - - > .board - display grid - grid-gap 4px - width 300px - height 300px - margin 0 auto - color var(--text) - - > div - background transparent - border solid 2px var(--faceDivider) - border-radius 6px - overflow hidden - cursor pointer - - * - pointer-events none - user-select none - width 100% - height 100% - - &[data-none] - border-color transparent - - &.form - > div - > .card + .card - margin-top 16px - - input[type='range'] - width 100% - - .card - max-width 400px - border-radius 4px - background var(--face) - color var(--text) - box-shadow 0 2px 12px 0 var(--reversiRoomFormShadow) - - > header - padding 18px 20px - border-bottom 1px solid var(--faceDivider) - - > div - padding 20px - color var(--text) - - > footer - position sticky - bottom 0 - padding 16px - background var(--reversiRoomFooterBg) - border-top solid 1px var(--faceDivider) - - > .status - margin 0 0 16px 0 - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue deleted file mode 100644 index d33471a049..0000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> -<div class="vchtoekanapleubgzioubdtmlkribzfd"> - <div v-if="game"> - <x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/> - </div> - <div class="matching" v-else-if="matching"> - <h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b><mk-user-name :user="matching"/></b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1> - <div class="cancel"> - <form-button round @click="cancel">{{ $t('matching.cancel') }}</form-button> - </div> - </div> - <div v-else-if="gameId"> - ... - </div> - <div class="index" v-else> - <x-index @go="nav" @matching="onMatching"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import XGameroom from './reversi.gameroom.vue'; -import XIndex from './reversi.index.vue'; -import Progress from '../../../../scripts/loading'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.vue'), - components: { - XGameroom, - XIndex - }, - - props: { - gameId: { - type: String, - required: false - }, - selfNav: { - type: Boolean, - require: false, - default: true - } - }, - - data() { - return { - game: null, - matching: null, - connection: null, - pingClock: null - }; - }, - - watch: { - game() { - this.$emit('gamed', this.game); - }, - - gameId() { - this.fetch(); - } - }, - - mounted() { - this.fetch(); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('gamesReversi'); - - this.connection.on('matched', this.onMatched); - - this.pingClock = setInterval(() => { - if (this.matching) { - this.connection.send('ping', { - id: this.matching.id - }); - } - }, 3000); - } - }, - - beforeDestroy() { - if (this.connection) { - this.connection.dispose(); - clearInterval(this.pingClock); - } - }, - - methods: { - fetch() { - if (this.gameId == null) { - this.game = null; - } else { - Progress.start(); - this.$root.api('games/reversi/games/show', { - gameId: this.gameId - }).then(game => { - this.game = game; - Progress.done(); - }); - } - }, - - async nav(game, actualNav = true) { - if (this.selfNav) { - // 受け取ったゲーム情報が省略されたものなら完全な情報を取得する - if (game != null && game.map == null) { - game = await this.$root.api('games/reversi/games/show', { - gameId: game.id - }); - } - - this.game = game; - } else { - this.$emit('nav', game, actualNav); - } - }, - - onMatching(user) { - this.matching = user; - }, - - cancel() { - this.matching = null; - this.$root.api('games/reversi/match/cancel'); - }, - - accept(invitation) { - this.$root.api('games/reversi/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.matching = null; - - this.nav(game); - } - }); - }, - - onMatched(game) { - this.matching = null; - this.game = game; - this.nav(game, false); - }, - - goIndex() { - this.nav(null); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vchtoekanapleubgzioubdtmlkribzfd - color var(--text) - background var(--bg) - - > .matching - > h1 - margin 0 - padding 24px - font-size 20px - text-align center - font-weight normal - - > .cancel - margin 0 auto - padding 24px 0 0 0 - max-width 200px - text-align center - border-top dashed 1px #c4cdd4 - -</style> diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue deleted file mode 100644 index 1e88147399..0000000000 --- a/src/client/app/common/views/components/google.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<div class="mk-google"> - <input type="search" v-model="query" :placeholder="q"> - <button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: ['q'], - data() { - return { - query: null - }; - }, - mounted() { - this.query = this.q; - }, - methods: { - search() { - const engine = this.$store.state.settings.webSearchEngine || - 'https://www.google.com/?#q={{query}}'; - const url = engine.replace('{{query}}', this.query) - window.open(url, '_blank'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-google - display flex - margin 8px 0 - - > input - flex-shrink 1 - padding 10px - width 100% - height 40px - font-size 16px - color var(--googleSearchFg) - background var(--googleSearchBg) - border solid 1px var(--googleSearchBorder) - border-radius 4px 0 0 4px - - &:hover - border-color var(--googleSearchHoverBorder) - - > button - flex-shrink 0 - padding 0 16px - border solid 1px var(--googleSearchBorder) - border-left none - border-radius 0 4px 4px 0 - - &:hover - background-color var(--googleSearchHoverButton) - - &:active - box-shadow 0 2px 4px rgba(#000, 0.15) inset - -</style> diff --git a/src/client/app/common/views/components/image-viewer.vue b/src/client/app/common/views/components/image-viewer.vue deleted file mode 100644 index 63b5e28d00..0000000000 --- a/src/client/app/common/views/components/image-viewer.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<ui-modal ref="modal" v-hotkey.global="keymap"> - <img :src="image.url" :alt="image.name" :title="image.name" @click="close" /> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['image'], - computed: { - keymap(): any { - return { - 'esc': this.close, - }; - } - }, - methods: { - close() { - (this.$refs.modal as any).close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -img - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 100% - max-height 100% - margin auto - cursor zoom-out - image-orientation from-image - -</style> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts deleted file mode 100644 index 88cd4931d4..0000000000 --- a/src/client/app/common/views/components/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import Vue from 'vue'; - -import dummy from './dummy.vue'; -import userName from './user-name.vue'; -import followButton from './follow-button.vue'; -import error from './error.vue'; -import noteSkeleton from './note-skeleton.vue'; -import instance from './instance.vue'; -import cwButton from './cw-button.vue'; -import tagCloud from './tag-cloud.vue'; -import trends from './trends.vue'; -import analogClock from './analog-clock.vue'; -import menu from './menu.vue'; -import noteHeader from './note-header.vue'; -import renote from './renote.vue'; -import signin from './signin.vue'; -import signup from './signup.vue'; -import forkit from './forkit.vue'; -import acct from './acct.vue'; -import avatar from './avatar.vue'; -import nav from './nav.vue'; -import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue'; -import poll from './poll.vue'; -import reactionIcon from './reaction-icon.vue'; -import reactionsViewer from './reactions-viewer.vue'; -import time from './time.vue'; -import mediaList from './media-list.vue'; -import uploader from './uploader.vue'; -import streamIndicator from './stream-indicator.vue'; -import ellipsis from './ellipsis.vue'; -import urlPreview from './url-preview.vue'; -import fileTypeIcon from './file-type-icon.vue'; -import emoji from './emoji.vue'; -import welcomeTimeline from './welcome-timeline.vue'; -import userList from './user-list.vue'; -import frac from './frac.vue'; -import uiInput from './ui/input.vue'; -import uiButton from './ui/button.vue'; -import uiHorizonGroup from './ui/horizon-group.vue'; -import uiCard from './ui/card.vue'; -import uiForm from './ui/form.vue'; -import uiTextarea from './ui/textarea.vue'; -import uiSwitch from './ui/switch.vue'; -import uiRadio from './ui/radio.vue'; -import uiSelect from './ui/select.vue'; -import uiInfo from './ui/info.vue'; -import uiMargin from './ui/margin.vue'; -import uiHr from './ui/hr.vue'; -import uiPagination from './ui/pagination.vue'; -import uiModal from './ui/modal.vue'; -import formButton from './ui/form/button.vue'; -import formRadio from './ui/form/radio.vue'; - -Vue.component('mfm', misskeyFlavoredMarkdown); -Vue.component('mk-dummy', dummy); -Vue.component('mk-user-name', userName); -Vue.component('mk-follow-button', followButton); -Vue.component('mk-error', error); -Vue.component('mk-note-skeleton', noteSkeleton); -Vue.component('mk-instance', instance); -Vue.component('mk-cw-button', cwButton); -Vue.component('mk-tag-cloud', tagCloud); -Vue.component('mk-trends', trends); -Vue.component('mk-analog-clock', analogClock); -Vue.component('mk-menu', menu); -Vue.component('mk-note-header', noteHeader); -Vue.component('mk-renote', renote); -Vue.component('mk-signin', signin); -Vue.component('mk-signup', signup); -Vue.component('mk-forkit', forkit); -Vue.component('mk-acct', acct); -Vue.component('mk-avatar', avatar); -Vue.component('mk-nav', nav); -Vue.component('mk-poll', poll); -Vue.component('mk-reaction-icon', reactionIcon); -Vue.component('mk-reactions-viewer', reactionsViewer); -Vue.component('mk-time', time); -Vue.component('mk-media-list', mediaList); -Vue.component('mk-uploader', uploader); -Vue.component('mk-stream-indicator', streamIndicator); -Vue.component('mk-ellipsis', ellipsis); -Vue.component('mk-url-preview', urlPreview); -Vue.component('mk-file-type-icon', fileTypeIcon); -Vue.component('mk-emoji', emoji); -Vue.component('mk-welcome-timeline', welcomeTimeline); -Vue.component('mk-user-list', userList); -Vue.component('mk-frac', frac); -Vue.component('ui-input', uiInput); -Vue.component('ui-button', uiButton); -Vue.component('ui-horizon-group', uiHorizonGroup); -Vue.component('ui-card', uiCard); -Vue.component('ui-form', uiForm); -Vue.component('ui-textarea', uiTextarea); -Vue.component('ui-switch', uiSwitch); -Vue.component('ui-radio', uiRadio); -Vue.component('ui-select', uiSelect); -Vue.component('ui-info', uiInfo); -Vue.component('ui-margin', uiMargin); -Vue.component('ui-hr', uiHr); -Vue.component('ui-pagination', uiPagination); -Vue.component('ui-modal', uiModal); -Vue.component('form-button', formButton); -Vue.component('form-radio', formRadio); diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue deleted file mode 100644 index 497e4976f5..0000000000 --- a/src/client/app/common/views/components/instance.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta"> - <div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div> - - <h1>{{ meta.name || 'Misskey' }}</h1> - <p v-html="meta.description || this.$t('@.about')"></p> - <router-link to="/">{{ $t('start') }}</router-link> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/instance.vue'), - data() { - return { - meta: null - } - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.nhasjydimbopojusarffqjyktglcuxjy - color var(--text) - background var(--face) - text-align center - - > .banner - height 100px - background-position center - background-size cover - - > h1 - margin 16px - font-size 16px - - > p - margin 16px - font-size 14px - - > a - display block - padding-bottom 16px - -</style> diff --git a/src/client/app/common/views/components/integrations.integration.vue b/src/client/app/common/views/components/integrations.integration.vue deleted file mode 100644 index 51995843b1..0000000000 --- a/src/client/app/common/views/components/integrations.integration.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<a class="zxrjzpcj" :href="url" :class="service" rel="noopener" target="_blank"> - <fa :icon="icon" size="lg" fixed-width /><span>{{ text }}</span> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['url', 'text', 'icon', 'service'] -}); -</script> - -<style lang="stylus" scoped> -.zxrjzpcj - display inline-block - padding 6px 8px 6px 6px - margin-top 4px - margin-bottom 4px - border-radius 32px - white-space nowrap - - &:hover - text-decoration none - - &.twitter - color #fff - background #1da1f3 - - &:hover - background #0c87cf - - &.github - color #fff - background #171515 - - &:hover - background #000 - - &.discord - color #fff - background #7289da - - &:hover - background #4968ce - -</style> diff --git a/src/client/app/common/views/components/integrations.vue b/src/client/app/common/views/components/integrations.vue deleted file mode 100644 index 7a341a14fd..0000000000 --- a/src/client/app/common/views/components/integrations.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<div class="nbogcrmo" :v-if="user.twitter || user.github || user.discord"> - <x-integration v-if="user.twitter" service="twitter" :url="`https://twitter.com/${user.twitter.screenName}`" :text="user.twitter.screenName" :icon="['fab', 'twitter']"/> - <x-integration v-if="user.github" service="github" :url="`https://github.com/${user.github.login}`" :text="user.github.login" :icon="['fab', 'github']"/> - <x-integration v-if="user.discord" service="discord" :url="`https://discordapp.com/users/${user.discord.id}`" :text="`${user.discord.username}#${user.discord.discriminator}`" :icon="['fab', 'discord']"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XIntegration from './integrations.integration.vue'; - -export default Vue.extend({ - components: { - XIntegration - }, - props: ['user'] -}); -</script> - -<style lang="stylus" scoped> -.nbogcrmo - > * - margin-right 10px - -</style> diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue deleted file mode 100644 index 4e459ad666..0000000000 --- a/src/client/app/common/views/components/media-banner.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<div class="mk-media-banner"> - <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false"> - <span class="icon"><fa icon="exclamation-triangle"/></span> - <b>{{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> - <div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'"> - <audio class="audio" - :src="media.url" - :title="media.name" - controls - ref="audio" - @volumechange="volumechange" - preload="metadata" /> - </div> - <a class="download" v-else - :href="media.url" - :title="media.name" - :download="media.name" - > - <span class="icon"><fa icon="download"/></span> - <b>{{ media.name }}</b> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/media-banner.vue'), - props: { - media: { - type: Object, - required: true - } - }, - data() { - return { - hide: true - }; - }, - mounted() { - const audioTag = this.$refs.audio as HTMLAudioElement; - if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume; - }, - methods: { - volumechange() { - const audioTag = this.$refs.audio as HTMLAudioElement; - this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume }); - }, - }, -}) -</script> - -<style lang="stylus" scoped> -.mk-media-banner - width 100% - border-radius 4px - margin-top 4px - overflow hidden - - > .download, - > .sensitive - display flex - align-items center - font-size 12px - padding 8px 12px - white-space nowrap - - > * - display block - - > b - overflow hidden - text-overflow ellipsis - - > *:not(:last-child) - margin-right .2em - - > .icon - font-size 1.6em - - > .download - background var(--noteAttachedFile) - - > .sensitive - background #111 - color #fff - - > .audio - .audio - display block - width 100% - -</style> diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue deleted file mode 100644 index b8b164aed0..0000000000 --- a/src/client/app/common/views/components/media-image.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else - :href="image.url" - :style="style" - :title="image.name" - @click.prevent="onClick" -> - <div v-if="image.type === 'image/gif'">GIF</div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import ImageViewer from './image-viewer.vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - i18n: i18n('common/views/components/media-image.vue'), - props: { - image: { - type: Object, - required: true - }, - raw: { - default: false - } - }, - data() { - return { - hide: true - }; - }, - computed: { - style(): any { - let url = `url(${ - this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.image.thumbnailUrl) - : this.image.thumbnailUrl - })`; - - if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) { - url = null; - } else if (this.raw || this.$store.state.device.loadRawImages) { - url = `url(${this.image.url})`; - } - - return { - 'background-color': this.image.properties.avgColor || 'transparent', - 'background-image': url - }; - } - }, - methods: { - onClick() { - const viewer = this.$root.new(ImageViewer, { - image: this.image - }); - this.$once('hook:beforeDestroy', () => { - viewer.close(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gqnyydlzavusgskkfvwvjiattxdzsqlf - display block - cursor zoom-in - overflow hidden - width 100% - height 100% - background-position center - background-size contain - background-repeat no-repeat - - > div - background-color var(--text) - border-radius 6px - color var(--secondary) - display inline-block - font-size 14px - font-weight bold - left 12px - opacity .5 - padding 0 6px - text-align center - top 12px - pointer-events none - -.qjewsnkgzzxlxtzncydssfbgjibiehcy - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > * - display block - -</style> diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue deleted file mode 100644 index bfbc9366d3..0000000000 --- a/src/client/app/common/views/components/media-list.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="mk-media-list"> - <template v-for="media in mediaList.filter(media => !previewable(media))"> - <x-banner :media="media" :key="media.id"/> - </template> - <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> - <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> - <template v-for="media in mediaList"> - <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> - <x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> - </template> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XBanner from './media-banner.vue'; -import XImage from './media-image.vue'; - -export default Vue.extend({ - components: { - XBanner, - XImage - }, - props: { - mediaList: { - required: true - }, - raw: { - default: false - } - }, - mounted() { - //#region for Safari bug - if (this.$refs.grid) { - this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` - : this.$store.state.device.inDeckMode ? '128px' : this.$root.isMobile ? '173px' : '287px'; - } - //#endregion - }, - methods: { - previewable(file) { - return (file.type.startsWith('video') || file.type.startsWith('image')) && file.thumbnailUrl; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-list - > .gird-container - width 100% - margin-top 4px - - &:before - content '' - display block - padding-top 56.25% // 16:9 - - > div - position absolute - top 0 - right 0 - bottom 0 - left 0 - display grid - grid-gap 4px - - > * - overflow hidden - border-radius 4px - - &[data-count="1"] - grid-template-rows 1fr - - &[data-count="2"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr - - &[data-count="3"] - grid-template-columns 1fr 0.5fr - grid-template-rows 1fr 1fr - - > *:nth-child(1) - grid-row 1 / 3 - - > *:nth-child(3) - grid-column 2 / 3 - grid-row 2 / 3 - - &[data-count="4"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr - - > *:nth-child(1) - grid-column 1 / 2 - grid-row 1 / 2 - - > *:nth-child(2) - grid-column 2 / 3 - grid-row 1 / 2 - - > *:nth-child(3) - grid-column 1 / 2 - grid-row 2 / 3 - - > *:nth-child(4) - grid-column 2 / 3 - grid-row 2 / 3 - -</style> diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue deleted file mode 100644 index 4e9f9e90d6..0000000000 --- a/src/client/app/common/views/components/mention.vue +++ /dev/null @@ -1,87 +0,0 @@ -<template> -<router-link class="ldlomzub" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')"> - <span class="me" v-if="isMe">{{ $t('@.you') }}</span> - <span class="main"> - <span class="username">@{{ username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span> - </span> -</router-link> -<a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else> - <span class="main"> - <span class="username">@{{ username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }">@{{ toUnicode(host) }}</span> - </span> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { toUnicode } from 'punycode'; -import { host as localHost } from '../../../config'; - -export default Vue.extend({ - i18n: i18n(), - props: { - username: { - type: String, - required: true - }, - host: { - type: String, - required: true - } - }, - data() { - return { - localHost - }; - }, - computed: { - url(): string { - switch (this.host) { - case 'twitter.com': - case 'github.com': - return `https://${this.host}/${this.username}`; - default: - return `/${this.canonical}`; - } - }, - canonical(): string { - return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`; - }, - isMe(): boolean { - return this.$store.getters.isSignedIn && ( - `@${this.username}@${toUnicode(this.host)}` === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase() - ); - } - }, - methods: { - toUnicode - } -}); -</script> - -<style lang="stylus" scoped> -.ldlomzub - color var(--mfmMention) - - > .me - pointer-events none - user-select none - padding 0 4px - background var(--mfmMention) - border solid var(--lineWidth) var(--mfmMention) - border-radius 4px 0 0 4px - color var(--mfmMentionForeground) - - & + .main - padding 0 4px - border solid var(--lineWidth) var(--mfmMention) - border-radius 0 4px 4px 0 - - > .main - > .host.fade - opacity 0.5 - -</style> diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue deleted file mode 100644 index 68fa0f5e62..0000000000 --- a/src/client/app/common/views/components/menu.vue +++ /dev/null @@ -1,196 +0,0 @@ -<template> -<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv" :class="{ isMobile: $root.isMobile }"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ bubble }" ref="popover"> - <template v-for="item, i in items"> - <div v-if="item === null"></div> - <button v-if="item" @click="clicked(item.action)" :tabindex="i"> - <fa v-if="item.icon" :icon="item.icon"/>{{ item.text }} - </button> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: { - source: { - required: true - }, - items: { - type: Array, - required: true - } - }, - data() { - return { - bubble: !this.$root.isMobile - }; - }, - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.$root.isMobile) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - this.bubble = false; - } - - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - this.bubble = false; - } - - if (top < 0) { - top = 0; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - clicked(fn) { - fn(); - this.close(); - }, - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => { - this.$emit('closed'); - this.destroyDom(); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.onchrpzrvnoruiaenfcqvccjfuupzzwv - $bg-color = var(--popupBg) - - position initial - - &.isMobile - > .popover - > button - font-size 15px - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - position absolute - z-index 10001 - padding 8px 0 - background $bg-color - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - $balloon-size = 16px - - &.bubble - margin-top $balloon-size - transform-origin center -($balloon-size) - - &:before - &:after - content "" - display block - position absolute - pointer-events none - - &:before - top -($balloon-size * 2) - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size $bg-color - - > button - display block - padding 8px 16px - width 100% - color var(--popupFg) - white-space nowrap - - &:hover - color var(--primaryForeground) - background var(--primary) - text-decoration none - - &:active - color var(--primaryForeground) - background var(--primaryDarken10) - - > [data-icon] - margin-right 4px - - > div - margin 8px 0 - height var(--lineWidth) - background var(--faceDivider) - -</style> diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue deleted file mode 100644 index bd63bab2c1..0000000000 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ /dev/null @@ -1,335 +0,0 @@ -<template> -<div class="mk-messaging-form" - @dragover.stop="onDragover" - @drop.stop="onDrop" -> - <textarea - v-model="text" - ref="textarea" - @keypress="onKeypress" - @paste="onPaste" - :placeholder="$t('input-message-here')" - v-autocomplete="{ model: 'text' }" - ></textarea> - <div class="file" @click="file = null" v-if="file">{{ file.name }}</div> - <mk-uploader ref="uploader" @uploaded="onUploaded"/> - <button class="send" @click="send" :disabled="!canSend || sending" :title="$t('send')"> - <template v-if="!sending"><fa icon="paper-plane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template> - </button> - <button class="attach-from-local" @click="chooseFile" :title="$t('attach-from-local')"> - <fa icon="upload"/> - </button> - <button class="attach-from-drive" @click="chooseFileFromDrive" :title="$t('attach-from-drive')"> - <fa :icon="['far', 'folder-open']"/> - </button> - <input ref="file" type="file" @change="onChangeFile"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as autosize from 'autosize'; -import { formatTimeString } from '../../../../../misc/format-time-string'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.form.vue'), - props: { - user: { - type: Object, - requird: false, - }, - group: { - type: Object, - requird: false, - }, - }, - data() { - return { - text: null, - file: null, - sending: false - }; - }, - computed: { - draftId(): string { - return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; - }, - canSend(): boolean { - return (this.text != null && this.text != '') || this.file != null; - }, - room(): any { - return this.$parent; - } - }, - watch: { - text() { - this.saveDraft(); - }, - file() { - this.saveDraft(); - - if (this.room.isBottom()) { - this.room.scrollToBottom(); - } - } - }, - mounted() { - autosize(this.$refs.textarea); - - // 書きかけの投稿を復元 - const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId]; - if (draft) { - this.text = draft.data.text; - this.file = draft.data.file; - } - }, - methods: { - async onPaste(e: ClipboardEvent) { - const data = e.clipboardData; - const items = data.items; - - if (items.length == 1) { - if (items[0].kind == 'file') { - const file = items[0].getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`; - const name = this.$store.state.settings.pasteDialog - ? await this.$root.dialog({ - title: this.$t('@.post-form.enter-file-name'), - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); - } - } else { - if (items[0].kind == 'file') { - this.$root.dialog({ - type: 'error', - text: this.$t('only-one-file-attached') - }); - } - } - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - if (isFile || isDriveFile) { - e.preventDefault(); - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDrop(e): void { - // ファイルだったら - if (e.dataTransfer.files.length == 1) { - e.preventDefault(); - this.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - e.preventDefault(); - this.$root.dialog({ - type: 'error', - text: this.$t('only-one-file-attached') - }); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - this.file = JSON.parse(driveFile); - e.preventDefault(); - } - //#endregion - }, - - onKeypress(e) { - if ((e.which == 10 || e.which == 13) && e.ctrlKey && this.canSend) { - this.send(); - } - }, - - chooseFile() { - (this.$refs.file as any).click(); - }, - - chooseFileFromDrive() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - this.file = file; - }); - }, - - onChangeFile() { - this.upload((this.$refs.file as any).files[0]); - }, - - upload(file: File, name?: string) { - (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name); - }, - - onUploaded(file) { - this.file = file; - }, - - send() { - this.sending = true; - this.$root.api('messaging/messages/create', { - userId: this.user ? this.user.id : undefined, - groupId: this.group ? this.group.id : undefined, - text: this.text ? this.text : undefined, - fileId: this.file ? this.file.id : undefined - }).then(message => { - this.clear(); - }).catch(err => { - console.error(err); - }).then(() => { - this.sending = false; - }); - }, - - clear() { - this.text = ''; - this.file = null; - this.deleteDraft(); - }, - - saveDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - data[this.draftId] = { - updatedAt: new Date(), - data: { - text: this.text, - file: this.file - } - } - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - - deleteDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - delete data[this.draftId]; - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging-form - > textarea - cursor auto - display block - width 100% - min-width 100% - max-width 100% - height 64px - margin 0 - padding 8px - resize none - font-size 1em - color var(--inputText) - outline none - border none - border-top solid 1px var(--faceDivider) - border-radius 0 - box-shadow none - background transparent - - > .file - padding 8px - color #444 - background #eee - cursor pointer - - > .send - position absolute - bottom 0 - right 0 - margin 0 - padding 10px 14px - font-size 1em - color #aaa - transition color 0.1s ease - - &:hover - color var(--primary) - - &:active - color var(--primaryDarken10) - transition color 0s ease - - .files - display block - margin 0 - padding 0 8px - list-style none - - &:after - content '' - display block - clear both - - > li - display block - float left - margin 4px - padding 0 - width 64px - height 64px - background-color #eee - background-repeat no-repeat - background-position center center - background-size cover - cursor move - - &:hover - > .remove - display block - - > .remove - display none - position absolute - right -6px - top -6px - margin 0 - padding 0 - background transparent - outline none - border none - border-radius 0 - box-shadow none - cursor pointer - - .attach-from-local - .attach-from-drive - margin 0 - padding 10px 14px - font-size 1em - font-weight normal - text-decoration none - color #aaa - transition color 0.1s ease - - &:hover - color var(--primary) - - &:active - color var(--primaryDarken10) - transition color 0s ease - - input[type=file] - display none - -</style> diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue deleted file mode 100644 index 1ab6359415..0000000000 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div class="message" :data-is-me="isMe"> - <mk-avatar class="avatar" :user="message.user" target="_blank"/> - <div class="content"> - <div class="balloon" :data-no-text="message.text == null"> - <button class="delete-button" v-if="isMe" :title="$t('@.delete')" @click="del"> - <img src="/assets/desktop/remove.png" alt="Delete"/> - </button> - <div class="content" v-if="!message.isDeleted"> - <mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> - <div class="file" v-if="message.file"> - <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> - <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name" - :style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/> - <p v-else>{{ message.file.name }}</p> - </a> - </div> - </div> - <div class="content" v-else> - <p class="is-deleted">{{ $t('deleted') }}</p> - </div> - </div> - <div></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <footer> - <template v-if="isGroup"> - <span class="read" v-if="message.reads.length > 0">{{ $t('is-read') }} {{ message.reads.length }}</span> - </template> - <template v-else> - <span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span> - </template> - <mk-time :time="message.createdAt"/> - <template v-if="message.is_edited"><fa icon="pencil-alt"/></template> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { parse } from '../../../../../mfm/parse'; -import { unique } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.message.vue'), - props: { - message: { - required: true - }, - isGroup: { - required: false - } - }, - computed: { - isMe(): boolean { - return this.message.userId == this.$store.state.i.id; - }, - urls(): string[] { - if (this.message.text) { - const ast = parse(this.message.text); - return unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - } else { - return null; - } - } - }, - methods: { - del() { - this.$root.api('messaging/messages/delete', { - messageId: this.message.id - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.message - $me-balloon-color = var(--primary) - - padding 10px 12px 10px 12px - background-color transparent - - > .avatar - display block - position absolute - top 10px - width 54px - height 54px - border-radius 8px - transition all 0.1s ease - - > .content - - > .balloon - display flex - align-items center - padding 0 - max-width calc(100% - 16px) - min-height 38px - border-radius 16px - - &:before - content "" - pointer-events none - display block - position absolute - top 12px - - & + * - clear both - - &:hover - > .delete-button - display block - - > .delete-button - display none - position absolute - z-index 1 - top -4px - right -4px - margin 0 - padding 0 - cursor pointer - outline none - border none - border-radius 0 - box-shadow none - background transparent - - > img - vertical-align bottom - width 16px - height 16px - cursor pointer - - > .content - max-width 100% - - > .is-deleted - display block - margin 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(#000, 0.5) - - > .text - display block - margin 0 - padding 8px 16px - overflow hidden - overflow-wrap break-word - word-break break-word - font-size 1em - color rgba(#000, 0.8) - - & + .file - > a - border-radius 0 0 16px 16px - - > .file - > a - display block - max-width 100% - border-radius 16px - overflow hidden - text-decoration none - - &:hover - text-decoration none - - > p - background #ccc - - > * - display block - margin 0 - width 100% - max-height 512px - object-fit contain - - > p - padding 30px - text-align center - color #555 - background #ddd - - > .mk-url-preview - margin 8px 0 - - > footer - display block - margin 2px 0 0 0 - font-size 10px - color var(--messagingRoomMessageInfo) - - > .read - margin 0 8px - - > [data-icon] - margin-left 4px - - &:not([data-is-me]) - > .avatar - left 12px - - > .content - padding-left 66px - - > .balloon - $color = var(--messagingRoomMessageBg) - float left - background $color - - &[data-no-text] - background transparent - - &:not([data-no-text]):before - left -14px - border-top solid 8px transparent - border-right solid 8px $color - border-bottom solid 8px transparent - border-left solid 8px transparent - - > .content - > .text - color var(--messagingRoomMessageFg) - - > footer - text-align left - - &[data-is-me] - > .avatar - right 12px - - > .content - padding-right 66px - - > .balloon - float right - background $me-balloon-color - - &[data-no-text] - background transparent - - &:not([data-no-text]):before - right -14px - left auto - border-top solid 8px transparent - border-right solid 8px transparent - border-bottom solid 8px transparent - border-left solid 8px $me-balloon-color - - > .content - - > p.is-deleted - color rgba(#fff, 0.5) - - > .text >>> - &, * - color #fff !important - - > footer - text-align right - - > .read - user-select none - - &[data-is-deleted] - > .balloon - opacity 0.5 - -</style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue deleted file mode 100644 index d5fa4143a0..0000000000 --- a/src/client/app/common/views/components/messaging-room.vue +++ /dev/null @@ -1,436 +0,0 @@ -<template> -<div class="mk-messaging-room" - @dragover.prevent.stop="onDragover" - @drop.prevent.stop="onDrop" -> - <div class="body"> - <p class="init" v-if="init"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}</p> - <p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p> - <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p> - <button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> - <template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} - </button> - <template v-for="(message, i) in _messages"> - <x-message :message="message" :key="message.id" :is-group="group != null"/> - <p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> - <span>{{ _messages[i + 1]._datetext }}</span> - </p> - </template> - </div> - <footer> - <transition name="fade"> - <div class="new-message" v-show="showIndicator"> - <button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> - </div> - </transition> - <x-form :user="user" :group="group" ref="form"/> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XMessage from './messaging-room.message.vue'; -import XForm from './messaging-room.form.vue'; -import { url } from '../../../config'; -import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.vue'), - - components: { - XMessage, - XForm - }, - - props: { - user: { - type: Object, - requird: false, - }, - group: { - type: Object, - requird: false, - }, - isNaked: { - type: Boolean, - requird: false, - }, - }, - - data() { - return { - init: true, - fetchingMoreMessages: false, - messages: [], - existMoreMessages: false, - connection: null, - showIndicator: false, - timer: null, - faArrowCircleDown, faFlag - }; - }, - - computed: { - _messages(): any[] { - return (this.messages as any).map(message => { - const date = new Date(message.createdAt).getDate(); - const month = new Date(message.createdAt).getMonth() + 1; - message._date = date; - message._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return message; - }); - }, - - form(): any { - return this.$refs.form; - } - }, - - mounted() { - this.connection = this.$root.stream.connectToChannel('messaging', { - otherparty: this.user ? this.user.id : undefined, - group: this.group ? this.group.id : undefined, - }); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - this.connection.on('deleted', this.onDeleted); - - if (this.isNaked) { - window.addEventListener('scroll', this.onScroll, { passive: true }); - } else { - this.$el.addEventListener('scroll', this.onScroll, { passive: true }); - } - - document.addEventListener('visibilitychange', this.onVisibilitychange); - - this.fetchMessages().then(() => { - this.init = false; - this.scrollToBottom(); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - - if (this.isNaked) { - window.removeEventListener('scroll', this.onScroll); - } else { - this.$el.removeEventListener('scroll', this.onScroll); - } - - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, - - methods: { - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - - if (isFile || isDriveFile) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, - - onDrop(e): void { - // ファイルだったら - if (e.dataTransfer.files.length == 1) { - this.form.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - this.$root.dialog({ - type: 'error', - text: this.$t('only-one-file-attached') - }); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.form.file = file; - } - //#endregion - }, - - fetchMessages() { - return new Promise((resolve, reject) => { - const max = this.existMoreMessages ? 20 : 10; - - this.$root.api('messaging/messages', { - userId: this.user ? this.user.id : undefined, - groupId: this.group ? this.group.id : undefined, - limit: max + 1, - untilId: this.existMoreMessages ? this.messages[0].id : undefined - }).then(messages => { - if (messages.length == max + 1) { - this.existMoreMessages = true; - messages.pop(); - } else { - this.existMoreMessages = false; - } - - this.messages.unshift.apply(this.messages, messages.reverse()); - resolve(); - }); - }); - }, - - fetchMoreMessages() { - this.fetchingMoreMessages = true; - this.fetchMessages().then(() => { - this.fetchingMoreMessages = false; - }); - }, - - onMessage(message) { - // サウンドを再生する - if (this.$store.state.device.enableSounds) { - const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - - const isBottom = this.isBottom(); - - this.messages.push(message); - if (message.userId != this.$store.state.i.id && !document.hidden) { - this.connection.send('read', { - id: message.id - }); - } - - if (isBottom) { - // Scroll to bottom - this.$nextTick(() => { - this.scrollToBottom(); - }); - } else if (message.userId != this.$store.state.i.id) { - // Notify - this.notifyNewMessage(); - } - }, - - onRead(x) { - if (this.user) { - if (!Array.isArray(x)) x = [x]; - for (const id of x) { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist].isRead = true; - } - } - } else if (this.group) { - for (const id of x.ids) { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist].reads.push(x.userId); - } - } - } - }, - - onDeleted(id) { - const msg = this.messages.find(m => m.id === id); - if (msg) { - this.messages = this.messages.filter(m => m.id !== msg.id); - } - }, - - isBottom() { - const asobi = 64; - const current = this.isNaked - ? window.scrollY + window.innerHeight - : this.$el.scrollTop + this.$el.offsetHeight; - const max = this.isNaked - ? document.body.offsetHeight - : this.$el.scrollHeight; - return current > (max - asobi); - }, - - scrollToBottom() { - if (this.isNaked) { - window.scroll(0, document.body.offsetHeight); - } else { - this.$el.scrollTop = this.$el.scrollHeight; - } - }, - - onIndicatorClick() { - this.showIndicator = false; - this.scrollToBottom(); - }, - - notifyNewMessage() { - this.showIndicator = true; - - if (this.timer) clearTimeout(this.timer); - - this.timer = setTimeout(() => { - this.showIndicator = false; - }, 4000); - }, - - onScroll() { - const el = this.isNaked ? window.document.documentElement : this.$el; - const current = el.scrollTop + el.clientHeight; - if (current > el.scrollHeight - 1) { - this.showIndicator = false; - } - }, - - onVisibilitychange() { - if (document.hidden) return; - for (const message of this.messages) { - if (message.userId !== this.$store.state.i.id && !message.isRead) { - this.connection.send('read', { - id: message.id - }); - } - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging-room - background var(--messagingRoomBg) - - > .body - width 100% - max-width 600px - margin 0 auto - min-height calc(100% - 103px) - - > .init, - > .empty - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color var(--messagingRoomInfo) - opacity 0.5 - - [data-icon] - margin-right 4px - - > .no-history - display block - margin 0 - padding 16px - text-align center - font-size 0.8em - color var(--messagingRoomInfo) - opacity 0.5 - - [data-icon] - margin-right 4px - - > .more - display block - margin 16px auto - padding 0 12px - line-height 24px - color #fff - background rgba(#000, 0.3) - border-radius 12px - - &:hover - background rgba(#000, 0.4) - - &:active - background rgba(#000, 0.5) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - - > .message - // something - - > .date - display block - margin 8px 0 - text-align center - - &:before - content '' - display block - position absolute - height 1px - width 90% - top 16px - left 0 - right 0 - margin 0 auto - background var(--messagingRoomDateDividerLine) - - > span - display inline-block - margin 0 - padding 0 16px - //font-weight bold - line-height 32px - color var(--messagingRoomDateDividerText) - background var(--messagingRoomBg) - - > footer - position -webkit-sticky - position sticky - z-index 2 - bottom 0 - width 100% - max-width 600px - margin 0 auto - padding 0 - background var(--messagingRoomBg) - background-clip content-box - - > .new-message - position absolute - top -48px - width 100% - padding 8px 0 - text-align center - - > button - display inline-block - margin 0 - padding 0 12px 0 30px - cursor pointer - line-height 32px - font-size 12px - color var(--primaryForeground) - background var(--primary) - border-radius 16px - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > i - position absolute - top 0 - left 10px - line-height 32px - font-size 16px - -.fade-enter-active, .fade-leave-active - transition opacity 0.1s - -.fade-enter, .fade-leave-to - transition opacity 0.5s - opacity 0 - -</style> diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue deleted file mode 100644 index 52f55e4333..0000000000 --- a/src/client/app/common/views/components/messaging.vue +++ /dev/null @@ -1,500 +0,0 @@ -<template> -<div class="mk-messaging" :data-compact="compact"> - <div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }"> - <div class="form"> - <label for="search-input"><i><fa icon="search"/></i></label> - <input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" :placeholder="$t('search-user')"/> - </div> - <div class="result"> - <ol class="users" v-if="result.length > 0" ref="searchResult"> - <li v-for="(user, i) in result" - @keydown.enter="navigate(user)" - @keydown="onSearchResultKeydown(i)" - @click="navigate(user)" - tabindex="-1" - > - <mk-avatar class="avatar" :user="user" :key="user.id"/> - <span class="name"><mk-user-name :user="user" :key="user.id"/></span> - <span class="username">@{{ user | acct }}</span> - </li> - </ol> - </div> - </div> - <div class="history" v-if="messages.length > 0"> - <a v-for="message in messages" - class="user" - :href="message.groupId ? `/i/messaging/group/${message.groupId}` : `/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-is-me="isMe(message)" - :data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead" - @click.prevent="message.groupId ? navigateGroup(message.group) : navigate(isMe(message) ? message.recipient : message.user)" - :key="message.id" - > - <div> - <mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> - <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> - <mk-time :time="message.createdAt"/> - </header> - <header v-else> - <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> - <mk-time :time="message.createdAt"/> - </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> - </div> - </div> - </a> - </div> - <p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <ui-margin> - <ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button> - <ui-button @click="startGroup()"><fa :icon="faUsers"/> {{ $t('start-with-group') }}</ui-button> - </ui-margin> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging.vue'), - props: { - compact: { - type: Boolean, - default: false - }, - headerTop: { - type: Number, - default: 0 - } - }, - data() { - return { - fetching: true, - moreFetching: false, - messages: [], - q: null, - result: [], - connection: null, - faUser, faUsers - }; - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('messagingIndex'); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - this.$root.api('messaging/history', { group: false }).then(userMessages => { - this.$root.api('messaging/history', { group: true }).then(groupMessages => { - const messages = userMessages.concat(groupMessages); - messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - this.messages = messages; - this.fetching = false; - }); - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - getAcct, - isMe(message) { - return message.userId == this.$store.state.i.id; - }, - onMessage(message) { - if (message.recipientId) { - this.messages = this.messages.filter(m => !( - (m.recipientId == message.recipientId && m.userId == message.userId) || - (m.recipientId == message.userId && m.userId == message.recipientId))); - - this.messages.unshift(message); - } else if (message.groupId) { - this.messages = this.messages.filter(m => m.groupId !== message.groupId); - this.messages.unshift(message); - } - }, - onRead(ids) { - for (const id of ids) { - const found = this.messages.find(m => m.id == id); - if (found) { - if (found.recipientId) { - found.isRead = true; - } else if (found.groupId) { - found.reads.push(this.$store.state.i.id); - } - } - } - }, - search() { - if (this.q == '') { - this.result = []; - return; - } - this.$root.api('users/search', { - query: this.q, - localOnly: false, - limit: 10, - detail: false - }).then(users => { - this.result = users.filter(user => user.id != this.$store.state.i.id); - }); - }, - navigate(user) { - this.$emit('navigate', user); - }, - navigateGroup(group) { - this.$emit('navigateGroup', group); - }, - onSearchKeydown(e) { - switch (e.which) { - case 9: // [TAB] - case 40: // [↓] - e.preventDefault(); - e.stopPropagation(); - (this.$refs.searchResult as any).childNodes[0].focus(); - break; - } - }, - onSearchResultKeydown(i, e) { - const list = this.$refs.searchResult as any; - - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (true) { - case e.which == 27: // [ESC] - cancel(); - (this.$refs.search as any).focus(); - break; - - case e.which == 9 && e.shiftKey: // [TAB] + [Shift] - case e.which == 38: // [↑] - cancel(); - (list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus(); - break; - - case e.which == 9: // [TAB] - case e.which == 40: // [↓] - cancel(); - (list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); - break; - } - }, - async startUser() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.navigate(user); - }, - async startGroup() { - const groups1 = await this.$root.api('users/groups/owned'); - const groups2 = await this.$root.api('users/groups/joined'); - const { canceled, result: group } = await this.$root.dialog({ - type: null, - title: this.$t('select-group'), - select: { - items: groups1.concat(groups2).map(group => ({ - value: group, text: group.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - this.navigateGroup(group); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging - - &[data-compact] - font-size 0.8em - - > .history - > a - &:last-child - border-bottom none - - &:not([data-is-me]):not([data-is-read]) - > div - background-image none - border-left solid 4px #3aa2dc - - > div - padding 16px - - > header - > .mk-time - font-size 1em - - > .avatar - width 42px - height 42px - margin 0 12px 0 0 - - > .search - display block - position -webkit-sticky - position sticky - top 0 - left 0 - z-index 1 - width 100% - box-shadow 0 0 2px rgba(#000, 0.2) - - > .form - background rgba(0, 0, 0, 0.02) - - > label - display block - position absolute - top 0 - left 8px - z-index 1 - height 100% - width 38px - pointer-events none - - > i - display block - position absolute - top 0 - right 0 - bottom 0 - left 0 - width 1em - line-height 48px - margin auto - color #555 - - > input - margin 0 - padding 0 0 0 42px - width 100% - font-size 1em - line-height 48px - color var(--faceText) - outline none - background transparent - border none - border-radius 5px - box-shadow none - - > .result - display block - top 0 - left 0 - z-index 2 - width 100% - margin 0 - padding 0 - background #fff - - > .users - margin 0 - padding 0 - list-style none - - > li - display inline-block - z-index 1 - width 100% - padding 8px 32px - vertical-align top - white-space nowrap - overflow hidden - color rgba(#000, 0.8) - text-decoration none - transition none - cursor pointer - - &:hover - &:focus - color #fff - background var(--primary) - - .name - color #fff - - .username - color #fff - - &:active - color #fff - background var(--primaryDarken10) - - .name - color #fff - - .username - color #fff - - .avatar - vertical-align middle - min-width 32px - min-height 32px - max-width 32px - max-height 32px - margin 0 8px 0 0 - border-radius 6px - - .name - margin 0 8px 0 0 - /*font-weight bold*/ - font-weight normal - color rgba(#000, 0.8) - - .username - font-weight normal - color rgba(#000, 0.3) - - > .history - > a - display block - text-decoration none - background var(--face) - border-bottom solid 1px var(--faceDivider) - - * - pointer-events none - user-select none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - .avatar - filter saturate(200%) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - &[data-is-read] - &[data-is-me] - opacity 0.8 - - &:not([data-is-me]):not([data-is-read]) - > div - background-image url("/assets/unread.svg") - background-repeat no-repeat - background-position 0 center - - &:after - content "" - display block - clear both - - > div - max-width 500px - margin 0 auto - padding 20px 30px - - &:after - content "" - display block - clear both - - > header - display flex - align-items center - margin-bottom 2px - white-space nowrap - overflow hidden - - > .name - margin 0 - padding 0 - overflow hidden - text-overflow ellipsis - font-size 1em - color var(--noteHeaderName) - font-weight bold - transition all 0.1s ease - - > .username - margin 0 8px - color var(--noteHeaderAcct) - - > .mk-time - margin 0 0 0 auto - color var(--noteHeaderInfo) - font-size 80% - - > .avatar - float left - width 54px - height 54px - margin 0 16px 0 0 - border-radius 8px - transition all 0.1s ease - - > .body - - > .text - display block - margin 0 0 0 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1.1em - color var(--faceText) - - .me - opacity 0.7 - - > .image - display block - max-width 100% - max-height 512px - - > .no-history - margin 0 - padding 2em 1em - text-align center - color #999 - font-weight 500 - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - - // TODO: element base media query - @media (max-width 400px) - > .search - > .result - > .users - > li - padding 8px 16px - - > .history - > a - &:not([data-is-me]):not([data-is-read]) - > div - background-image none - border-left solid 4px #3aa2dc - - > div - padding 16px - font-size 14px - - > .avatar - margin 0 12px 0 0 - -</style> diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts deleted file mode 100644 index 561c3d8e30..0000000000 --- a/src/client/app/common/views/components/mfm.ts +++ /dev/null @@ -1,321 +0,0 @@ -import Vue, { VNode } from 'vue'; -import { length } from 'stringz'; -import { MfmForest } from '../../../../../mfm/types'; -import { parse, parsePlain } from '../../../../../mfm/parse'; -import MkUrl from './url.vue'; -import MkMention from './mention.vue'; -import { concat, sum } from '../../../../../prelude/array'; -import MkFormula from './formula.vue'; -import MkCode from './code.vue'; -import MkGoogle from './google.vue'; -import { host } from '../../../config'; -import { preorderF, countNodesF } from '../../../../../prelude/tree'; - -function sumTextsLength(ts: MfmForest): number { - const textNodes = preorderF(ts).filter(n => n.type === 'text'); - return sum(textNodes.map(x => length(x.props.text))); -} - -export default Vue.component('misskey-flavored-markdown', { - props: { - text: { - type: String, - required: true - }, - plain: { - type: Boolean, - default: false - }, - nowrap: { - type: Boolean, - default: false - }, - author: { - type: Object, - default: null - }, - i: { - type: Object, - default: null - }, - customEmojis: { - required: false, - }, - isNote: { - type: Boolean, - default: true - }, - }, - - render(createElement) { - if (this.text == null || this.text == '') return; - - const ast = (this.plain ? parsePlain : parse)(this.text); - - let bigCount = 0; - let motionCount = 0; - - const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => { - switch (token.node.type) { - case 'text': { - const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n'); - - if (!this.plain) { - const x = text.split('\n') - .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); - x[x.length - 1].pop(); - return x; - } else { - return [createElement('span', text.replace(/\n/g, ' '))]; - } - } - - case 'bold': { - return [createElement('b', genEl(token.children))]; - } - - case 'strike': { - return [createElement('del', genEl(token.children))]; - } - - case 'italic': { - return (createElement as any)('i', { - attrs: { - style: 'font-style: oblique;' - }, - }, genEl(token.children)); - } - - case 'big': { - bigCount++; - const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5; - const isMany = bigCount > 3; - return (createElement as any)('strong', { - attrs: { - style: `display: inline-block; font-size: ${ isMany ? '100%' : '150%' };` - }, - directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { - name: 'animate-css', - value: { classes: 'tada', iteration: 'infinite' } - }] - }, genEl(token.children)); - } - - case 'small': { - return [createElement('small', { - attrs: { - style: 'opacity: 0.7;' - }, - }, genEl(token.children))]; - } - - case 'center': { - return [createElement('div', { - attrs: { - style: 'text-align:center;' - } - }, genEl(token.children))]; - } - - case 'motion': { - motionCount++; - const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block;' - }, - directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { - name: 'animate-css', - value: { classes: 'rubberBand', iteration: 'infinite' } - }] - }, genEl(token.children)); - } - - case 'spin': { - motionCount++; - const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; - const direction = - token.node.props.attr == 'left' ? 'reverse' : - token.node.props.attr == 'alternate' ? 'alternate' : - 'normal'; - const style = (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) - ? '' - : `animation: spin 1.5s linear infinite; animation-direction: ${direction};`; - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block;' + style - }, - }, genEl(token.children)); - } - - case 'jump': { - motionCount++; - const isLong = sumTextsLength(token.children) > 30 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; - return (createElement as any)('span', { - attrs: { - style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;' - }, - }, genEl(token.children)); - } - - case 'flip': { - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block; transform: scaleX(-1);' - }, - }, genEl(token.children)); - } - - case 'url': { - return [createElement(MkUrl, { - key: Math.random(), - props: { - url: token.node.props.url, - rel: 'nofollow noopener', - }, - attrs: { - style: 'color:var(--mfmUrl);' - } - })]; - } - - case 'link': { - return [createElement('a', { - attrs: { - class: 'link', - href: token.node.props.url, - rel: 'nofollow noopener', - target: '_blank', - title: token.node.props.url, - style: 'color:var(--mfmLink);' - } - }, genEl(token.children))]; - } - - case 'mention': { - return [createElement(MkMention, { - key: Math.random(), - props: { - host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, - username: token.node.props.username - } - })]; - } - - case 'hashtag': { - return [createElement('router-link', { - key: Math.random(), - attrs: { - to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, - style: 'color:var(--mfmHashtag);' - } - }, `#${token.node.props.hashtag}`)]; - } - - case 'blockCode': { - return [createElement(MkCode, { - key: Math.random(), - props: { - code: token.node.props.code, - lang: token.node.props.lang, - } - })]; - } - - case 'inlineCode': { - return [createElement(MkCode, { - key: Math.random(), - props: { - code: token.node.props.code, - lang: token.node.props.lang, - inline: true - } - })]; - } - - case 'quote': { - if (this.shouldBreak) { - return [createElement('div', { - attrs: { - class: 'quote' - } - }, genEl(token.children))]; - } else { - return [createElement('span', { - attrs: { - class: 'quote' - } - }, genEl(token.children))]; - } - } - - case 'title': { - return [createElement('div', { - attrs: { - class: 'title' - } - }, genEl(token.children))]; - } - - case 'emoji': { - const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; - return [createElement('mk-emoji', { - key: Math.random(), - attrs: { - emoji: token.node.props.emoji, - name: token.node.props.name - }, - props: { - customEmojis: this.customEmojis || customEmojis, - normal: this.plain - } - })]; - } - - case 'mathInline': { - //const MkFormula = () => import('./formula.vue').then(m => m.default); - return [createElement(MkFormula, { - key: Math.random(), - props: { - formula: token.node.props.formula, - block: false - } - })]; - } - - case 'mathBlock': { - //const MkFormula = () => import('./formula.vue').then(m => m.default); - return [createElement(MkFormula, { - key: Math.random(), - props: { - formula: token.node.props.formula, - block: true - } - })]; - } - - case 'search': { - //const MkGoogle = () => import('./google.vue').then(m => m.default); - return [createElement(MkGoogle, { - key: Math.random(), - props: { - q: token.node.props.query - } - })]; - } - - default: { - console.log('unknown ast type:', token.node.type); - - return []; - } - } - })); - - // Parse ast to DOM - return createElement('span', genEl(ast)); - } -}); diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue deleted file mode 100644 index 40c444242c..0000000000 --- a/src/client/app/common/views/components/misskey-flavored-markdown.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MfmCore from './mfm'; - -export default Vue.extend({ - components: { - MfmCore - } -}); -</script> - -<style lang="stylus" scoped> -.havbbuyv - white-space pre-wrap - - &.nowrap - white-space pre - word-wrap normal // https://codeday.me/jp/qa/20190424/690106.html - - >>> .title - display block - margin-bottom 4px - padding 4px - font-size 90% - text-align center - background var(--mfmTitleBg) - border-radius 4px - - >>> .quote - display block - margin 8px - padding 6px 0 6px 12px - color var(--mfmQuote) - border-left solid 3px var(--mfmQuoteLine) - - >>> pre code - font-size 80% - -</style> diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue deleted file mode 100644 index 41b65604de..0000000000 --- a/src/client/app/common/views/components/nav.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<span class="mk-nav"> - <a :href="aboutUrl">{{ $t('about') }}</a> - <template v-if="ToSUrl !== null"> - <i>・</i> - <a :href="ToSUrl" target="_blank">{{ $t('tos') }}</a> - </template> - <i>・</i> - <a :href="repositoryUrl" rel="noopener" target="_blank">{{ $t('repository') }}</a> - <i>・</i> - <a :href="feedbackUrl" rel="noopener" target="_blank">{{ $t('feedback') }}</a> - <i>・</i> - <a href="/dev" target="_blank">{{ $t('develop') }}</a> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { lang } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/nav.vue'), - data() { - return { - aboutUrl: `/docs/${lang}/about`, - repositoryUrl: 'https://github.com/syuilo/misskey', - feedbackUrl: 'https://github.com/syuilo/misskey/issues/new', - ToSUrl: null - } - }, - - mounted() { - this.$root.getMeta(true).then(meta => { - this.repositoryUrl = meta.repositoryUrl; - this.feedbackUrl = meta.feedbackUrl; - this.ToSUrl = meta.ToSUrl; - }) - } -}); -</script> - -<style lang="stylus" scoped> -.mk-nav - a - color inherit -</style> diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue deleted file mode 100644 index a72863e1dd..0000000000 --- a/src/client/app/common/views/components/note-header.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> - <mk-user-name :user="note.user"/> - </router-link> - <span class="is-admin" v-if="note.user.isAdmin">admin</span> - <span class="is-bot" v-if="note.user.isBot">bot</span> - <span class="is-cat" v-if="note.user.isCat">cat</span> - <span class="username"><mk-acct :user="note.user"/></span> - <div class="info"> - <span class="app" v-if="note.app && !mini && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span> - <span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> - <span class="visibility" v-if="note.visibility != 'public'"> - <fa v-if="note.visibility == 'home'" icon="home"/> - <fa v-if="note.visibility == 'followers'" icon="unlock"/> - <fa v-if="note.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span> - </div> -</header> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - note: { - type: Object, - required: true - }, - mini: { - type: Boolean, - required: false, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.bvonvjxbwzaiskogyhbwgyxvcgserpmu - display flex - align-items baseline - white-space nowrap - - > .avatar - flex-shrink 0 - margin-right 8px - width 20px - height 20px - border-radius 100% - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color var(--noteHeaderName) - font-size 1em - font-weight bold - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-admin - > .is-bot - > .is-cat - flex-shrink 0 - align-self center - margin 0 .5em 0 0 - padding 1px 6px - font-size 80% - color var(--noteHeaderBadgeFg) - background var(--noteHeaderBadgeBg) - border-radius 3px - - &.is-admin - background var(--noteHeaderAdminBg) - color var(--noteHeaderAdminFg) - - > .username - margin 0 .5em 0 0 - overflow hidden - text-overflow ellipsis - color var(--noteHeaderAcct) - flex-shrink 2147483647 - - > .info - margin-left auto - font-size 0.9em - - > * - color var(--noteHeaderInfo) - - > .mobile - margin-right 8px - - > .app - margin-right 8px - padding-right 8px - border-right solid 1px var(--faceDivider) - - > .visibility - margin-left 8px - - > .localOnly - margin-left 4px - -</style> diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue deleted file mode 100644 index 1dcf58dd36..0000000000 --- a/src/client/app/common/views/components/note-menu.vue +++ /dev/null @@ -1,237 +0,0 @@ -<template> -<div style="position:initial"> - <mk-menu :source="source" :items="items" @closed="closed"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; -import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; -import { faCopy, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/note-menu.vue'), - props: ['note', 'source'], - data() { - return { - isFavorited: false, - isWatching: false - }; - }, - computed: { - items(): any[] { - if (this.$store.getters.isSignedIn) { - return [{ - icon: 'at', - text: this.$t('mention'), - action: this.mention - }, null, { - icon: 'info-circle', - text: this.$t('detail'), - action: this.detail - }, { - icon: faCopy, - text: this.$t('copy-content'), - action: this.copyContent - }, { - icon: 'link', - text: this.$t('copy-link'), - action: this.copyLink - }, this.note.uri ? { - icon: 'external-link-square-alt', - text: this.$t('remote'), - action: () => { - window.open(this.note.uri, '_blank'); - } - } : undefined, - null, - this.isFavorited ? { - icon: 'star', - text: this.$t('unfavorite'), - action: () => this.toggleFavorite(false) - } : { - icon: 'star', - text: this.$t('favorite'), - action: () => this.toggleFavorite(true) - }, - this.note.userId != this.$store.state.i.id ? this.isWatching ? { - icon: faEyeSlash, - text: this.$t('unwatch'), - action: () => this.toggleWatch(false) - } : { - icon: faEye, - text: this.$t('watch'), - action: () => this.toggleWatch(true) - } : undefined, - this.note.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.note.id) ? { - icon: 'thumbtack', - text: this.$t('unpin'), - action: () => this.togglePin(false) - } : { - icon: 'thumbtack', - text: this.$t('pin'), - action: () => this.togglePin(true) - } : undefined, - ...(this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? [ - null, - this.note.userId == this.$store.state.i.id ? { - icon: 'undo-alt', - text: this.$t('delete-and-edit'), - action: this.deleteAndEdit - } : undefined, - { - icon: ['far', 'trash-alt'], - text: this.$t('delete'), - action: this.del - }] - : [] - )] - .filter(x => x !== undefined); - } else { - return [{ - icon: 'info-circle', - text: this.$t('detail'), - action: this.detail - }, { - icon: faCopy, - text: this.$t('copy-content'), - action: this.copyContent - }, { - icon: 'link', - text: this.$t('copy-link'), - action: this.copyLink - }, this.note.uri ? { - icon: 'external-link-square-alt', - text: this.$t('remote'), - action: () => { - window.open(this.note.uri, '_blank'); - } - } : undefined] - .filter(x => x !== undefined); - } - } - }, - - created() { - this.$root.api('notes/state', { - noteId: this.note.id - }).then(state => { - this.isFavorited = state.isFavorited; - this.isWatching = state.isWatching; - }); - }, - - methods: { - mention() { - this.$post({ mention: this.note.user }); - }, - - detail() { - this.$router.push(`/notes/${this.note.id}`); - }, - - copyContent() { - copyToClipboard(this.note.text); - this.$root.dialog({ - type: 'success', - splash: true - }); - }, - - copyLink() { - copyToClipboard(`${url}/notes/${this.note.id}`); - this.$root.dialog({ - type: 'success', - splash: true - }); - }, - - togglePin(pin: boolean) { - this.$root.api(pin ? 'i/pin' : 'i/unpin', { - noteId: this.note.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - this.destroyDom(); - }).catch(e => { - if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - this.$root.dialog({ - type: 'error', - text: this.$t('pin-limit-exceeded') - }); - } - }); - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('notes/delete', { - noteId: this.note.id - }).then(() => { - this.destroyDom(); - }); - }); - }, - - deleteAndEdit() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-and-edit-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('notes/delete', { - noteId: this.note.id - }).then(() => { - this.destroyDom(); - }); - this.$post({ - initialNote: this.note, - reply: this.note.reply, - }); - }); - }, - - toggleFavorite(favorite: boolean) { - this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: this.note.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - this.destroyDom(); - }); - }, - - toggleWatch(watch: boolean) { - this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', { - noteId: this.note.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - this.destroyDom(); - }); - }, - - closed() { - this.$emit('closed'); - this.$nextTick(() => { - this.destroyDom(); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/note-skeleton.vue b/src/client/app/common/views/components/note-skeleton.vue deleted file mode 100644 index a2e09e3222..0000000000 --- a/src/client/app/common/views/components/note-skeleton.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div> - <vue-content-loading v-if="width" :width="width" :height="100" :primary="primary" :secondary="secondary"> - <circle cx="30" cy="30" r="30" /> - <rect x="75" y="13" rx="4" ry="4" :width="150 + r1" height="15" /> - <rect x="75" y="39" rx="4" ry="4" :width="260 + r2" height="10" /> - <rect x="75" y="59" rx="4" ry="4" :width="230 + r3" height="10" /> - </vue-content-loading> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import VueContentLoading from 'vue-content-loading'; -import * as tinycolor from 'tinycolor2'; - -export default Vue.extend({ - components: { - VueContentLoading, - }, - - data() { - return { - width: 0, - r1: (Math.random() * 100) - 50, - r2: (Math.random() * 100) - 50, - r3: (Math.random() * 100) - 50 - }; - }, - - computed: { - text(): tinycolor.Instance { - const text = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')); - return text; - }, - - primary(): string { - return '#' + this.text.clone().toHex(); - }, - - secondary(): string { - return '#' + this.text.clone().darken(20).toHex(); - } - }, - - mounted() { - let width = this.$el.clientWidth; - if (width < 400) width = 400; - this.width = width; - } -}); -</script> diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue deleted file mode 100644 index e3e73bd08f..0000000000 --- a/src/client/app/common/views/components/page-preview.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1"> - <div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div> - <article> - <header> - <h1 :title="page.title">{{ page.title }}</h1> - </header> - <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> - <footer> - <img class="icon" :src="page.user.avatarUrl"/> - <p>{{ page.user | userName }}</p> - </footer> - </article> -</router-link> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - page: { - type: Object, - required: true - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.vhpxefrj - display block - overflow hidden - width 100% - border solid var(--lineWidth) var(--urlPreviewBorder) - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color var(--urlPreviewBorderHover) - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - display flex - justify-content center - align-items center - - > button - font-size 3.5em - opacity: 0.7 - - &:hover - font-size 4em - opacity 0.9 - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color var(--urlPreviewTitle) - - > p - margin 0 - color var(--urlPreviewText) - font-size 0.8em - - > footer - margin-top 8px - height 16px - - > img - display inline-block - width 16px - height 16px - margin-right 4px - vertical-align top - - > p - display inline-block - margin 0 - color var(--urlPreviewInfo) - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 700px) - > .thumbnail - position relative - width 100% - height 100px - - & + article - left 0 - width 100% - - @media (max-width 550px) - font-size 12px - - > .thumbnail - height 80px - - > article - padding 12px - - @media (max-width 500px) - font-size 10px - - > .thumbnail - height 70px - - > article - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - -</style> diff --git a/src/client/app/common/views/components/page/page.block.vue b/src/client/app/common/views/components/page/page.block.vue deleted file mode 100644 index 56d1822013..0000000000 --- a/src/client/app/common/views/components/page/page.block.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<component :is="'x-' + value.type" :value="value" :page="page" :script="script" :key="value.id" :h="h"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XText from './page.text.vue'; -import XSection from './page.section.vue'; -import XImage from './page.image.vue'; -import XButton from './page.button.vue'; -import XNumberInput from './page.number-input.vue'; -import XTextInput from './page.text-input.vue'; -import XTextareaInput from './page.textarea-input.vue'; -import XSwitch from './page.switch.vue'; -import XIf from './page.if.vue'; -import XTextarea from './page.textarea.vue'; -import XPost from './page.post.vue'; -import XCounter from './page.counter.vue'; -import XRadioButton from './page.radio-button.vue'; - -export default Vue.extend({ - components: { - XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton - }, - - props: { - value: { - required: true - }, - script: { - required: true - }, - page: { - required: true - }, - h: { - required: true - } - }, -}); -</script> diff --git a/src/client/app/common/views/components/page/page.button.vue b/src/client/app/common/views/components/page/page.button.vue deleted file mode 100644 index 87112aca0d..0000000000 --- a/src/client/app/common/views/components/page/page.button.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<div> - <ui-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - methods: { - click() { - if (this.value.action === 'dialog') { - this.script.eval(); - this.$root.dialog({ - text: this.script.interpolate(this.value.content) - }); - } else if (this.value.action === 'resetRandom') { - this.script.aiScript.updateRandomSeed(Math.random()); - this.script.eval(); - } else if (this.value.action === 'pushEvent') { - this.$root.api('page-push', { - pageId: this.script.page.id, - event: this.value.event, - ...(this.value.var ? { - var: this.script.vars[this.value.var] - } : {}) - }); - - this.$root.dialog({ - type: 'success', - text: this.script.interpolate(this.value.message) - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 200px - max-width 450px - margin 8px 0 -</style> diff --git a/src/client/app/common/views/components/page/page.counter.vue b/src/client/app/common/views/components/page/page.counter.vue deleted file mode 100644 index 8d55319fe9..0000000000 --- a/src/client/app/common/views/components/page/page.counter.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div> - <ui-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: 0, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - }, - - methods: { - click() { - this.v = this.v + (this.value.inc || 1); - } - } -}); -</script> - -<style lang="stylus" scoped> -.llumlmnx - display inline-block - min-width 300px - max-width 450px - margin 8px 0 -</style> diff --git a/src/client/app/common/views/components/page/page.if.vue b/src/client/app/common/views/components/page/page.if.vue deleted file mode 100644 index 417ef0c553..0000000000 --- a/src/client/app/common/views/components/page/page.if.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<div v-show="script.vars[value.var]"> - <x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - }, - page: { - required: true - }, - h: { - required: true - } - }, - - beforeCreate() { - this.$options.components.XBlock = require('./page.block.vue').default - }, -}); -</script> diff --git a/src/client/app/common/views/components/page/page.image.vue b/src/client/app/common/views/components/page/page.image.vue deleted file mode 100644 index 1285445eb0..0000000000 --- a/src/client/app/common/views/components/page/page.image.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div class="lzyxtsnt"> - <img v-if="image" :src="image.url"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - page: { - required: true - }, - }, - - data() { - return { - image: null, - }; - }, - - created() { - this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId); - } -}); -</script> - -<style lang="stylus" scoped> -.lzyxtsnt - > img - max-width 100% -</style> diff --git a/src/client/app/common/views/components/page/page.number-input.vue b/src/client/app/common/views/components/page/page.number-input.vue deleted file mode 100644 index 31da37330a..0000000000 --- a/src/client/app/common/views/components/page/page.number-input.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<div> - <ui-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</ui-input> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: this.value.default, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 300px - max-width 450px - margin 8px 0 -</style> diff --git a/src/client/app/common/views/components/page/page.post.vue b/src/client/app/common/views/components/page/page.post.vue deleted file mode 100644 index cb695e21e9..0000000000 --- a/src/client/app/common/views/components/page/page.post.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="ngbfujlo"> - <ui-textarea class="textarea" :value="text" readonly></ui-textarea> - <ui-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('pages'), - - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - text: this.script.interpolate(this.value.text), - posted: false, - posting: false, - }; - }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); - }, - - methods: { - post() { - this.posting = true; - this.$root.api('notes/create', { - text: this.text, - }).then(() => { - this.posted = true; - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ngbfujlo - padding 0 32px 32px 32px - border solid 2px var(--pageBlockBorder) - border-radius 6px - - @media (max-width 600px) - padding 0 16px 16px 16px - - > .textarea - margin-top 16px - margin-bottom 16px - -</style> diff --git a/src/client/app/common/views/components/page/page.radio-button.vue b/src/client/app/common/views/components/page/page.radio-button.vue deleted file mode 100644 index 27c11bebad..0000000000 --- a/src/client/app/common/views/components/page/page.radio-button.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<div> - <div>{{ script.interpolate(value.title) }}</div> - <ui-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</ui-radio> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: this.value.default, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - } -}); -</script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.section.vue b/src/client/app/common/views/components/page/page.section.vue deleted file mode 100644 index 03c009d9c3..0000000000 --- a/src/client/app/common/views/components/page/page.section.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<section class="sdgxphyu"> - <component :is="'h' + h">{{ value.title }}</component> - - <div class="children"> - <x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h + 1"/> - </div> -</section> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - }, - page: { - required: true - }, - h: { - required: true - } - }, - - beforeCreate() { - this.$options.components.XBlock = require('./page.block.vue').default - }, -}); -</script> - -<style lang="stylus" scoped> -.sdgxphyu - margin 1.5em 0 - - > h2 - font-size 1.35em - margin 0 0 0.5em 0 - - > h3 - font-size 1em - margin 0 0 0.5em 0 - - > h4 - font-size 1em - margin 0 0 0.5em 0 - - > .children - //padding 16px - -</style> diff --git a/src/client/app/common/views/components/page/page.switch.vue b/src/client/app/common/views/components/page/page.switch.vue deleted file mode 100644 index 53695f1b36..0000000000 --- a/src/client/app/common/views/components/page/page.switch.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<div class="hkcxmtwj"> - <ui-switch v-model="v">{{ script.interpolate(value.text) }}</ui-switch> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: this.value.default, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.hkcxmtwj - display inline-block - margin 16px auto - - & + .hkcxmtwj - margin-left 16px - -</style> diff --git a/src/client/app/common/views/components/page/page.text-input.vue b/src/client/app/common/views/components/page/page.text-input.vue deleted file mode 100644 index cf917dd5a8..0000000000 --- a/src/client/app/common/views/components/page/page.text-input.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<div> - <ui-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</ui-input> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: this.value.default, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 300px - max-width 450px - margin 8px 0 -</style> diff --git a/src/client/app/common/views/components/page/page.text.vue b/src/client/app/common/views/components/page/page.text.vue deleted file mode 100644 index 326fd39050..0000000000 --- a/src/client/app/common/views/components/page/page.text.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="mrdgzndn"> - <mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/> - - <mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { parse } from '../../../../../../mfm/parse'; -import { unique } from '../../../../../../prelude/array'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - text: this.script.interpolate(this.value.text), - }; - }, - - computed: { - urls(): string[] { - if (this.text) { - const ast = parse(this.text); - // TODO: 再帰的にURL要素がないか調べる - return unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - } else { - return []; - } - } - }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); - } -}); -</script> - -<style lang="stylus" scoped> -.mrdgzndn - &:not(:first-child) - margin-top 0.5em - - &:not(:last-child) - margin-bottom 0.5em - - > .url - margin 0.5em 0 -</style> diff --git a/src/client/app/common/views/components/page/page.textarea-input.vue b/src/client/app/common/views/components/page/page.textarea-input.vue deleted file mode 100644 index eece59fefb..0000000000 --- a/src/client/app/common/views/components/page/page.textarea-input.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div> - <ui-textarea class="" v-model="v">{{ script.interpolate(value.text) }}</ui-textarea> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - v: this.value.default, - }; - }, - - watch: { - v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); - this.script.eval(); - } - } -}); -</script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.textarea.vue b/src/client/app/common/views/components/page/page.textarea.vue deleted file mode 100644 index 03c8542cb0..0000000000 --- a/src/client/app/common/views/components/page/page.textarea.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<ui-textarea class="" :value="text" readonly></ui-textarea> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - text: this.script.interpolate(this.value.text), - }; - }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); - } -}); -</script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.vue b/src/client/app/common/views/components/page/page.vue deleted file mode 100644 index 1bfb93780f..0000000000 --- a/src/client/app/common/views/components/page/page.vue +++ /dev/null @@ -1,222 +0,0 @@ -<template> -<div class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter, serif: page.font === 'serif' }"> - <header v-if="showTitle"> - <div class="title">{{ page.title }}</div> - </header> - - <div v-if="script"> - <x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/> - </div> - - <footer v-if="showFooter"> - <small>@{{ page.user.username }}</small> - <template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId"> - <router-link :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> - <a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin-this-page') }}</a> - <a v-else @click="pin(true)">{{ $t('pin-this-page') }}</a> - </template> - <router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link> - <div class="like"> - <button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button> - <button @click="like()" v-else :title="$t('like')"><fa :icon="faHeart"/></button> - <span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; -import { faHeart } from '@fortawesome/free-regular-svg-icons'; -import XBlock from './page.block.vue'; -import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator'; -import { collectPageVars } from '../../../scripts/collect-page-vars'; -import { url } from '../../../../config'; - -class Script { - public aiScript: ASEvaluator; - private onError: any; - public vars: Record<string, any>; - public page: Record<string, any>; - - constructor(page, aiScript, onError) { - this.page = page; - this.aiScript = aiScript; - this.onError = onError; - this.eval(); - } - - public eval() { - try { - this.vars = this.aiScript.evaluateVars(); - } catch (e) { - this.onError(e); - } - } - - public interpolate(str: string) { - if (str == null) return null; - return str.replace(/{(.+?)}/g, match => { - const v = this.vars[match.slice(1, -1).trim()]; - return v == null ? 'NULL' : v.toString(); - }); - } -} - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XBlock - }, - - props: { - page: { - type: Object, - required: true - }, - showTitle: { - type: Boolean, - required: false, - default: true - }, - showFooter: { - type: Boolean, - required: false, - default: false - }, - }, - - data() { - return { - script: null, - faHeartS, faHeart - }; - }, - - created() { - const pageVars = this.getPageVars(); - this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, { - randomSeed: Math.random(), - user: this.page.user, - visitor: this.$store.state.i, - page: this.page, - url: url - }), e => { - console.dir(e); - }); - }, - - methods: { - getPageVars() { - return collectPageVars(this.page.content); - }, - - like() { - this.$root.api('pages/like', { - pageId: this.page.id, - }).then(() => { - this.page.isLiked = true; - this.page.likedCount++; - }); - }, - - unlike() { - this.$root.api('pages/unlike', { - pageId: this.page.id, - }).then(() => { - this.page.isLiked = false; - this.page.likedCount--; - }); - }, - - pin(pin) { - this.$root.api('i/update', { - pinnedPageId: pin ? this.page.id : null, - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.iroscrza - overflow hidden - background var(--face) - - &.serif - > div - font-family serif - - &.center - text-align center - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > header - > .title - z-index 1 - margin 0 - padding 16px 32px - font-size 20px - font-weight bold - color var(--text) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - @media (max-width 600px) - padding 16px 32px - font-size 20px - - @media (max-width 400px) - padding 10px 20px - font-size 16px - - > div - color var(--text) - padding 24px 32px - font-size 16px - - @media (max-width 600px) - padding 24px 32px - font-size 16px - - @media (max-width 400px) - padding 20px 20px - font-size 15px - - > footer - color var(--text) - padding 0 32px 28px 32px - - @media (max-width 600px) - padding 0 32px 28px 32px - - @media (max-width 400px) - padding 0 20px 20px 20px - font-size 14px - - > small - display block - opacity 0.5 - - > a - font-size 90% - - > a + a - margin-left 8px - - > .like - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/particle.vue b/src/client/app/common/views/components/particle.vue deleted file mode 100644 index 33c118f000..0000000000 --- a/src/client/app/common/views/components/particle.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<div class="vswabwbm" :style="{ top: `${y - 50}px`, left: `${x - 50}px` }" :class="{ active }"></div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - x: { - type: Number, - required: true - }, - y: { - type: Number, - required: true - } - }, - data() { - return { - active: false - } - }, - mounted() { - setTimeout(() => { - this.active = true; - }, 1); - - setTimeout(() => { - this.destroyDom(); - }, 1000); - } -}); -</script> - -<style lang="stylus" scoped> -.vswabwbm - pointer-events none - position fixed - z-index 1000000 - width 100px - height 100px - background url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABqUAAABkCAYAAAAPKjqIAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA25pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1ZmMyNTFlNy02ZmI3LTg3NDMtYWFkNy1kZWQ2ZWY1NzIzYWUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDIyMEQ0QjBFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDIyMEQ0QUZFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MUU1NkMyNUZFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MUU1NkMyNjBFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5nGnsGAABRHklEQVR42uydB3xUVfbH731TMiUNkkACCQRQOrYkoq4gYEVFZUXsfQUbCuta1nV33f/adQVFRVwrdnFlEV2wgQYVIYmutABSQnqAEFJmMsnMvPt/Z5LJDggkwMy8Mr/v5zO8kmHeuee8W94975zLhRAMAAAAAAAAAAAAAAAAAAAAgEgiQQUAAAAAAAAAAAAAAAAAAAAg0sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4pj1KjjnHNYDAAAAAAAAAAAAAAAAAEBM8sela9NO2tnSeOGlOR61ZRFCdOl7vKtf1BpwSgEAAAAAAAAAAAAcOkXP1mYJbjuT+fg3uTMcW6ARdfnwnWVnZfOEycX9j/rX1SOTF0Mj6vL61vwJybVsakU6y78ta/QT0Ii6vFix/Bqv5L/VLKSNmd85bpxwSa4PWlHXHk0m9kac8K/sk58wRguOkFhvrxpM1o9MptbKzO+cg9S2R1d9TWaYDgAAAAAAAAAAAJGi4LnaVMlrf0kwNohzsaiyd/EDmFRU0R4zm4bZHWwNYzRxJFjBLNcpedOdP0Az6jB7wdahSQnyZ3XKfnrjrhvfXLnnXDim1OO5dfOT4j22f/ucjPVsZONf35pffF3/0YugGfXwC/H3FKs7S9k9sTaHfahsYQ8VIQchYybWwk0jS0/claecWg6tqIewyqNazV4zY7zPD2lxCcopXTgJ4ZQCAAAAAAAAAGAoCme6znP3trxI+/Yq74t5dzofhlbUQ2q1/8OeKF/Ufjgko3rIJmX7KjSjlkH4RW0OqfZDxsg2cEqpRG9L5WmMpXccm7auGaNs4JRSiYqaIdb0dHfHsSeOd4NW1EU2yVXKhpxSrFVybYJG1MUim15oMbGRgUipVakF0Ii6uOp3PpySlEZOwrV/GDd8p17khlMKAAAAAAAAAI6Ql1e0XttrpzyG9tNKXXfn3Z6yC1pRh4XvF9laZMsik7/tuLWH5e+rZjcuOXFaQhG0oxKc9drr0M8GQikqmkOIbXudkFgFtKIeWSVD5jcNrvuzj7EMOnb2dX0ArajHo+OG73y+LP+PvUtNdzOrf2XGGifsoTIZovziGnefK2Vf84pp/c7eCI2oy829R81TNvOgCW1w+7BL6pXNPXqTG04pAAAAAAAAADgC3ly5Z3xCLXul1SwFjhvSEwYom9HQjDp0K+1lZpl7n5P8pj7KBk4plRBm8a6yOT147DfJ86EV9ajqveGD9Moh/SSZj2FcFFX2Lp4DragHvcRQ8FztMRvyTHlNdTvW3XLKOaXQirq0ryOFtaQ0wqReV1XAHgAYC97Vxac0JzjnsB4AAAAAAIhJyAniEY7naL/F7v+i76Z1t2N9luiQ/2RVPG1H353RFDz3n489r7WapatDv9d9XW1y6HdAZCic6R7ATc2tOXeklIWe/+rD1nybl51K+34Tq7DvaDwe0WuRZ9Xsxhza7i8qrWimaywzseGyXyzNmxG/DtqKTl9BW6xPpA1erFh+DW3bowwA7AFgD83yUsniQbSdkj0ekWkagLIA0PbCS3M0v15UV31NcEoBAAAAAIAOKAWZ4OwvbQNF9n+/O9n6BrSirj1S69grtL+rG7uR7EFOkS2npjSEfs/P2PWwVeQpnO26oSXV8jLtx+3y/i53mvPVfe1EdG9iVaMnW7Ogscjbw2EXAXu4m9hVudPj3wl9eO+9Y/AVSjvmZMzz732dViAC9pjpOs+RIAKLz7sb+YTcGc5PoRX1mL1g69DBCXLA+behURo2bWL/9dCKuny4flUgqeikoSeaoA31mVOZHxg33dJr9LXQhvrACQKAMeiqrwnp+wAAAAAAQIDn19TlmJrYayFrn782e8HWgmzf7k2lRw25V0hSFpflsj6bix9HVE7kIefT7hBHBzk9Fr5f9P6aobZBjn3ibzwO/8nKBk6pCOPuaflbcJ0i2lc2AadUz/LVb5viho+haKkEN/t+zRDXDGgr8nAfP5+1N1hcSJOUTYdTqv1N0lehpShiZj33uw9U4aSKpB1Ng+uqgvvQiPo02TwXQQvaAc4obQFnFACxNmwEAAAAAABAIa7JOXzfc/b0zLztjow7/Nw0NXDCZGLbBw6jCJCbobHIsm5EY/cMOWWvc712Zsez9VvX7eg7YptZsH7B8za3aQU0Fnm8Ettm8rPewf3g+XYn7fXtHxAlBGdvKpuLQvaBilT02PBOr+rBGbRf3asY6Zf2w9dXrUyrn2BujEb6HUpXuWh+Ia2lxsbefhReJNkHevGj5Js9J2SflvxjtFKtXtd/9CJofv8svuyn/jubbJlJV7tX6SE9ldF5La90Im2vL+izANpQF4r89r5nu8ntlX6+5pMh+dCI+v34Llf8ZbDHkYP0fQAAAABQlTlLNvWRe/YOrMUi1VS8ecs5A7G4s0pQqp/49My1oeeaqsuHywMyf7X2x53HWDEYiwKhaxVZffKb515gCzg9KKqNSdKT1kZ739Y476u3He98GNqKPLR+0Y4B0gO037PE9RekhFOfgudqU2mL9aJ+zS8PLR3OnNbHA/2ro+m1AVPP+RBaUQdygKSWb/7a7LSdQMf+2urTh8ydsAyaUa9u1GxI/Yn2vbK3xCNLZ49/7/it0Iw6kAMkNcH3UfvhGnbL7hMQka8erxxb9lfOxV9pXwj+txt/zvobtKIe8/K2vefzmibTvjnTNQaOEJXbq6kF33HGT6T9lDOYHW3Vr0H6PgAAAABoHpqk+alX95WM8fTAiV59b39u3fzBtw+7pB7aiT603sSbK/ec1+A0PUbHiS7/fXTumdUt1R02ahtqVkNb0YGcUKtmNz5XMMbHbhvRrSh4vn1/HDQUXXJnOLYwREMdkMA6TrJ4zm9puT7Olbhqs2XXXyddMfbzSF4Tzqj9s2h+oVnsEB9aRWtgjQ7mTTj3l4eWjjj6gXFroZ3ok1L+y1Sz035CRy9qS36SmhRoRh08laYbg/sWyZLdI20PRYX8A5pRBxPzXxpyOML8LxNF+MFJqBIWq2+Iz9u27JnExRXKBk4pjeCvcaZAC+rCGaP1GU8UTKyacEkeHFJHAJxSAAAAAFCN7aPto/Z2dvD0pKYzT1F2FkM70YMiDYSw9jX5pET2PfMIxu5vG3RLnqKZrrE/+H3P+UyWh4Lfd7r4fdBa9DhxWkIRtHBgyBHS2s39JufsRHOyWMO97r9eeMo50JkK+J3N1/s9POC0a3E2nJjFrIsXzS+Mw1uk0adXVb8Mq7RhUOg5Ka11sLKBUwrEPJKFl+/VdnkZXrZREUem+ztWbw86ptb4LvaXsvegF9X6cmZ6X9kEInOYhWHMrzJpA/bcv3NLMvO2movT7qlZxC6BTtSk2zhpWv2bjjf7JDcUQxtHBtL3AQAAiAnojeGM8qF9ORN9lJ7PofSASYE/CF6v9ChuwXhpVeb67Zg4iywFM5uGcTMfyf1soKL7gYotBn13unlo6Hd+85VvvWKTjYqNNgkT2yR8YmXejPh10F746kLP6kHHSj4p/VDufUqzaEnKHnvcylYv1RvZLFfXpG/8OZbrzHPr5ieZk9MvtLWIum4Fzi+wBoI6fFGw8DF3c+q9wWNvi1wat8c+AO159Cl8t/BlipIKPSdVD+6JaCZ12vrBVeYKi+Tq0VE39rQgUkolaA2KHimti4Pp+zx7mq8+5oXRb0Mz6kCR+vGbN79U3+jIS01u+qRkrPNu9BnqQin8EjIbM1OdTe+NeWvkTmhE/TpC22ittwYAMA5d9TXBKQUAAGGGIg641zaSm1h/4WdDeFsUSILS3ia3t197lE2jYKJa+U6x8p2twuJZiQmb8FL0bG2WkONGcVk6W3CWq/SM9LawqZP/5lcMtJELVigk+TMutSzHeiFHRiCVU9WQ8YpOr1bqwGjl1K9SDpQPtvi3Z7bZpm8583er9Jj+e6KNBY8zNwTyR9QqdSefFrKvyChejMn/Q4fWwlGGiIOVtqckXE6+gJOR8WylZdvQntYsZviw8q3eNXKf761NPJOOPUn+gj75CWNwb0affxfkf8KbTeeFnvMetS1zUq+rKqCdKNeLd5adlWW1dkS6Ugq/464dcTI0ow5YU0pbBByFGxsG70k+thrjfgAAAAAYETilAIghaNJ34NLKk2Vb0gmyV2R6/VJPi8nf02QxBSJB/F5/vddvqrGY5BpKVSB56n/cNK7XCkychQ+amJU4v1rI7HR2+PnhC7nEvpKFeBNRIYcHvdHlsCVO5j5+udK/nRGm/uZLYRbvuj0NH+BNsa4TcAr6bX9kIpD6IdQRFXAuBaKgmFjNBa/ym+UGyWciZy2Tzf7kFadZPwlN6RfimOr4DcbZB9zkeTRWnYazt302SjLbT5Z9zSum9Tt7+cG+u2p2Yw5FRVVlFn8WqbeA2yIRh5xN0VOxkmrutfKvn/A1me4KPedPYNff3HvUPLQA0WXfSClhYrtMlXEZeOtdHbbMXTKpLjH1nIAtavvdh8l3AAAAAAAAYgM4pQAwOJQCIi2l5RrZK5/LmfiNciruEH+iRTD+nWSR/rOzNm4eQuQPnUAEyI7BVzAvu4WFf6HiQmZhcyp6bHgHzsPOoeg05rdN5X42g+0nEidM1AoTm8lMnrmYYDsw5IxiPvtDgokrWTAyjfP1QhLvMFn8uzOH6x+Xrk1LTx24I/ScSfjnjlzqn80kfhGX+RXKKCeY7s/PGX+bmZsfiCXn1PNl+fc0W/jjwWO7V9x7W9boJ37ViLRHRkXSGbUvQedULEROwSmlHSiNYs+K9JctTmmMzy27LU7Xb7GmFAAAAAAAAABEFzilADAoxVMXjfWZEh7gTIwLa6PB+FKzv/GhIXMnLIOWD05g0rV6yDXMyx5UDrMifLkyZmEPVqUXz8Mb3/u3Ra+qIbcIH39MuYsdUeqB3Nws7qvMKJ4Dm+xti/TKIfdyP7+/3RZ+pa/+WObiibzpzh8O5beeX1e/1Oe3jw0ed292n3v1yOSOdFAFs1wnSYLfo4xhLmABxxd3C5N4pLpX8eNGtwk5w385zd2873lbbXXy7cMuqe9oo8qHnK1mWtBgGtNoOsSibovvl+Ts6O5cFXquW+L2PkgZBwAAAAAAAAAgFoFTCgCDQc4oYUt+Uvb6ciJ5HcliLuKePXfDObV/AmmwvNKLLPyRUZ1RKFvkm2MlLVaXbeEzzQuJmol2R7ReNvuvgU3aI3KEeC9YL5Q+eoHM5D8fbhpKipbK7NZvipCkrCRPy8JQh1QogbSZTPq7MpaZGBRFufhlRo7QId2kDK3bse/52vXdejw6bvjOQKSa33ZUzgynJtrwopmusczk2WzUSDZKo2iOs//OJss1Xl/zK1Oyx29E6wwAAAAAAAAAIBaBUwqoQvlxl1hom/nf+V5oIzxQmr5EZ+szVpPv8mhet9VvfrfBZb0Taf3a+F8UCPsbC6Ykiz5+YWJ/jYVokM4oetY1TfjYTBVt0WETbmYzcu5wzo5VWxTOarqCydI/26Ojyhhnt+TOcH4aVRlmus5jgs1hgchF7maSfFPu9Ph3jKrz2VVf/9DCTSODx3HCv3JaxpiT2h21e7TmlCOnJa0Vtq8Dd81LRffbXM2/8Tjt321NYk8jVam67PrPM7lse+pdbs5LrAM2zUo/88EaaEU9di9+IIFJBbf56rOPd2dn/CX7xAfh8FTbJp+dfV9rNznX3tQwM2ncyu+gkUObeAj3c3zJqgcH2dh3D0ueYW/3GD1rASyh/rPSbxL/9Afa7372Z49BI+qzI3/6xG1DMweNTP0D7KEBPqx8q3eWtfrq6q/GzsKYVxus3PXUfQU1fecEs00A9Z9Fvj4upQpZJ7QDZWk5nPYKTikQUUqHTEr1ZeaeLjjLVSyRq9xF/VnbOi7O9q+4GC1mz9hW5W+FXLBCc3nhV32KP8Q6LIcARUfJ5oS3lXqaoVI9q5J8jVfGetQUpaGSvPZ3FTucoZH270vZ0nx5LK5rRJ1iZsWQuYKJazTVJzE+r7x38dRYecCgh6qdrM8jsszGDNzQq0+31RltqfoszVPUTBen1NOXglFTwsQeyLvT+bCR9P5SyeJBVtk5sFVybfJbnff7uDzILKSN/rqaO07+/NwTZItnjVbbhTb72EYEI7jIIcW81oeCf2/p3vJq7uW5v9NVe/T9kkDkslSR+rPeXxSo/uLBnp6KPh0PgLaklm/SJ95yul7kp8lQufeuYzML83YYJSpvxwdT31c2wShQ1mPyXKue7DF4Y8Pg9UcnbTZKv1i/dORvTL6W/ODxsvqX4/RS7/OfrIrPaCnOPvqBcWvD+btHOo9wpM/zjcuO+4l72TG03+o/P7n7+Ica9/c9emnypz/dK7RiL3rhUDq2T/PouzOaInmdQ7FPOOZWyAHiSdr8Ee1bd+SmH+jFBnK4m+K+OKbZP36zFl5+IHs0Xmiqi9T9oZbTNjD22vHntXb3zmGpv6Qf0B7UXo9OuS3wopMWnO1kD6O+FPvNjlmnOpgvf7svPetgk+7kcN+enZzW8M2pP6jdbtH9YeSXYckp1fenptcO1hZRm7UmJ/XYEUW7fj5QPwNbhG+uR5KEr7NyUl3a6UvdBueVNqA2i7ahL9DBKQXCDg3oW3rmXMcFu0K5a8Yclt2UcYbg7J24mqLXEU11cNbdmX+X8Pqf0kR9s5j+MOyZ0f+IRTtQKizhty9RLUXcgRvB9dzUfI5RU2IdaFLFaU5aEB7nIHcrXWVt+35KONajImehy1c/MdKTDFpg30idbvVpX9w4aPBZWpCt4BnXn7ifBZwdXGKPy2bPU8xvmxo4lvl7ek3t93xZ/j1WF3+0Y6BnaxoaTBVHKfJcvoYCrd97bXU4MY8cU/99Y80KUxPruIdsfv/Oo+84rqde7PFFwcLH3M2p9wZsYfd/elHe6PP1XKfpzcSmHQl7rf2Wfd0NZr3IX/hu4ct+S8v1tJ9a23jpgKnnfKj3drYzpxRNTpzs/O44n6WuTEtRbSTXoB1irVW0Dmrl1o3FqdJxRnBM7euU2nPP0bZ9n2XoodxRUvV/gYN0+3taiN6hdjelZOMmS5Iz3etqvjsc4/lwzx8c7nN99aozP4zfs3OisLDV9TOOPnF/z5YU3WblNYEXVFpFzz+pHcFDE+5mZq+k/Uaff9D4947fGs7fD4dtDtseXzzYkyV993xgLLb90cv2N6lI33Ga/70k6Ex028ZMUrOeLL7sp/6+HUlb6hr5+9cU9LtMS3Y4Elvs1SZ59gw9mI6pHrXGxV1M+/HV3j+qWUfmnV882lfu/Nqc6RpzzSdD8sP9+0dil2jNP5Jzd0cfT8C52+xIW5fX4+/D1bLHa3mlE03Mf2k464beIIfUlhy+grGkocpIYH3lsoty1RrTUP1Ii/eUh7vf0BvkSJRk9gjtd+bgjSSHG0FkNILO9kCfzsyjT+sx/dtDaW8lBkAnkDNqy1mP39XSI2czE+ylw3VIBW5M+r/Kb9Bv0W8G0/2BvVlz6zczteKQCthNkYVkijU7UMop4bN9pzmHVFsrP5RkC6zlEwNQp38EDqlaimRiFnajbJFz3f6GhNzfO5y5v3f2afs4nHSO/kbfCXxX+T+H8aBzBslIshpN/zTJSGvn0Oe5dfOTQh1SgQFIYnWiVmQNREdJ4kpG6S5ldi9vtW0gJ1XAUSXESorY0aMNQh1SBEVKBSZ+ZrlO0oNDiiAZSVaS2Wdt2Wu9MVeSaaue2qOgQyowUdFsOo+i2PRcx8mxEXoc3xw3P7j/y0NLh1NkG320WH9IpqBDiqhLTD3HEA1vuv294K45qeQvoX+iid0RrtUeciRShFvpi+88qhWxe1X1yyCHVKDdUrZDf6k/KvxDIHHInyOFIgjIqdGUnLbAb44bva8DhCauHCVVa1ibI3Eiq25+P5ASU2UoQoocUgF7ZEvjwqH3SNnzkNutKcmXky1cvovO2Z9DiupJ0CEVKL+yT85FNe3R7LMmROaxIHy2OdzfIud4+olfTKLPgd5yt8orrg86pAi7/+u/aOHR25Hp/k5rdgjH79Fb6wd1SCl1JOiQIprSLar2JdmnJf+Y2K/xzqSr3asi0V9o4Xc6Q7atu7Kjfrh3Dgs4e1Xi+oI+CyLtkIp2X37IjUNO6rFtDqnASGBo8tjlqvXr5KiNdYcUIcn1VwX341xNp6glBxxSbTTU2zqyejk2Jww7ZHtCheBgUPq4gDOKCXKQ9AnjT/eh36TfpmtA0yEd363fzORMnq41uUimWHJMBSbehKA0U1kaFjOLZNTrJHtXIYdIZuWQdw7VIUWRS8qNe35VVnF6zu8d1+ZOc75Ka9rsb/KeztHf6Dv0Xfo/9H8Dv3FoA+szSFaS2Uj6Lzm16dsWuyOfPjyp52pri2WvyQZKIaclmQPrSUn8EmXXz9pSywZJ4b64s4xiG3JKS5KnQk/ReSQrySxqhrzsj2crAw8UzLOptSH7dlorruhp9xu01XIdorQSwsT2SpPo3zCgWc/3Ek0mesuzj5M8ticDn+ziKXSeHFKebmmrA6kWlU8S3/otRV5oSfbqntv2hB7HNXJDPLDTRKLcLSOLPvu+ud66ZeBe40TZ5rlbzYmrUCiC2+sV/wnIya0bKYVfuH77SCalwjGhRXagCff9pbiSRdqvHNOS6T+qp32mlH0UIcXTWhYzV+u9R6L7SHOo1yBHFNniQJGCniSWvO+5FvPJPdS0B00o9hy863j6hGtyMVK20WtGnUO1x4Sl/Y+ZtGDY7CPVVST1FanfL9hzPr2EVxpyqlTtMSLZIhyTvZG0SSR/e3v/swuD+xQp1W4jQ3EkTqZoO6j2LBtVSBFSbUf16ymlIgPqjvvre/+NbFHSal8g/5i3yMhl1bLDtoP/5i6QJXY/fSrLjnvjUP870veB/UKTQUP/+SW9KfOHKF3yqfU3nfFHo+dI7Qwtpew7YN2LgVR+9BZ874rBy5XdXJ2IXFjRe8Moo76tUTTL9RhFvByKPoTEpuVNd4Zl0EhRHVxmsw/lfqC0cTnTnfcZQf8UHUXOqNBzx67NYMVH7/K1xnnNccK/Mq6l+dpgKjktEZrKr2NwJ7GTw3VvRJPXyr9+wtdkuit4bLGKi4Z+enwlOVP1eF+tmt2Y4/E0bazPrvBQ30/OqNC14ihikRzEWpX/9a35E5KrTK/Svlwn/vLb80+dE66HDy2NhSktXtzuuBtCz7V4+10VcPxqqZ94tjbLaiu5+pdezXUml/21cPWHh/OcFA277Jrz9rtN9pZL9jo5tHRYaC53LdikMmNbVTjG9pF4Xg23ncgpKNVV7Z1SOd1+qRZS+GlR/9GyTei6U5Tmz+s5f7Saa4Lo0TbhtAdFFFpsn+QHbUKRblpYxyhW6wdB0YPN8YkzaN/e1DAT9lDfJpTCb9vQzEGdrXUE20Rn3EVzoyP6fjJgzfbzt8T6fCXQRz3Rgr8Ea0qBw6Z84MXxrdl5i44kTd9h2ZSxr60lBRMyN/2rKRb1vvrW/Csl5n9LD7LKzHTVMS+Mftuotiic6XqBCXaLvhpFNid3hvNWA9riPMUWn3RRCW5mEdMo2ikissx23cC8fHaX15/i7HzFJp/q3Qb7c0oNXZX3+rkX2K6nVH63D7ukXsvyF810fxGMstO7s5BsYbE6+kneppUnLBh9lN7vL6rfVAaK9uSttl8tak1pNWNhjbYjfQCJ5Jh4f04ph7lishHWbIrkw2CkbbNl7pJJprjKjvR+tqSWb9In3nI6bKGujWgikdL20T6lXFR7/SK92CCSdiEnCJMKbqP9VulkTPBqpC0jJy5FgOh9glfv9QP9BexiFPtA99G3E3Su/Tqipo3glAKHhVoOqQ67xqhjihZZzeq+h8KC43QickvZ7uShRsxpWzjLPZHJ4iNdCi/x3+ZOdywwii3aJ6o3sL3Trx2IMsHF+LwZ8esiKtPMpmFc8MWsa2kda4XVMzjv9pRderfFnMr8N1wSD0SxJLm61fQobMnWS2ReIPKxckhR+9pwtcLEZgbWndJz3ZjlOqm6d3Gh3idz6M3D9Iohuczs2bwfp5Tf7W9INrpTSusOkI70fe1QqsVqd2auEe0SyWeiSNiGHFNJcu3Fbs5L4vuuf8RI0R+RtgeeK7VnA9hF+7aBPVA/YAvYReu2gf7VsRH0HlvPIOEsE5xSoIPy4y6xtPbI+Vwth1SHbckxtaPorP0tWGtU1t+5vFD2+nL0JLNkMRcNfWZUrpHsQOtkOEyJJaxrThAtUuv2N2QbZbLwECLWCoXVMz5azp92Zxk5pjq//w0SwVY00zW2KcG/lPYdjdLwSDv/wm8v+/a9ItwkcaXW0o91FXKyZewYOEyvafv2hdL4VfXYtC6zYsjc0PR9Ro3+1OPDB61dFucsvpT26+p7PQuHFJ5djGAP2EebdoBNtG0X2EQbdQP20J4tYBft2Ab6V88+0D2eQQ61XBJuARCkpUfOP9V2SAVuXkUGkiVW9E5p+/TmkCJIZpLdSLZwmBOfYPp1SBEp7WXQPTRR3SWHFOfro+mQIuhadE26dhcatFsCZdH7YInx55wNJhbfZHpcTw6pwEDHaxvxq5SLgp+qV1v0rhx8ulEcUgSVhcpUmbX+RmZhN5IzirZVmcV34OFDG7+fO8OxZcSUnEfoYzSHVDQXB1Z9IWLUD01eF3aATfSkE9hEWzqIdXtotfyoJ+rqAPpXT0fQvTb1pWW7wCkFAmw98zF6A1ZLC4pf2y6ToaHIHBOXn9Sr/CQ7lcEItqA3wZlgU/Tfs7EpgbLoHJPP1JV1GGq5qfkcNdLj0TXp2iRDmMqi3bpBKS3bUt+VubwND+nvZvJsVv71h57igjv1aAuKkhJMlERywNrZJzLNliiRZW6m9eAoOoq2Rl1IGA8fsWcL2EY/uoFttKcH2ASgbqCO6LHcsdx24aWG2LYPdK9NPWnVLnBKAVY6ZFKq4OwVzVVSRSaSzci6Tyn/ZarSOGTouCHNoDIYpFu4S/nHZICCmNrLolsoski5t87o9IucXZtzR0qZWnIGrs07d+ZTWXQdLSWL+wPlMLG5eoySCNjJQg5n7u6wCQs42XQHRRRFIlLtUBxOkXBOUZmobHj4wENhrOgGttGuTmAbgDqCOgJQF1BX9F9mtFWRf4bTwrVQT/RdJyS9V6hwfGIdb1buU8pGi2+NO9tlMyT0xjvzyXfpviBKGQJl0TG05owhoqQ6Gkg2JVAmnWLymjpN28UZn5c7w/mp2rKSDCRLOMqkyboxy3USa1s7q7a5teEZvd5TgQic3zucwupJY23RbbntZdMVnDG3Vgan4R4/RaJsAGj5gQzPIAD1A3VFrzrAZDtkAwAAoL/2Wmv9BSKlYpztgy7sx7SVtm9frm2X0XD0+8Z1sZ6jpEIatQwqi57LIMm2y5kxoqSCmNrLpDsoHaTSTU7q5Gu1srVZMw7ddlkOmsaPyqTHVJdcsGvadtgHRlhLJpDqUSnLXmXTCZSWszKzeLmWBqXhHNRS2YyQelRrDwCYqAKoH6g3QN+UDrrAAS0AoM/2OZb6E7zUANtA/+BQgVMqxvH1PfmvkFEdOJNvQFk00kn79DU5beQyOSwJFyjSH/Thm0vsZTXWkToQgfWlFJk6sYijrWz6YdH8QjMTbHJAes7mGaZuBMuilC1QRv1IPjic6yyF6yEhXL/TVjYxGA+CeCCMJV3ANgCALjQUNigBAP32mejrAZ5DgJZ0oSWbwCkVw5QPvJje2p+kA1EntctqGL6+amUaZ2KcUcpDZaEy6VH2omdrs1hbejKjkdteNn3dS0LqpE3ibtns0VxazzaZuPvIyqYt0iuGUL1IUT5ledOdPxilYrSXhdYiS2kvo07aWeOnt0MKPxCLD8Wx/rAOu6DMsMfBydr48W7YBGWFPQDuP9QNgDpiNHnglIphWvvmnce0uZbUvjjbZTUM3RNbzjLa/aTXMgk5bpRhOz4dlk3pHEcf7O+cs8+0FCUVJBAtpch2JGXTHJyd3rbhyyI9IIr2mo8dZWovo9ah1I+yxbNGq4PQcP0elVGPaS4BAACASFA28MLu0AIAAAAtPPPpXQ6gPeCUiuUGirPxkFUdOBOGi8zRa5m4LJ1t1Dqut7IVzGwaxtoicw7SFsgfared6lS2lPYy6mOAIPMxgXJJ8meRGpx2NkCNlHMqWKZgGbWO3ZI4XIvO2HBDZaSy4mEQD4MgRp5FNH5fot4A9ZFN0AEA+m+PjdyfIJIQAHC4wCkV24yFrKp1i8cYcDiiyzIJJoYadoCos7Jxzo7ttEzmls81q+8uyNaVMmpo8D6obct+VvvBIPyRPW1lCpZR83VDHNxZq4WHsnD9bjjLCkCk7/tYkQ8AoBoeqAAAAIDex5axPNZFquoDA6dUjFI6ZFKqsumjI5H7tMtsCCTOhhjtntJvmfhQ49Z0fZVNcN7v4MXh67UcLRKQTZHxiMqoEdpTqNGaZP7qzA0btTD4Ceegqb1Mfioj0sUBAAAAQJMjeQGnFAAAAACMCZxSMYpr9LUjILM6fH3VyjQhRIbR7ikqE5VNTzIXPFebqkjuMG5NF462MuqkQ/LzgQd9MGdso9bL0JmMnZVRK9jNCX3bdysnXJLrM1rNaC9T5T5lBQAAAADQDL03feyFFgAAAABgROCUilHitq7rAZnVodlnTTDqfaW3snGvPcnodV1XZeTsoPePYKJa60XoVMZOyqgdW/CgnDVh1c8RRjuFOcS8Zp+yarhqMHesjE9iqawAAADAwYdjHEoAAAAAgCGBUypGERJLhMzqcNTwOsNG5uitbLLZn2z0uq6zMiYc/MGcNWi9AF2QURdOqeC6PpzzPUatG8Gy6WENI5mz5lgZn8RSWQEAAAAAAAAAgFgETqlYRfBEyKwOvjJXmlFvK72VzeSTEo1e1Y1URpmzRsgYJTi3uhL9bE+WOW7h+0U2Q1cSpaxaEuePS9emPb+mLsfwetc4sxdsHUq2gCbU5cPKt3o/t25+EjShLovmF5phBwAAAAAAAEC4MEMFMQoXDUzoUGYjVLos505/pUEbFKVsepLXb5YbJK+xffNURqOURRLajzIiGYUBdP3DWH62nwd8IqMYG75t9oKtp0+b2H+9MbtDoZlosJdXtF7rcrLXfcpNVDJkePXza+rOv21EtyIMWqIHOaIye/T9kpvdx2QxJ3v+J9cDtx3vfBiaiS7kALGbR8xv9DWf4VRa/hcrll93c+9R86CZ6PP61vwJjUk7FnZX6sO7lfN+rK2zn377sEvqoZno882OWafWxTveUXZ7eHd7P7WtGHmpEdd91APkqDUPLp67J0Uca5Ybt9fU97gB9UI98p+sii/P3PxnX2NrliNeen3SFWM/h1bUtceO73dfT/uWyzz/vPDSHA+0oq49tnzgPTN9QO3P4987fis0oi704qF1gdQLtgAAkVIxC5dZA2RWh81ruxl2vQy9lU3ymfYYva7rrIwHjTISQvspPLsgo+YjqWhS3s9NU0Na33TeP+MOo9UNIUQgtaVW0sWR3skhFap3xs2vBuqxYPaYGZiqXNbeKQP+Tg6pDivYmx6iqCmMHKOLMyntT5K9+YzgcZw//nWKmoJmogtNvFuTmhZ2tJtxqSeQbaAZdWh3SGVRlbB0t/y2ecS6W6AVdSDdt0jSDfY6U46lPvm3A2TP09CKepBDinsa77ZYWi7ztjQvmbNkU5+wzkPoYG0vLcm4ZXP5Z009mp+mT91SeTbuUJXt8ba3SHjZv6o2pGxefNlP/aER9SAH4e5H034mW8zL2/YeNKIuVB+WXVH0C31QN1R69ocKYhNL2cqNkFkdnDnpO4x6X+mtbMLSbPg3GvVURsFE9UEfthg/WvMPhJ3I2FkZtUCvesevUpZxc+tAA1aPnm1GEZpwFGam9f7VBIpPjjum7b5hhl2L8Nd1RN2y7u9eT+7VvS9GjtHFI0k9fzUOLElNh2aiy/ah25xdsQ2IPOQgZG0OqQ76pu50QjPqkOCVjgs9burWkAytqIc5UR4Xepy2u2IwtKLq89CJIeM6vNijIl9ftTJNsUHH82n1lpRjoRX1KPlmzwlBe/i9puOhEXXp3b3xZItkyaaPq8V6HjQSfeCUitWBgixthszqMPrujCbOeZXh7imlTFQ2Pcmcd3vKLmVTa+CqXtteRl0gJFZx0L8LMUjzZehExs7KqAUoTZ9Zalkdei6uwf6GkSoGvaXmSvRnVeT+lxVe+uM9s7d9Nkr9+/9LpY/b22lpNjUvi0BbHak+IKy/R+nbVImMaTV/FXooS76G9a7yQq3pW69ydFlewfeyg89r2cHK0tZhBB9dKB0Zb9n1Y+g5u4d9CM1EH0rT593t/SjkVMvPTcMWxEo7oTXZHD1rX9urD2+K/zrW2m8tyWUWplc7unGvVNqjYvD3aDVUHdW+HtxL2OF4z8j1QOvyj3lr5E6zxf9BQAbGfuk3qOpb3J/q0e1a1/dBeyT0a3wOGlGX3Q1xnzuc9fOTEnfnpzqbELmmyjOfEFCCzju4w2XLWY9tVzZ9dCJu6YDP7zPMm8prbv36K87EOEMNPRlfOuKFMafrTe6ime4vlHbwDIO2bV/mzHCcqRd5C2e5JzJZfHTQMpk9fXLuSCnT5L30bG2W8NlKD/olif82d7pjgdZtQenKsuIyA46pigzvX8O1pk44xhzh6LMLZrlOWjFxzfLWOG/H2pq167v1eHTccFXXxXtz5Z7xu+32Vyl1HzkG5V92Xk5OwsKZrvNyZzg/DVt7HYGxX7jGUlTWwsk/pphamwMTGh6bdRXfvfPsaK3VQdEIpUcNuZdZ5MmMy7VMlu8+0nW9tDDWPhL7kE5s2RUP7epdf0ZyLf+5df3Q26KxPsSLFcuvsTSlXdWasKPK1Op6ZEr2+I16t0O47FP9xYM9y5xZFwX2K459JZLrCpGDmFL2UYQUOaSu6z96kZbbIrVsQzYxe7tlpZ57Z2GkZKF1KFqOXXsTRUhVbxr+xYWnnBP2NQe1ahMtPq8v/H5JTkv3mlN6dq/76bQe0yM60atFu2jNJmQPb0lcSvWxu1ZGasyA+tF1gqmwwrlujp7nLtW2EUVMkYMqluqFHupJpNGSbWJ13t1ozyDhLrOZgViG3sC+VkeyGgi+mhnMKdVWJh12EEz8omwM6ZRqL5ueBO70HhLcRk62VzUpfptsR1xGtSmc1XQF28bnMeY10XHvMN5FNOg5kkFZuAZNv/T8cWJr3N5joEHZu09SNovU1P3VI5MXK5sMmnjsXTVkPDP1PKngudodspDDmvbxSO0QycGs4KzW1NrcYQebp/XE1sSetMbZE9GwQfvk/sPtnw4ouo62eosIDge0dorfb77XXmdiLRLLcfYvp0mN+yJ93Zt7j1LaITYPw/VfU2bv8amfSScE6kh2Rb9I2qN9cveeSD0IG+FBfdd/nsltktgqFsdY1ZczX8w4Y8atkZCn3RmM9Vm6wNZlT99T7fU/lG4xPdB/7O8j0n+0OwWLoO3OIec2byy9Py05tXzy4GtnR9AeoItQNPqz2xvS80++NSJ6C6czKlag8X+kXvqJlENKD315uJ9VAADhA+n7Yhgu2GLIqg5mf8PHRrufdFsmzr8wbiXXV9lyZzi2KJuDRkFxH79cs+ruXLay9jJqG5k/pvxr6iiXzK8wWtXos+P47H3PtUquTVqRL6V60BvlXHxULrNXKvy21ZLPtIci8Yw+LqEy/nRRft2+5x1cTlVTrifm7nh8hTO+nj5Pzqm7UW8Pwkd6/W6NyXulUfTZGtLULM8Vr1XcfdM/d64+//WdM9vX2Yk5/JL9hOB+ckuFqi/WnPp67cVkkz8uXZvGYpQGu39cSIU7R01ZyA5HYgstTtwdjkzkkArd6n9Iz3UtT19r7Vmyh/2+pnrX0+Sggh3Ul2lmWc2cXj7vipdKFg+C/tWXe/SKF3L+2O/rJtoyoAnG/vD0I3q3h5HSiFPfoVd7GDUNLznSj1QGOKViGOvOIkqTVaoDUUvbZTUMm8b1WqFsWgxUpJb2MukOYW5ermz8Bqzi/vay6avDZvygUZFCiLGFM90DtCY3yUSyHUnZNIRjb6Wz7HBO/B7u4Cccg7nny/Lveb48f/uPF6397bFrM5jdJ0rjhH9lvJ9de6SpwcIFOWZ2MT455FSGkMRlzG87SouD47AOspUykh0oZV/o6SZvi2oRbJTKUvjtfwgey8L8kp4eRsJxXbc3bn7osc/mWqKWPa56Y9tYqzfxMa+wDUtttd3xuKvfhbH4QFiXYP7fejZS3D/Vkp0cUs1Cmr+ROR7/vKz367H6oC45GxbGMVFC+82S/KKa9vi8PrGKPnOWbNJLivaIQBFSoVu12qtLPl9WeMHyV29Uq63QSpu1vTXl8/SjbB9LNvZ0tNLx/qo/3/bZKBoHHumYVktt1pHIktLabWGNxf9URkGaKi/MUaTW4awdqrs1Mrso7+TEtM09Zf4EbfXY5hpxTUK/sP1Lr/YwIn3X93P9pnl0KTShHcIR2Yn0fTFM5n/ne7eclUOL7P1B46J+QLIarfL+OHX5R1aT73IjlKfVb/4oGutLRIK821N2Fc10LzPaulLK4GtZrlI2vcktuPiACXbNQb5iUr51l7K9VWOS38VCoosOWDY92MDEZnI/C75Z7FfOODLKh5yt7H8axvvzkNI8hONB5/Wt+RN2WfjjbaMfL/t5eBVL9LjOmpKpDWdUp3bZ11mogh0i/QAaLCOtIUUp+yhCihxS0/qdrTsHu6HGLKecU7Tw+yW5tHZK3O6e30845UrVUiSViMTuod7ZPsLTP5r3fCTq4OEwfsTVNyo2eeFY8w9N2Sc+qFobRvrf2N40CZkNidU6QjZY+H7R0JHdFyWln/5gjVpyJDP51Ob2/berU/LYYb58qKV6crh1pD1l3xNqyt6Uve0Kp8uSw5r73aIcvhLL/Ui7I+piNWUwx9l/V9q4+xo5t5lepFx+pPel2nXkSMdgH4+6IXBPqvXGnJfJC81y43a17wuN1ZH71bLHu5XzCi1MunBSr6sqYI02KLVlvkrXpufWcK3hqXZ7Fa7nxfYU61Ff/5migSRJ+CK5fmssg0ipGMdSVkgTdC4Ni+hql9Fw2Nmef6Is2kBw+TWj3V96LVNVZvFnyqb24IVjU7QULRWQRZGpk6/VtpdN8+Td6XxYSOxkJokrGWeBqBAu9orcCdsAtbNBale+0/XO5NcTprIlfqTW9J9zR0pZpszmhFYLZvLM5SbP6uC6Rlp4UAi3Q4rKRmUMrDuR2HOqZGLDWrhprdoOqWkT+6/fZq7viAySuG9KtHWtheuRY4rWAVF7zY677SWf2libPSzcs+7mXdKcWB3Dky3UdEgRffvWva48TS4WjJU4uHy3XupGJOShl7PSz1TPIUWM6FPzCGtI+Cyj3j77Xue2hVppP4xyXxwqZzdn/t3l9L5ck1A81Sg60bNNTK2uR/rGp1ybXZiyQu+6MML6OI2taVdyKekOI5dfT3YKh0PKSOkt1SalyLHYCHowQltF47twOKSMMNaNiBxaX5AOFSXybDnrsSeZdqOlnhrw+X13G1X36277ulKpgxk6rz9Vw54f00vPZaDJUIcpsYT6f4PcWrVuf0P26LszmvQofOFM1wtMsFs6ue++zJnhOFML8hbNdH/RaaQdZ3NyZzhv1Z8tyOEmNisFcAtrc988HUbfBQlESjn4XmvfxTW7R2s1CqdgZtMwifEeLl9DQbAuF810jc2Z4YzYS5RdGRNGasxEZavMLF5ecmrTty3c1OEstHvFvbdljX5CbXtQKqxSa2vzo+OG74yGnrU4tqV1agZl7z6J1l/TSrpLLdsAzx/6swlsoy2bwA7asg3soQ1bwA7a7DdgJ23aBPUF/QbsoI5dulpOREoBFldS8DemzbWlSttlMyx+Id2NMqgPTfhSyjLDdHRKWfTqkAp0kibPo6yTdb7ICVT0rGua2rKSDF1I/ehvL5PuyJ3h2EIOQErhJ/lsf9BzvaAUCN3q076wtlh89KF1pLScFi5vRvw6ckBFsy4HI9MO9okkJbm1J4c6pNqEYrdpwR63nDOwNBwOqWg8CETi98mp279fZY2XexZyYSqmdTn03B4YKdqA1uSgtVIoyhA2MY4csa4L2EFb+oE9tKEb2EEf+ohlOxllzTWj2iZaOonmtYC+gVMKsMxN/2pq7TfsOq3JRTKRbEbWfffvZ39AkUY67tiqqAyGMIbJM5eiQQww3HC3lUW/UPoyZRjzdmffEz7+WMEs10lqyUnXJhk6tYhSFiqTXu0hc/HngL5lPq3gudpUvZaDZD/20z6/OeVfI8wnLxgx6ubeo+bprm7McC5T856PZF2KZARYrDwYRvIBsNEu/yn02OrzPAYniPrX/WDDG9PMdbw8y70rP0uWShd+vyQHNlH/+iWrHhxU8P0rUxf98uIli+YXmmETfd0HsA3soaX+Nxq/j3qA+mI0HcAO6ukGuo+NsW64kPRsyHB9AGND5k5YpmhVQ2/B8z+0yWRsMv873yv5Gq/Uq/wkO5XBCLagtGTCJB7RezmoDHpOsdZRDs7+r3MnoXBwmX2ixvpSdE26NsnQSVvmbiuLjuvGdOcPHdFSrfZ/6La9Csiu3DNKWahMui2HYHaj9YXBMtE6D3HCv3Lvas6eN/qDyZGORaMxnpX80q9SDad3a4mH/tW7Fjk74rzxD3VUFcmTKCyOvxnBJnq+LjkGq319i/1MmpO2M+598+DiubCJuteiRcqrvpz5QtVXs7ZWL5jzVfUXD/Y0Uh+ix98nm5Djduuyp+8hJy76dPX7KHrRRK9Rt4i0hU1gB23oCLqPjbFuWJ8xcTuAIAM+v5cmG9/QgChvtMsSE5DzrdVvfldvcpPMRnMcVvcqflxpqdfruHdbHyiDAaC0cV10EqYwIVaumt0YtbfDA9dSrsm6sAYZlYHKond7CMZuZpStk4lraO0fvclPMpPsbWUIlEW3UESRHm1wMNsEo6RoEVm+e+fZtI6UUxbzUt3iAi2sJxWth4RDfVCI5mSVg8nvhx57bNZVR7ogdiw9HEbi92WZm8kRFXquuXdJOuyh7vWsCTv3Wj+yW6Pver1HS0XLJpG6xoge39zRwqWbWxjP9iS2jmVNPd8xWv+hN5ufmJL/tLdp9+xqr/+hyj3ONUZyFIb2z4ejQzVenn6pZPEgsbN0F9u+dilt6TgWxlFavvYFy1+98bLlz7TSZ/SKF3L0Wg+Mck1y1v5m5T8+po9e7RHJ+1aN+kd2GPvD04/QmreoH9q4HtmEXjo5lP8DpxTYi/U3nfE7pq5j6o12GWKKBpf1Tj2l8SNZSWaj2YEmRAUXN7JO1jPSKH6SncpgFHtUpm/4RxedhCmSV/qscJZ7YqRlomvQtVgXHFIke6AMBiCwtpTEnqJ9IdgbekrjR7KSzAGTKGUwgpNQZmJH/pNVuo9SoTJQWULP3T7sknpyRN3Sa/S1tA5YrI0HtBrpn7oi/v5Ws+0+ckaZLdanKotTJxhV91r/zSAXXprjiZPlV0PPmRrEG0ayhx6vY/Yk7mQGJVL3c6TbNLufZ4cei8SWfrCNevYgtsm+vZ73y5xZFxm53pQOviipfNAFeWWDLhimfFKVj4U+yrlE5W9WtbP4eKvX33CwY7RT0b0WTeo6WOOcjod8qfmvsIe61/lXY9m9dZydSx8926OzZ49o/L9wQI6oXabmlTWSuCff+dkrGOuqfx1yEPbyeVe81a3h20P5f3BKgb2gCe0Bn993nbL7lAqXf4qubaRJ9a4y5q2RO9vT+LXoQNwWkpVkNqItKK2XMDHdDTRIZj2nJNsfNOEmmDy5i2t9pTBZfFQ40/VCJCbr6Tfpt+karCsOKUrbp8hOZTCKPcozNjyobAqVT5bktb+rhze/SUaSlWQm2dvLoP92akb8Ooc58TS9l4PKQGXB6Esf40NyFk7LGHPS9Zlj7nl03HDDT7wf7gNcNB/SW9cPva3V5LuDnFMma+PkyYOvnW1EW+jpt+t7N842yc0/Bo9NTL7FaM824dJdtOqJybr9i70HzWIJ2i/12i3C4nCuDj3ut6epyMh9aN+NC+uzNi0qkASjZ5T7lM+jQogRyrkG5W+tasvXJ8VZGHqclpxajnZKvbZLksRefYZV8LVGsIcefzvW0OMyOOQoRN1Qv360cjE8MOayrz7mUNLAcqUzhOHAftl65mOXCs7I6+yM8KVcXLAb+39x3/uxrvPVt+ZfKTH/W1qWUWamq455YfTbRrdF0Uz3R0r7OFEPsirt4YKcGY7fGtUWhbNdNzAvO5Q3YMqYhT1YlV4870gngsipkVE95Brl+g+yNsdGF5+22Y2505yvGs4WtH5XMHUhZ3NyZzhv1ba8rheYYLcou7VKRRlphCip0HuzV/mQUcHUd/prY11jKzOLl8fiiygAgMMjHM+t0XiGDKQuydo5bKTrh/L0Mx+sgV3Uf47fkT99or+175nNJlGybtfop2Op79k+6EJat9HXd+NCzawDTOtIxTUk3VluizuRSXH/zDvlxrkMqMrzP/zj8R6ZtqN2lHs29yk77U9GrCNH0oeokZqMInLIIXVxQtbjlEkAdlDPJjTJ/m5T6Zu0b5Ltf8s/+dYitBrqQikua611FxrFHpHwzUSzjlCb1b118M2yVPLDx6NueKWr5YFTChyU0kEXZHj7nvKosntthC7xhmX793/ss/HjKmi7jXV35t8lvP6nNFnvLKY/DHtmdEys90UTCpmVQxYpbeQZGm8LvyzvVTzBSBE5+6NolusxIbN7D/G/lQkTmytxz7ycO1LKDul6z9ZmycJ2DfezqexQnFEskCLu8ZzpzvsMawtan0kweuvYpOj3gbw7nQ9rUc6CZ1x/Uuz3kLLrV4YMZ+rVeXPQMlIaRW9cT71FGxXMbBrGLC01eben7EKvDwCIxoM7nh0BAAAAfffn6MsB6oQ+6gmcUiCsFE9dNDZu27q/KHfLmLDYj7GvW/oN+78hcycsg3Z/TXvEFEWGxGlEpBaZmW6MhQipUChlm8OUSPdorkZFLHT7G8aOvjujKRbsERL1cjidxnrOxSJhYpuEn63ngu8UlubAG2fca08SXKRxExvK/WygEHyC0osOPczGTfPRQ2GxxaymK5jMA+2BFp1wezkxJXFl7vT4d4xbL9wDaKuXKDC9yQsAAAAAAAAAAESTQ/HXaM1HAqcUiAhlgy4a3tr3JIqamqx8+hzify9VPh9Yt//wRtbGf6+FNg8OOQJlc8LbSh3NULmuVdEaUrHqQCTHlNOctEBrEVMUIeXy1U+MFYdUkMOMmIqOTQweIbUv7WkVX1J2TZzxeT+MFZuY1Xe68Fk3marKHrnlnIGl4brWLw8tHS6ltQ6Wd1o3HP3AuLUHra+mpOcFE9coh35mYVOMmEZxX1bNbswxidYdhxoRGPX6+2xtlp9be5w4LQEpLwAAAAAAAAAAAIMBpxSIOGUDLzy6td/JY5hgA3l7JIlyN/UP2Iexre3HhcrBJuu2FV9nbVr4C7R2aCy+7Kf+fXs2fSB7fTlqXF+ymIu218RPHv/e8Vtj2Q6Uyq935eCnDztKJ+wNIJtT0WvD742esu9AtKdl+xuj9cO1gV+Y2F+1msYukhTOdJ3HBP+gfLA5bntmqD1Edf+Na7LCkY9+y9wlk3q66uZ39D0b48ftz0lOKeE4kz5oi3LjbmWEMzl3hvPTmKkXbeX3aDUCiSKkBJNteks1CAAAAAAAAAAAgK4BpxQABoEWs+//rftO4fXThHe00vm1cIvpT1tPdTyDRej/R+Es90Qmi38quykqiVDLJH5T7nTHgli3Rfu6Rm+wQ1zvKQKUKd3RtUZcr6irUJTOuty4ggYn26tjNnNX7m0juh1xRMyuOW+/a2uWLgsee+zye6m3XHl58Jicxr2qB9/F/fx+ZfjjIJvIFnliLEbjBNZCk22986Y7f9CSXAWzXCdJkqdC65FcAAAAAAAAAAAAOHy66muSoCoAtA05hYY9M/ofuxocWYJJs5RTLRG8XAtdg65F14RDam/IGSSsnsGUqiza16Zr0rXhkGqDnECKPk5Qwxb72OSEWHZIEeT8cTn8L+17fuTX1mPD8ftuzksOdLzmpaL7h9aYtrdFzgmHxyQ+cPsbhsZqejhy+lRmbPgvOdDphQa15SEZSBaSCQ4pAAAAAAAAAAAAEIiUAkBnfH3VyrSURM/9EheXhmu9KVo3Shb8/doG2yNj3hq5E1runMCb/4L/PdJrTdHaUTIXf9Za5IOWCERNMf5cW9q2qHRA6zkTt8e6MyqUOUs29ZEzMxb55Lhj6LhvOfPXdWt78WX4Ku+fm1sbnjnc9c92L34gQS4Z8pKt2Xyqx+77dpv/nGlc2C5IEr88muEr7hH83rb4gX8aMSXnkVi3g6/ZHJ/t272pV/mQUTITO9RKl0fpBCXGe6CeAAAAAAAAAAAAsQHS9wFgcOgN9KO+rBrlMyVeYLJIow513SlaL8rvlZeb/Q0fbz4jYzmiog4PSl1m8pruUFrSSe2pw8LRwrmVFu5Dv8X/bKxGfBxOfcioGDqByeJ+1r7GXQQoZBJ/pKr3+kWoL/sn4JzqnZbmE87C0PO/+dIXuKeFJH/GpZblhxo1Q2nphBw3isvS2cG6lmr9L0ttLe34zr5p/WKN535uftHPTVNp3yy1rJbKqyYMW5Ow22FJOlOYm5fn3Z6yK5zX++WhpcPjUquv9PLuReu7p/47WCcKnqtN5T77KLe3/ovDdUQCAAAAAAAAAABAf8ApBUCMQRFUPZ07hntNSb04E4FJea9f6klbi0muCTQMjBda/PWVNa4eaxERFV7yn6yKd1gSLmibNBdj2aGvdVTGGV9Gk/Zub+PHmMw9fMhRKPmkG5UbfjI78vW/ahlnH8hm+RU4CLvGyytar3U52euh545b5WHOBlNoJ75e6cU3KqOQTUxiFYIzF/PzusDfTKIbF8zJZNabCT5QGaUM2k8UXOFg9kk6Y3Jm8EQsO6XeXLln/G674z+h50zCP/f2Y+03037QUcSl5sJwpNEjh1SGo2ZNqO63e8+5R8j2XNnkL023LJpE53e2nv1CLKbtIyd5eUJir8zGhkotOLBp3TXaXnhpjgctFAAAAAAAAACASAGnFAAAqAhFdjC/7ShmYsNpcl1pahO54M5AA82FS2nCGmgynvnZWmbybMZ6K5EhkELMxMc1C3Gqzc/7K73H0ANHtHG38rf1HpPYauf8W9kvlqqV+kzPPL+mLmfvSClRffI3redzWTpHkvkYpS6ccuhRhdyt1JnvZUl8zWX+Xu4Mx5Zd/3km17YhfUGbY0oqr3KnjT/6gXFrY1LnP7n+5DNZHgo9R9FSmes2jSwbPHQWN/lOtlkqatzrrdNPKekZeFlBtnjWHG70VOmL7zza3c3vCz1XbD0rzfXDZt79hJ1Voef3+HOSY8nJTve/iHMvlWRzoiz5GniLY9xtI7p1OLQpmtA+sPrYeFvpj5N6XVURzmvTyykJJx5lC+1PZr249aJ4ufpftO/o5n/qistH3YtWSl1Ofb324j7C0//k5JZPp03svx4aUR9yJCMCGgAAAAAAgCMHTikAAABgPwSiRrz2JMHkQPQAZ5JHWJrrw53eLJahaKmWhJbfMy7XMlm+O3RSnqI2MnYMHCb5+SDO+DFM5hmKERKUPyW0f6WRCeUjiSrBxGrZJDZW9di0bn9RHuXHXWJpnnTLIPuHczZm/ne+N1b1/WtHYFukFJflMm5v6nBWCb9t2W3DksbRPkV3Os2Jecoo0KGMqNy0/lSzr3H7gRxI9H27OaEvrRPV1/bvKbZm6bL//VUqj//9pVlb5i6Z5HL53gv9f06n+bIBU8/5MGZssbbxZ252H9Ohc59j9W3DE46l/dnbPhsVb+rxTfBv3ctr8i485ZywRGCuuzP/Ln/fhsdp386TP9uQabuQJtlffuF7f+j3mjPSR8SKI4TamqpT3ZP9ski3+F0Lp2SP37jv33f/pvH/bFLpuHrR79mbe4+aF2mZrnit4u4Ef9NjweNzE5qciGD7Hx9WvtU7zbyr32k9pn8bjeuRM+pB94CFTGbjM7jvmU+uS5sBK6jL7AVbh35Wn3BTMm/6+K1r+2FNQgAAAAAAnQGnFAAAAABAjNCWNlE8poyQ0s2m5mWCfT6Rs7MWcJNnbOj3bh3c84ADqEAaUnNSz4DDlvOE9hFlIzlu3b76mqDDavfiBxLkkiEvtTmmpPIaZ9IMcjwVT1001juIfxH6m3a347hYiWCjCe6yEVm/co5mrSmzkINoTmX+GzZfwtUdf5Dqn7o+c8w94bhu37LqvRwb5Ayktb5qdra2hJ6PFacU6WTHMfH/kezNZwTP7esEfL4s/55ujv85iJoa+ZB9HVdHAkXFtZSYT4jL9v14yzkDAwvgnf/6zpm9fbvvCH5neIolZpyEL5UsHtQipB59VqUW7M8R982OWafWxTu+VHbjvLu9H12SedvFkZbpqje2jS2Wk78KHp/Vt6Lno+OGI701a3PaWoeuf572t0i2398+7JL6aFz3hNfqtiqdVLZgrOTH67v1hyX2blNKra3N0bxHqY6YW1K/tLH6JS9OyTwPVlCfJ+fU3Timxboub7rzB2gDAACAFumqr0mCqgAAAAAA9M3vTra+cecxcRnZxWvsFA0VmEBsNX+11+DQbzvoW+fkdKLUiJS2kiY7Ah9ln86FRlB1H/9QI63f1TRiY/qyrAH9gpFQQ+ZOWJbsaHgy+D3aj6WUiuR42lfHdBxMC2aT29Z3DOIW0q5wXfdA57v3kW8PHlvNze/HigOkKm/ngFCHFFGZ7bgj9NjB5dTQ456VPD5c1//wnWVnWbbu2kapE2lLx4E6wZs+Dn6Hy84lsWKP18q/fiI+QRSnJPq/Kf+NayNFRO37HZvkO1XZxNG+pbslLxpyZWW51pLzI1BXle0ptZ66WLAH6f+DDW9MW/j9kpwDThKcUDChRZJuoE8Pk/uaaMnGJVaMHv3XUARZctXmkmO2l9Y8t25+UrSuK8vWwDrFfh6XBSv8D3Lavj614J+v5ZVOjOZ1yTEpC/NLKyy+F2GFvaEUxmpct3Cm64Vvn2l6HxbQBrSEQsEs10nQBAD6AE4pAAAAAACDEBqBkLlpwz/klsS5lEIujtd9IZXVXBfOa6Wf+WDNvg6RPjdf8UdaR4o+tB9zA2tFx+SIovWkaBuqc1f9zoflZvuXPq9lh8fc+KZoqJkbruvy+Iy/BPcpfd/a5LRPaP+35586Z8vgxHRv/9R+10w5/YpYrhv7OgUTpPJneMuuH2m/viX+zXClUiTcTfJ1+zumdGTd+3vTXRbrGed1q74wFvROk7dxdvcfgsfd7E1ZjXLmnft+7+emYQuUTWA9tAx3wwvhloOi1Ka+vNF/0z93rqYJfjpHESdn960YaefyJbSNhXWlKCLNnOzeYu3T8qw4rrRw0S8vXrK/71VZ+q4O7vfsXvdTtOQ7K6viOrLHTRk7x8RC/SAH4eI1b74yv/z5fx3IFmpyqaN6dqul4b5ma9OdsWAPirKllwjoQ/sH+p51gdRL6XmvYyfU/CGa8lHUrcR9U2RT7YMY8f4PckhtXZtR8+HEddOifnEuvrVJ/DNYYW9eObbsrwerQ5HCz609JInlwQK/btvUujald4cFtDUu15I8SN8HAAAAAACAzvnloaXDabthUOKGWJhc74zQdInkCBSmstPCmZ7vYLzz7vLH3XWmjslKRzf/U1dcPureWLQDRXV0T3fuFYHU0uzYb+pKmjTZPnSbM9yp4igFmdPb+mXHc6TsjNlUZOT8sHS3/DZ43LzHVnR5r2tyD2S79G4t8ZN6XVURbjlofbWNzPE4Rag5uHz3t9el/CsW7fFu5bxCe52pI2Jth7P74AO1U+RMNdt9TcF0oOHm1NdrAykz73VuWxirfci8l756x2JpCayZ6fXGvXewlznmnV88Oi3eUz7+veO3YgQSGcjJ1NSj+WnBxKr0evPVB9M1Ra31G1T17Zi3RiIFa4RYfNlP/SuLU6+WrGL19QV9FnRmu7grmudgPBp5m3SlDVp9a/6Vx7ww+m1oTBuQY8joa8piTSkAAAAAAABAzPL61vwJnjjebfvGboujuQ4Lrc9WkrT+JcG8p3Bm+T67fuiU0BSYsQY5CJPimjrWUwv3+l2dAafU/zgUp1SkoMmY/3P3dwePY3n9qH+ve3uvyRiTtXHyhKNvnh9tOSiSsEqY26KhJLa46NpuMVc/yCne1LSjNfRc713HJ6rRdpMDcvWuhPe8wjZsmN835e5bur0Sa/agdqJuqWj83xnx+nVz825SSx5ay0tI3oEJffc8HynHsNbrx66He6xTGqyj6dic6RpzzSdD8tWSh8ZZdnNCX0ozHqtjq3l5297zeU2TzRb/B9cU9LsMo351IUds/x67T66pj38ALyt03Sllxq0DAAAAAAAAMBrX9R+9SI3rtk9iXgELtJH5nePG2hz2oaN1W7aUaPpoSnb4I28OxqWO2uWf1qUvEZLrHDpuivM+Fau24FLSHYy5KbURrRFU1su8e3q0ZZAkgTfn2/Em7fnIUp/c4SRsccZ/r4YclcJ8YccrvzIbH4u2oIiON15aUmq1yH3ouNUrlar1MsFn9Qk3pQrbMNpfZzK/pGxizimlpXZi1otbL2qRFTv4zcy7Je5s5dRxsWaPpJLetp3Me3Tw2F/jTFFLFnJIOUyJ65lgvYpmuZ7Kme68L9bs0Zay0jSZ9skxpejkd2q+/FT0rGuaKWnrFf7d/e/PmeFcFov2MDP/025XEuuZFAj2V/UZoOC52tTuloIxFQ3HLtH6S3FYUwoAAAAAAAAAQESgyV5yEE4efO3sSKSC68r1KTKK1vM6N6HJSWt7xaotSP+mT0/o79vjyOSLcgae1mP6t2rYg9aNogipYPq+WLVHTX2PG1pNvjuc7vjHeX1Krhr1g+jFfQs7DiS2OFbt0ZA28FxK20cf2ldLjkTRWhnct3BPTEaCUDshVThPp9R99Ml2yferJYtXdEthMQ5NbAvB/8YZ+4UicwZMtnyhliw2W/wgarZoX8js9Fi0B6WpJDvQPm3VdjzYHeIZq7ffSFP3rY/Eoj0aLzTVeWVvCe1v3dF9hdryWBIqF+1KSXjfkVkxS+u6Q/o+AAAAAAAAAAAAgBiEUnM97up3Ie3fbS/51OhrXWgdSl33vjt9WgO39jovvW5mLKaL01r9KN7d52GbL+7sk2TpxhOnJRRBK+pSONP1AhMsj3H2YO4M56fQiPr2iLNuvblF7v+73GnOV2O1nUhY6O+mhXXtCt8tfNlvabnesdv25xFTclRxFHbZ10Rf1OMHAAAAAAAAAAAAAAAAAAAAtKXwU/P6XfXtcDh4AAAAAAAAAAAAAAAAAAAAQKTBmlIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACLO/wswAA1Niv+YaMCdAAAAAElFTkSuQmCC") no-repeat; - background-size initial - background-position 0 0 - transition background-position 1s steps(25) - transition-duration 0s - - &.active - transition-duration 1s - background-position -2500px 0 - -</style> diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue deleted file mode 100644 index 51c73003d1..0000000000 --- a/src/client/app/common/views/components/poll-editor.vue +++ /dev/null @@ -1,235 +0,0 @@ -<template> -<div class="zmdxowus"> - <p class="caution" v-if="choices.length < 2"> - <fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }} - </p> - <ul ref="choices"> - <li v-for="(choice, i) in choices"> - <input :value="choice" @input="onInput(i, $event)" :placeholder="$t('choice-n').replace('{}', i + 1)"> - <button @click="remove(i)" :title="$t('remove')"> - <fa icon="times"/> - </button> - </li> - </ul> - <button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button> - <button class="add" v-else disabled>{{ $t('no-more') }}</button> - <button class="destroy" @click="destroy" :title="$t('destroy')"> - <fa icon="times"/> - </button> - <section> - <ui-switch v-model="multiple">{{ $t('multiple') }}</ui-switch> - <div> - <ui-select v-model="expiration"> - <template #label>{{ $t('expiration') }}</template> - <option value="infinite">{{ $t('infinite') }}</option> - <option value="at">{{ $t('at') }}</option> - <option value="after">{{ $t('after') }}</option> - </ui-select> - <section v-if="expiration === 'at'"> - <ui-input v-model="atDate" type="date"> - <template #title>{{ $t('deadline-date') }}</template> - </ui-input> - <ui-input v-model="atTime" type="time"> - <template #title>{{ $t('deadline-time') }}</template> - </ui-input> - </section> - <section v-if="expiration === 'after'"> - <ui-input v-model="after" type="number"> - <template #title>{{ $t('interval') }}</template> - </ui-input> - <ui-select v-model="unit"> - <template #title>{{ $t('unit') }}</template> - <option value="second">{{ $t('second') }}</option> - <option value="minute">{{ $t('minute') }}</option> - <option value="hour">{{ $t('hour') }}</option> - <option value="day">{{ $t('day') }}</option> - </ui-select> - </section> - </div> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { erase } from '../../../../../prelude/array'; -import { addTimespan } from '../../../../../prelude/time'; -import { formatDateTimeString } from '../../../../../misc/format-time-string'; - -export default Vue.extend({ - i18n: i18n('common/views/components/poll-editor.vue'), - data() { - return { - choices: ['', ''], - multiple: false, - expiration: 'infinite', - atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'), - atTime: '00:00', - after: 0, - unit: 'second' - }; - }, - watch: { - choices() { - this.$emit('updated'); - } - }, - methods: { - onInput(i, e) { - Vue.set(this.choices, i, e.target.value); - }, - - add() { - this.choices.push(''); - this.$nextTick(() => { - (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); - }); - }, - - remove(i) { - this.choices = this.choices.filter((_, _i) => _i != i); - }, - - destroy() { - this.$emit('destroyed'); - }, - - get() { - const at = () => { - return new Date(`${this.atDate} ${this.atTime}`).getTime(); - }; - - const after = () => { - let base = parseInt(this.after); - switch (this.unit) { - case 'day': base *= 24; - case 'hour': base *= 60; - case 'minute': base *= 60; - case 'second': return base *= 1000; - default: return null; - } - }; - - return { - choices: erase('', this.choices), - multiple: this.multiple, - ...( - this.expiration === 'at' ? { expiresAt: at() } : - this.expiration === 'after' ? { expiredAfter: after() } : {}) - }; - }, - - set(data) { - if (data.choices.length == 0) return; - this.choices = data.choices; - if (data.choices.length == 1) this.choices = this.choices.concat(''); - this.multiple = data.multiple; - if (data.expiresAt) { - this.expiration = 'at'; - this.atDate = this.atTime = data.expiresAt; - } else if (typeof data.expiredAfter === 'number') { - this.expiration = 'after'; - this.after = data.expiredAfter; - } else { - this.expiration = 'infinite'; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.zmdxowus - padding 8px - - > .caution - margin 0 0 8px 0 - font-size 0.8em - color #f00 - - > [data-icon] - margin-right 4px - - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 8px 0 - padding 0 - width 100% - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > input - padding 6px 8px - width 300px - font-size 14px - color var(--inputText) - background var(--pollEditorInputBg) - border solid 1px var(--primaryAlpha01) - border-radius 4px - - &:hover - border-color var(--primaryAlpha02) - - &:focus - border-color var(--primaryAlpha05) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .add - margin 8px 0 0 0 - vertical-align top - color var(--primary) - z-index 1 - - > .destroy - position absolute - top 0 - right 0 - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > section - margin 16px 0 -16px 0 - - > div - margin 0 8px - - &:last-child - flex 1 0 auto - - > section - align-items center - display flex - margin -32px 0 0 - - > :first-child - margin-right 16px - - > .ui-input - flex 1 0 auto -</style> diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue deleted file mode 100644 index bd5eeaf832..0000000000 --- a/src/client/app/common/views/components/poll.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="mk-poll" :data-done="closed || isVoted"> - <ul> - <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> - <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> - <span> - <template v-if="choice.isVoted"><fa icon="check"/></template> - <mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> - <span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span> - </span> - </li> - </ul> - <p> - <span>{{ $t('total-votes').replace('{}', total) }}</span> - <span> · </span> - <a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a> - <span v-if="isVoted">{{ $t('voted') }}</span> - <span v-else-if="closed">{{ $t('closed') }}</span> - <span v-if="remaining > 0"> · {{ timer }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { sum } from '../../../../../prelude/array'; -export default Vue.extend({ - i18n: i18n('common/views/components/poll.vue'), - props: ['note'], - data() { - return { - remaining: -1, - showResult: false - }; - }, - computed: { - poll(): any { - return this.note.poll; - }, - total(): number { - return sum(this.poll.choices.map(x => x.votes)); - }, - closed(): boolean { - return !this.remaining; - }, - timer(): string { - return this.$t( - this.remaining > 86400 ? 'remaining-days' : - this.remaining > 3600 ? 'remaining-hours' : - this.remaining > 60 ? 'remaining-minutes' : 'remaining-seconds') - .replace('{s}', Math.floor(this.remaining % 60)) - .replace('{m}', Math.floor(this.remaining / 60) % 60) - .replace('{h}', Math.floor(this.remaining / 3600) % 24) - .replace('{d}', Math.floor(this.remaining / 86400)); - }, - isVoted(): boolean { - return !this.poll.multiple && this.poll.choices.some(c => c.isVoted); - } - }, - created() { - this.showResult = this.isVoted; - - if (this.note.poll.expiresAt) { - const update = () => { - if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000)) - requestAnimationFrame(update); - else - this.showResult = true; - }; - - update(); - } - }, - methods: { - toggleShowResult() { - this.showResult = !this.showResult; - }, - vote(id) { - if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; - this.$root.api('notes/polls/vote', { - noteId: this.note.id, - choice: id - }).then(() => { - if (!this.showResult) this.showResult = !this.poll.multiple; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-poll - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 4px 0 - padding 4px 8px - width 100% - color var(--pollChoiceText) - border solid 1px var(--pollChoiceBorder) - border-radius 4px - overflow hidden - cursor pointer - - &:hover - background rgba(#000, 0.05) - - &:active - background rgba(#000, 0.1) - - > .backdrop - position absolute - top 0 - left 0 - height 100% - background var(--primary) - transition width 1s ease - - > span - > [data-icon] - margin-right 4px - - > .votes - margin-left 4px - - > p - color var(--text) - - a - color inherit - - &[data-done] - > ul > li - cursor default - - &:hover - background transparent - - &:active - background transparent - -</style> diff --git a/src/client/app/common/views/components/post-form-attaches.vue b/src/client/app/common/views/components/post-form-attaches.vue deleted file mode 100644 index e051b6a808..0000000000 --- a/src/client/app/common/views/components/post-form-attaches.vue +++ /dev/null @@ -1,139 +0,0 @@ -<template> -<div class="skeikyzd" v-show="files.length != 0"> - <x-draggable class="files" :list="files" animation="150"> - <div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> - <x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> - <img class="remove" @click.stop="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> - <div class="sensitive" v-if="file.isSensitive"> - <fa class="icon" :icon="faExclamationTriangle"/> - </div> - </div> - </x-draggable> - <p class="remain">{{ 4 - files.length }}/4</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import XMenu from '../../../common/views/components/menu.vue'; -import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import XFileThumbnail from './drive-file-thumbnail.vue' - -export default Vue.extend({ - i18n: i18n('common/views/components/post-form-attaches.vue'), - - components: { - XDraggable, - XFileThumbnail - }, - - props: { - files: { - type: Array, - required: true - }, - detachMediaFn: { - type: Function, - required: false - } - }, - - data() { - return { - faExclamationTriangle - }; - }, - - methods: { - detachMedia(id) { - if (this.detachMediaFn) this.detachMediaFn(id) - else if (this.$parent.detachMedia) this.$parent.detachMedia(id) - }, - toggleSensitive(file) { - this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: !file.isSensitive - }).then(() => { - file.isSensitive = !file.isSensitive; - }); - }, - showFileMenu(file, ev: MouseEvent) { - this.$root.new(XMenu, { - items: [{ - type: 'item', - text: file.isSensitive ? this.$t('unmark-as-sensitive') : this.$t('mark-as-sensitive'), - icon: file.isSensitive ? faEyeSlash : faEye, - action: () => { this.toggleSensitive(file) } - }, { - type: 'item', - text: this.$t('attach-cancel'), - icon: faTimesCircle, - action: () => { this.detachMedia(file.id) } - }], - source: ev.currentTarget || ev.target - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.skeikyzd - padding 4px - - > .files - display flex - flex-wrap wrap - - > div - width 64px - height 64px - margin 4px - cursor move - - &:hover > .remove - display block - - > .thumbnail - width 100% - height 100% - z-index 1 - color var(--text) - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - z-index 1000 - - > .sensitive - display flex - position absolute - width 64px - height 64px - top 0 - left 0 - z-index 2 - background rgba(17, 17, 17, .7) - color #fff - - > .icon - margin auto - - > .remain - display block - position absolute - top 8px - right 8px - margin 0 - padding 0 - color var(--primaryAlpha04) - -</style> diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue deleted file mode 100644 index afe51d7833..0000000000 --- a/src/client/app/common/views/components/reaction-icon.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n(), - props: { - reaction: { - type: String, - required: true - }, - }, - data() { - return { - customEmojis: [] - }; - }, - created() { - this.$root.getMeta().then(meta => { - if (meta && meta.emojis) this.customEmojis = meta.emojis; - }); - }, - computed: { - str(): any { - switch (this.reaction) { - case 'like': return '👍'; - case 'love': return '❤'; - case 'laugh': return '😆'; - case 'hmm': return '🤔'; - case 'surprise': return '😮'; - case 'congrats': return '🎉'; - case 'angry': return '💢'; - case 'confused': return '😥'; - case 'rip': return '😇'; - case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? '🍣' : '🍮'; - case 'star': return '⭐'; - default: return this.reaction; - } - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-reaction-icon - img - vertical-align middle - width 1em - height 1em -</style> diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue deleted file mode 100644 index f363fe9779..0000000000 --- a/src/client/app/common/views/components/reaction-picker.vue +++ /dev/null @@ -1,323 +0,0 @@ -<template> -<div class="rdfaahpb" v-hotkey.global="keymap"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover"> - <p v-if="!$root.isMobile">{{ title }}</p> - <div class="buttons" ref="buttons" :class="{ showFocus }"> - <button v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" @mouseover="onMouseover" @mouseout="onMouseout" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction" v-particle><mk-reaction-icon :reaction="reaction"/></button> - </div> - <div v-if="enableEmojiReaction" class="text"> - <input v-model="text" :placeholder="$t('input-reaction-placeholder')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; -import { emojiRegex } from '../../../../../misc/emoji-regex'; - -export default Vue.extend({ - i18n: i18n('common/views/components/reaction-picker.vue'), - props: { - source: { - required: true - }, - - reactions: { - required: false - }, - - showFocus: { - type: Boolean, - required: false, - default: false - }, - - animation: { - type: Boolean, - required: false, - default: true - } - }, - - data() { - return { - rs: this.reactions || this.$store.state.settings.reactions, - title: this.$t('choose-reaction'), - text: null, - enableEmojiReaction: false, - focus: null - }; - }, - - computed: { - keymap(): any { - return { - 'esc': this.close, - 'enter|space|plus': this.choose, - 'up|k': this.focusUp, - 'left|h|shift+tab': this.focusLeft, - 'right|l|tab': this.focusRight, - 'down|j': this.focusDown, - '1': () => this.react('like'), - '2': () => this.react('love'), - '3': () => this.react('laugh'), - '4': () => this.react('hmm'), - '5': () => this.react('surprise'), - '6': () => this.react('congrats'), - '7': () => this.react('angry'), - '8': () => this.react('confused'), - '9': () => this.react('rip'), - '0': () => this.react('pudding'), - }; - } - }, - - watch: { - focus(i) { - this.$refs.buttons.children[i].focus(); - - if (this.showFocus) { - this.title = this.$refs.buttons.children[i].title; - } - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.enableEmojiReaction = meta.enableEmojiReaction; - }); - - this.$nextTick(() => { - this.focus = 0; - - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - if (this.$root.isMobile) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = (y - (height / 2)) + 'px'; - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = y + 'px'; - } - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: this.animation ? 100 : 0, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: this.animation ? 500 : 0 - }); - }); - }, - - methods: { - react(reaction) { - this.$emit('chosen', reaction); - }, - - reactText() { - if (!this.text) return; - this.react(this.text); - }, - - tryReactText() { - if (!this.text) return; - if (!this.text.match(emojiRegex)) return; - this.reactText(); - }, - - onMouseover(e) { - this.title = e.target.title; - }, - - onMouseout(e) { - this.title = this.$t('choose-reaction'); - }, - - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: this.animation ? 200 : 0, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: this.animation ? 200 : 0, - easing: 'easeInBack', - complete: () => { - this.$emit('closed'); - this.destroyDom(); - } - }); - }, - - focusUp() { - this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5); - }, - - focusDown() { - this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5); - }, - - focusRight() { - this.focus = this.focus == 9 ? 0 : (this.focus + 1); - }, - - focusLeft() { - this.focus = this.focus == 0 ? 9 : (this.focus - 1); - }, - - choose() { - this.$refs.buttons.childNodes[this.focus].click(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rdfaahpb - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - $bgcolor = var(--popupBg) - position absolute - z-index 10001 - background $bgcolor - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - &.isMobile - > div - width 280px - - > button - width 50px - height 50px - font-size 28px - border-radius 4px - - &:not(.isMobile) - $arrow-size = 16px - - margin-top $arrow-size - transform-origin center -($arrow-size) - - &:before - content "" - display block - position absolute - top -($arrow-size * 2) - left s('calc(50% - %s)', $arrow-size) - border-top solid $arrow-size transparent - border-left solid $arrow-size transparent - border-right solid $arrow-size transparent - border-bottom solid $arrow-size $bgcolor - - > p - display block - margin 0 - padding 8px 10px - font-size 14px - color var(--popupFg) - border-bottom solid var(--lineWidth) var(--faceDivider) - line-height 20px - - > .buttons - padding 4px 4px 8px 4px - width 216px - text-align center - - &.showFocus - > button:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 0 - right 0 - bottom 0 - left 0 - border 2px solid var(--primaryAlpha03) - border-radius 4px - - > button - padding 0 - width 40px - height 40px - font-size 24px - border-radius 2px - - > * - height 1em - - &:hover - background var(--reactionPickerButtonHoverBg) - - &:active - background var(--primary) - box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15) - - > .text - width 216px - padding 0 8px 8px 8px - - > input - width 100% - padding 10px - margin 0 - text-align center - font-size 16px - color var(--desktopPostFormTextareaFg) - background var(--desktopPostFormTextareaBg) - outline none - border solid 1px var(--primaryAlpha01) - border-radius 4px - transition border-color .2s ease - - &:hover - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - border-color var(--primaryAlpha05) - transition border-color 0s ease - -</style> diff --git a/src/client/app/common/views/components/reactions-viewer.details.vue b/src/client/app/common/views/components/reactions-viewer.details.vue deleted file mode 100644 index 778b936896..0000000000 --- a/src/client/app/common/views/components/reactions-viewer.details.vue +++ /dev/null @@ -1,122 +0,0 @@ -<template> -<transition name="zoom-in-top"> - <div class="buebdbiu" ref="popover" v-if="show"> - <i18n path="few-users" v-if="users.length <= 10"> - <span slot="users"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - </span> - <mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" /> - </i18n> - <i18n path="many-users" v-if="10 < users.length"> - <span slot="users"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - </span> - <span slot="omitted">{{ count - 10 }}</span> - <mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" /> - </i18n> - </div> -</transition> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/reactions-viewer.details.vue'), - props: { - reaction: { - type: String, - required: true, - }, - users: { - type: Array, - required: true, - }, - count: { - type: Number, - required: true, - }, - source: { - required: true, - } - }, - data() { - return { - show: false - }; - }, - mounted() { - this.show = true; - - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - if (this.source == null) { - this.destroyDom(); - return; - } - const rect = this.source.getBoundingClientRect(); - - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - 28) + 'px'; - popover.style.top = (y + 16) + 'px'; - }); - } - methods: { - close() { - this.show = false; - setTimeout(this.destroyDom, 300); - } - } -}) -</script> - -<style lang="stylus" scoped> -.buebdbiu - $bgcolor = var(--popupBg) - z-index 10000 - display block - position absolute - max-width 240px - font-size 0.8em - padding 6px 8px - background $bgcolor - text-align center - color var(--text) - border-radius 4px - box-shadow 0 var(--lineWidth) 4px rgba(#000, 0.25) - pointer-events none - transform-origin center -16px - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - left 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(#000, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - left 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px $bgcolor - border-left solid 14px transparent -</style> diff --git a/src/client/app/common/views/components/reactions-viewer.reaction.vue b/src/client/app/common/views/components/reactions-viewer.reaction.vue deleted file mode 100644 index dade012c29..0000000000 --- a/src/client/app/common/views/components/reactions-viewer.reaction.vue +++ /dev/null @@ -1,208 +0,0 @@ -<template> -<span - class="reaction" - :class="{ reacted: note.myReaction == reaction }" - @click="toggleReaction(reaction)" - v-if="count > 0" - v-particle="!isMe" - @mouseover="onMouseover" - @mouseleave="onMouseleave" - ref="reaction" -> - <mk-reaction-icon :reaction="reaction" ref="icon"/> - <span>{{ count }}</span> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Icon from './reaction-icon.vue'; -import anime from 'animejs'; -import XDetails from './reactions-viewer.details.vue'; - -export default Vue.extend({ - props: { - reaction: { - type: String, - required: true, - }, - count: { - type: Number, - required: true, - }, - isInitial: { - type: Boolean, - required: true, - }, - note: { - type: Object, - required: true, - }, - canToggle: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - details: null, - detailsTimeoutId: null, - isHovering: false - }; - }, - computed: { - isMe(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; - }, - }, - mounted() { - if (!this.isInitial) this.anime(); - }, - watch: { - count(newCount, oldCount) { - if (oldCount < newCount) this.anime(); - if (this.details != null) this.openDetails(); - }, - }, - methods: { - toggleReaction() { - if (this.isMe) return; - if (!this.canToggle) return; - - const oldReaction = this.note.myReaction; - if (oldReaction) { - this.$root.api('notes/reactions/delete', { - noteId: this.note.id - }).then(() => { - if (oldReaction !== this.reaction) { - this.$root.api('notes/reactions/create', { - noteId: this.note.id, - reaction: this.reaction - }); - } - }); - } else { - this.$root.api('notes/reactions/create', { - noteId: this.note.id, - reaction: this.reaction - }); - } - }, - onMouseover() { - this.isHovering = true; - this.detailsTimeoutId = setTimeout(this.openDetails, 300); - }, - onMouseleave() { - this.isHovering = false; - clearTimeout(this.detailsTimeoutId); - this.closeDetails(); - }, - openDetails() { - if (this.$root.isMobile) return; - this.$root.api('notes/reactions', { - noteId: this.note.id, - type: this.reaction, - limit: 11 - }).then((reactions: any[]) => { - const users = reactions - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) - .map(x => x.user); - - this.closeDetails(); - if (!this.isHovering) return; - this.details = this.$root.new(XDetails, { - reaction: this.reaction, - users, - count: this.count, - source: this.$refs.reaction - }); - }); - }, - closeDetails() { - if (this.details != null) { - this.details.close(); - this.details = null; - } - }, - anime() { - if (this.$store.state.device.reduceMotion) return; - if (document.hidden) return; - - this.$nextTick(() => { - if (this.$refs.icon == null) return; - - const rect = this.$refs.icon.$el.getBoundingClientRect(); - - const x = rect.left; - const y = rect.top; - - const icon = new Icon({ - parent: this, - propsData: { - reaction: this.reaction - } - }).$mount(); - - icon.$el.style.position = 'absolute'; - icon.$el.style.zIndex = 100; - icon.$el.style.top = (y + window.scrollY) + 'px'; - icon.$el.style.left = (x + window.scrollX) + 'px'; - icon.$el.style.fontSize = window.getComputedStyle(this.$refs.icon.$el).fontSize; - - document.body.appendChild(icon.$el); - - anime({ - targets: icon.$el, - opacity: [1, 0], - translateY: [0, -64], - duration: 1000, - easing: 'linear', - complete: () => { - icon.destroyDom(); - } - }); - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.reaction - display inline-block - height 32px - margin 2px - padding 0 6px - border-radius 4px - cursor pointer - - &, * - -webkit-touch-callout none - -webkit-user-select none - -khtml-user-select none - -moz-user-select none - -ms-user-select none - user-select none - - * - user-select none - pointer-events none - - &.reacted - background var(--primary) - - > span - color var(--primaryForeground) - - &:not(.reacted) - background var(--reactionViewerButtonBg) - - &:hover - background var(--reactionViewerButtonHoverBg) - - > span - font-size 1.1em - line-height 32px - color var(--text) -</style> diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue deleted file mode 100644 index 9701d2481a..0000000000 --- a/src/client/app/common/views/components/reactions-viewer.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="mk-reactions-viewer" :class="{ isMe }"> - <x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XReaction from './reactions-viewer.reaction.vue'; - -export default Vue.extend({ - components: { - XReaction - }, - data() { - return { - initialReactions: new Set(Object.keys(this.note.reactions)) - }; - }, - props: { - note: { - type: Object, - required: true - }, - }, - computed: { - isMe(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-reactions-viewer - margin: 4px -2px - - &:empty - display none - - &.isMe - > span - cursor default !important - - &:hover - background var(--reactionViewerButtonBg) !important -</style> diff --git a/src/client/app/common/views/components/renote.vue b/src/client/app/common/views/components/renote.vue deleted file mode 100644 index 58a0a26593..0000000000 --- a/src/client/app/common/views/components/renote.vue +++ /dev/null @@ -1,104 +0,0 @@ -<template> -<div class="puqkfets" :class="{ mini: narrow }"> - <mk-avatar class="avatar" :user="note.user"/> - <fa icon="retweet"/> - <i18n path="@.renoted-by" tag="span"> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user"> - <mk-user-name :user="note.user"/> - </router-link> - </i18n> - <div class="info"> - <span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span> - <mk-time :time="note.createdAt"/> - <span class="visibility" v-if="note.visibility != 'public'"> - <fa v-if="note.visibility == 'home'" icon="home"/> - <fa v-if="note.visibility == 'followers'" icon="unlock"/> - <fa v-if="note.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - note: { - type: Object, - required: true - } - }, - inject: { - narrow: { - default: false - } - }, -}); -</script> - -<style lang="stylus" scoped> -.puqkfets - display flex - align-items center - padding 8px 16px - line-height 28px - white-space pre - color var(--renoteText) - background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%) - - &:not(.mini) - padding 8px 16px - - @media (min-width 500px) - padding 8px 16px - - @media (min-width 600px) - padding 16px 32px 8px 32px - - > .avatar - flex-shrink 0 - display inline-block - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - > [data-icon] - margin-right 4px - - > span - overflow hidden - flex-shrink 1 - text-overflow ellipsis - white-space nowrap - - > .name - font-weight bold - - > .info - margin-left auto - font-size 0.9em - - > .mobile - margin-right 8px - - > .mk-time - flex-shrink 0 - - > .visibility - margin-left 8px - - [data-icon] - margin-right 0 - - > .localOnly - margin-left 4px - - [data-icon] - margin-right 0 - -</style> diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue deleted file mode 100644 index 813a91b5c0..0000000000 --- a/src/client/app/common/views/components/settings/2fa.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<div class="2fa totp-section"> - <p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p> - <ui-info warn>{{ $t('caution') }}</ui-info> - <p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p> - <template v-if="$store.state.i.twoFactorEnabled"> - <h2 class="heading">{{ $t('totp-header') }}</h2> - <p>{{ $t('already-registered') }}</p> - <ui-button @click="unregister">{{ $t('unregister') }}</ui-button> - - <template v-if="supportsCredentials"> - <hr class="totp-method-sep"> - - <h2 class="heading">{{ $t('security-key-header') }}</h2> - <p>{{ $t('security-key') }}</p> - <div class="key-list"> - <div class="key" v-for="key in $store.state.i.securityKeysList"> - <h3> - {{ key.name }} - </h3> - <div class="last-used"> - {{ $t('last-used') }} - <mk-time :time="key.lastUsed"/> - </div> - <ui-button @click="unregisterKey(key)"> - {{ $t('unregister') }} - </ui-button> - </div> - </div> - - <ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0"> - {{ $t('use-password-less-login') }} - </ui-switch> - - <ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info> - <ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button> - - <ol v-if="registration && !registration.error"> - <li v-if="registration.stage >= 0"> - {{ $t('activate-key') }} - <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" /> - </li> - <li v-if="registration.stage >= 1"> - <ui-form :disabled="registration.stage != 1 || registration.saving"> - <ui-input v-model="keyName" :max="30"> - <span>{{ $t('security-key-name') }}</span> - </ui-input> - <ui-button @click="registerKey" :disabled="this.keyName.length == 0"> - {{ $t('register-security-key') }} - </ui-button> - <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" /> - </ui-form> - </li> - </ol> - </template> - </template> - <div v-if="data && !$store.state.i.twoFactorEnabled"> - <ol> - <li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank">{{ $t('howtoinstall') }}</a></li> - <li>{{ $t('scan') }}<br><img :src="data.qr"></li> - <li>{{ $t('done') }}<br> - <ui-input v-model="token">{{ $t('token') }}</ui-input> - <ui-button primary @click="submit">{{ $t('submit') }}</ui-button> - </li> - </ol> - <ui-info>{{ $t('info') }}</ui-info> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { hostname } from '../../../../config'; -import { hexifyAB } from '../../../scripts/2fa'; - -function stringifyAB(buffer) { - return String.fromCharCode.apply(null, new Uint8Array(buffer)); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.2fa.vue'), - data() { - return { - data: null, - supportsCredentials: !!navigator.credentials, - usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin, - registration: null, - keyName: '', - token: null - }; - }, - methods: { - register() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); - }); - }, - - unregister() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/unregister', { - password: password - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - this.$notify(this.$t('unregistered')); - this.$store.state.i.twoFactorEnabled = false; - }); - }); - }, - - submit() { - this.$root.api('i/2fa/done', { - token: this.token - }).then(() => { - this.$notify(this.$t('success')); - this.$store.state.i.twoFactorEnabled = true; - }).catch(() => { - this.$notify(this.$t('failed')); - }); - }, - - registerKey() { - this.registration.saving = true; - this.$root.api('i/2fa/key-done', { - password: this.registration.password, - name: this.keyName, - challengeId: this.registration.challengeId, - // we convert each 16 bits to a string to serialise - clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON), - attestationObject: hexifyAB(this.registration.credential.response.attestationObject) - }).then(key => { - this.registration = null; - key.lastUsed = new Date(); - this.$notify(this.$t('success')); - }) - }, - - unregisterKey(key) { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - return this.$root.api('i/2fa/remove-key', { - password, - credentialId: key.id - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - this.$notify(this.$t('key-unregistered')); - }); - }); - }, - - addSecurityKey() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/register-key', { - password - }).then(registration => { - this.registration = { - password, - challengeId: registration.challengeId, - stage: 0, - publicKeyOptions: { - challenge: Buffer.from( - registration.challenge - .replace(/\-/g, "+") - .replace(/_/g, "/"), - 'base64' - ), - rp: { - id: hostname, - name: 'Misskey' - }, - user: { - id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)), - name: this.$store.state.i.username, - displayName: this.$store.state.i.name, - }, - pubKeyCredParams: [{alg: -7, type: 'public-key'}], - timeout: 60000, - attestation: 'direct' - }, - saving: true - }; - return navigator.credentials.create({ - publicKey: this.registration.publicKeyOptions - }); - }).then(credential => { - this.registration.credential = credential; - this.registration.saving = false; - this.registration.stage = 1; - }).catch(err => { - console.warn('Error while registering?', err); - this.registration.error = err.message; - this.registration.stage = -1; - }); - }); - }, - updatePasswordLessLogin() { - this.$root.api('i/2fa/password-less', { - value: !!this.usePasswordLessLogin - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.totp-section - .totp-method-sep - margin 1.5em 0 1em - border none - border-top solid var(--lineWidth) var(--faceDivider) - - h2.heading - margin 0 - - .key - padding 1em - margin 0.5em 0 - background #161616 - border-radius 6px - - h3 - margin-top 0 - margin-bottom .3em - - .last-used - margin-bottom .5em -</style> diff --git a/src/client/app/common/views/components/settings/api.vue b/src/client/app/common/views/components/settings/api.vue deleted file mode 100644 index 184fa069fb..0000000000 --- a/src/client/app/common/views/components/settings/api.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="key"/> API</template> - - <section class="fit-top"> - <ui-input :value="$store.state.i.token" readonly> - <span>{{ $t('token') }}</span> - </ui-input> - <p>{{ $t('intro') }}</p> - <ui-info warn>{{ $t('caution') }}</ui-info> - <p>{{ $t('regeneration-of-token') }}</p> - <ui-button @click="regenerateToken"><fa icon="sync-alt"/> {{ $t('regenerate-token') }}</ui-button> - </section> - - <section> - <header><fa icon="terminal"/> {{ $t('console.title') }}</header> - <ui-input v-model="endpoint" :datalist="endpoints" @change="onEndpointChange()"> - <span>{{ $t('console.endpoint') }}</span> - </ui-input> - <ui-textarea v-model="body"> - <span>{{ $t('console.parameter') }} (JSON or JSON5)</span> - <template #desc>{{ $t('console.credential-info') }}</template> - </ui-textarea> - <ui-button @click="send" :disabled="sending"> - <template v-if="sending">{{ $t('console.sending') }}</template> - <template v-else><fa icon="paper-plane"/> {{ $t('console.send') }}</template> - </ui-button> - <ui-textarea v-if="res" v-model="res" readonly tall> - <span>{{ $t('console.response') }}</span> - </ui-textarea> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as JSON5 from 'json5'; - -export default Vue.extend({ - i18n: i18n('common/views/components/api-settings.vue'), - - data() { - return { - endpoint: '', - body: '{}', - res: null, - sending: false, - endpoints: [] - }; - }, - - created() { - this.$root.api('endpoints').then(endpoints => { - this.endpoints = endpoints; - }); - }, - - methods: { - regenerateToken() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/regenerate_token', { - password: password - }); - }); - }, - - send() { - this.sending = true; - this.$root.api(this.endpoint, JSON5.parse(this.body)).then(res => { - this.sending = false; - this.res = JSON5.stringify(res, null, 2); - }, err => { - this.sending = false; - this.res = JSON5.stringify(err, null, 2); - }); - }, - - onEndpointChange() { - this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => { - const body = {}; - for (const p of endpoint.params) { - body[p.name] = - p.type === 'String' ? '' : - p.type === 'Number' ? 0 : - p.type === 'Boolean' ? false : - p.type === 'Array' ? [] : - p.type === 'Object' ? {} : - null; - } - this.body = JSON5.stringify(body, null, 2); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue deleted file mode 100644 index d163f1e746..0000000000 --- a/src/client/app/common/views/components/settings/app-type.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<ui-card> - <template #title><fa :icon="faMobileAlt"/> {{ $t('title') }}</template> - - <section class="fit-top"> - <p>{{ $t('intro') }}</p> - <ui-select v-model="appTypeForce" :placeholder="$t('intro')"> - <option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option> - </ui-select> - <ui-info warn>{{ $t('info') }}</ui-info> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { faMobileAlt } from '@fortawesome/free-solid-svg-icons' - -export default Vue.extend({ - i18n: i18n('common/views/components/settings/app-type.vue'), - - data() { - return { - faMobileAlt - }; - }, - - computed: { - appTypeForce: { - get() { return this.$store.state.device.appTypeForce; }, - set(value) { - this.$store.commit('device/set', { key: 'appTypeForce', value }); - this.reload(); - } - }, - }, - - methods: { - reload() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.reload-to-apply-the-setting'), - showCancelButton: true - }).then(({ canceled }) => { - if (!canceled) { - location.reload(); - } - }); - }, - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/apps.vue b/src/client/app/common/views/components/settings/apps.vue deleted file mode 100644 index c5beaa1fe2..0000000000 --- a/src/client/app/common/views/components/settings/apps.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<div class="root"> - <ui-info v-if="!fetching && apps.length == 0">{{ $t('no-apps') }}</ui-info> - <div class="apps" v-if="apps.length != 0"> - <div v-for="app in apps"> - <p><b>{{ app.name }}</b></p> - <p>{{ app.description }}</p> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.apps.vue'), - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - this.$root.api('i/authorized_apps').then(apps => { - this.apps = apps; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root - > .apps - > div - padding 16px 0 0 0 - border-bottom solid 1px #eee -</style> diff --git a/src/client/app/common/views/components/settings/drive.vue b/src/client/app/common/views/components/settings/drive.vue deleted file mode 100644 index da028e85ef..0000000000 --- a/src/client/app/common/views/components/settings/drive.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="cloud"/> {{ $t('@.drive') }}</template> - - <section v-if="!fetching" class="juakhbxthdewydyreaphkepoxgxvfogn"> - <div class="meter"><div :style="meterStyle"></div></div> - <p>{{ $t('max') }}: <b>{{ capacity | bytes }}</b> {{ $t('in-use') }}: <b>{{ usage | bytes }}</b></p> - </section> - - <section> - <header>{{ $t('stats') }}</header> - <div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div> - </section> - - <section> - <header>{{ $t('default-upload-folder') }}</header> - <ui-input v-model="uploadFolderName" readonly>{{ $t('default-upload-folder-name') }}</ui-input> - <ui-button @click="chooseUploadFolder()">{{ $t('change-default-upload-folder') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as tinycolor from 'tinycolor2'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - i18n: i18n('common/views/components/drive-settings.vue'), - data() { - return { - fetching: true, - usage: null, - capacity: null, - uploadFolderName: null - }; - }, - - computed: { - meterStyle(): any { - return { - width: `${this.usage / this.capacity * 100}%`, - background: tinycolor({ - h: 180 - (this.usage / this.capacity * 180), - s: 0.7, - l: 0.5 - }) - }; - }, - - uploadFolder: { - get() { return this.$store.state.settings.uploadFolder; }, - set(value) { this.$store.dispatch('settings/set', { key: 'uploadFolder', value }); } - }, - }, - - mounted() { - if (this.uploadFolder == null) { - this.uploadFolderName = this.$t('@._settings.root'); - } else { - this.$root.api('drive/folders/show', { - folderId: this.uploadFolder - }).then(folder => { - this.uploadFolderName = folder.name; - }); - } - - this.$root.api('drive').then(info => { - this.capacity = info.capacity; - this.usage = info.usage; - this.fetching = false; - - this.$nextTick(() => { - this.renderChart(); - }); - }); - }, - - methods: { - renderChart() { - this.$root.api('charts/user/drive', { - userId: this.$store.state.i.id, - span: 'day', - limit: 21 - }).then(stats => { - const addition = []; - const deletion = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - for (let i = 0; i < 21; i++) { - const x = new Date(y, m, d - i); - addition.push([ - x, - stats.incSize[i] - ]); - deletion.push([ - x, - -stats.decSize[i] - ]); - } - - const chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'bar', - stacked: true, - height: 150, - zoom: { - enabled: false - }, - toolbar: { - show: false - } - }, - plotOptions: { - bar: { - columnWidth: '80%' - } - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, - } - }, - }, - tooltip: { - shared: true, - intersect: false - }, - dataLabels: { - enabled: false - }, - legend: { - show: false - }, - series: [{ - name: 'Additions', - data: addition - }, { - name: 'Deletions', - data: deletion - }], - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' - }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - crosshairs: { - width: 1, - opacity: 1 - } - }, - yaxis: { - labels: { - formatter: v => Vue.filter('bytes')(v, 0), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - } - } - }); - - chart.render(); - }); - }, - - chooseUploadFolder() { - this.$chooseDriveFolder().then(folder => { - this.uploadFolder = folder ? folder.id : null; - this.uploadFolderName = folder ? folder.name : this.$t('@._settings.root'); - }) - } - } -}); -</script> - -<style lang="stylus" scoped> -.juakhbxthdewydyreaphkepoxgxvfogn - > .meter - $size = 12px - - margin-bottom 16px - background rgba(0, 0, 0, 0.1) - border-radius ($size / 2) - overflow hidden - - > div - height $size - border-radius ($size / 2) - - > p - margin 0 - -</style> diff --git a/src/client/app/common/views/components/settings/integration.vue b/src/client/app/common/views/components/settings/integration.vue deleted file mode 100644 index 71ad8b4509..0000000000 --- a/src/client/app/common/views/components/settings/integration.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<ui-card v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration"> - <template #title><fa icon="share-alt"/> {{ $t('title') }}</template> - - <section v-if="enableTwitterIntegration"> - <header><fa :icon="['fab', 'twitter']"/> Twitter</header> - <p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> - <ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button> - </section> - - <section v-if="enableDiscordIntegration"> - <header><fa :icon="['fab', 'discord']"/> Discord</header> - <p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p> - <ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button> - </section> - - <section v-if="enableGithubIntegration"> - <header><fa :icon="['fab', 'github']"/> GitHub</header> - <p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p> - <ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { apiUrl } from '../../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/integration-settings.vue'), - - data() { - return { - apiUrl, - twitterForm: null, - discordForm: null, - githubForm: null, - enableTwitterIntegration: false, - enableDiscordIntegration: false, - enableGithubIntegration: false, - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.enableGithubIntegration = meta.enableGithubIntegration; - }); - }, - - mounted() { - if (!document.cookie.match(/i=(\w+)/)) { - document.cookie = `i=${this.$store.state.i.token}; path=/;` + - ` domain=${document.location.hostname}; max-age=31536000;` + - (document.location.protocol.startsWith('https') ? ' secure' : ''); - } - this.$watch('$store.state.i', () => { - if (this.$store.state.i.twitter) { - if (this.twitterForm) this.twitterForm.close(); - } - if (this.$store.state.i.discord) { - if (this.discordForm) this.discordForm.close(); - } - if (this.$store.state.i.github) { - if (this.githubForm) this.githubForm.close(); - } - }, { - deep: true - }); - }, - - methods: { - connectTwitter() { - this.twitterForm = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, - - disconnectTwitter() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - }, - - connectDiscord() { - this.discordForm = window.open(apiUrl + '/connect/discord', - 'discord_connect_window', - 'height=570, width=520'); - }, - - disconnectDiscord() { - window.open(apiUrl + '/disconnect/discord', - 'discord_disconnect_window', - 'height=570, width=520'); - }, - - connectGithub() { - this.githubForm = window.open(apiUrl + '/connect/github', - 'github_connect_window', - 'height=570, width=520'); - }, - - disconnectGithub() { - window.open(apiUrl + '/disconnect/github', - 'github_disconnect_window', - 'height=570, width=520'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/settings/language.vue b/src/client/app/common/views/components/settings/language.vue deleted file mode 100644 index f81775f09b..0000000000 --- a/src/client/app/common/views/components/settings/language.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="language"/> {{ $t('title') }}</template> - - <section class="fit-top"> - <ui-select v-model="lang" :placeholder="$t('pick-language')"> - <optgroup :label="$t('recommended')"> - <option value="">{{ $t('auto') }}</option> - </optgroup> - - <optgroup :label="$t('specify-language')"> - <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> - </optgroup> - </ui-select> - <ui-info>Current: <i>{{ currentLanguage }}</i></ui-info> - <ui-info warn>{{ $t('info') }}</ui-info> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { langs } from '../../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/language-settings.vue'), - - data() { - return { - langs, - currentLanguage: 'Unknown', - }; - }, - - computed: { - lang: { - get() { return this.$store.state.device.lang; }, - set(value) { this.$store.commit('device/set', { key: 'lang', value }); } - }, - }, - - created() { - try { - const locale = JSON.parse(localStorage.getItem('locale') || "{}"); - const localeKey = localStorage.getItem('localeKey'); - this.currentLanguage = `${locale.meta.lang} (${localeKey})`; - } catch { } - }, - - methods: { - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/mute-and-block.user.vue b/src/client/app/common/views/components/settings/mute-and-block.user.vue deleted file mode 100644 index 29ef1f7a67..0000000000 --- a/src/client/app/common/views/components/settings/mute-and-block.user.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<div class="muteblockuser"> - <div class="avatar-link"> - <a :href="user | userPage(null, true)"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div class="text"> - <div><mk-user-name :user="user"/></div> - <div class="username">@{{ user | acct }}</div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/mute-and-block.user.vue'), - props: ['user'], -}); -</script> - -<style lang="stylus" scoped> -.muteblockuser - display flex - padding 16px - - > .avatar-link - > a - > .avatar - width 40px - height 40px - - > .text - color var(--text) - margin-left 16px -</style> diff --git a/src/client/app/common/views/components/settings/mute-and-block.vue b/src/client/app/common/views/components/settings/mute-and-block.vue deleted file mode 100644 index 8ff5804168..0000000000 --- a/src/client/app/common/views/components/settings/mute-and-block.vue +++ /dev/null @@ -1,181 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="ban"/> {{ $t('mute-and-block') }}</template> - - <section> - <header>{{ $t('mute') }}</header> - <ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info> - <div class="users" v-if="mute.length != 0"> - <div class="user" v-for="user in mute" :key="user.id"> - <x-user :user="user"/> - <span @click="unmute(user)"> - <fa icon="times"/> - </span> - </div> - <ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button> - </div> - </section> - - <section> - <header>{{ $t('block') }}</header> - <ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info> - <div class="users" v-if="block.length != 0"> - <div class="user" v-for="user in block" :key="user.id"> - <x-user :user="user"/> - <span @click="unblock(user)"> - <fa icon="times"/> - </span> - </div> - <ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button> - </div> - </section> - - <section> - <header>{{ $t('word-mute') }}</header> - <ui-textarea v-model="mutedWords"> - {{ $t('muted-words') }}<template #desc>{{ $t('muted-words-description') }}</template> - </ui-textarea> - <ui-button @click="save">{{ $t('save') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XUser from './mute-and-block.user.vue'; - -const fetchLimit = 30; - -export default Vue.extend({ - i18n: i18n('common/views/components/mute-and-block.vue'), - - components: { - XUser - }, - - data() { - return { - muteFetching: true, - blockFetching: true, - mute: [], - block: [], - muteCursor: undefined, - blockCursor: undefined, - mutedWords: '' - }; - }, - - computed: { - _mutedWords: { - get() { return this.$store.state.settings.mutedWords; }, - set(value) { this.$store.dispatch('settings/set', { key: 'mutedWords', value }); } - }, - }, - - mounted() { - this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n'); - - this.updateMute(); - this.updateBlock(); - }, - - methods: { - save() { - this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != '')); - }, - - unmute(user) { - this.$root.dialog({ - type: 'warning', - text: this.$t('unmute-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('mute/delete', { - userId: user.id - }).then(() => { - this.muteCursor = undefined; - this.updateMute(); - }); - }); - }, - - unblock(user) { - this.$root.dialog({ - type: 'warning', - text: this.$t('unblock-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('blocking/delete', { - userId: user.id - }).then(() => { - this.updateBlock(); - }); - }); - }, - - updateMute() { - this.muteFetching = true; - this.$root.api('mute/list', { - limit: fetchLimit + 1, - untilId: this.muteCursor, - }).then((items: Object[]) => { - const past = this.muteCursor ? this.mute : []; - - if (items.length === fetchLimit + 1) { - items.pop() - this.muteCursor = items[items.length - 1].id; - } else { - this.muteCursor = undefined; - } - - this.mute = past.concat(items.map(x => x.mutee)); - this.muteFetching = false; - }); - }, - - updateBlock() { - this.blockFetching = true; - this.$root.api('blocking/list', { - limit: fetchLimit + 1, - untilId: this.blockCursor, - }).then((items: Object[]) => { - const past = this.blockCursor ? this.block : []; - - if (items.length === fetchLimit + 1) { - items.pop() - this.blockCursor = items[items.length - 1].id; - } else { - this.blockCursor = undefined; - } - - this.block = past.concat(items.map(x => x.blockee)); - this.blockFetching = false; - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> - .users - > .user - display flex - align-items center - justify-content flex-end - border-radius 6px - - &:hover - background-color var(--primary) - - > span - margin-left auto - cursor pointer - padding 16px - - > button - margin-top 16px -</style> - diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue deleted file mode 100644 index 2554fe6331..0000000000 --- a/src/client/app/common/views/components/settings/notification.vue +++ /dev/null @@ -1,44 +0,0 @@ -<template> -<ui-card> - <template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template> - <section> - <ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> - {{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> - </ui-switch> - <section> - <ui-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</ui-button> - <ui-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</ui-button> - <ui-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</ui-button> - </section> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/notification-settings.vue'), - - methods: { - onChangeAutoWatch(v) { - this.$root.api('i/update', { - autoWatch: v - }); - }, - - readAllUnreadNotes() { - this.$root.api('i/read_all_unread_notes'); - }, - - readAllMessagingMessages() { - this.$root.api('i/read_all_messaging_messages'); - }, - - readAllNotifications() { - this.$root.api('notifications/mark_all_as_read'); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/password.vue b/src/client/app/common/views/components/settings/password.vue deleted file mode 100644 index c867561518..0000000000 --- a/src/client/app/common/views/components/settings/password.vue +++ /dev/null @@ -1,63 +0,0 @@ -<template> -<div> - <ui-button @click="reset">{{ $t('reset') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/password-settings.vue'), - methods: { - async reset() { - const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({ - title: this.$t('enter-current-password'), - input: { - type: 'password' - } - }); - if (canceled1) return; - - const { canceled: canceled2, result: newPassword } = await this.$root.dialog({ - title: this.$t('enter-new-password'), - input: { - type: 'password' - } - }); - if (canceled2) return; - - const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({ - title: this.$t('enter-new-password-again'), - input: { - type: 'password' - } - }); - if (canceled3) return; - - if (newPassword !== newPassword2) { - this.$root.dialog({ - title: null, - text: this.$t('not-match') - }); - return; - } - this.$root.api('i/change_password', { - currentPassword, - newPassword - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('changed') - }); - }).catch(() => { - this.$root.dialog({ - type: 'error', - text: this.$t('failed') - }); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue deleted file mode 100644 index 0c291f9029..0000000000 --- a/src/client/app/common/views/components/settings/profile.vue +++ /dev/null @@ -1,442 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="user"/> {{ $t('title') }}</template> - - <section class="esokaraujimuwfttfzgocmutcihewscl"> - <div class="header" :style="bannerStyle"> - <mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true"/> - </div> - - <ui-form :disabled="saving"> - <ui-input v-model="name" :max="30"> - <span>{{ $t('name') }}</span> - </ui-input> - - <ui-input v-model="username" readonly> - <span>{{ $t('account') }}</span> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </ui-input> - - <ui-input v-model="location"> - <span>{{ $t('location') }}</span> - <template #prefix><fa icon="map-marker-alt"/></template> - </ui-input> - - <ui-input v-model="birthday" type="date"> - <template #title>{{ $t('birthday') }}</template> - <template #prefix><fa icon="birthday-cake"/></template> - </ui-input> - - <ui-textarea v-model="description" :max="500"> - <span>{{ $t('description') }}</span> - <template #desc>{{ $t('you-can-include-hashtags') }}</template> - </ui-textarea> - - <ui-select v-model="lang"> - <template #label>{{ $t('language') }}</template> - <template #icon><fa icon="language"/></template> - <option v-for="lang in unique(Object.values(langmap).map(x => x.nativeName)).map(name => Object.keys(langmap).find(k => langmap[k].nativeName == name))" :value="lang" :key="lang">{{ langmap[lang].nativeName }}</option> - </ui-select> - - <ui-input type="file" @change="onAvatarChange"> - <span>{{ $t('avatar') }}</span> - <template #icon><fa icon="image"/></template> - <template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template> - </ui-input> - - <ui-input type="file" @change="onBannerChange"> - <span>{{ $t('banner') }}</span> - <template #icon><fa icon="image"/></template> - <template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template> - </ui-input> - - <div class="fields"> - <header>{{ $t('profile-metadata') }}</header> - <ui-horizon-group> - <ui-input v-model="fieldName0">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue0">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName1">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue1">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName2">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue2">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName3">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue3">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - </div> - - <ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </ui-form> - </section> - - <section> - <header><fa :icon="faCogs"/> {{ $t('advanced') }}</header> - - <div> - <ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch> - <ui-switch v-model="isBot" @change="save(false)">{{ $t('is-bot') }}</ui-switch> - <ui-switch v-model="alwaysMarkNsfw">{{ $t('@._settings.always-mark-nsfw') }}</ui-switch> - </div> - </section> - - <section> - <header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header> - - <div> - <ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch> - <ui-switch v-model="carefulBot" :disabled="isLocked" @change="save(false)">{{ $t('careful-bot') }}</ui-switch> - <ui-switch v-model="autoAcceptFollowed" :disabled="!isLocked && !carefulBot" @change="save(false)">{{ $t('auto-accept-followed') }}</ui-switch> - </div> - </section> - - <section v-if="enableEmail"> - <header><fa :icon="faEnvelope"/> {{ $t('email') }}</header> - - <div> - <template v-if="$store.state.i.email != null"> - <ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info> - <ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info> - </template> - <ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input> - <ui-button @click="updateEmail()" :disabled="email === $store.state.i.email"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </div> - </section> - - <section> - <header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header> - - <div> - <ui-select v-model="exportTarget"> - <option value="notes">{{ $t('export-targets.all-notes') }}</option> - <option value="following">{{ $t('export-targets.following-list') }}</option> - <option value="mute">{{ $t('export-targets.mute-list') }}</option> - <option value="blocking">{{ $t('export-targets.blocking-list') }}</option> - <option value="user-lists">{{ $t('export-targets.user-lists') }}</option> - </ui-select> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button> - <ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button> - </ui-horizon-group> - </div> - </section> - - <section> - <details> - <summary>{{ $t('danger-zone') }}</summary> - <ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button> - </details> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { apiUrl, host } from '../../../../config'; -import { toUnicode } from 'punycode'; -import langmap from 'langmap'; -import { unique } from '../../../../../../prelude/array'; -import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons'; -import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/profile-editor.vue'), - - data() { - return { - unique, - langmap, - host: toUnicode(host), - enableEmail: false, - email: null, - name: null, - username: null, - location: null, - description: null, - fieldName0: null, - fieldValue0: null, - fieldName1: null, - fieldValue1: null, - fieldName2: null, - fieldValue2: null, - fieldName3: null, - fieldValue3: null, - lang: null, - birthday: null, - avatarId: null, - bannerId: null, - isCat: false, - isBot: false, - isLocked: false, - carefulBot: false, - autoAcceptFollowed: false, - saving: false, - avatarUploading: false, - bannerUploading: false, - exportTarget: 'notes', - faDownload, faUpload, faSave, faEnvelope, faUnlockAlt, faBoxes, faCogs - }; - }, - - computed: { - alwaysMarkNsfw: { - get() { return this.$store.state.i.alwaysMarkNsfw; }, - set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); } - }, - - bannerStyle(): any { - if (this.$store.state.i.bannerUrl == null) return {}; - return { - backgroundColor: this.$store.state.i.bannerColor, - backgroundImage: `url(${ this.$store.state.i.bannerUrl })` - }; - }, - }, - - created() { - this.$root.getMeta().then(meta => { - this.enableEmail = meta.enableEmail; - }); - this.email = this.$store.state.i.email; - this.name = this.$store.state.i.name; - this.username = this.$store.state.i.username; - this.location = this.$store.state.i.location; - this.description = this.$store.state.i.description; - this.lang = this.$store.state.i.lang; - this.birthday = this.$store.state.i.birthday; - this.avatarId = this.$store.state.i.avatarId; - this.bannerId = this.$store.state.i.bannerId; - this.isCat = this.$store.state.i.isCat; - this.isBot = this.$store.state.i.isBot; - this.isLocked = this.$store.state.i.isLocked; - this.carefulBot = this.$store.state.i.carefulBot; - this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; - - this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; - this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; - this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null; - this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null; - this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null; - this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null; - this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null; - this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null; - }, - - methods: { - onAvatarChange([file]) { - this.avatarUploading = true; - - const data = new FormData(); - data.append('file', file); - data.append('i', this.$store.state.i.token); - - fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: data - }) - .then(response => response.json()) - .then(f => { - this.avatarId = f.id; - this.avatarUploading = false; - }) - .catch(e => { - this.avatarUploading = false; - alert('%18n:@upload-failed%'); - }); - }, - - onBannerChange([file]) { - this.bannerUploading = true; - - const data = new FormData(); - data.append('file', file); - data.append('i', this.$store.state.i.token); - - fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: data - }) - .then(response => response.json()) - .then(f => { - this.bannerId = f.id; - this.bannerUploading = false; - }) - .catch(e => { - this.bannerUploading = false; - alert('%18n:@upload-failed%'); - }); - }, - - save(notify) { - const fields = [ - { name: this.fieldName0, value: this.fieldValue0 }, - { name: this.fieldName1, value: this.fieldValue1 }, - { name: this.fieldName2, value: this.fieldValue2 }, - { name: this.fieldName3, value: this.fieldValue3 }, - ]; - - this.saving = true; - - this.$root.api('i/update', { - name: this.name || null, - location: this.location || null, - description: this.description || null, - lang: this.lang, - birthday: this.birthday || null, - avatarId: this.avatarId || undefined, - bannerId: this.bannerId || undefined, - fields, - isCat: !!this.isCat, - isBot: !!this.isBot, - isLocked: !!this.isLocked, - carefulBot: !!this.carefulBot, - autoAcceptFollowed: !!this.autoAcceptFollowed - }).then(i => { - this.saving = false; - this.$store.state.i.avatarId = i.avatarId; - this.$store.state.i.avatarUrl = i.avatarUrl; - this.$store.state.i.bannerId = i.bannerId; - this.$store.state.i.bannerUrl = i.bannerUrl; - - if (notify) { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - }).catch(err => { - this.saving = false; - switch(err.id) { - case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191': - this.$root.dialog({ - type: 'error', - title: this.$t('unable-to-process'), - text: this.$t('avatar-not-an-image') - }); - break; - case '75aedb19-2afd-4e6d-87fc-67941256fa60': - this.$root.dialog({ - type: 'error', - title: this.$t('unable-to-process'), - text: this.$t('banner-not-an-image') - }); - break; - default: - this.$root.dialog({ - type: 'error', - text: this.$t('unable-to-process') - }); - } - }); - }, - - updateEmail() { - this.$root.dialog({ - title: this.$t('@.enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/update_email', { - password: password, - email: this.email == '' ? null : this.email - }); - }); - }, - - doExport() { - this.$root.api( - this.exportTarget == 'notes' ? 'i/export-notes' : - this.exportTarget == 'following' ? 'i/export-following' : - this.exportTarget == 'mute' ? 'i/export-mute' : - this.exportTarget == 'blocking' ? 'i/export-blocking' : - this.exportTarget == 'user-lists' ? 'i/export-user-lists' : - null, {}).then(() => { - this.$root.dialog({ - type: 'info', - text: this.$t('export-requested') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - doImport() { - this.$chooseDriveFile().then(file => { - this.$root.api( - this.exportTarget == 'following' ? 'i/import-following' : - this.exportTarget == 'user-lists' ? 'i/import-user-lists' : - null, { - fileId: file.id - }).then(() => { - this.$root.dialog({ - type: 'info', - text: this.$t('import-requested') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }); - }, - - async deleteAccount() { - const { canceled: canceled, result: password } = await this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }); - if (canceled) return; - - this.$root.api('i/delete-account', { - password - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('account-deleted') - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.esokaraujimuwfttfzgocmutcihewscl - > .header - height 150px - overflow hidden - background-size cover - background-position center - border-radius 4px - - > .avatar - position absolute - top 0 - bottom 0 - left 0 - right 0 - display block - width 72px - height 72px - margin auto - -.fields - > header - padding 8px 0px - font-weight bold - -</style> diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue deleted file mode 100644 index 3a0ba561af..0000000000 --- a/src/client/app/common/views/components/settings/settings.vue +++ /dev/null @@ -1,671 +0,0 @@ -<template> -<div class="nqfhvmnl"> - <template v-if="page == null || page == 'profile'"> - <x-profile/> - <x-integration/> - </template> - - <template v-if="page == null || page == 'appearance'"> - <x-theme/> - - <ui-card> - <template #title><fa icon="desktop"/> {{ $t('@._settings.appearance') }}</template> - - <section v-if="!$root.isMobile"> - <ui-switch v-model="showPostFormOnTopOfTl">{{ $t('@._settings.post-form-on-timeline') }}</ui-switch> - <ui-button @click="customizeHome">{{ $t('@.customize-home') }}</ui-button> - </section> - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.wallpaper') }}</header> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="updateWallpaper">{{ $t('@._settings.choose-wallpaper') }}</ui-button> - <ui-button @click="deleteWallpaper">{{ $t('@._settings.delete-wallpaper') }}</ui-button> - </ui-horizon-group> - </section> - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.navbar-position') }}</header> - <ui-radio v-model="navbar" value="top">{{ $t('@._settings.navbar-position-top') }}</ui-radio> - <ui-radio v-model="navbar" value="left">{{ $t('@._settings.navbar-position-left') }}</ui-radio> - <ui-radio v-model="navbar" value="right">{{ $t('@._settings.navbar-position-right') }}</ui-radio> - </section> - <section> - <ui-switch v-model="useShadow">{{ $t('@._settings.use-shadow') }}</ui-switch> - <ui-switch v-model="roundedCorners">{{ $t('@._settings.rounded-corners') }}</ui-switch> - <ui-switch v-model="circleIcons">{{ $t('@._settings.circle-icons') }}</ui-switch> - <ui-switch v-model="reduceMotion">{{ $t('@._settings.reduce-motion') }}</ui-switch> - <ui-switch v-model="contrastedAcct">{{ $t('@._settings.contrasted-acct') }}</ui-switch> - <ui-switch v-model="showFullAcct">{{ $t('@._settings.show-full-acct') }}</ui-switch> - <ui-switch v-model="showVia">{{ $t('@._settings.show-via') }}</ui-switch> - <ui-switch v-model="useOsDefaultEmojis">{{ $t('@._settings.use-os-default-emojis') }}</ui-switch> - <ui-switch v-model="iLikeSushi">{{ $t('@._settings.i-like-sushi') }}</ui-switch> - </section> - <section> - <ui-switch v-model="suggestRecentHashtags">{{ $t('@._settings.suggest-recent-hashtags') }}</ui-switch> - <ui-switch v-model="showClockOnHeader" v-if="!$root.isMobile">{{ $t('@._settings.show-clock-on-header') }}</ui-switch> - <ui-switch v-model="alwaysShowNsfw">{{ $t('@._settings.always-show-nsfw') }}</ui-switch> - <ui-switch v-model="showReplyTarget">{{ $t('@._settings.show-reply-target') }}</ui-switch> - <ui-switch v-model="disableAnimatedMfm">{{ $t('@._settings.disable-animated-mfm') }}</ui-switch> - <ui-switch v-model="disableShowingAnimatedImages">{{ $t('@._settings.disable-showing-animated-images') }}</ui-switch> - <ui-switch v-model="remainDeletedNote">{{ $t('@._settings.remain-deleted-note') }}</ui-switch> - <ui-switch v-model="enableMobileQuickNotificationView">{{ $t('@._settings.enable-quick-notification-view') }}</ui-switch> - </section> - <section> - <header>{{ $t('@._settings.line-width') }}</header> - <ui-radio v-model="lineWidth" :value="0.5">{{ $t('@._settings.line-width-thin') }}</ui-radio> - <ui-radio v-model="lineWidth" :value="1">{{ $t('@._settings.line-width-normal') }}</ui-radio> - <ui-radio v-model="lineWidth" :value="2">{{ $t('@._settings.line-width-thick') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.font-size') }}</header> - <ui-radio v-model="fontSize" :value="-2">{{ $t('@._settings.font-size-x-small') }}</ui-radio> - <ui-radio v-model="fontSize" :value="-1">{{ $t('@._settings.font-size-small') }}</ui-radio> - <ui-radio v-model="fontSize" :value="0">{{ $t('@._settings.font-size-medium') }}</ui-radio> - <ui-radio v-model="fontSize" :value="1">{{ $t('@._settings.font-size-large') }}</ui-radio> - <ui-radio v-model="fontSize" :value="2">{{ $t('@._settings.font-size-x-large') }}</ui-radio> - </section> - <section v-if="$root.isMobile"> - <header>{{ $t('@._settings.post-style') }}</header> - <ui-radio v-model="postStyle" value="standard">{{ $t('@._settings.post-style-standard') }}</ui-radio> - <ui-radio v-model="postStyle" value="smart">{{ $t('@._settings.post-style-smart') }}</ui-radio> - </section> - <section v-if="$root.isMobile"> - <header>{{ $t('@._settings.notification-position') }}</header> - <ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('@._settings.notification-position-bottom') }}</ui-radio> - <ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('@._settings.notification-position-top') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.deck-column-align') }}</header> - <ui-radio v-model="deckColumnAlign" value="center">{{ $t('@._settings.deck-column-align-center') }}</ui-radio> - <ui-radio v-model="deckColumnAlign" value="left">{{ $t('@._settings.deck-column-align-left') }}</ui-radio> - <ui-radio v-model="deckColumnAlign" value="flexible">{{ $t('@._settings.deck-column-align-flexible') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.deck-column-width') }}</header> - <ui-radio v-model="deckColumnWidth" value="narrow">{{ $t('@._settings.deck-column-width-narrow') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="narrower">{{ $t('@._settings.deck-column-width-narrower') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="normal">{{ $t('@._settings.deck-column-width-normal') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="wider">{{ $t('@._settings.deck-column-width-wider') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="wide">{{ $t('@._settings.deck-column-width-wide') }}</ui-radio> - </section> - <section> - <ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@._settings.show-reversi-board-labels') }}</ui-switch> - <ui-switch v-model="games_reversi_useAvatarStones">{{ $t('@._settings.use-avatar-reversi-stones') }}</ui-switch> - </section> - </ui-card> - </template> - - <template v-if="page == null || page == 'behavior'"> - <ui-card> - <template #title><fa icon="sliders-h"/> {{ $t('@._settings.behavior') }}</template> - - <section> - <ui-switch v-model="fetchOnScroll">{{ $t('@._settings.fetch-on-scroll') }} - <template #desc>{{ $t('@._settings.fetch-on-scroll-desc') }}</template> - </ui-switch> - <ui-switch v-model="keepCw">{{ $t('@._settings.keep-cw') }} - <template #desc>{{ $t('@._settings.keep-cw-desc') }}</template> - </ui-switch> - <ui-switch v-if="$root.isMobile" v-model="disableViaMobile">{{ $t('@._settings.disable-via-mobile') }}</ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.reactions') }}</header> - <ui-textarea v-model="reactions"> - {{ $t('@._settings.reactions') }}<template #desc>{{ $t('@._settings.reactions-description') }}</template> - </ui-textarea> - <ui-horizon-group> - <ui-button @click="save('reactions', reactions.trim().split('\n'))" primary><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - <ui-button @click="previewReaction()" ref="reactionsPreviewButton"><fa :icon="faEye"/> {{ $t('@._settings.preview') }}</ui-button> - </ui-horizon-group> - </section> - - <section> - <header>{{ $t('@._settings.timeline') }}</header> - <ui-switch v-model="showMyRenotes">{{ $t('@._settings.show-my-renotes') }}</ui-switch> - <ui-switch v-model="showRenotedMyNotes">{{ $t('@._settings.show-renoted-my-notes') }}</ui-switch> - <ui-switch v-model="showLocalRenotes">{{ $t('@._settings.show-local-renotes') }}</ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.note-visibility') }}</header> - <ui-switch v-model="rememberNoteVisibility">{{ $t('@._settings.remember-note-visibility') }}</ui-switch> - <section> - <header>{{ $t('@._settings.default-note-visibility') }}</header> - <ui-select v-model="defaultNoteVisibility"> - <option value="public">{{ $t('@.note-visibility.public') }}</option> - <option value="home">{{ $t('@.note-visibility.home') }}</option> - <option value="followers">{{ $t('@.note-visibility.followers') }}</option> - <option value="specified">{{ $t('@.note-visibility.specified') }}</option> - <option value="local-public">{{ $t('@.note-visibility.local-public') }}</option> - <option value="local-home">{{ $t('@.note-visibility.local-home') }}</option> - <option value="local-followers">{{ $t('@.note-visibility.local-followers') }}</option> - </ui-select> - </section> - </section> - - <section> - <header>{{ $t('@._settings.sync') }}</header> - <ui-input v-if="$root.isMobile" v-model="mobileHomeProfile" :datalist="Object.keys($store.state.settings.mobileHomeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input> - <ui-input v-else v-model="homeProfile" :datalist="Object.keys($store.state.settings.homeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input> - <ui-input v-model="deckProfile" :datalist="Object.keys($store.state.settings.deckProfiles)">{{ $t('@._settings.deck-profile') }}</ui-input> - </section> - - <section> - <header>{{ $t('@._settings.web-search-engine') }}</header> - <ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }} - <template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template> - </ui-input> - <ui-button @click="save('webSearchEngine', webSearchEngine)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - </section> - - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.paste') }}</header> - <ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }} - <template v-if="pastedFileName === this.$store.state.settings.pastedFileName" #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template> - <template v-else #desc>{{ pastedFileNamePreview() }}</template> - </ui-input> - <ui-button @click="save('pastedFileName', pastedFileName)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - - <ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }} - <template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template> - </ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.room') }}</header> - <ui-select v-model="roomGraphicsQuality"> - <template #label>{{ $t('@._settings._room.graphicsQuality') }}</template> - <option value="ultra">{{ $t('@._settings._room._graphicsQuality.ultra') }}</option> - <option value="high">{{ $t('@._settings._room._graphicsQuality.high') }}</option> - <option value="medium">{{ $t('@._settings._room._graphicsQuality.medium') }}</option> - <option value="low">{{ $t('@._settings._room._graphicsQuality.low') }}</option> - <option value="cheep">{{ $t('@._settings._room._graphicsQuality.cheep') }}</option> - </ui-select> - <ui-switch v-model="roomUseOrthographicCamera">{{ $t('@._settings._room.useOrthographicCamera') }}</ui-switch> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="volume-up"/> {{ $t('@._settings.sound') }}</template> - - <section> - <ui-switch v-model="enableSounds">{{ $t('@._settings.enable-sounds') }} - <template #desc>{{ $t('@._settings.enable-sounds-desc') }}</template> - </ui-switch> - <label>{{ $t('@._settings.volume') }}</label> - <input type="range" - v-model="soundVolume" - :disabled="!enableSounds" - max="1" - step="0.1" - /> - <ui-button @click="soundTest"><fa icon="volume-up"/> {{ $t('@._settings.test') }}</ui-button> - </section> - </ui-card> - - <x-language/> - <x-app-type/> - </template> - - <template v-if="page == null || page == 'notification'"> - <x-notification/> - </template> - - <template v-if="page == null || page == 'drive'"> - <x-drive/> - </template> - - <template v-if="page == null || page == 'hashtags'"> - <ui-card> - <template #title><fa icon="hashtag"/> {{ $t('@._settings.tags') }}</template> - <section> - <x-tags/> - </section> - </ui-card> - </template> - - <template v-if="page == null || page == 'muteAndBlock'"> - <x-mute-and-block/> - </template> - - <!-- - <template v-if="page == null || page == 'apps'"> - <ui-card> - <template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template> - <section> - <x-apps/> - </section> - </ui-card> - </template> - --> - - <template v-if="page == null || page == 'security'"> - <ui-card> - <template #title><fa icon="unlock-alt"/> {{ $t('@._settings.password') }}</template> - <section> - <x-password/> - </section> - </ui-card> - - <ui-card v-if="!$root.isMobile"> - <template #title><fa icon="mobile-alt"/> {{ $t('@.2fa') }}</template> - <section> - <x-2fa/> - </section> - </ui-card> - - <!-- - <ui-card> - <template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template> - <section> - <x-signins/> - </section> - </ui-card> - --> - </template> - - <template v-if="page == null || page == 'api'"> - <x-api/> - </template> - - <template v-if="page == null || page == 'other'"> - <ui-card> - <template #title><fa icon="sync-alt"/> {{ $t('@._settings.update') }}</template> - <section> - <p> - <span>{{ $t('@._settings.version') }} <i>{{ version }}</i></span> - <template v-if="latestVersion !== undefined"> - <br> - <span>{{ $t('@._settings.latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></span> - </template> - </p> - <ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> - <template v-if="checkingForUpdate">{{ $t('@._settings.update-checking') }}<mk-ellipsis/></template> - <template v-else>{{ $t('@._settings.do-update') }}</template> - </ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="cogs"/> {{ $t('@._settings.advanced-settings') }}</template> - <section> - <ui-switch v-model="debug"> - {{ $t('@._settings.debug-mode') }}<template #desc>{{ $t('@._settings.debug-mode-desc') }}</template> - </ui-switch> - </section> - </ui-card> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import X2fa from './2fa.vue'; -import XApps from './apps.vue'; -import XSignins from './signins.vue'; -import XTags from './tags.vue'; -import XIntegration from './integration.vue'; -import XTheme from './theme.vue'; -import XDrive from './drive.vue'; -import XMuteAndBlock from './mute-and-block.vue'; -import XPassword from './password.vue'; -import XProfile from './profile.vue'; -import XApi from './api.vue'; -import XLanguage from './language.vue'; -import XAppType from './app-type.vue'; -import XNotification from './notification.vue'; -import MkReactionPicker from '../reaction-picker.vue'; - -import { url, version } from '../../../../config'; -import checkForUpdate from '../../../scripts/check-for-update'; -import { formatTimeString } from '../../../../../../misc/format-time-string'; -import { faSave, faEye } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n(), - components: { - X2fa, - XApps, - XSignins, - XTags, - XIntegration, - XTheme, - XDrive, - XMuteAndBlock, - XPassword, - XProfile, - XApi, - XLanguage, - XAppType, - XNotification, - }, - props: { - page: { - type: String, - required: false, - default: null - } - }, - data() { - return { - meta: null, - version, - reactions: this.$store.state.settings.reactions.join('\n'), - webSearchEngine: this.$store.state.settings.webSearchEngine, - pastedFileName : this.$store.state.settings.pastedFileName, - latestVersion: undefined, - checkingForUpdate: false, - faSave, faEye - }; - }, - computed: { - useOsDefaultEmojis: { - get() { return this.$store.state.device.useOsDefaultEmojis; }, - set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); } - }, - - reduceMotion: { - get() { return this.$store.state.device.reduceMotion; }, - set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } - }, - - keepCw: { - get() { return this.$store.state.settings.keepCw; }, - set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); } - }, - - navbar: { - get() { return this.$store.state.device.navbar; }, - set(value) { this.$store.commit('device/set', { key: 'navbar', value }); } - }, - - deckColumnAlign: { - get() { return this.$store.state.device.deckColumnAlign; }, - set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); } - }, - - deckColumnWidth: { - get() { return this.$store.state.device.deckColumnWidth; }, - set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); } - }, - - enableSounds: { - get() { return this.$store.state.device.enableSounds; }, - set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } - }, - - soundVolume: { - get() { return this.$store.state.device.soundVolume; }, - set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); } - }, - - debug: { - get() { return this.$store.state.device.debug; }, - set(value) { this.$store.commit('device/set', { key: 'debug', value }); } - }, - - alwaysShowNsfw: { - get() { return this.$store.state.device.alwaysShowNsfw; }, - set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } - }, - - postStyle: { - get() { return this.$store.state.device.postStyle; }, - set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); } - }, - - disableViaMobile: { - get() { return this.$store.state.settings.disableViaMobile; }, - set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } - }, - - useShadow: { - get() { return this.$store.state.device.useShadow; }, - set(value) { this.$store.commit('device/set', { key: 'useShadow', value }); } - }, - - roundedCorners: { - get() { return this.$store.state.device.roundedCorners; }, - set(value) { this.$store.commit('device/set', { key: 'roundedCorners', value }); } - }, - - lineWidth: { - get() { return this.$store.state.device.lineWidth; }, - set(value) { this.$store.commit('device/set', { key: 'lineWidth', value }); } - }, - - fontSize: { - get() { return this.$store.state.device.fontSize; }, - set(value) { this.$store.commit('device/set', { key: 'fontSize', value }); } - }, - - fetchOnScroll: { - get() { return this.$store.state.settings.fetchOnScroll; }, - set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } - }, - - rememberNoteVisibility: { - get() { return this.$store.state.settings.rememberNoteVisibility; }, - set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } - }, - - defaultNoteVisibility: { - get() { return this.$store.state.settings.defaultNoteVisibility; }, - set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } - }, - - pasteDialog: { - get() { return this.$store.state.settings.pasteDialog; }, - set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); } - }, - - showReplyTarget: { - get() { return this.$store.state.settings.showReplyTarget; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } - }, - - showMyRenotes: { - get() { return this.$store.state.settings.showMyRenotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); } - }, - - showRenotedMyNotes: { - get() { return this.$store.state.settings.showRenotedMyNotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); } - }, - - showLocalRenotes: { - get() { return this.$store.state.settings.showLocalRenotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); } - }, - - showPostFormOnTopOfTl: { - get() { return this.$store.state.settings.showPostFormOnTopOfTl; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); } - }, - - suggestRecentHashtags: { - get() { return this.$store.state.settings.suggestRecentHashtags; }, - set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); } - }, - - showClockOnHeader: { - get() { return this.$store.state.settings.showClockOnHeader; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); } - }, - - circleIcons: { - get() { return this.$store.state.settings.circleIcons; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'circleIcons', value }); - this.reload(); - } - }, - - contrastedAcct: { - get() { return this.$store.state.settings.contrastedAcct; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); - this.reload(); - } - }, - - showFullAcct: { - get() { return this.$store.state.settings.showFullAcct; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); - this.reload(); - } - }, - - showVia: { - get() { return this.$store.state.settings.showVia; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showVia', value }); } - }, - - iLikeSushi: { - get() { return this.$store.state.settings.iLikeSushi; }, - set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); } - }, - - roomUseOrthographicCamera: { - get() { return this.$store.state.device.roomUseOrthographicCamera; }, - set(value) { this.$store.commit('device/set', { key: 'roomUseOrthographicCamera', value }); } - }, - - roomGraphicsQuality: { - get() { return this.$store.state.device.roomGraphicsQuality; }, - set(value) { this.$store.commit('device/set', { key: 'roomGraphicsQuality', value }); } - }, - - games_reversi_showBoardLabels: { - get() { return this.$store.state.settings.gamesReversiShowBoardLabels; }, - set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiShowBoardLabels', value }); } - }, - - games_reversi_useAvatarStones: { - get() { return this.$store.state.settings.gamesReversiUseAvatarStones; }, - set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiUseAvatarStones', value }); } - }, - - disableAnimatedMfm: { - get() { return this.$store.state.settings.disableAnimatedMfm; }, - set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } - }, - - disableShowingAnimatedImages: { - get() { return this.$store.state.device.disableShowingAnimatedImages; }, - set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); } - }, - - remainDeletedNote: { - get() { return this.$store.state.settings.remainDeletedNote; }, - set(value) { this.$store.dispatch('settings/set', { key: 'remainDeletedNote', value }); } - }, - - mobileNotificationPosition: { - get() { return this.$store.state.device.mobileNotificationPosition; }, - set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); } - }, - - enableMobileQuickNotificationView: { - get() { return this.$store.state.device.enableMobileQuickNotificationView; }, - set(value) { this.$store.commit('device/set', { key: 'enableMobileQuickNotificationView', value }); } - }, - - homeProfile: { - get() { return this.$store.state.device.homeProfile; }, - set(value) { this.$store.commit('device/set', { key: 'homeProfile', value }); } - }, - - mobileHomeProfile: { - get() { return this.$store.state.device.mobileHomeProfile; }, - set(value) { this.$store.commit('device/set', { key: 'mobileHomeProfile', value }); } - }, - - deckProfile: { - get() { return this.$store.state.device.deckProfile; }, - set(value) { this.$store.commit('device/set', { key: 'deckProfile', value }); } - }, - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - methods: { - reload() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.reload-to-apply-the-setting'), - showCancelButton: true - }).then(({ canceled }) => { - if (!canceled) { - location.reload(); - } - }); - }, - save(key, value) { - this.$store.dispatch('settings/set', { - key, - value - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('@._settings.saved') - }) - }); - }, - customizeHome() { - location.href = '/?customize'; - }, - updateWallpaper() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - this.$store.dispatch('settings/set', { key: 'wallpaper', value: file.url }); - }); - }, - deleteWallpaper() { - this.$store.dispatch('settings/set', { key: 'wallpaper', value: null }); - }, - checkForUpdate() { - this.checkingForUpdate = true; - checkForUpdate(this.$root, true, true).then(newer => { - this.checkingForUpdate = false; - this.latestVersion = newer; - if (newer == null) { - this.$root.dialog({ - title: this.$t('@._settings.no-updates'), - text: this.$t('@._settings.no-updates-desc') - }); - } else { - this.$root.dialog({ - title: this.$t('@._settings.update-available'), - text: this.$t('@._settings.update-available-desc') - }); - } - }); - }, - soundTest() { - const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - }, - pastedFileNamePreview() { - return `${formatTimeString(new Date(), this.pastedFileName).replace(/{{number}}/g, `1`)}.png` - }, - previewReaction() { - const picker = this.$root.new(MkReactionPicker, { - source: this.$refs.reactionsPreviewButton.$el, - reactions: this.reactions.trim().split('\n'), - showFocus: false, - }); - picker.$once('chosen', reaction => { - picker.close(); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/signins.vue b/src/client/app/common/views/components/settings/signins.vue deleted file mode 100644 index 048fa2fc5b..0000000000 --- a/src/client/app/common/views/components/settings/signins.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<div class="root"> -<div class="signins" v-if="signins.length != 0"> - <div v-for="signin in signins"> - <header @click="signin._show = !signin._show"> - <template v-if="signin.success"><fa icon="check"/></template> - <template v-else><fa icon="times"/></template> - <span class="ip">{{ signin.ip }}</span> - <mk-time :time="signin.createdAt"/> - </header> - <div class="headers" v-show="signin._show"> - <!-- TODO --> - </div> - </div> -</div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - signins: [], - connection: null - }; - }, - - mounted() { - this.$root.api('i/signin_history').then(signins => { - this.signins = signins; - this.fetching = false; - }); - - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('signin', this.onSignin); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onSignin(signin) { - this.signins.unshift(signin); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root - > .signins - > div - border-bottom solid 1px #eee - - > header - display flex - padding 8px 0 - line-height 32px - cursor pointer - - > [data-icon] - margin-right 8px - text-align left - - &.check - color #0fda82 - - &.times - color #ff3100 - - > .ip - display inline-block - text-align left - padding 8px - line-height 16px - font-family monospace - font-size 14px - color #444 - background #f8f8f8 - border-radius 4px - - > .mk-time - margin-left auto - text-align right - color #777 - - > .headers - overflow auto - margin 0 0 16px 0 - max-height 100px - white-space pre-wrap - word-break break-all - -</style> diff --git a/src/client/app/common/views/components/settings/tags.vue b/src/client/app/common/views/components/settings/tags.vue deleted file mode 100644 index 2e17f35e3e..0000000000 --- a/src/client/app/common/views/components/settings/tags.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<div class="vfcitkilproprqtbnpoertpsziierwzi"> - <div v-for="timeline in timelines" class="timeline" :key="timeline.id"> - <ui-input v-model="timeline.title" @change="save"> - <span>{{ $t('title') }}</span> - </ui-input> - <ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" :pre="true" @input="onQueryChange(timeline, $event)"> - <span>{{ $t('query') }}</span> - </ui-textarea> - </div> - <ui-button class="add" @click="add">{{ $t('add') }}</ui-button> - <ui-button class="save" @click="save">{{ $t('save') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.tags.vue'), - data() { - return { - timelines: this.$store.state.settings.tagTimelines - }; - }, - - methods: { - add() { - this.timelines.push({ - id: uuid(), - title: '', - query: '' - }); - }, - - save() { - const timelines = this.timelines - .filter(timeline => timeline.title) - .map(timeline => { - if (!(timeline.query && timeline.query[0] && timeline.query[0][0])) { - timeline.query = timeline.title.split('\n').map(tags => tags.split(' ')); - } - return timeline; - }); - - this.$store.dispatch('settings/set', { key: 'tagTimelines', value: timelines }); - }, - - onQueryChange(timeline, value) { - timeline.query = value.split('\n').map(tags => tags.split(' ')); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vfcitkilproprqtbnpoertpsziierwzi - > .timeline - padding-bottom 16px - border-bottom solid 1px rgba(#000, 0.1) - - > .add - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue deleted file mode 100644 index d916a57508..0000000000 --- a/src/client/app/common/views/components/settings/theme.vue +++ /dev/null @@ -1,558 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="palette"/> {{ $t('theme') }}</template> - <section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top"> - <div class="dark"> - <div class="toggleWrapper"> - <input type="checkbox" class="dn" id="dn" v-model="darkmode"/> - <label for="dn" class="toggle"> - <span class="toggle__handler"> - <span class="crater crater--1"></span> - <span class="crater crater--2"></span> - <span class="crater crater--3"></span> - </span> - <span class="star star--1"></span> - <span class="star star--2"></span> - <span class="star star--3"></span> - <span class="star star--4"></span> - <span class="star star--5"></span> - <span class="star star--6"></span> - </label> - </div> - </div> - - <label> - <ui-select v-model="light" :placeholder="$t('light-theme')"> - <template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template> - <optgroup :label="$t('light-themes')"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('dark-themes')"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - </label> - - <label> - <ui-select v-model="dark" :placeholder="$t('dark-theme')"> - <template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template> - <optgroup :label="$t('dark-themes')"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('light-themes')"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - </label> - - <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank">{{ $t('find-more-theme') }}</a> - - <details class="creator"> - <summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary> - <div> - <span>{{ $t('base-theme') }}:</span> - <ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio> - <ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio> - </div> - <div> - <ui-input v-model="myThemeName"> - <span>{{ $t('theme-name') }}</span> - </ui-input> - <ui-textarea v-model="myThemeDesc"> - <span>{{ $t('desc') }}</span> - </ui-textarea> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div> - <color-picker v-model="myThemePrimary"/> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div> - <color-picker v-model="myThemeSecondary"/> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('text-color') }}:</div> - <color-picker v-model="myThemeText"/> - </div> - <ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button> - <ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button> - </details> - - <details> - <summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary> - <ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button> - <input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/> - <p>{{ $t('import-by-code') }}:</p> - <ui-textarea v-model="installThemeCode"> - <span>{{ $t('theme-code') }}</span> - </ui-textarea> - <ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button> - </details> - - <details> - <summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary> - <ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')"> - <optgroup :label="$t('builtin-themes')"> - <option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('my-themes')"> - <option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('installed-themes')"> - <option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - <template v-if="selectedTheme"> - <ui-input readonly :value="selectedTheme.author"> - <span>{{ $t('author') }}</span> - </ui-input> - <ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc"> - <span>{{ $t('desc') }}</span> - </ui-textarea> - <ui-textarea readonly tall :value="selectedThemeCode"> - <span>{{ $t('theme-code') }}</span> - </ui-textarea> - <ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button> - <ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button> - </template> - </details> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme'; -import { Chrome } from 'vue-color'; -import { v4 as uuid } from 'uuid'; -import * as tinycolor from 'tinycolor2'; -import * as JSON5 from 'json5'; -import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/theme.vue'), - components: { - ColorPicker: Chrome - }, - - data() { - return { - builtinThemes: builtinThemes, - installThemeCode: null, - selectedThemeId: null, - myThemeBase: 'light', - myThemeName: '', - myThemeDesc: '', - myThemePrimary: lightTheme.vars.primary, - myThemeSecondary: lightTheme.vars.secondary, - myThemeText: lightTheme.vars.text, - faMoon, faSun - }; - }, - - computed: { - themes(): Theme[] { - return builtinThemes.concat(this.$store.state.device.themes); - }, - - darkThemes(): Theme[] { - return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark'); - }, - - lightThemes(): Theme[] { - return this.themes.filter(t => t.base == 'light' || t.kind == 'light'); - }, - - installedThemes(): Theme[] { - return this.$store.state.device.themes; - }, - - light: { - get() { return this.$store.state.device.lightTheme; }, - set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); } - }, - - dark: { - get() { return this.$store.state.device.darkTheme; }, - set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); } - }, - - selectedTheme() { - if (this.selectedThemeId == null) return null; - return this.themes.find(x => x.id == this.selectedThemeId); - }, - - selectedThemeCode() { - if (this.selectedTheme == null) return null; - return JSON5.stringify(this.selectedTheme, null, '\t'); - }, - - myTheme(): any { - return { - name: this.myThemeName, - author: this.$store.state.i.username, - desc: this.myThemeDesc, - base: this.myThemeBase, - vars: { - primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(), - secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(), - text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString() - } - }; - }, - - darkmode: { - get() { return this.$store.state.device.darkmode; }, - set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } - }, - }, - - watch: { - myThemeBase(v) { - const theme = v == 'light' ? lightTheme : darkTheme; - this.myThemePrimary = theme.vars.primary; - this.myThemeSecondary = theme.vars.secondary; - this.myThemeText = theme.vars.text; - } - }, - - methods: { - install(code) { - let theme; - - try { - theme = JSON5.parse(code); - } catch (e) { - this.$root.dialog({ - type: 'error', - text: this.$t('invalid-theme') - }); - return; - } - - if (theme.id == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('invalid-theme') - }); - return; - } - - if (this.$store.state.device.themes.some(t => t.id == theme.id)) { - this.$root.dialog({ - type: 'info', - text: this.$t('already-installed') - }); - return; - } - - const themes = this.$store.state.device.themes.concat(theme); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'success', - text: this.$t('installed').replace('{}', theme.name) - }); - }, - - uninstall() { - const theme = this.selectedTheme; - const themes = this.$store.state.device.themes.filter(t => t.id != theme.id); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'info', - text: this.$t('uninstalled').replace('{}', theme.name) - }); - }, - - import_() { - (this.$refs.file as any).click(); - }, - - export_() { - const blob = new Blob([this.selectedThemeCode], { - type: 'application/json5' - }); - this.$refs.export.$el.href = window.URL.createObjectURL(blob); - }, - - onUpdateImportFile() { - const f = (this.$refs.file as any).files[0]; - - const reader = new FileReader(); - - reader.onload = e => { - this.install(e.target.result); - }; - - reader.readAsText(f); - }, - - preview() { - applyTheme(this.myTheme, false); - }, - - gen() { - const theme = this.myTheme; - - if (theme.name == null || theme.name.trim() == '') { - this.$root.dialog({ - type: 'warning', - text: this.$t('theme-name-required') - }); - return; - } - - theme.id = uuid(); - - const themes = this.$store.state.device.themes.concat(theme); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nicnklzforebnpfgasiypmpdaaglujqm - > .dark - margin-top 48px - margin-bottom 110px - - .toggleWrapper { - position: absolute; - top: 50%; - left: 50%; - overflow: hidden; - padding: 0 200px; - transform: translate3d(-50%, -50%, 0); - - input { - position: absolute; - left: -99em; - } - } - - .toggle { - cursor: pointer; - display: inline-block; - position: relative; - width: 90px; - height: 50px; - background-color: #83D8FF; - border-radius: 90px - 6; - transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - - &:before { - content: 'Light'; - position: absolute; - left: -60px; - top: 15px; - font-size: 18px; - color: var(--primary); - } - - &:after { - content: 'Dark'; - position: absolute; - right: -58px; - top: 15px; - font-size: 18px; - color: var(--text); - } - } - - .toggle__handler { - display: inline-block; - position: relative; - z-index: 1; - top: 3px; - left: 3px; - width: 50px - 6; - height: 50px - 6; - background-color: #FFCF96; - border-radius: 50px; - box-shadow: 0 2px 6px rgba(0,0,0,.3); - transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; - transform: rotate(-45deg); - - .crater { - position: absolute; - background-color: #E8CDA5; - opacity: 0; - transition: opacity 200ms ease-in-out !important; - border-radius: 100%; - } - - .crater--1 { - top: 18px; - left: 10px; - width: 4px; - height: 4px; - } - - .crater--2 { - top: 28px; - left: 22px; - width: 6px; - height: 6px; - } - - .crater--3 { - top: 10px; - left: 25px; - width: 8px; - height: 8px; - } - } - - .star { - position: absolute; - background-color: #ffffff; - transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - border-radius: 50%; - } - - .star--1 { - top: 10px; - left: 35px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--2 { - top: 18px; - left: 28px; - z-index: 1; - width: 30px; - height: 3px; - } - - .star--3 { - top: 27px; - left: 40px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--4, - .star--5, - .star--6 { - opacity: 0; - transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - - .star--4 { - top: 16px; - left: 11px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - .star--5 { - top: 32px; - left: 17px; - z-index: 0; - width: 3px; - height: 3px; - transform: translate3d(3px,0,0); - } - - .star--6 { - top: 36px; - left: 28px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - input:checked { - + .toggle { - background-color: #749DD6; - - &:before { - color: var(--text); - } - - &:after { - color: var(--primary); - } - - .toggle__handler { - background-color: #FFE5B5; - transform: translate3d(40px, 0, 0) rotate(0); - - .crater { opacity: 1; } - } - - .star--1 { - width: 2px; - height: 2px; - } - - .star--2 { - width: 4px; - height: 4px; - transform: translate3d(-5px, 0, 0); - } - - .star--3 { - width: 2px; - height: 2px; - transform: translate3d(-7px, 0, 0); - } - - .star--4, - .star--5, - .star--6 { - opacity: 1; - transform: translate3d(0,0,0); - } - .star--4 { - transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - .star--5 { - transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - .star--6 { - transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - } - } - - > a - display block - margin-top -16px - margin-bottom 16px - - > details - border-top solid var(--lineWidth) var(--faceDivider) - - > summary - padding 16px 0 - - > *:last-child - margin-bottom 16px - - > .creator - > div - padding 16px 0 - border-bottom solid var(--lineWidth) var(--faceDivider) -</style> diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue deleted file mode 100644 index bb4a6605bd..0000000000 --- a/src/client/app/common/views/components/signin.vue +++ /dev/null @@ -1,248 +0,0 @@ -<template> -<form class="mk-signin" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> - <div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div> - <div class="normal-signin" v-if="!totpLogin"> - <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange"> - <span>{{ $t('username') }}</span> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </ui-input> - <ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required> - <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> - </ui-input> - <ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button> - <p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p> - <p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p> - <p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p> - </div> - <div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }"> - <div v-if="user && user.securityKeys" class="twofa-group tap-group"> - <p>{{ $t('tap-key') }}</p> - <ui-button @click="queryKey" v-if="!queryingKey"> - {{ $t('@.error.retry') }} - </ui-button> - </div> - <div class="or-hr" v-if="user && user.securityKeys"> - <p class="or-msg">{{ $t('or') }}</p> - </div> - <div class="twofa-group totp-group"> - <p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p> - <ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required> - <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> - </ui-input> - <ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required> - <span>{{ $t('@.2fa') }}</span> - <template #prefix><fa icon="gavel"/></template> - </ui-input> - <ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button> - </div> - </div> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { apiUrl, host } from '../../../config'; -import { toUnicode } from 'punycode'; -import { hexifyAB } from '../../scripts/2fa'; - -export default Vue.extend({ - i18n: i18n('common/views/components/signin.vue'), - - props: { - withAvatar: { - type: Boolean, - required: false, - default: true - } - }, - - data() { - return { - signing: false, - user: null, - username: '', - password: '', - token: '', - apiUrl, - host: toUnicode(host), - meta: null, - totpLogin: false, - credential: null, - challengeData: null, - queryingKey: false, - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - - methods: { - onUsernameChange() { - this.$root.api('users/show', { - username: this.username - }).then(user => { - this.user = user; - }, () => { - this.user = null; - }); - }, - - queryKey() { - this.queryingKey = true; - return navigator.credentials.get({ - publicKey: { - challenge: Buffer.from( - this.challengeData.challenge - .replace(/\-/g, '+') - .replace(/_/g, '/'), - 'base64' - ), - allowCredentials: this.challengeData.securityKeys.map(key => ({ - id: Buffer.from(key.id, 'hex'), - type: 'public-key', - transports: ['usb', 'nfc', 'ble', 'internal'] - })), - timeout: 60 * 1000 - } - }).catch(() => { - this.queryingKey = false; - return Promise.reject(null); - }).then(credential => { - this.queryingKey = false; - this.signing = true; - return this.$root.api('signin', { - username: this.username, - password: this.password, - signature: hexifyAB(credential.response.signature), - authenticatorData: hexifyAB(credential.response.authenticatorData), - clientDataJSON: hexifyAB(credential.response.clientDataJSON), - credentialId: credential.id, - challengeId: this.challengeData.challengeId - }); - }).then(res => { - localStorage.setItem('i', res.i); - location.reload(); - }).catch(err => { - if (err === null) return; - this.$root.dialog({ - type: 'error', - text: this.$t('login-failed') - }); - this.signing = false; - }); - }, - - onSubmit() { - this.signing = true; - - if (!this.totpLogin && this.user && this.user.twoFactorEnabled) { - if (window.PublicKeyCredential && this.user.securityKeys) { - this.$root.api('signin', { - username: this.username, - password: this.password - }).then(res => { - this.totpLogin = true; - this.signing = false; - this.challengeData = res; - return this.queryKey(); - }).catch(() => { - this.$root.dialog({ - type: 'error', - text: this.$t('login-failed') - }); - this.challengeData = null; - this.totpLogin = false; - this.signing = false; - }); - } else { - this.totpLogin = true; - this.signing = false; - } - } else { - this.$root.api('signin', { - username: this.username, - password: this.password, - token: this.user && this.user.twoFactorEnabled ? this.token : undefined - }).then(res => { - localStorage.setItem('i', res.i); - location.reload(); - }).catch(() => { - this.$root.dialog({ - type: 'error', - text: this.$t('login-failed') - }); - this.signing = false; - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-signin - color #555 - - .or-hr, - .or-hr .or-msg, - .twofa-group, - .twofa-group p - color var(--text) - - .tap-group > button - margin-bottom 1em - - .securityKeys .or-hr - & - position relative - - .or-msg - &:before - right 100% - margin-right 0.125em - - &:after - left 100% - margin-left 0.125em - - &:before, &:after - content "" - position absolute - top 50% - width 100% - height 2px - background #555 - - & - position relative - margin auto - left 0 - right 0 - top 0 - bottom 0 - font-size 1.5em - height 1.5em - width 3em - text-align center - - &.signing - &, * - cursor wait !important - - > .avatar - margin 0 auto 0 auto - width 64px - height 64px - background #ddd - background-position center - background-size cover - border-radius 100% - -</style> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue deleted file mode 100644 index 893f6575fb..0000000000 --- a/src/client/app/common/views/components/signup.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> - <template v-if="meta"> - <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> - <span>{{ $t('invitation-code') }}</span> - <template #prefix><fa icon="id-card-alt"/></template> - <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template> - </ui-input> - <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> - <span>{{ $t('username') }}</span> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - <template #desc> - <span v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</span> - <span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</span> - <span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</span> - <span v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</span> - <span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</span> - <span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</span> - <span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</span> - </template> - </ui-input> - <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill"> - <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> - <template #desc> - <p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('weak-password') }}</p> - <p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('normal-password') }}</p> - <p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('strong-password') }}</p> - </template> - </ui-input> - <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill"> - <span>{{ $t('password') }} ({{ $t('retype') }})</span> - <template #prefix><fa icon="lock"/></template> - <template #desc> - <p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('password-matched') }}</p> - <p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('password-not-matched') }}</p> - </template> - </ui-input> - <ui-switch v-model="ToSAgreement" v-if="meta.ToSUrl"> - <i18n path="agree-to"> - <a :href="meta.ToSUrl" target="_blank">{{ $t('tos') }}</a> - </i18n> - </ui-switch> - <div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div> - <ui-button type="submit" :disabled=" submitting || !(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button> - </template> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -const getPasswordStrength = require('syuilo-password-strength'); -import { host, url } from '../../../config'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('common/views/components/signup.vue'), - - data() { - return { - host: toUnicode(host), - username: '', - password: '', - retypedPassword: '', - invitationCode: '', - url, - usernameState: null, - passwordStrength: '', - passwordRetypeState: null, - meta: {}, - submitting: false, - ToSAgreement: false - } - }, - - computed: { - shouldShowProfileUrl(): boolean { - return (this.username != '' && - this.usernameState != 'invalid-format' && - this.usernameState != 'min-range' && - this.usernameState != 'max-range'); - } - }, - - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - - mounted() { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - head.appendChild(script); - }, - - methods: { - onChangeUsername() { - if (this.username == '') { - this.usernameState = null; - return; - } - - const err = - !this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : - this.username.length < 1 ? 'min-range' : - this.username.length > 20 ? 'max-range' : - null; - - if (err) { - this.usernameState = err; - return; - } - - this.usernameState = 'wait'; - - this.$root.api('username/available', { - username: this.username - }).then(result => { - this.usernameState = result.available ? 'ok' : 'unavailable'; - }).catch(err => { - this.usernameState = 'error'; - }); - }, - - onChangePassword() { - if (this.password == '') { - this.passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(this.password); - this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - }, - - onChangePasswordRetype() { - if (this.retypedPassword == '') { - this.passwordRetypeState = null; - return; - } - - this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match'; - }, - - onSubmit() { - if (this.submitting) return; - this.submitting = true; - - this.$root.api('signup', { - username: this.username, - password: this.password, - invitationCode: this.invitationCode, - 'g-recaptcha-response': this.meta.enableRecaptcha ? (window as any).grecaptcha.getResponse() : null - }).then(() => { - this.$root.api('signin', { - username: this.username, - password: this.password - }).then(res => { - localStorage.setItem('i', res.i); - location.href = '/'; - }); - }).catch(() => { - this.submitting = false; - - this.$root.dialog({ - type: 'error', - text: this.$t('some-error') - }); - - if (this.meta.enableRecaptcha) { - (window as any).grecaptcha.reset(); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-signup - min-width 302px -</style> diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue deleted file mode 100644 index 8ab1cfcfeb..0000000000 --- a/src/client/app/common/views/components/stream-indicator.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="mk-stream-indicator"> - <p v-if="stream.state == 'initializing'"> - <fa icon="spinner" pulse/> - <span>{{ $t('connecting') }}<mk-ellipsis/></span> - </p> - <p v-if="stream.state == 'reconnecting'"> - <fa icon="spinner" pulse/> - <span>{{ $t('reconnecting') }}<mk-ellipsis/></span> - </p> - <p v-if="stream.state == 'connected'"> - <fa icon="check"/> - <span>{{ $t('connected') }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; - -export default Vue.extend({ - i18n: i18n('common/views/components/stream-indicator.vue'), - computed: { - stream() { - return this.$root.stream; - } - }, - created() { - this.$root.stream.on('_connected_', this.onConnected); - this.$root.stream.on('_disconnected_', this.onDisconnected); - - this.$nextTick(() => { - if (this.stream.state == 'connected') { - this.$el.style.opacity = '0'; - } - }); - }, - beforeDestroy() { - this.$root.stream.off('_connected_', this.onConnected); - this.$root.stream.off('_disconnected_', this.onDisconnected); - }, - methods: { - onConnected() { - setTimeout(() => { - anime({ - targets: this.$el, - opacity: 0, - easing: 'linear', - duration: 200 - }); - }, 1000); - }, - onDisconnected() { - anime({ - targets: this.$el, - opacity: 1, - easing: 'linear', - duration: 100 - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-stream-indicator - pointer-events none - position fixed - z-index 16384 - bottom 8px - right 8px - margin 0 - padding 6px 12px - font-size 0.9em - color #fff - background rgba(#000, 0.8) - border-radius 4px - - > p - display block - margin 0 - - > [data-icon] - margin-right 0.25em - -</style> diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue deleted file mode 100644 index 3fa5e3b9d4..0000000000 --- a/src/client/app/common/views/components/tag-cloud.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="jtivnzhfwquxpsfidertopbmwmchmnmo"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> - <div v-else> - <vue-word-cloud - :words="tags.slice(0, 20).map(x => [x.tag, x.count])" - :color="color" - :spacing="1"> - <template slot-scope="{word, text, weight}"> - <div style="cursor: pointer;" :title="weight"> - {{ text }} - </div> - </template> - </vue-word-cloud> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as VueWordCloud from 'vuewordcloud'; - -export default Vue.extend({ - i18n: i18n('common/views/components/tag-cloud.vue'), - components: { - [VueWordCloud.name]: VueWordCloud - }, - data() { - return { - tags: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - fetch() { - this.$root.api('hashtags/trend').then(tags => { - this.tags = tags; - this.fetching = false; - }); - }, - color([, weight]) { - const peak = Math.max.apply(null, this.tags.map(x => x.count)); - const w = weight / peak; - - if (w > 0.9) { - return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69'; - } else if (w > 0.5) { - return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7'; - } else { - return this.$store.state.device.darkmode ? '#fff' : '#555'; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.jtivnzhfwquxpsfidertopbmwmchmnmo - height 100% - width 100% - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - - > div - height 100% - width 100% - -</style> diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue deleted file mode 100644 index 8cfcc4cb4f..0000000000 --- a/src/client/app/common/views/components/time.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<time class="mk-time" :title="absolute"> - <span v-if=" mode == 'relative' ">{{ relative }}</span> - <span v-if=" mode == 'absolute' ">{{ absolute }}</span> - <span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span> -</time> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - time: { - type: [Date, String], - required: true - }, - mode: { - type: String, - default: 'relative' - } - }, - data() { - return { - tickId: null, - now: new Date() - }; - }, - computed: { - _time(): Date { - return typeof this.time == 'string' ? new Date(this.time) : this.time; - }, - absolute(): string { - return this._time.toLocaleString(); - }, - relative(): string { - const time = this._time; - const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; - return ( - ago >= 31536000 ? this.$t('@.time.years_ago') .replace('{}', (~~(ago / 31536000)).toString()) : - ago >= 2592000 ? this.$t('@.time.months_ago') .replace('{}', (~~(ago / 2592000)).toString()) : - ago >= 604800 ? this.$t('@.time.weeks_ago') .replace('{}', (~~(ago / 604800)).toString()) : - ago >= 86400 ? this.$t('@.time.days_ago') .replace('{}', (~~(ago / 86400)).toString()) : - ago >= 3600 ? this.$t('@.time.hours_ago') .replace('{}', (~~(ago / 3600)).toString()) : - ago >= 60 ? this.$t('@.time.minutes_ago').replace('{}', (~~(ago / 60)).toString()) : - ago >= 10 ? this.$t('@.time.seconds_ago').replace('{}', (~~(ago % 60)).toString()) : - ago >= -1 ? this.$t('@.time.just_now') : - ago < -1 ? this.$t('@.time.future') : - this.$t('@.time.unknown')); - } - }, - created() { - if (this.mode == 'relative' || this.mode == 'detail') { - this.tickId = window.requestAnimationFrame(this.tick); - } - }, - destroyed() { - if (this.mode === 'relative' || this.mode === 'detail') { - window.clearTimeout(this.tickId); - } - }, - methods: { - tick() { - this.now = new Date(); - - this.tickId = setTimeout(() => { - window.requestAnimationFrame(this.tick); - }, 10000); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/trends.chart.vue b/src/client/app/common/views/components/trends.chart.vue deleted file mode 100644 index 5c4f74b6b4..0000000000 --- a/src/client/app/common/views/components/trends.chart.vue +++ /dev/null @@ -1,89 +0,0 @@ -<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> diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue deleted file mode 100644 index 536d55247c..0000000000 --- a/src/client/app/common/views/components/trends.vue +++ /dev/null @@ -1,100 +0,0 @@ -<template> -<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> - <!-- トランジションを有効にするとなぜかメモリリークする --> - <transition-group v-else 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> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XChart from './trends.chart.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/trends.vue'), - components: { - XChart - }, - data() { - return { - stats: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - fetch() { - this.$root.api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.csqvmxybqbycalfhkxvyfrgbrdalkaoc - > .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(--faceDivider) - - > .tag - flex 1 - overflow hidden - font-size 14px - color var(--text) - - > 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> diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue deleted file mode 100644 index 59a5c858a7..0000000000 --- a/src/client/app/common/views/components/ui/button.vue +++ /dev/null @@ -1,224 +0,0 @@ -<template> -<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" - :is="link ? 'a' : 'button'" - :class="{ inline, primary, wait, round: $store.state.device.roundedCorners }" - :type="type" - @click="$emit('click')" - @mousedown="onMousedown" -> - <div ref="ripples" class="ripples"></div> - <div class="content"> - <slot></slot> - </div> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - inject: { - horizonGrouped: { - default: false - } - }, - props: { - type: { - type: String, - required: false - }, - primary: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - link: { - type: Boolean, - required: false, - default: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - wait: { - type: Boolean, - required: false, - default: false - }, - }, - mounted() { - if (this.autofocus) { - this.$nextTick(() => { - this.$el.focus(); - }); - } - }, - methods: { - onMousedown(e: MouseEvent) { - function distance(p, q) { - return Math.hypot(p.x - q.x, p.y - q.y); - } - - function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) { - const origin = {x: circleCenterX, y: circleCenterY}; - const dist1 = distance({x: 0, y: 0}, origin); - const dist2 = distance({x: boxW, y: 0}, origin); - const dist3 = distance({x: 0, y: boxH}, origin); - const dist4 = distance({x: boxW, y: boxH }, origin); - return Math.max(dist1, dist2, dist3, dist4) * 2; - } - - const rect = e.target.getBoundingClientRect(); - - const ripple = document.createElement('div'); - ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px'; - ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px'; - - this.$refs.ripples.appendChild(ripple); - - const circleCenterX = e.clientX - rect.left; - const circleCenterY = e.clientY - rect.top; - - const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); - - setTimeout(() => { - ripple.style.transform = 'scale(' + (scale / 2) + ')'; - }, 1); - setTimeout(() => { - ripple.style.transition = 'all 1s ease'; - ripple.style.opacity = '0'; - }, 1000); - setTimeout(() => { - if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple); - }, 2000); - } - } -}); -</script> - -<style lang="stylus" scoped> -.dmtdnykelhudezerjlfpbhgovrgnqqgr - display block - width 100% - margin 0 - padding 8px 10px - text-align center - font-weight normal - font-size 14px - line-height 24px - border none - outline none - box-shadow none - text-decoration none - user-select none - color var(--text) - background var(--buttonBg) - - &.round - border-radius 6px - - &:not(:disabled):hover - background var(--buttonHoverBg) - - &:not(:disabled):active - background var(--buttonActiveBg) - - &.primary - color var(--primaryForeground) - background var(--primary) - - &:not(:disabled):hover - background var(--primaryLighten5) - - &:not(:disabled):active - background var(--primaryDarken5) - - * - pointer-events none - user-select none - - &:disabled - opacity 0.7 - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - - &.round:focus:after - border-radius 10px - - &:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr - margin-top 16px - - &.inline - display inline-block - width auto - min-width 100px - - &.primary - font-weight bold - - &.wait - background linear-gradient( - 45deg, - var(--primaryDarken10) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryDarken10) 50%, - var(--primaryDarken10) 75%, - var(--primary) 75%, - var(--primary) - ) - background-size 32px 32px - animation stripe-bg 1.5s linear infinite - opacity 0.7 - cursor wait - - @keyframes stripe-bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - - > .ripples - position absolute - z-index 0 - top 0 - left 0 - width 100% - height 100% - overflow hidden - - >>> div - position absolute - width 2px - height 2px - border-radius 100% - background rgba(0, 0, 0, 0.1) - opacity 1 - transform scale(1) - transition all 0.5s cubic-bezier(0, .5, .5, 1) - - &.round > .ripples - border-radius 6px - - &.primary > .ripples >>> div - background rgba(0, 0, 0, 0.15) - - > .content - z-index 1 - -</style> diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue deleted file mode 100644 index a83013f5d0..0000000000 --- a/src/client/app/common/views/components/ui/card.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="ui-card" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header> - <slot name="title"></slot> - </header> - - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - provide() { - return { - isCardChild: true - }; - } -}); -</script> - -<style lang="stylus" scoped> -.ui-card - margin 16px - max-width 850px - color var(--faceText) - background var(--face) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > header - padding 16px - font-weight bold - font-size 20px - color var(--faceText) - - @media (min-width 500px) - padding 24px 32px - - > section - padding 20px 16px - border-top solid var(--lineWidth) var(--faceDivider) - - @media (min-width 500px) - padding 32px - - &.fit-top - padding-top 0 - - &.fit-bottom - padding-bottom 0 - - > header - margin-bottom 16px - font-weight bold - color var(--faceText) - - > section - margin 16px 0 - - > header - font-weight bold - color var(--text) - -</style> diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue deleted file mode 100644 index 5c5bbd7256..0000000000 --- a/src/client/app/common/views/components/ui/form.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<div class="ui-form"> - <fieldset :disabled="disabled"> - <slot></slot> - </fieldset> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - disabled: { - type: Boolean, - required: false - } - } -}); -</script> - -<style lang="stylus" scoped> - - -.ui-form - > fieldset - margin 0 - padding 0 - border none - -</style> diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue deleted file mode 100644 index 3fd7b47629..0000000000 --- a/src/client/app/common/views/components/ui/form/button.vue +++ /dev/null @@ -1,81 +0,0 @@ -<template> -<div class="nvemkhtwcnnpkdrwfcbzuwhfulejhmzg" :class="{ round, primary }"> - <button @click="$emit('click')"> - <slot></slot> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - round: { - type: Boolean, - required: false, - default: false - }, - primary: { - type: Boolean, - required: false, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg - display inline-block - - & + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg - margin-left 12px - - > button - display inline-block - margin 0 - padding 12px 20px - font-size 14px - border 1px solid var(--formButtonBorder) - border-radius 4px - outline none - box-shadow none - color var(--text) - transition 0.1s - - * - pointer-events none - - &:hover - &:focus - color var(--primary) - background var(--formButtonHoverBg) - border-color var(--formButtonHoverBorder) - - &:active - color var(--primaryDarken20) - background var(--formButtonActiveBg) - border-color var(--primary) - transition all 0s - - &.primary - > button - border 1px solid var(--primary) - background var(--primary) - color var(--primaryForeground) - - &:hover - &:focus - background var(--primaryLighten20) - border-color var(--primaryLighten20) - - &:active - background var(--primaryDarken20) - border-color var(--primaryDarken20) - transition all 0s - - &.round - > button - border-radius 64px - -</style> diff --git a/src/client/app/common/views/components/ui/form/radio.vue b/src/client/app/common/views/components/ui/form/radio.vue deleted file mode 100644 index 396b2997e5..0000000000 --- a/src/client/app/common/views/components/ui/form/radio.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<div - class="uywduthvrdnlpsvsjkqigicixgyfctto" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input type="radio" - :disabled="disabled" - > - <span class="button"> - <span></span> - </span> - <span class="label"><slot></slot></span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'model', - event: 'change' - }, - props: { - model: { - required: false - }, - value: { - required: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.model === this.value; - } - }, - methods: { - toggle() { - this.$emit('change', this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.uywduthvrdnlpsvsjkqigicixgyfctto - display inline-flex - margin 0 16px 0 0 - cursor pointer - transition all 0.3s - - > * - user-select none - - &:hover - > .button - border solid 2px var(--inputLabel) - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - border-color var(--primary) - - &:after - background-color var(--primary) - transform scale(1) - opacity 1 - - > .label - color var(--primary) - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - display inline-block - flex-shrink 0 - width 20px - height 20px - background none - border solid 2px var(--radioBorder) - border-radius 100% - transition inherit - - &:after - content '' - display block - position absolute - top 3px - right 3px - bottom 3px - left 3px - border-radius 100% - opacity 0 - transform scale(0) - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - - > .label - margin-left 8px - display block - font-size 14px - line-height 20px - cursor pointer - -</style> diff --git a/src/client/app/common/views/components/ui/horizon-group.vue b/src/client/app/common/views/components/ui/horizon-group.vue deleted file mode 100644 index 33d0300101..0000000000 --- a/src/client/app/common/views/components/ui/horizon-group.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="vnxwkwuf" :class="{ inputs, noGrow }" :data-children-count="children"> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - provide: { - horizonGrouped: true - }, - props: { - inputs: { - type: Boolean, - required: false, - default: false - }, - noGrow: { - type: Boolean, - required: false, - default: false - } - }, - data() { - return { - children: 0 - }; - }, - mounted() { - this.$nextTick(() => { - this.children = this.$slots.default.length; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.vnxwkwuf - margin 16px 0 - - &.inputs - margin 32px 0 - - &.fit-top - margin-top 0 - - &.fit-bottom - margin-bottom 0 - - &:not(.noGrow) - display flex - - > * - flex 1 - min-width 0 !important - - > *:not(:last-child) - margin-right 16px !important - - &[data-children-count="3"] - @media (max-width 600px) - display block - - > * - display block - width 100% !important - margin 16px 0 !important - - &:first-child - margin-top 0 !important - - &:last-child - margin-bottom 0 !important - -</style> diff --git a/src/client/app/common/views/components/ui/hr.vue b/src/client/app/common/views/components/ui/hr.vue deleted file mode 100644 index 38572cfcc3..0000000000 --- a/src/client/app/common/views/components/ui/hr.vue +++ /dev/null @@ -1,15 +0,0 @@ -<template> -<div class="evrzpitu"></div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({}); -</script> - -<style lang="stylus" scoped> -.evrzpitu - margin 16px 0 - border-bottom solid var(--lineWidth) var(--faceDivider) - -</style> diff --git a/src/client/app/common/views/components/ui/info.vue b/src/client/app/common/views/components/ui/info.vue deleted file mode 100644 index 30fd8cb344..0000000000 --- a/src/client/app/common/views/components/ui/info.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<div class="ymxyweixqwsxauxldgpvecjepnwxbylu" :class="{ warn }"> - <i v-if="warn"><fa icon="exclamation-triangle"/></i> - <i v-else><fa icon="info-circle"/></i> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - warn: { - type: Boolean, - required: false, - default: false - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.ymxyweixqwsxauxldgpvecjepnwxbylu - margin 16px 0 - padding 16px - font-size 90% - background var(--infoBg) - color var(--infoFg) - - &.warn - background var(--infoWarnBg) - color var(--infoWarnFg) - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > i - margin-right 4px - -</style> diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue deleted file mode 100644 index 1b339a9ae0..0000000000 --- a/src/client/app/common/views/components/ui/input.vue +++ /dev/null @@ -1,503 +0,0 @@ -<template> -<div class="ui-input" :class="[{ focused, filled, inline, disabled }, styl]"> - <div class="icon" ref="icon"><slot name="icon"></slot></div> - <div class="input"> - <div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> - <div class="value" ref="passwordMetar"></div> - </div> - <span class="label" ref="label"><slot></slot></span> - <span class="title" ref="title"> - <slot name="title"></slot> - <span class="warning" v-if="invalid"><fa :icon="['fa', 'exclamation-circle']"/>{{ $refs.input.validationMessage }}</span> - </span> - <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> - <template v-if="type != 'file'"> - <input v-if="debounce" ref="input" - v-debounce="500" - :type="type" - v-model.lazy="v" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false" - @keydown="$emit('keydown', $event)" - @change="$emit('change', $event)" - :list="id" - > - <input v-else ref="input" - :type="type" - v-model="v" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false" - @keydown="$emit('keydown', $event)" - @change="$emit('change', $event)" - :list="id" - > - <datalist :id="id" v-if="datalist"> - <option v-for="data in datalist" :value="data"/> - </datalist> - </template> - <template v-else> - <input ref="input" - type="text" - :value="filePlaceholder" - readonly - @click="chooseFile" - > - <input ref="file" - type="file" - :value="value" - @change="onChangeFile" - > - </template> - <div class="suffix" ref="suffix"><slot name="suffix"></slot></div> - </div> - <div class="toggle" v-if="withPasswordToggle"> - <a @click="togglePassword"> - <span v-if="type == 'password'"><fa :icon="['fa', 'eye']"/> {{ $t('@.show-password') }}</span> - <span v-if="type != 'password'"><fa :icon="['far', 'eye-slash']"/> {{ $t('@.hide-password') }}</span> - </a> - </div> - <div class="desc"><slot name="desc"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import debounce from 'v-debounce'; -const getPasswordStrength = require('syuilo-password-strength'); - -export default Vue.extend({ - directives: { - debounce - }, - inject: { - horizonGrouped: { - default: false - } - }, - props: { - value: { - required: false - }, - type: { - type: String, - required: false - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - pattern: { - type: String, - required: false - }, - placeholder: { - type: String, - required: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - autocomplete: { - required: false - }, - spellcheck: { - required: false - }, - debounce: { - required: false - }, - withPasswordMeter: { - type: Boolean, - required: false, - default: false - }, - withPasswordToggle: { - type: Boolean, - required: false, - default: false - }, - datalist: { - type: Array, - required: false, - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - styl: { - type: String, - required: false, - default: 'line' - } - }, - data() { - return { - v: this.value, - focused: false, - invalid: false, - passwordStrength: '', - id: Math.random().toString() - }; - }, - computed: { - filled(): boolean { - return this.v != '' && this.v != null; - }, - filePlaceholder(): string { - if (this.type != 'file') return null; - if (this.v == null) return null; - - if (typeof this.v == 'string') return this.v; - - if (Array.isArray(this.v)) { - return this.v.map(file => file.name).join(', '); - } else { - return this.v.name; - } - } - }, - watch: { - value(v) { - this.v = v; - }, - v(v) { - if (this.type === 'number') { - this.$emit('input', parseInt(v, 10)); - } else { - this.$emit('input', v); - } - - if (this.withPasswordMeter) { - if (v == '') { - this.passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(v); - this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; - } - - this.invalid = this.$refs.input.validity.badInput; - } - }, - mounted() { - if (this.autofocus) { - this.$nextTick(() => { - this.$refs.input.focus(); - }); - } - - this.$nextTick(() => { - // このコンポーネントが作成された時、非表示状態である場合がある - // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する - const clock = setInterval(() => { - if (this.$refs.prefix) { - this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; - if (this.$refs.prefix.offsetWidth) { - this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px'; - } - } - if (this.$refs.suffix) { - if (this.$refs.suffix.offsetWidth) { - this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px'; - } - } - }, 100); - - this.$once('hook:beforeDestroy', () => { - clearInterval(clock); - }); - }); - - this.$on('keydown', (e: KeyboardEvent) => { - if (e.code == 'Enter') { - this.$emit('enter'); - } - }); - }, - methods: { - focus() { - this.$refs.input.focus(); - }, - togglePassword() { - if (this.type == 'password') { - this.type = 'text' - } else { - this.type = 'password' - } - }, - chooseFile() { - this.$refs.file.click(); - }, - onChangeFile() { - this.v = Array.from((this.$refs.file as any).files); - this.$emit('input', this.v); - this.$emit('change', this.v); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 32px 0 - - > .icon - position absolute - top 0 - left 0 - width 24px - text-align center - line-height 32px - color var(--inputLabel) - - &:not(:empty) + .input - margin-left 28px - - > .input - - if !fill - &:before - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 1px - background var(--inputBorder) - - &:after - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 2px - background var(--primary) - opacity 0 - transform scaleX(0.12) - transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) - will-change border opacity transform - - > .password-meter - position absolute - top 0 - left 0 - width 100% - height 100% - border-radius 6px - overflow hidden - opacity 0.3 - - &[data-strength=''] - display none - - &[data-strength='low'] - > .value - background #d73612 - - &[data-strength='medium'] - > .value - background #d7ca12 - - &[data-strength='high'] - > .value - background #61bb22 - - > .value - display block - width 0 - height 100% - background transparent - border-radius 6px - transition all 0.1s ease - - > .label - position absolute - z-index 1 - top fill ? 6px : 0 - left 0 - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > .title - position absolute - z-index 1 - top fill ? -24px : -17px - left 0 !important - pointer-events none - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(.75) - white-space nowrap - width 133% - overflow hidden - text-overflow ellipsis - - > .warning - margin-left 0.5em - color var(--infoWarnFg) - - > svg - margin-right 0.1em - - > input - display block - width 100% - margin 0 - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - line-height 32px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - if fill - padding 6px 12px - background rgba(#000, 0.035) - border-radius 6px - - &[type='file'] - display none - - > .prefix - > .suffix - display block - position absolute - z-index 1 - top 0 - font-size 16px - line-height fill ? 44px : 32px - color var(--inputLabel) - pointer-events none - - &:empty - display none - - > * - display inline-block - min-width 16px - max-width 150px - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .prefix - left 0 - padding-right 4px - - if fill - padding-left 12px - - > .suffix - right 0 - padding-left 4px - - if fill - padding-right 12px - - > .toggle - cursor pointer - padding-left 0.5em - font-size 0.7em - opacity 0.7 - text-align left - - > a - color var(--inputLabel) - text-decoration none - - > .desc - margin 6px 0 - font-size 13px - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - transform scaleX(1) - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top fill ? -24px : -17px - left 0 !important - transform scale(0.75) - -.ui-input - &.fill - root(true) - &:not(.fill) - root(false) - - &.inline - display inline-block - margin 0 - - &.disabled - opacity 0.7 - - &, * - cursor not-allowed !important - -</style> diff --git a/src/client/app/common/views/components/ui/margin.vue b/src/client/app/common/views/components/ui/margin.vue deleted file mode 100644 index 508116f070..0000000000 --- a/src/client/app/common/views/components/ui/margin.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<div class="zdcrxcne"> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({}); -</script> - -<style lang="stylus" scoped> -.zdcrxcne - margin 16px - -</style> diff --git a/src/client/app/common/views/components/ui/modal.vue b/src/client/app/common/views/components/ui/modal.vue deleted file mode 100644 index 413dc39fa5..0000000000 --- a/src/client/app/common/views/components/ui/modal.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<div class="modal"> - <div class="bg" ref="bg" @click="onBgClick" /> - <slot class="main" /> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: { - closeOnBgClick: { - type: Boolean, - required: false, - default: true - }, - openAnimeDuration: { - type: Number, - required: false, - default: 100 - }, - closeAnimeDuration: { - type: Number, - required: false, - default: 100 - } - }, - mounted() { - anime({ - targets: this.$refs.bg, - opacity: 1, - duration: this.openAnimeDuration, - easing: 'linear' - }); - }, - methods: { - onBgClick() { - this.$emit('bg-click'); - if (this.closeOnBgClick) this.close(); - }, - close() { - this.$emit('before-close'); - - anime({ - targets: this.$refs.bg, - opacity: 0, - duration: this.closeAnimeDuration, - easing: 'linear', - complete: () => (this as any).destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.modal - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - -.bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.7) - opacity 0 - -.main - z-index 1 -</style> diff --git a/src/client/app/common/views/components/ui/pagination.vue b/src/client/app/common/views/components/ui/pagination.vue deleted file mode 100644 index 67aa89d369..0000000000 --- a/src/client/app/common/views/components/ui/pagination.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div class="mwermpua" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <slot :items="items"></slot> - </sequential-entrance> - <div class="more" v-if="more"> - <ui-button @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import paging from '../../../scripts/paging'; - -export default Vue.extend({ - mixins: [ - paging({ - captureWindowScroll: false, - }), - ], - - props: { - pagination: { - required: true - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mwermpua - > .more - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue deleted file mode 100644 index 468318b58e..0000000000 --- a/src/client/app/common/views/components/ui/radio.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div - class="ui-radio" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input type="radio" - :disabled="disabled" - > - <span class="button"> - <span></span> - </span> - <span class="label"><slot></slot></span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'model', - event: 'change' - }, - props: { - model: { - required: false - }, - value: { - required: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.model === this.value; - } - }, - methods: { - toggle() { - this.$emit('change', this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ui-radio - display inline-block - margin 0 32px 0 0 - cursor pointer - transition all 0.3s - - > * - user-select none - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - border-color var(--radioActive) - - &:after - background-color var(--radioActive) - transform scale(1) - opacity 1 - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - position absolute - width 20px - height 20px - background none - border solid 2px var(--inputLabel) - border-radius 100% - transition inherit - - &:after - content '' - display block - position absolute - top 3px - right 3px - bottom 3px - left 3px - border-radius 100% - opacity 0 - transform scale(0) - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - - > .label - margin-left 28px - display block - font-size 16px - line-height 20px - cursor pointer - -</style> diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue deleted file mode 100644 index 1057d60d07..0000000000 --- a/src/client/app/common/views/components/ui/select.vue +++ /dev/null @@ -1,238 +0,0 @@ -<template> -<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]"> - <div class="icon" ref="icon"><slot name="icon"></slot></div> - <div class="input" @click="focus"> - <span class="label" ref="label"><slot name="label"></slot></span> - <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> - <select ref="input" - v-model="v" - :required="required" - :disabled="disabled" - @focus="focused = true" - @blur="focused = false" - > - <slot></slot> - </select> - <div class="suffix"><slot name="suffix"></slot></div> - </div> - <div class="text"><slot name="text"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - inject: { - horizonGrouped: { - default: false - } - }, - props: { - value: { - required: false - }, - required: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - styl: { - type: String, - required: false, - default: 'line' - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - }, - data() { - return { - focused: false - }; - }, - computed: { - v: { - get() { - return this.value; - }, - set(v) { - this.$emit('input', v); - } - }, - filled(): boolean { - return this.v != '' && this.v != null; - } - }, - mounted() { - if (this.$refs.prefix) { - this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; - } - }, - methods: { - focus() { - this.$refs.input.focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 32px 0 - - > .icon - position absolute - top 0 - left 0 - width 24px - text-align center - line-height 32px - color var(--inputLabel) - - &:not(:empty) + .input - margin-left 28px - - > .input - display flex - - if fill - padding 6px 12px - background rgba(#000, 0.035) - border-radius 6px - else - &:before - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 1px - background var(--inputBorder) - - &:after - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 2px - background var(--primary) - opacity 0 - transform scaleX(0.12) - transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) - will-change border opacity transform - - > .label - position absolute - top fill ? 6px : 0 - left 0 - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > select - display block - flex 1 - width 100% - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - height 32px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - * - color #000 - - > .prefix - > .suffix - display block - align-self center - justify-self center - font-size 16px - line-height 32px - color rgba(#000, 0.54) - pointer-events none - - &:empty - display none - - > * - display block - min-width 16px - - > .prefix - padding-right 4px - - > .suffix - padding-left 4px - - > .text - margin 6px 0 - font-size 13px - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - transform scaleX(1) - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top fill ? -24px : -17px - left 0 !important - transform scale(0.75) - -.ui-select - &.fill - root(true) - &:not(.fill) - root(false) - - &.inline - display inline-block - margin 0 - - &.disabled - opacity 0.7 - - &, * - cursor not-allowed !important - -</style> diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue deleted file mode 100644 index 8e3997ae78..0000000000 --- a/src/client/app/common/views/components/ui/switch.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div - class="ui-switch" - :class="{ disabled, checked }" - role="switch" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input - type="checkbox" - ref="input" - :disabled="disabled" - @keydown.enter="toggle" - > - <span class="button"> - <span></span> - </span> - <span class="label"> - <span :aria-hidden="!checked"><slot></slot></span> - <p :aria-hidden="!checked"> - <slot name="desc"></slot> - </p> - </span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'value', - event: 'change' - }, - props: { - value: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.value; - } - }, - methods: { - toggle() { - if (this.disabled) return; - this.$emit('change', !this.checked); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ui-switch - display flex - margin 32px 0 - cursor pointer - transition all 0.3s - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > * - user-select none - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - background-color var(--switchActiveTrack) - border-color var(--switchActiveTrack) - - > * - background-color var(--switchActive) - transform translateX(14px) - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - display inline-block - flex-shrink 0 - margin 3px 0 0 0 - width 34px - height 14px - background var(--switchTrack) - outline none - border-radius 14px - transition inherit - - > * - position absolute - top -3px - left 0 - border-radius 100% - transition background-color 0.3s, transform 0.3s - width 20px - height 20px - background-color #fff - box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12) - - > .label - margin-left 8px - display block - font-size 16px - cursor pointer - transition inherit - color var(--text) - - > span - display block - line-height 20px - transition inherit - - > p - margin 0 - opacity 0.7 - font-size 90% - -</style> diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue deleted file mode 100644 index d265c7ac6d..0000000000 --- a/src/client/app/common/views/components/ui/textarea.vue +++ /dev/null @@ -1,194 +0,0 @@ -<template> -<div class="ui-textarea" :class="{ focused, filled, tall, pre }"> - <div class="input"> - <span class="label" ref="label"><slot></slot></span> - <textarea ref="input" - :value="value" - :required="required" - :readonly="readonly" - :pattern="pattern" - :autocomplete="autocomplete" - @input="$emit('input', $event.target.value)" - @focus="focused = true" - @blur="focused = false" - ></textarea> - </div> - <div class="desc"><slot name="desc"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: false - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - pattern: { - type: String, - required: false - }, - autocomplete: { - type: String, - required: false - }, - tall: { - type: Boolean, - required: false, - default: false - }, - pre: { - type: Boolean, - required: false, - default: false - }, - }, - data() { - return { - focused: false, - passwordStrength: '' - } - }, - computed: { - filled(): boolean { - return this.value != '' && this.value != null; - } - }, - methods: { - focus() { - this.$refs.input.focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 42px 0 32px 0 - - &:last-child - margin-bottom 0 - - > .input - padding 12px - - if fill - background rgba(#000, 0.035) - border-radius 6px - else - &:before - content '' - display block - position absolute - top 0 - bottom 0 - left 0 - right 0 - background none - border solid 1px var(--inputBorder) - border-radius 3px - pointer-events none - - &:after - content '' - display block - position absolute - top 0 - bottom 0 - left 0 - right 0 - background none - border solid 2px var(--primary) - border-radius 3px - opacity 0 - transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) - pointer-events none - - > .label - position absolute - top 6px - left 12px - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > textarea - display block - width 100% - min-width 100% - max-width 100% - min-height 100px - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - > .desc - margin 6px 0 - font-size 13px - opacity 0.7 - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top -24px - left 0 !important - transform scale(0.75) - - &.tall - > .input - > textarea - min-height 200px - - &.pre - > .input - > textarea - white-space pre - -.ui-textarea.fill - root(true) - -.ui-textarea:not(.fill) - root(false) - -</style> diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue deleted file mode 100644 index 9f02da6c1e..0000000000 --- a/src/client/app/common/views/components/uploader.vue +++ /dev/null @@ -1,231 +0,0 @@ -<template> -<div class="mk-uploader"> - <ol v-if="uploads.length > 0"> - <li v-for="ctx in uploads" :key="ctx.id"> - <div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div> - <div class="top"> - <p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p> - <p class="status"> - <span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span> - <span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> - <span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span> - </p> - </div> - <progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress> - <div class="progress initing" v-if="ctx.progress == undefined"></div> - <div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div> - </li> - </ol> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { apiUrl } from '../../../config'; -import getMD5 from '../../scripts/get-md5'; - -export default Vue.extend({ - i18n: i18n('common/views/components/uploader.vue'), - data() { - return { - uploads: [] - }; - }, - methods: { - checkExistence(fileData: ArrayBuffer): Promise<any> { - return new Promise((resolve, reject) => { - const data = new FormData(); - data.append('md5', getMD5(fileData)); - - this.$root.api('drive/files/find-by-hash', { - md5: getMD5(fileData) - }).then(resp => { - resolve(resp.length > 0 ? resp[0] : null); - }); - }); - }, - - upload(file: File, folder: any, name?: string) { - if (folder && typeof folder == 'object') folder = folder.id; - - const id = Math.random(); - - const reader = new FileReader(); - reader.onload = (e: any) => { - this.checkExistence(e.target.result).then(result => { - if (result !== null) { - this.$emit('uploaded', result); - return; - } - - const ctx = { - id: id, - name: name || file.name || 'untitled', - progress: undefined, - img: window.URL.createObjectURL(file) - }; - - this.uploads.push(ctx); - this.$emit('change', this.uploads); - - const data = new FormData(); - data.append('i', this.$store.state.i.token); - data.append('force', 'true'); - data.append('file', file); - - if (folder) data.append('folderId', folder); - if (name) data.append('name', name); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = (e: any) => { - const driveFile = JSON.parse(e.target.response); - - this.$emit('uploaded', driveFile); - - this.uploads = this.uploads.filter(x => x.id != id); - this.$emit('change', this.uploads); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - if (ctx.progress == undefined) ctx.progress = {}; - ctx.progress.max = e.total; - ctx.progress.value = e.loaded; - } - }; - - xhr.send(data); - }) - } - reader.readAsArrayBuffer(file); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-uploader - overflow auto - - &:empty - display none - - > ol - display block - margin 0 - padding 0 - list-style none - - > li - display grid - margin 8px 0 0 0 - padding 0 - height 36px - width: 100% - box-shadow 0 -1px 0 var(--primaryAlpha01) - border-top solid 8px transparent - grid-template-columns 36px calc(100% - 44px) - grid-template-rows 1fr 8px - column-gap 8px - box-sizing content-box - - &:first-child - margin 0 - box-shadow none - border-top none - - > .img - display block - background-size cover - background-position center center - grid-column 1 / 2 - grid-row 1 / 3 - - > .top - display flex - grid-column 2 / 3 - grid-row 1 / 2 - - > .name - display block - padding 0 8px 0 0 - margin 0 - font-size 0.8em - color var(--primaryAlpha07) - white-space nowrap - text-overflow ellipsis - overflow hidden - flex-shrink 1 - - > [data-icon] - margin-right 4px - - > .status - display block - margin 0 0 0 auto - padding 0 - font-size 0.8em - flex-shrink 0 - - > .initing - color var(--primaryAlpha05) - - > .kb - color var(--primaryAlpha05) - - > .percentage - display inline-block - width 48px - text-align right - - color var(--primaryAlpha07) - - &:after - content '%' - - > progress - display block - background transparent - border none - border-radius 4px - overflow hidden - grid-column 2 / 3 - grid-row 2 / 3 - z-index 2 - - &::-webkit-progress-value - background var(--primary) - - &::-webkit-progress-bar - background var(--primaryAlpha01) - - > .progress - display block - border none - border-radius 4px - background linear-gradient( - 45deg, - var(--primaryLighten30) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryLighten30) 50%, - var(--primaryLighten30) 75%, - var(--primary) 75%, - var(--primary) - ) - background-size 32px 32px - animation bg 1.5s linear infinite - grid-column 2 / 3 - grid-row 2 / 3 - z-index 1 - - &.initing - opacity 0.3 - - @keyframes bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - -</style> diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue deleted file mode 100644 index 80aae5999d..0000000000 --- a/src/client/app/common/views/components/url-preview.vue +++ /dev/null @@ -1,343 +0,0 @@ -<template> -<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button> - <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> -</div> -<div v-else-if="tweetUrl && detail" class="twitter"> - <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> - <a :href="url"></a> - </blockquote> -</div> -<div v-else class="mk-url-preview"> - <component :is="hasRoute ? 'router-link' : 'a'" :class="{ mini: narrow, compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching"> - <div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`"> - <button v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="['far', 'play-circle']"/></button> - </div> - <article> - <header> - <h1 :title="title">{{ title }}</h1> - </header> - <p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> - <footer> - <img class="icon" v-if="icon" :src="icon"/> - <p :title="sitename">{{ sitename }}</p> - </footer> - </article> - </component> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url as local, lang } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/url-preview.vue'), - - props: { - url: { - type: String, - require: true - }, - - detail: { - type: Boolean, - required: false, - default: false - }, - - compact: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - const isSelf = this.url.startsWith(local); - const hasRoute = - (this.url.substr(local.length) === '/') || - this.url.substr(local.length).startsWith('/@') || - this.url.substr(local.length).startsWith('/notes/') || - this.url.substr(local.length).startsWith('/tags/') || - this.url.substr(local.length).startsWith('/pages/'); - return { - local, - fetching: true, - title: null, - description: null, - thumbnail: null, - icon: null, - sitename: null, - player: { - url: null, - width: null, - height: null - }, - tweetUrl: null, - playerEnabled: false, - self: isSelf, - hasRoute: hasRoute, - attr: hasRoute ? 'to' : 'href', - target: hasRoute ? null : '_blank' - }; - }, - - created() { - const requestUrl = new URL(this.url); - - if (this.detail && requestUrl.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(requestUrl.pathname)) { - this.tweetUrl = requestUrl; - const twttr = (window as any).twttr || {}; - const loadTweet = () => twttr.widgets.load(this.$refs.tweet); - - if (twttr.widgets) { - Vue.nextTick(loadTweet); - } else { - const wjsId = 'twitter-wjs'; - if (!document.getElementById(wjsId)) { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('id', wjsId); - script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); - head.appendChild(script); - } - twttr.ready = loadTweet; - (window as any).twttr = twttr; - } - return; - } - - if (requestUrl.hostname === 'music.youtube.com') { - requestUrl.hostname = 'youtube.com'; - } - - const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP'); - - requestUrl.hash = ''; - - fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => { - res.json().then(info => { - 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; - this.player = info.player; - }) - }); - } -}); -</script> - -<style lang="stylus" scoped> -.player - position relative - width 100% - - > button - position absolute - top -1.5em - right 0 - font-size 1em - width 1.5em - height 1.5em - padding 0 - margin 0 - color var(--text) - background rgba(128, 128, 128, 0.2) - opacity 0.7 - - &:hover - opacity 0.9 - - > iframe - height 100% - left 0 - position absolute - top 0 - width 100% - -.mk-url-preview - > a - display block - font-size 14px - border solid var(--lineWidth) var(--urlPreviewBorder) - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color var(--urlPreviewBorderHover) - - > article > header > h1 - text-decoration underline - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - display flex - justify-content center - align-items center - - > button - font-size 3.5em - opacity: 0.7 - - &:hover - font-size 4em - opacity 0.9 - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color var(--urlPreviewTitle) - - > p - margin 0 - color var(--urlPreviewText) - font-size 0.8em - - > footer - margin-top 8px - height 16px - - > img - display inline-block - width 16px - height 16px - margin-right 4px - vertical-align top - - > p - display inline-block - margin 0 - color var(--urlPreviewInfo) - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 700px) - > .thumbnail - position relative - width 100% - height 100px - - & + article - left 0 - width 100% - - @media (max-width 550px) - font-size 12px - - > .thumbnail - height 80px - - > article - padding 12px - - @media (max-width 500px) - font-size 10px - - > .thumbnail - height 70px - - > article - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - - &.compact - > .thumbnail - position: absolute - width 56px - height 100% - - > article - left 56px - width calc(100% - 56px) - padding 4px - - > header - margin-bottom 2px - - > footer - margin-top 2px - - &.mini - font-size 10px - - > .thumbnail - position relative - width 100% - height 60px - - > article - left 0 - width 100% - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - - &.compact - > .thumbnail - position absolute - width 56px - height 100% - - > article - left 56px - width calc(100% - 56px) - padding 4px - - > header - margin-bottom 2px - - > footer - margin-top 2px - - &.compact - > article - > header h1, p, footer - overflow hidden - white-space nowrap - text-overflow ellipsis -</style> diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue deleted file mode 100644 index 3a304ad6e7..0000000000 --- a/src/client/app/common/views/components/url.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<component :is="hasRoute ? 'router-link' : 'a'" class="mk-url" :[attr]="hasRoute ? url.substr(local.length) : url" :rel="rel" :target="target"> - <template v-if="!self"> - <span class="schema">{{ schema }}//</span> - <span class="hostname">{{ hostname }}</span> - <span class="port" v-if="port != ''">:{{ port }}</span> - </template> - <template v-if="pathname === '/' && self"> - <span class="self">{{ hostname }}</span> - </template> - <span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span> - <span class="query">{{ query }}</span> - <span class="hash">{{ hash }}</span> - <fa icon="external-link-square-alt" v-if="target === '_blank'"/> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { toUnicode as decodePunycode } from 'punycode'; -import { url as local } from '../../../config'; - -export default Vue.extend({ - props: ['url', 'rel'], - data() { - const isSelf = this.url.startsWith(local); - const hasRoute = isSelf && ( - (this.url.substr(local.length) === '/') || - this.url.substr(local.length).startsWith('/@') || - this.url.substr(local.length).startsWith('/notes/') || - this.url.substr(local.length).startsWith('/tags/') || - this.url.substr(local.length).startsWith('/pages/')); - return { - local, - schema: null, - hostname: null, - port: null, - pathname: null, - query: null, - hash: null, - self: isSelf, - hasRoute: hasRoute, - attr: hasRoute ? 'to' : 'href', - target: hasRoute ? null : '_blank' - }; - }, - created() { - const url = new URL(this.url); - this.schema = url.protocol; - this.hostname = decodePunycode(url.hostname); - this.port = url.port; - this.pathname = decodeURIComponent(url.pathname); - this.query = decodeURIComponent(url.search); - this.hash = decodeURIComponent(url.hash); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-url - word-break break-all - - > [data-icon] - padding-left 2px - font-size .9em - font-weight 400 - font-style normal - - > .self - font-weight bold - - > .schema - opacity 0.5 - - > .hostname - font-weight bold - - > .pathname - opacity 0.8 - - > .query - opacity 0.5 - - > .hash - font-style italic -</style> diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue deleted file mode 100644 index 4ba4e67e54..0000000000 --- a/src/client/app/common/views/components/user-list.vue +++ /dev/null @@ -1,165 +0,0 @@ -<template> -<ui-container :body-togglable="true" :expanded="expanded"> - <template #header><slot></slot></template> - - <mk-error v-if="error" @retry="init()"/> - - <div class="efvhhmdq" :class="{ iconOnly }" v-size="[{ lt: 500, class: 'narrow' }]"> - <div class="no-users" v-if="empty"> - <p>{{ $t('no-users') }}</p> - </div> - <div class="user" v-for="user in users" :key="user.id"> - <mk-avatar class="avatar" :user="user"/> - <div class="body" v-if="!iconOnly"> - <div class="name"> - <router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link> - <p class="username">@{{ user | acct }}</p> - </div> - <div class="description" v-if="user.description" :title="user.description"> - <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/> - </div> - <mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> - </div> - </div> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore()" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-list.vue'), - - mixins: [ - paging({}), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - }, - iconOnly: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - - computed: { - users() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.efvhhmdq - &.narrow - > .user > .body > .name - width 100% - - > .user > .body > .description - display none - - &.iconOnly - padding 12px - - > .user - display inline-block - padding 0 - border-bottom none - - > .avatar - display inline-block - margin 4px - - > .no-users - text-align center - color var(--text) - - > .user - display flex - padding 16px - border-bottom solid 1px var(--faceDivider) - - &:last-child - border-bottom none - - > .avatar - display block - flex-shrink 0 - margin 0 12px 0 0 - width 42px - height 42px - border-radius 8px - - > .body - display flex - width calc(100% - 54px) - - > .name - width 45% - - > .name - margin 0 - font-size 16px - line-height 24px - color var(--text) - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color var(--text) - opacity 0.7 - - > .description - width 55% - color var(--text) - line-height 42px - white-space nowrap - overflow hidden - text-overflow ellipsis - opacity 0.7 - font-size 14px - padding-right 40px - - > .koudoku-button - position absolute - top 8px - right 0 - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue deleted file mode 100644 index 532dcf35c2..0000000000 --- a/src/client/app/common/views/components/user-menu.vue +++ /dev/null @@ -1,228 +0,0 @@ -<template> -<div style="position:initial"> - <mk-menu :source="source" :items="items" @closed="closed"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faExclamationCircle, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-menu.vue'), - - props: ['user', 'source'], - - data() { - let menu = [{ - icon: ['fas', 'at'], - text: this.$t('mention'), - action: () => { - this.$post({ mention: this.user }); - } - }, null, { - icon: ['fas', 'list'], - text: this.$t('push-to-list'), - action: this.pushList - }] as any; - - if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { - menu = menu.concat([null, { - icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], - text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), - action: this.toggleMute - }, { - icon: 'ban', - text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), - action: this.toggleBlock - }, null, { - icon: faExclamationCircle, - text: this.$t('report-abuse'), - action: this.reportAbuse - }]); - } - - if (this.$store.getters.isSignedIn && (this.$store.state.i.isAdmin || this.$store.state.i.isModerator)) { - menu = menu.concat([null, { - icon: faMicrophoneSlash, - text: this.user.isSilenced ? this.$t('unsilence') : this.$t('silence'), - action: this.toggleSilence - }, { - icon: faSnowflake, - text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'), - action: this.toggleSuspend - }]); - } - - return { - items: menu - }; - }, - - methods: { - closed() { - this.$nextTick(() => { - this.destroyDom(); - }); - }, - - async pushList() { - const t = this.$t('select-list'); // なぜか後で参照すると null になるので最初にメモリに確保しておく - const lists = await this.$root.api('users/lists/list'); - const { canceled, result: listId } = await this.$root.dialog({ - type: null, - title: t, - select: { - items: lists.map(list => ({ - value: list.id, text: list.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - await this.$root.api('users/lists/push', { - listId: listId, - userId: this.user.id - }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }, - - async toggleMute() { - if (this.user.isMuted) { - if (!await this.getConfirmed(this.$t('unmute-confirm'))) return; - - this.$root.api('mute/delete', { - userId: this.user.id - }).then(() => { - this.user.isMuted = false; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } else { - if (!await this.getConfirmed(this.$t('mute-confirm'))) return; - - this.$root.api('mute/create', { - userId: this.user.id - }).then(() => { - this.user.isMuted = true; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - }, - - async toggleBlock() { - if (this.user.isBlocking) { - if (!await this.getConfirmed(this.$t('unblock-confirm'))) return; - - this.$root.api('blocking/delete', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = false; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } else { - if (!await this.getConfirmed(this.$t('block-confirm'))) return; - - this.$root.api('blocking/create', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = true; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - }, - - async reportAbuse() { - const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく - const { canceled, result: comment } = await this.$root.dialog({ - title: this.$t('report-abuse-detail'), - input: true - }); - if (canceled) return; - this.$root.api('users/report-abuse', { - userId: this.user.id, - comment: comment - }).then(() => { - this.$root.dialog({ - type: 'success', - text: reported - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async toggleSilence() { - if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return; - - this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: this.user.id - }).then(() => { - this.user.isSilenced = !this.user.isSilenced; - this.$root.dialog({ - type: 'success', - splash: true - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async toggleSuspend() { - if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return; - - this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: this.user.id - }).then(() => { - this.user.isSuspended = !this.user.isSuspended; - this.$root.dialog({ - type: 'success', - splash: true - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async getConfirmed(text: string): Promise<Boolean> { - const confirm = await this.$root.dialog({ - type: 'warning', - showCancelButton: true, - title: 'confirm', - text, - }); - - return !confirm.canceled; - }, - } -}); -</script> diff --git a/src/client/app/common/views/components/user-name.vue b/src/client/app/common/views/components/user-name.vue deleted file mode 100644 index 425cb587c4..0000000000 --- a/src/client/app/common/views/components/user-name.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> -<mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - }, - nowrap: { - type: Boolean, - default: true - }, - } -}); -</script> diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue deleted file mode 100644 index 5aa481ed9a..0000000000 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ /dev/null @@ -1,233 +0,0 @@ -<template> -<div class="gqyayizv"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover"> - <div @click="choose('public')" :class="{ active: v == 'public' }"> - <div><fa icon="globe"/></div> - <div> - <span>{{ $t('public') }}</span> - </div> - </div> - <div @click="choose('home')" :class="{ active: v == 'home' }"> - <div><fa icon="home"/></div> - <div> - <span>{{ $t('home') }}</span> - <span>{{ $t('home-desc') }}</span> - </div> - </div> - <div @click="choose('followers')" :class="{ active: v == 'followers' }"> - <div><fa icon="unlock"/></div> - <div> - <span>{{ $t('followers') }}</span> - <span>{{ $t('followers-desc') }}</span> - </div> - </div> - <div @click="choose('specified')" :class="{ active: v == 'specified' }"> - <div><fa icon="envelope"/></div> - <div> - <span>{{ $t('specified') }}</span> - <span>{{ $t('specified-desc') }}</span> - </div> - </div> - <div @click="choose('local-public')" :class="{ active: v == 'local-public' }"> - <div><fa icon="globe"/></div> - <div> - <span>{{ $t('local-public') }}</span> - <span>{{ $t('local-public-desc') }}</span> - </div> - </div> - <div @click="choose('local-home')" :class="{ active: v == 'local-home' }"> - <div><fa icon="home"/></div> - <div> - <span>{{ $t('local-home') }}</span> - </div> - </div> - <div @click="choose('local-followers')" :class="{ active: v == 'local-followers' }"> - <div><fa icon="unlock"/></div> - <div> - <span>{{ $t('local-followers') }}</span> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; - -export default Vue.extend({ - i18n: i18n('common/views/components/visibility-chooser.vue'), - props: { - source: { - required: true - }, - currentVisibility: { - type: String, - required: false - } - }, - data() { - return { - v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility) - } - }, - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.$root.isMobile) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - choose(visibility) { - if (this.$store.state.settings.rememberNoteVisibility) { - this.$store.commit('device/setVisibility', visibility); - } - this.$emit('chosen', visibility); - this.destroyDom(); - }, - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => this.destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gqyayizv - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - $bgcolor = var(--popupBg) - position absolute - z-index 10001 - width 240px - padding 8px 0 - background $bgcolor - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - &:not(.isMobile) - $arrow-size = 10px - - margin-top $arrow-size - transform-origin center -($arrow-size) - - &:before - content "" - display block - position absolute - top -($arrow-size * 2) - left s('calc(50% - %s)', $arrow-size) - border-top solid $arrow-size transparent - border-left solid $arrow-size transparent - border-right solid $arrow-size transparent - border-bottom solid $arrow-size $bgcolor - - > div - display flex - padding 8px 14px - font-size 12px - color var(--popupFg) - cursor pointer - - &:hover - background var(--faceClearButtonHover) - - &:active - background var(--faceClearButtonActive) - - &.active - color var(--primaryForeground) - background var(--primary) - - > * - user-select none - pointer-events none - - > *:first-child - display flex - justify-content center - align-items center - margin-right 10px - width 16px - - > *:last-child - flex 1 1 auto - - > span:first-child - display block - font-weight bold - - > span:last-child:not(:first-child) - opacity 0.6 - -</style> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue deleted file mode 100644 index d812549b1e..0000000000 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="mk-welcome-timeline"> - <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div"> - <div v-for="note in notes" :key="note.id"> - <mk-avatar class="avatar" :user="note.user" target="_blank"/> - <div class="body"> - <header> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> - <mk-user-name :user="note.user"/> - </router-link> - <span class="username">@{{ note.user | acct }}</span> - <div class="info"> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> - </div> - </header> - <div class="text"> - <mfm v-if="note.text" :text="note.cw != null ? note.cw : note.text" :author="note.user" :custom-emojis="note.emojis"/> - </div> - </div> - </div> - </transition-group> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - max: { - type: Number, - required: false, - default: undefined - } - }, - - data() { - return { - fetching: true, - notes: [], - connection: null - }; - }, - - mounted() { - this.fetch(); - - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - - this.connection.on('note', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - fetch(cb?) { - this.fetching = true; - this.$root.api('notes', { - limit: this.max, - local: true, - reply: false, - renote: false, - file: false, - poll: false - }).then(notes => { - this.notes = notes; - this.fetching = false; - }); - }, - - onNote(note) { - if (note.replyId != null) return; - if (note.renoteId != null) return; - if (note.poll != null) return; - if (note.localOnly) return; - - this.notes.unshift(note); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.ldzpakcixzickvggyixyrhqwjaefknon-enter -.ldzpakcixzickvggyixyrhqwjaefknon-leave-to - opacity 0 - transform translateY(-30px) - -.mk-welcome-timeline - background var(--face) - - > div - > * - transition transform .3s ease, opacity .3s ease - - > div - padding 16px - overflow-wrap break-word - font-size .9em - color var(--noteText) - border-bottom 1px solid var(--faceDivider) - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - position -webkit-sticky - position sticky - top 16px - width 42px - height 42px - border-radius 6px - - > .body - float right - width calc(100% - 42px) - padding-left 12px - - > header - display flex - align-items center - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - font-weight bold - text-overflow ellipsis - color var(--noteHeaderName) - - > .username - margin 0 .5em 0 0 - color var(--noteHeaderAcct) - - > .info - margin-left auto - font-size 0.9em - - > .created-at - color var(--noteHeaderInfo) - - > .text - text-align left - -</style> diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue deleted file mode 100644 index 974c58235d..0000000000 --- a/src/client/app/common/views/deck/deck.column-core.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XTlColumn from './deck.tl-column.vue'; -import XNotificationsColumn from './deck.notifications-column.vue'; -import XWidgetsColumn from './deck.widgets-column.vue'; -import XMentionsColumn from './deck.mentions-column.vue'; -import XDirectColumn from './deck.direct-column.vue'; - -export default Vue.extend({ - components: { - XTlColumn, - XNotificationsColumn, - XWidgetsColumn, - XMentionsColumn, - XDirectColumn - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: false, - default: false - } - }, - - methods: { - focus() { - this.$children[0].focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.column-template.vue b/src/client/app/common/views/deck/deck.column-template.vue deleted file mode 100644 index 5923285162..0000000000 --- a/src/client/app/common/views/deck/deck.column-template.vue +++ /dev/null @@ -1,45 +0,0 @@ -<template> -<x-column> - <template #header> - <fa v-if="icon" :icon="icon"/>{{ title }} - </template> - - <div> - <component :is="component" @init="init" v-bind="$attrs"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; - -export default Vue.extend({ - components: { - XColumn, - }, - - props: { - component: { - required: true - } - }, - - data() { - return { - title: null, - icon: null, - }; - }, - - mounted() { - }, - - methods: { - init(v) { - this.title = v.title; - this.icon = v.icon; - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.column.vue b/src/client/app/common/views/deck/deck.column.vue deleted file mode 100644 index ac69a97df5..0000000000 --- a/src/client/app/common/views/deck/deck.column.vue +++ /dev/null @@ -1,444 +0,0 @@ -<template> -<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready, isMobile: $root.isMobile, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" - @dragover.prevent.stop="onDragover" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - v-hotkey="keymap"> - <header :class="{ indicate: count > 0 }" - draggable="true" - @click="goTop" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu"> - <button class="toggleActive" @click="toggleActive" v-if="isStacked"> - <template v-if="active"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - <span class="header"><slot name="header"></slot></span> - <span class="count" v-if="count > 0">({{ count }})</span> - <button v-if="!isTemporaryColumn" class="menu" ref="menu" @click.stop="showMenu"><fa icon="caret-down"/></button> - <button v-else class="close" @click.stop="close"><fa icon="times"/></button> - </header> - <div ref="body" v-show="active"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Menu from '../../../common/views/components/menu.vue'; -import { countIf } from '../../../../../prelude/array'; -import { faArrowUp, faArrowDown } from '@fortawesome/free-solid-svg-icons'; -import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('deck'), - props: { - column: { - type: Object, - required: false, - default: null - }, - isStacked: { - type: Boolean, - required: false, - default: false - }, - name: { - type: String, - required: false - }, - menu: { - type: Array, - required: false, - default: null - }, - naked: { - type: Boolean, - required: false, - default: false - }, - narrow: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - count: 0, - active: true, - dragging: false, - draghover: false, - dropready: false, - faArrowUp, faArrowDown - }; - }, - - computed: { - isTemporaryColumn(): boolean { - return this.column == null; - }, - - keymap(): any { - return { - 'shift+up': () => this.$parent.$emit('parentFocus', 'up'), - 'shift+down': () => this.$parent.$emit('parentFocus', 'down'), - 'shift+left': () => this.$parent.$emit('parentFocus', 'left'), - 'shift+right': () => this.$parent.$emit('parentFocus', 'right'), - }; - } - }, - - inject: { - getColumnVm: { from: 'getColumnVm' } - }, - - watch: { - active(v) { - if (v && this.isScrollTop()) { - this.$emit('top'); - } - }, - dragging(v) { - this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); - } - }, - - provide() { - return { - column: this, - isScrollTop: this.isScrollTop, - count: v => this.count = v, - inNakedDeckColumn: !this.naked - }; - }, - - mounted() { - this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true }); - - if (!this.isTemporaryColumn) { - this.$root.$on('deck.column.dragStart', this.onOtherDragStart); - this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); - } - }, - - beforeDestroy() { - this.$refs.body.removeEventListener('scroll', this.onScroll); - - if (!this.isTemporaryColumn) { - this.$root.$off('deck.column.dragStart', this.onOtherDragStart); - this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); - } - }, - - methods: { - onOtherDragStart() { - this.dropready = true; - }, - - onOtherDragEnd() { - this.dropready = false; - }, - - toggleActive() { - if (!this.isStacked) return; - const deck = this.$store.state.device.deckProfile ? this.$store.state.settings.deckProfiles[this.$store.state.device.deckProfile] : this.$store.state.device.deck; - const vms = deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id)); - if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return; - this.active = !this.active; - }, - - isScrollTop() { - return this.active && this.$refs.body.scrollTop == 0; - }, - - onScroll() { - if (this.isScrollTop()) { - this.$emit('top'); - } - - if (this.$store.state.settings.fetchOnScroll) { - const current = this.$refs.body.scrollTop + this.$refs.body.clientHeight; - if (current > this.$refs.body.scrollHeight - 1) this.$emit('bottom'); - } - }, - - getMenu() { - const items = [{ - icon: 'pencil-alt', - text: this.$t('rename'), - action: () => { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.name, - allowEmpty: false - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$store.commit('renameDeckColumn', { id: this.column.id, name }); - }); - } - }, null, { - icon: 'arrow-left', - text: this.$t('swap-left'), - action: () => { - this.$store.commit('swapLeftDeckColumn', this.column.id); - } - }, { - icon: 'arrow-right', - text: this.$t('swap-right'), - action: () => { - this.$store.commit('swapRightDeckColumn', this.column.id); - } - }, this.isStacked ? { - icon: faArrowUp, - text: this.$t('swap-up'), - action: () => { - this.$store.commit('swapUpDeckColumn', this.column.id); - } - } : undefined, this.isStacked ? { - icon: faArrowDown, - text: this.$t('swap-down'), - action: () => { - this.$store.commit('swapDownDeckColumn', this.column.id); - } - } : undefined, null, { - icon: ['far', 'window-restore'], - text: this.$t('stack-left'), - action: () => { - this.$store.commit('stackLeftDeckColumn', this.column.id); - } - }, this.isStacked ? { - icon: faWindowMaximize, - text: this.$t('pop-right'), - action: () => { - this.$store.commit('popRightDeckColumn', this.column.id); - } - } : undefined, null, { - icon: ['far', 'trash-alt'], - text: this.$t('remove'), - action: () => { - this.$store.commit('removeDeckColumn', this.column.id); - } - }]; - - if (this.menu) { - items.unshift(null); - for (const i of this.menu.reverse()) { - items.unshift(i); - } - } - - return items; - }, - - onContextmenu(e) { - if (this.isTemporaryColumn) return; - this.$contextmenu(e, this.getMenu()); - }, - - showMenu() { - this.$root.new(Menu, { - source: this.$refs.menu, - items: this.getMenu() - }); - }, - - close() { - this.$router.push('/'); - }, - - goTop() { - this.$refs.body.scrollTo({ - top: 0, - behavior: 'smooth' - }); - }, - - onDragstart(e) { - // テンポラリカラムはドラッグさせない - if (this.isTemporaryColumn) { - e.preventDefault(); - return; - } - - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk-deck-column', this.column.id); - this.dragging = true; - }, - - onDragend(e) { - this.dragging = false; - }, - - onDragover(e) { - // テンポラリカラムにはドロップさせない - if (this.isTemporaryColumn) { - e.dataTransfer.dropEffect = 'none'; - return; - } - - // 自分自身がドラッグされている場合 - if (this.dragging) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; - - e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - - if (!this.dragging && isDeckColumn) this.draghover = true; - }, - - onDragleave() { - this.draghover = false; - }, - - onDrop(e) { - this.draghover = false; - this.$root.$emit('deck.column.dragEnd'); - - const id = e.dataTransfer.getData('mk-deck-column'); - if (id != null && id != '') { - this.$store.commit('swapDeckColumn', { - a: this.column.id, - b: id - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs - $header-height = 42px - - height 100% - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &.draghover - box-shadow 0 0 0 2px var(--primaryAlpha08) - - &:after - content "" - display block - position absolute - z-index 1000 - top 0 - left 0 - width 100% - height 100% - background var(--primaryAlpha02) - - &.dragging - box-shadow 0 0 0 2px var(--primaryAlpha04) - - &.dropready - * - pointer-events none - - &:not(.active) - flex-basis $header-height - min-height $header-height - - &:not(.isStacked).narrow - width 285px - min-width 285px - flex-grow 0 !important - - &.naked - background var(--deckAcrylicColumnBg) - - > header - background transparent - box-shadow none - - > button - color var(--text) - - &.isMobile - > header - box-shadow none - - > header - display flex - z-index 2 - line-height $header-height - padding 0 16px - font-size 14px - color var(--faceHeaderText) - background var(--faceHeader) - box-shadow 0 var(--lineWidth) rgba(#000, 0.15) - cursor pointer - - &, * - user-select none - - *:not(button) - pointer-events none - - &.indicate - box-shadow 0 3px 0 0 var(--primary) - - > .header - display inline-block - align-items center - overflow hidden - text-overflow ellipsis - white-space nowrap - - [data-icon] - margin-right 8px - - > .count - margin-left 4px - opacity 0.5 - - > span:only-of-type - width 100% - - > .toggleActive - > .menu - > .close - padding 0 - width $header-height - line-height $header-height - font-size 16px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - > .toggleActive - margin-left -16px - - > .menu - > .close - margin-left auto - margin-right -16px - - > div - height "calc(100% - %s)" % $header-height - overflow auto - overflow-x hidden - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/common/views/deck/deck.direct-column.vue b/src/client/app/common/views/deck/deck.direct-column.vue deleted file mode 100644 index 66d34520af..0000000000 --- a/src/client/app/common/views/deck/deck.direct-column.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked"> - <template #header><fa :icon="['far', 'envelope']"/>{{ name }}</template> - - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - visibility: 'specified' - } - } - }; - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.direct'); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - // Prepend a note - if (note.visibility == 'specified') { - (this.$refs.timeline as any).prepend(note); - } - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.hashtag-column.vue b/src/client/app/common/views/deck/deck.hashtag-column.vue deleted file mode 100644 index 0d719c2199..0000000000 --- a/src/client/app/common/views/deck/deck.hashtag-column.vue +++ /dev/null @@ -1,122 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="hashtag"/><span>{{ tag }}</span> - </template> - - <div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb"> - <div ref="chart" class="chart"></div> - <x-hashtag-tl :tag-tl="tagTl" class="tl" :key="JSON.stringify(tagTl)"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; -import XHashtagTl from './deck.hashtag-tl.vue'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - components: { - XColumn, - XHashtagTl - }, - - computed: { - tag(): string { - return this.$route.params.tag; - }, - - tagTl(): any { - return { - query: [[this.tag]] - }; - } - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('charts/hashtag', { - tag: this.tag, - span: 'hour', - limit: 24 - }).then(stats => { - const local = []; - const remote = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - const h = now.getHours(); - - for (let i = 0; i < 24; i++) { - const x = new Date(y, m, d, h - i); - local.push([x, stats.local.count[i]]); - remote.push([x, stats.remote.count[i]]); - } - - const chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 70, - sparkline: { - enabled: true - }, - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - padding: { - top: 16, - right: 16, - bottom: 16, - left: 16 - } - }, - stroke: { - curve: 'straight', - width: 2 - }, - series: [{ - name: 'Local', - data: local - }, { - name: 'Remote', - data: remote - }], - xaxis: { - type: 'datetime', - } - }); - - chart.render(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.xroyrflcmhhtmlwmyiwpfqiirqokfueb - background var(--deckColumnBg) - - > .chart - margin-bottom 16px - background var(--face) - - > .tl - background var(--face) - -</style> diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue deleted file mode 100644 index 94d2efc430..0000000000 --- a/src/client/app/common/views/deck/deck.hashtag-tl.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - components: { - XNotes - }, - - props: { - tagTl: { - type: Object, - required: true - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/search-by-tag', - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl.query - }) - } - }; - }, - - watch: { - mediaOnly() { - this.$refs.timeline.reload(); - } - }, - - mounted() { - if (this.connection) this.connection.close(); - this.connection = this.$root.stream.connectToChannel('hashtag', { - q: this.tagTl.query - }); - this.connection.on('note', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.list-tl.vue b/src/client/app/common/views/deck/deck.list-tl.vue deleted file mode 100644 index 26d6ea9d58..0000000000 --- a/src/client/app/common/views/deck/deck.list-tl.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - components: { - XNotes - }, - - props: { - list: { - type: Object, - required: true - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - watch: { - mediaOnly() { - this.$refs.timeline.reload(); - } - }, - - mounted() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - onUserAdded() { - this.$refs.timeline.reload(); - }, - - onUserRemoved() { - this.$refs.timeline.reload(); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.mentions-column.vue b/src/client/app/common/views/deck/deck.mentions-column.vue deleted file mode 100644 index 12d7b2a16b..0000000000 --- a/src/client/app/common/views/deck/deck.mentions-column.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked"> - <template #header><fa icon="at"/>{{ name }}</template> - - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.mentions'); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - (this.$refs.timeline as any).prepend(note); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.note-column.vue b/src/client/app/common/views/deck/deck.note-column.vue deleted file mode 100644 index bcc887e2fd..0000000000 --- a/src/client/app/common/views/deck/deck.note-column.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="['far', 'comment-alt']"/><mk-user-name :user="note.user" v-if="note"/> - </template> - - <div class="rvtscbadixhhbsczoorqoaygovdeecsx" v-if="note"> - <div class="is-remote" v-if="note.user.host != null"> - <details> - <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-post') }}</summary> - <a :href="note.url || note.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </details> - </div> - <mk-note :note="note" :detail="true" :key="note.id"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - }, - - data() { - return { - note: null, - fetching: true - }; - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rvtscbadixhhbsczoorqoaygovdeecsx - > .is-remote - padding 8px 16px - font-size 12px - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - -</style> diff --git a/src/client/app/common/views/deck/deck.notes.vue b/src/client/app/common/views/deck/deck.notes.vue deleted file mode 100644 index 5081d1f998..0000000000 --- a/src/client/app/common/views/deck/deck.notes.vue +++ /dev/null @@ -1,168 +0,0 @@ -<template> -<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div"> - <template v-for="(note, i) in _notes"> - <mk-note - :note="note" - :key="note.id" - :compact="true" - /> - <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - inject: ['column', 'isScrollTop', 'count'], - - mixins: [ - paging({ - limit: 20, - - onQueueChanged: (self, q) => { - self.count(q.length); - }, - - onPrepend: (self, note, silent) => { - // 弾く - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - } - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - }, - - _notes(): any[] { - return (this.notes as any).map(note => { - const date = new Date(note.createdAt).getDate(); - const month = new Date(note.createdAt).getMonth() + 1; - note._date = date; - note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return note; - }); - } - }, - - created() { - this.column.$on('top', this.onTop); - this.column.$on('bottom', this.onBottom); - this.init(); - }, - - beforeDestroy() { - this.column.$off('top', this.onTop); - this.column.$off('bottom', this.onBottom); - }, - - methods: { - focus() { - (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.eamppglmnmimdhrlzhplwpvyeaqmmhxu - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .empty - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 16px - opacity 0.3 - - > .notes - > .date - display block - margin 0 - line-height 28px - font-size 12px - text-align center - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > footer - > button - display block - margin 0 - padding 16px - width 100% - text-align center - color #ccc - background var(--face) - border-top solid var(--lineWidth) var(--faceDivider) - border-bottom-left-radius 6px - border-bottom-right-radius 6px - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - -</style> diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue deleted file mode 100644 index da122ff4db..0000000000 --- a/src/client/app/common/views/deck/deck.notification.vue +++ /dev/null @@ -1,186 +0,0 @@ -<template> -<div class="dsfykdcjpuwfvpefwufddclpjhzktmpw"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <mk-reaction-icon :reaction="notification.reaction" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="retweet" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-plus" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-clock" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification pollVote" v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="chart-pie" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.dsfykdcjpuwfvpefwufddclpjhzktmpw - > .notification - padding 16px - font-size 12px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > div - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - > .icon - margin-right 4px - - > .name - overflow hidden - text-overflow ellipsis - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - > .note-preview - color var(--noteText) - - > .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - > div > header - align-items normal - - &.renote - > div > header [data-icon] - color #77B255 - - &.follow - > div > header [data-icon] - color #53c7ce - - &.receiveFollowRequest - > div > header [data-icon] - color #888 - -</style> diff --git a/src/client/app/common/views/deck/deck.notifications-column.vue b/src/client/app/common/views/deck/deck.notifications-column.vue deleted file mode 100644 index b4361b054a..0000000000 --- a/src/client/app/common/views/deck/deck.notifications-column.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> - <template #header><fa :icon="['far', 'bell']"/>{{ name }}</template> - - <x-notifications :type="column.notificationType === 'all' ? null : column.notificationType"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotifications from './deck.notifications.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - XNotifications - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - menu: null, - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.notifications'); - } - }, - - created() { - if (this.column.notificationType == null) { - this.column.notificationType = 'all'; - this.$store.commit('updateDeckColumn', this.column); - } - - this.menu = [{ - icon: 'cog', - text: this.$t('@.notification-type'), - action: () => { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.column.notificationType, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.column.notificationType = type; - this.$store.commit('updateDeckColumn', this.column); - }); - } - }]; - }, -}); -</script> diff --git a/src/client/app/common/views/deck/deck.notifications.vue b/src/client/app/common/views/deck/deck.notifications.vue deleted file mode 100644 index aed2af64e9..0000000000 --- a/src/client/app/common/views/deck/deck.notifications.vue +++ /dev/null @@ -1,177 +0,0 @@ -<template> -<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> - <template v-for="(notification, i) in _notifications"> - <x-notification class="notification" :notification="notification" :key="notification.id"/> - <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? this.$t('@.loading') : this.$t('@.load-more') }} - </button> - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XNotification from './deck.notification.vue'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XNotification - }, - - inject: ['column', 'isScrollTop', 'count'], - - mixins: [ - paging({ - onQueueChanged: (self, q) => { - self.count(q.length); - }, - }), - ], - - props: { - type: { - type: String, - required: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'i/notifications', - limit: 20, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - - this.column.$on('top', this.onTop); - this.column.$on('bottom', this.onBottom); - }, - - beforeDestroy() { - this.connection.dispose(); - - this.column.$off('top', this.onTop); - this.column.$off('bottom', this.onBottom); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.oxynyeqmfvracxnglgulyqfgqxnxmehl - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .placeholder - padding 16px - opacity 0.3 - - > .notifications - - > .notification:not(:last-child) - border-bottom solid var(--lineWidth) var(--faceDivider) - - > .date - display block - margin 0 - line-height 28px - text-align center - font-size 12px - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - -</style> diff --git a/src/client/app/common/views/deck/deck.page-column.vue b/src/client/app/common/views/deck/deck.page-column.vue deleted file mode 100644 index 0ef391a51d..0000000000 --- a/src/client/app/common/views/deck/deck.page-column.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="faStickyNote"/>{{ page ? page.name : '' }} - </template> - - <div v-if="page"> - <x-page :page="page" :key="page.id"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XPage from '../../../common/views/components/page/page.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XPage - }, - - props: { - pageName: { - type: String, - required: true - }, - username: { - type: String, - required: true - }, - }, - - data() { - return { - page: null, - faStickyNote - }; - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('pages/show', { - name: this.pageName, - username: this.username, - }).then(page => { - this.page = page; - this.$emit('init', { - title: this.page.title, - icon: faStickyNote - }); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.search-column.vue b/src/client/app/common/views/deck/deck.search-column.vue deleted file mode 100644 index a2d1142fbe..0000000000 --- a/src/client/app/common/views/deck/deck.search-column.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="search"/><span>{{ q }}</span> - </template> - - <div> - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - components: { - XColumn, - XNotes - }, - - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - - computed: { - q(): string { - return this.$route.query.q; - } - }, - - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, -}); -</script> diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue deleted file mode 100644 index cad140ed5f..0000000000 --- a/src/client/app/common/views/deck/deck.tl-column.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked"> - <template #header> - <fa v-if="column.type == 'home'" icon="home"/> - <fa v-if="column.type == 'local'" :icon="['far', 'comments']"/> - <fa v-if="column.type == 'hybrid'" icon="share-alt"/> - <fa v-if="column.type == 'global'" icon="globe"/> - <fa v-if="column.type == 'list'" icon="list"/> - <fa v-if="column.type == 'hashtag'" icon="hashtag"/> - <span>{{ name }}</span> - </template> - - <div class="editor" style="padding:12px" v-if="edit"> - <ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">{{ $t('is-media-only') }}</ui-switch> - </div> - - <x-list-tl v-if="column.type == 'list'" - :list="column.list" - :media-only="column.isMediaOnly" - ref="tl" - /> - <x-hashtag-tl v-else-if="column.type == 'hashtag'" - :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" - :media-only="column.isMediaOnly" - ref="tl" - /> - <x-tl v-else - :src="column.type" - :media-only="column.isMediaOnly" - ref="tl" - /> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XTl from './deck.tl.vue'; -import XListTl from './deck.list-tl.vue'; -import XHashtagTl from './deck.hashtag-tl.vue'; - -export default Vue.extend({ - i18n: i18n('deck/deck.tl-column.vue'), - components: { - XColumn, - XTl, - XListTl, - XHashtagTl - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - edit: false, - menu: [{ - icon: 'cog', - text: this.$t('edit'), - action: () => { - this.edit = !this.edit; - } - }] - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - - switch (this.column.type) { - case 'home': return this.$t('@deck.home'); - case 'local': return this.$t('@deck.local'); - case 'hybrid': return this.$t('@deck.hybrid'); - case 'global': return this.$t('@deck.global'); - case 'list': return this.column.list.name; - case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; - } - } - }, - - methods: { - onChangeSettings(v) { - this.$store.commit('updateDeckColumn', this.column); - }, - - focus() { - this.$refs.tl.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue deleted file mode 100644 index e6c716070a..0000000000 --- a/src/client/app/common/views/deck/deck.tl.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div class="iwaalbte" v-if="disabled"> - <p> - <fa :icon="faMinusCircle"/> - {{ $t('disabled-timeline.title') }} - </p> - <p class="desc">{{ $t('disabled-timeline.description') }}</p> -</div> -<x-notes v-else ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; -import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('deck'), - - components: { - XNotes - }, - - props: { - src: { - type: String, - required: false, - default: 'home' - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - disabled: false, - faMinusCircle, - pagination: null - }; - }, - - computed: { - stream(): any { - switch (this.src) { - case 'home': return this.$root.stream.useSharedConnection('homeTimeline'); - case 'local': return this.$root.stream.useSharedConnection('localTimeline'); - case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline'); - case 'global': return this.$root.stream.useSharedConnection('globalTimeline'); - } - }, - - endpoint(): string { - switch (this.src) { - case 'home': return 'notes/timeline'; - case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; - case 'global': return 'notes/global-timeline'; - } - }, - }, - - watch: { - mediaOnly() { - (this.$refs.timeline as any).reload(); - } - }, - - created() { - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - }; - }, - - mounted() { - this.connection = this.stream; - - this.connection.on('note', this.onNote); - if (this.src == 'home') { - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - } - - this.$root.getMeta().then(meta => { - this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( - meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || - meta.disableGlobalTimeline && ['global'].includes(this.src)); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - onChangeFollowing() { - (this.$refs.timeline as any).reload(); - }, - - focus() { - (this.$refs.timeline as any).focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.iwaalbte - color var(--text) - text-align center - - > p - margin 16px - - &.desc - font-size 14px - -</style> diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue deleted file mode 100644 index 9fb50a6672..0000000000 --- a/src/client/app/common/views/deck/deck.user-column.home.vue +++ /dev/null @@ -1,229 +0,0 @@ -<template> -<div> - <ui-container v-if="user.pinnedPage" :body-togglable="true"> - <template #header><fa icon="thumbtack"/> {{ $t('pinned-page') }}</template> - <div> - <x-page :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - </div> - </ui-container> - <ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true"> - <template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template> - <div> - <mk-note v-for="n in user.pinnedNotes" :key="n.id" :note="n"/> - </div> - </ui-container> - <ui-container v-if="images.length > 0" :body-togglable="true" - :expanded="$store.state.device.expandUsersPhotos" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })"> - <template #header><fa :icon="['far', 'images']"/> {{ $t('images') }}</template> - <div class="sainvnaq"> - <router-link v-for="image in images" - :style="`background-image: url(${image.thumbnailUrl})`" - :key="`${image.id}:${image._note.id}`" - :to="image._note | notePage" - :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`" - ></router-link> - </div> - </ui-container> - <ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersActivity" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })"> - <template #header><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</template> - <div> - <div ref="chart"></div> - </div> - </ui-container> - <ui-container> - <template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template> - <div> - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')" :key="user.id"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XNotes from './deck.notes.vue'; -import { concat } from '../../../../../prelude/array'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - i18n: i18n('deck/deck.user-column.vue'), - - components: { - XNotes, - XPage: () => import('../../../common/views/components/page/page.vue').then(m => m.default), - }, - - props: { - user: { - type: Object, - required: true - } - }, - - data() { - return { - withFiles: false, - images: [], - chart: null as ApexCharts - }; - }, - - computed: { - pagination() { - return { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.withFiles, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - } - }, - - watch: { - user() { - this.fetch(); - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - file._note = note; - } - } - const files = concat(notes.map((n: any): any[] => n.files)); - this.images = files.filter(f => image.includes(f.type)).slice(0, 9); - }); - - this.$root.api('charts/user/notes', { - userId: this.user.id, - span: 'day', - limit: 21 - }).then(stats => { - const normal = []; - const reply = []; - const renote = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - for (let i = 0; i < 21; i++) { - const x = new Date(y, m, d - i); - normal.push([ - x, - stats.diffs.normal[i] - ]); - reply.push([ - x, - stats.diffs.reply[i] - ]); - renote.push([ - x, - stats.diffs.renote[i] - ]); - } - - if (this.chart) this.chart.destroy(); - - this.chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'bar', - stacked: true, - height: 100, - sparkline: { - enabled: true - }, - }, - plotOptions: { - bar: { - columnWidth: '80%' - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - padding: { - top: 16, - right: 16, - bottom: 16, - left: 16 - } - }, - tooltip: { - shared: true, - intersect: false - }, - series: [{ - name: 'Normal', - data: normal - }, { - name: 'Reply', - data: reply - }, { - name: 'Renote', - data: renote - }], - xaxis: { - type: 'datetime', - crosshairs: { - width: 1, - opacity: 1 - } - } - }); - - this.chart.render(); - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.sainvnaq - display grid - grid-template-columns 1fr 1fr 1fr - gap 8px - padding 16px - - > * - height 70px - background-position center center - background-size cover - background-clip content-box - border-radius 4px - -</style> diff --git a/src/client/app/common/views/deck/deck.user-column.vue b/src/client/app/common/views/deck/deck.user-column.vue deleted file mode 100644 index bc8cbc3154..0000000000 --- a/src/client/app/common/views/deck/deck.user-column.vue +++ /dev/null @@ -1,266 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="user"/><mk-user-name :user="user" v-if="user" :key="user.id"/> - </template> - - <div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user"> - <div class="is-remote" v-if="user.host != null"> - <details> - <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> - <a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </details> - </div> - <header :style="bannerStyle"> - <div> - <button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/> - <mk-avatar class="avatar" :user="user" :disable-preview="true" :key="user.id"/> - <router-link class="name" :to="user | userPage()"> - <mk-user-name :user="user" :key="user.id" :nowrap="false"/> - </router-link> - <span class="acct">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span> - <span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> - </div> - </header> - <div class="info"> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="counts"> - <div> - <router-link :to="user | userPage()"> - <b>{{ user.notesCount | number }}</b> - <span>{{ $t('posts') }}</span> - </router-link> - </div> - <div> - <router-link :to="user | userPage('following')"> - <b>{{ user.followingCount | number }}</b> - <span>{{ $t('following') }}</span> - </router-link> - </div> - <div> - <router-link :to="user | userPage('followers')"> - <b>{{ user.followersCount | number }}</b> - <span>{{ $t('followers') }}</span> - </router-link> - </div> - </div> - </div> - <router-view :user="user"></router-view> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; -import XColumn from './deck.column.vue'; -import XUserMenu from '../../../common/views/components/user-menu.vue'; - -export default Vue.extend({ - i18n: i18n('deck/deck.user-column.vue'), - components: { - XColumn, - }, - - data() { - return { - user: null, - fetching: true, - }; - }, - - computed: { - bannerStyle(): any { - if (this.user == null) return {}; - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - }, - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - }); - }, - - menu() { - const w = this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zubukjlciycdsyynicqrnlsmdwmymzqu - background var(--deckColumnBg) - - > .is-remote - padding 8px 16px - font-size 12px - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - - > header - overflow hidden - background-size cover - background-position center - - > div - padding 32px - background rgba(#000, 0.5) - color #fff - text-align center - - > .menu - position absolute - top 8px - left 8px - padding 8px - font-size 16px - text-shadow 0 0 8px #000 - - > .follow - position absolute - top 16px - right 16px - - > .avatar - display block - width 64px - height 64px - margin 0 auto - - > .name - display block - margin-top 8px - font-weight bold - text-shadow 0 0 8px #000 - color #fff - - > .acct - display block - font-size 14px - opacity 0.7 - text-shadow 0 0 8px #000 - - > .locked - opacity 0.8 - - > .followed - display inline-block - font-size 12px - background rgba(0, 0, 0, 0.5) - opacity 0.7 - margin-top: 2px - padding 4px - border-radius 4px - - > .info - padding 16px - font-size 12px - color var(--text) - text-align center - background var(--face) - - &:before - content "" - display blcok - position absolute - top -32px - left 0 - right 0 - width 0 - margin 0 auto - border-top solid 16px transparent - border-left solid 16px transparent - border-right solid 16px transparent - border-bottom solid 16px var(--face) - - > .fields - margin-top 8px - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .counts - display grid - grid-template-columns 2fr 2fr 2fr - margin-top 8px - border-top solid var(--lineWidth) var(--faceDivider) - - > div - padding 8px 8px 0 8px - text-align center - - > a - color var(--text) - - > b - display block - font-size 110% - - > span - display block - font-size 80% - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue deleted file mode 100644 index a3a26302e9..0000000000 --- a/src/client/app/common/views/deck/deck.vue +++ /dev/null @@ -1,394 +0,0 @@ -<template> -<mk-ui :class="$style.root"> - <div class="qlvquzbjribqcaozciifydkngcwtyzje" ref="body" :style="style" :class="`${$store.state.device.deckColumnAlign} ${$store.state.device.deckColumnWidth}`" v-hotkey.global="keymap"> - <template v-for="ids in layout"> - <div v-if="ids.length > 1" class="folder"> - <template v-for="id, i in ids"> - <x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true" @parentFocus="moveFocus(id, $event)"/> - </template> - </div> - <x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/> - </template> - <router-view></router-view> - <button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumnCore from './deck.column-core.vue'; -import Menu from '../../../common/views/components/menu.vue'; - -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('deck'), - - components: { - XColumnCore - }, - - computed: { - deck() { - return this.$store.getters.deck; - }, - - columns(): any[] { - if (this.deck == null) return []; - return this.deck.columns; - }, - - layout(): any[] { - if (this.deck == null) return []; - if (this.deck.layout == null) return this.deck.columns.map(c => [c.id]); - return this.deck.layout; - }, - - style(): any { - return { - height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)` - }; - }, - - keymap(): any { - return { - 't': this.focus - }; - } - }, - - watch: { - $route() { - if (this.$route.name == 'index') return; - this.$nextTick(() => { - this.$refs.body.scrollTo({ - left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth, - behavior: 'smooth' - }); - }); - } - }, - - provide() { - return { - inDeck: true, - getColumnVm: this.getColumnVm, - narrow: true - }; - }, - - created() { - if (this.deck == null) { - const deck = { - columns: [/*{ - type: 'widgets', - widgets: [] - }, */{ - id: uuid(), - type: 'home', - name: null, - }, { - id: uuid(), - type: 'notifications', - name: null, - }, { - id: uuid(), - type: 'local', - name: null, - }, { - id: uuid(), - type: 'global', - name: null, - }] - }; - - deck.layout = deck.columns.map(c => [c.id]); - - this.$store.commit('setDeck', deck); - } - }, - - mounted() { - document.title = this.$root.instanceName; - document.documentElement.style.overflow = 'hidden'; - }, - - beforeDestroy() { - document.documentElement.style.overflow = 'auto'; - }, - - methods: { - getColumnVm(id) { - return this.$refs[id][0]; - }, - - add() { - this.$root.new(Menu, { - source: this.$refs.add, - items: [{ - icon: 'home', - text: this.$t('@deck.home'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'home' - }); - } - }, { - icon: ['far', 'comments'], - text: this.$t('@deck.local'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'local' - }); - } - }, { - icon: 'share-alt', - text: this.$t('@deck.hybrid'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'hybrid' - }); - } - }, { - icon: 'globe', - text: this.$t('@deck.global'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'global' - }); - } - }, { - icon: 'at', - text: this.$t('@deck.mentions'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'mentions' - }); - } - }, { - icon: ['far', 'envelope'], - text: this.$t('@deck.direct'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'direct' - }); - } - }, { - icon: 'list', - text: this.$t('@deck.list'), - action: async () => { - const lists = await this.$root.api('users/lists/list'); - const { canceled, result: listId } = await this.$root.dialog({ - type: null, - title: this.$t('@deck.select-list'), - select: { - items: lists.map(list => ({ - value: list.id, text: list.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'list', - list: lists.find(l => l.id === listId) - }); - } - }, { - icon: 'hashtag', - text: this.$t('@deck.hashtag'), - action: () => { - this.$root.dialog({ - title: this.$t('enter-hashtag-tl-title'), - input: true - }).then(({ canceled, result: title }) => { - if (canceled) return; - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'hashtag', - tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id - }); - }); - } - }, { - icon: ['far', 'bell'], - text: this.$t('@deck.notifications'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'notifications' - }); - } - }, { - icon: 'calculator', - text: this.$t('@deck.widgets'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'widgets', - widgets: [] - }); - } - }] - }); - }, - - focus() { - // Flatten array of arrays - const ids = [].concat.apply([], this.layout); - const firstTl = ids.find(id => this.isTlColumn(id)); - - if (firstTl) { - this.$refs[firstTl][0].focus(); - } - }, - - moveFocus(id, direction) { - let targetColumn; - - if (direction == 'right') { - const currentColumnIndex = this.layout.findIndex(ids => ids.includes(id)); - this.layout.some((ids, i) => { - if (i <= currentColumnIndex) return false; - const tl = ids.find(id => this.isTlColumn(id)); - if (tl) { - targetColumn = tl; - return true; - } - }); - } else if (direction == 'left') { - const currentColumnIndex = [...this.layout].reverse().findIndex(ids => ids.includes(id)); - [...this.layout].reverse().some((ids, i) => { - if (i <= currentColumnIndex) return false; - const tl = ids.find(id => this.isTlColumn(id)); - if (tl) { - targetColumn = tl; - return true; - } - }); - } else if (direction == 'down') { - const currentColumn = this.layout.find(ids => ids.includes(id)); - const currentIndex = currentColumn.indexOf(id); - currentColumn.some((_id, i) => { - if (i <= currentIndex) return false; - if (this.isTlColumn(_id)) { - targetColumn = _id; - return true; - } - }); - } else if (direction == 'up') { - const currentColumn = [...this.layout.find(ids => ids.includes(id))].reverse(); - const currentIndex = currentColumn.indexOf(id); - currentColumn.some((_id, i) => { - if (i <= currentIndex) return false; - if (this.isTlColumn(_id)) { - targetColumn = _id; - return true; - } - }); - } - - if (targetColumn) { - this.$refs[targetColumn][0].focus(); - } - }, - - isTlColumn(id) { - const column = this.columns.find(c => c.id === id); - return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); - } - } -}); -</script> - -<style lang="stylus" module> -.root - height 100vh -</style> - -<style lang="stylus" scoped> -.qlvquzbjribqcaozciifydkngcwtyzje - display flex - flex 1 - padding 16px 0 16px 16px - overflow auto - overflow-y hidden - -webkit-overflow-scrolling touch - - @media (max-width 500px) - padding 8px 0 8px 8px - - > div - margin-right 8px - width 330px - min-width 330px - - &:last-of-type - margin-right 0 - - &.folder - display flex - flex-direction column - - > *:not(:last-child) - margin-bottom 8px - - &.narrow - > div - width 303px - min-width 303px - - &.narrower - > div - width 316.5px - min-width 316.5px - - &.wider - > div - width 343.5px - min-width 343.5px - - &.wide - > div - width 357px - min-width 357px - - &.center - > * - &:first-child - margin-left auto - - &:last-child - margin-right auto - - &.:not(.flexible) - > * - flex-grow 0 - flex-shrink 0 - - &.flexible - > * - flex-grow 1 - flex-shrink 0 - - > button - padding 0 16px - color var(--faceTextButton) - flex-grow 0 !important - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - -</style> diff --git a/src/client/app/common/views/deck/deck.widgets-column.vue b/src/client/app/common/views/deck/deck.widgets-column.vue deleted file mode 100644 index d9a7747909..0000000000 --- a/src/client/app/common/views/deck/deck.widgets-column.vue +++ /dev/null @@ -1,173 +0,0 @@ -<template> -<x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz"> - <template #header><fa icon="calculator"/>{{ name }}</template> - - <div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq"> - <template v-if="edit"> - <header> - <select v-model="widgetAdderSelected" @change="addWidget"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="trends">{{ $t('@.widgets.trends') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> - <option value="notifications">{{ $t('@.widgets.notifications') }}</option> - <option value="users">{{ $t('@.widgets.users') }}</option> - <option value="polls">{{ $t('@.widgets.polls') }}</option> - <option value="post-form">{{ $t('@.widgets.post-form') }}</option> - <option value="messaging">{{ $t('@.messaging') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - </header> - <x-draggable - :list="column.widgets" - animation="150" - @sort="onWidgetSort" - > - <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)"> - <button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck" :column="column"/> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck" :column="column"/> - </template> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - XDraggable - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - edit: false, - menu: null, - widgetAdderSelected: null - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.widgets'); - } - }, - - created() { - this.menu = [{ - icon: 'cog', - text: this.$t('edit'), - action: () => { - this.edit = !this.edit; - } - }]; - }, - - methods: { - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveWidgets(); - }, - - addWidget() { - this.$store.commit('addDeckWidget', { - id: this.column.id, - widget: { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - } - }); - - this.widgetAdderSelected = null; - }, - - removeWidget(widget) { - this.$store.commit('removeDeckWidget', { - id: this.column.id, - widget - }); - }, - - saveWidgets() { - this.$store.commit('updateDeckColumn', this.column); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wtdtxvecapixsepjtcupubtsmometobz - .gqpwvtwtprsbmnssnbicggtwqhmylhnq - > header - padding 16px - - > * - width 100% - padding 4px - - .widget, .customize-container - margin 8px - - &:first-of-type - margin-top 0 - - .customize-container - cursor move - - > *:not(.remove) - pointer-events none - - > .remove - position absolute - z-index 1 - top 8px - right 8px - width 32px - height 32px - color #fff - background rgba(#000, 0.7) - border-radius 4px - -</style> - diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts deleted file mode 100644 index 3f68baa01b..0000000000 --- a/src/client/app/common/views/directives/autocomplete.ts +++ /dev/null @@ -1,248 +0,0 @@ -import * as getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode'; - -export default { - bind(el, binding, vn) { - const self = el._autoCompleteDirective_ = {} as any; - self.x = new Autocomplete(el, vn.context, binding.value); - self.x.attach(); - }, - - unbind(el, binding, vn) { - const self = el._autoCompleteDirective_; - self.x.detach(); - } -}; - -/** - * オートコンプリートを管理するクラス。 - */ -class Autocomplete { - private suggestion: any; - private textarea: any; - private vm: any; - private currentType: string; - private opts: { - model: string; - }; - private opening: boolean; - - private get text(): string { - return this.vm[this.opts.model]; - } - - private set text(text: string) { - this.vm[this.opts.model] = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea, vm, opts) { - //#region BIND - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.suggestion = null; - this.textarea = textarea; - this.vm = vm; - this.opts = opts; - this.opening = false; - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - public attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - public detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * テキスト入力時 - */ - private onInput() { - const caretPos = this.textarea.selectionStart; - const text = this.text.substr(0, caretPos).split('\n').pop(); - - const mentionIndex = text.lastIndexOf('@'); - const hashtagIndex = text.lastIndexOf('#'); - const emojiIndex = text.lastIndexOf(':'); - - const max = Math.max( - mentionIndex, - hashtagIndex, - emojiIndex); - - if (max == -1) { - this.close(); - return; - } - - const isMention = mentionIndex != -1; - const isHashtag = hashtagIndex != -1; - const isEmoji = emojiIndex != -1; - - let opened = false; - - if (isMention) { - const username = text.substr(mentionIndex + 1); - if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { - this.open('user', username); - opened = true; - } - } - - if (isHashtag && !opened) { - const hashtag = text.substr(hashtagIndex + 1); - if (!hashtag.includes(' ')) { - this.open('hashtag', hashtag); - opened = true; - } - } - - if (isEmoji && !opened) { - const emoji = text.substr(emojiIndex + 1); - if (!emoji.includes(' ')) { - this.open('emoji', emoji); - opened = true; - } - } - - if (!opened) { - this.close(); - } - } - - /** - * サジェストを提示します。 - */ - private async open(type, q) { - if (type != this.currentType) { - this.close(); - } - if (this.opening) return; - this.opening = true; - this.currentType = type; - - //#region サジェストを表示すべき位置を計算 - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + caretPosition.left - this.textarea.scrollLeft; - const y = rect.top + caretPosition.top - this.textarea.scrollTop; - //#endregion - - if (this.suggestion) { - this.suggestion.x = x; - this.suggestion.y = y; - this.suggestion.q = q; - - this.opening = false; - } else { - const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default); - - // サジェスト要素作成 - this.suggestion = new MkAutocomplete({ - parent: this.vm, - propsData: { - textarea: this.textarea, - complete: this.complete, - close: this.close, - type: type, - q: q, - x, - y - } - }).$mount(); - - // 要素追加 - document.body.appendChild(this.suggestion.$el); - - this.opening = false; - } - } - - /** - * サジェストを閉じます。 - */ - private close() { - if (this.suggestion == null) return; - - this.suggestion.destroyDom(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * オートコンプリートする - */ - private complete(type, value) { - this.close(); - - const caret = this.textarea.selectionStart; - - if (type == 'user') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); - - const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; - - // 挿入 - this.text = `${trimmedBefore}@${acct} ${after}`; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (acct.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'hashtag') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('#')); - const after = source.substr(caret); - - // 挿入 - this.text = `${trimmedBefore}#${value} ${after}`; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'emoji') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf(':')); - const after = source.substr(caret); - - // 挿入 - this.text = trimmedBefore + value + after; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + value.length; - this.textarea.setSelectionRange(pos, pos); - }); - } - } -} diff --git a/src/client/app/common/views/directives/index.ts b/src/client/app/common/views/directives/index.ts deleted file mode 100644 index 1bb4fd6d4d..0000000000 --- a/src/client/app/common/views/directives/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Vue from 'vue'; - -import autocomplete from './autocomplete'; -import particle from './particle'; - -Vue.directive('autocomplete', autocomplete); -Vue.directive('particle', particle); diff --git a/src/client/app/common/views/directives/particle.ts b/src/client/app/common/views/directives/particle.ts deleted file mode 100644 index 5f8413117f..0000000000 --- a/src/client/app/common/views/directives/particle.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Particle from '../components/particle.vue'; - -export default { - bind(el, binding, vn) { - if (vn.context.$store.state.device.reduceMotion) return; - - el.addEventListener('click', () => { - if (binding.value === false) return; - - const rect = el.getBoundingClientRect(); - - const x = rect.left + (el.clientWidth / 2); - const y = rect.top + (el.clientHeight / 2); - - const particle = new Particle({ - parent: vn.context, - propsData: { - x, - y - } - }).$mount(); - - document.body.appendChild(particle.$el); - }); - } -}; diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts deleted file mode 100644 index 227ccae3a4..0000000000 --- a/src/client/app/common/views/filters/bytes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue'; - -Vue.filter('bytes', (v, digits = 0) => { - if (v == null) return '?'; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; - if (v == 0) return '0'; - const isMinus = v < 0; - if (isMinus) v = -v; - const i = Math.floor(Math.log(v) / Math.log(1024)); - return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; -}); diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts deleted file mode 100644 index 3dccbfc923..0000000000 --- a/src/client/app/common/views/filters/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue'; -import * as JSON5 from 'json5'; - -Vue.filter('json5', x => { - return JSON5.stringify(x, null, 2); -}); - -require('./bytes'); -require('./number'); -require('./user'); -require('./note'); diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts deleted file mode 100644 index 3c9c8b7485..0000000000 --- a/src/client/app/common/views/filters/note.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Vue from 'vue'; - -Vue.filter('notePage', note => { - return `/notes/${note.id}`; -}); diff --git a/src/client/app/common/views/filters/number.ts b/src/client/app/common/views/filters/number.ts deleted file mode 100644 index 8c799d9442..0000000000 --- a/src/client/app/common/views/filters/number.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -Vue.filter('number', n => n == null ? 'N/A' : n.toLocaleString()); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts deleted file mode 100644 index 9d4ae5c58b..0000000000 --- a/src/client/app/common/views/filters/user.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Vue from 'vue'; -import getAcct from '../../../../../misc/acct/render'; -import getUserName from '../../../../../misc/get-user-name'; -import { url } from '../../../config'; - -Vue.filter('acct', user => { - return getAcct(user); -}); - -Vue.filter('userName', user => { - return getUserName(user); -}); - -Vue.filter('userPage', (user, path?, absolute = false) => { - return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; -}); diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue deleted file mode 100644 index b4a4e1d502..0000000000 --- a/src/client/app/common/views/pages/explore.vue +++ /dev/null @@ -1,198 +0,0 @@ -<template> -<div> - <div class="localfedi7" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> - <header>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</header> - <div>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</div> - </div> - - <template v-if="tag == null"> - <mk-user-list :pagination="pinnedUsers" :expanded="false"> - <fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }} - </mk-user-list> - <mk-user-list :pagination="popularUsers" :expanded="false"> - <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyUpdatedUsers" :expanded="false"> - <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyRegisteredUsers" :expanded="false"> - <fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }} - </mk-user-list> - </template> - - <div class="localfedi7" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)` }"> - <header>{{ $t('explore-fediverse') }}</header> - </div> - - <ui-container :body-togglable="true" :expanded="false" ref="tags"> - <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template> - - <div class="vxjfqztj"> - <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> - <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> - </div> - </ui-container> - - <mk-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`"> - <fa :icon="faHashtag" fixed-width/>{{ tag }} - </mk-user-list> - <template v-if="tag == null"> - <mk-user-list :pagination="popularUsersF" :expanded="false"> - <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyUpdatedUsersF" :expanded="false"> - <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyRegisteredUsersF" :expanded="false"> - <fa :icon="faRocket" fixed-width/>{{ $t('recently-discovered-users') }} - </mk-user-list> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons'; -import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/explore.vue'), - - props: { - tag: { - type: String, - required: false - } - }, - - inject: { - inNakedDeckColumn: { - default: false - } - }, - - data() { - return { - pinnedUsers: { endpoint: 'pinned-users' }, - popularUsers: { endpoint: 'users', limit: 10, params: { - state: 'alive', - origin: 'local', - sort: '+follower', - } }, - recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: { - origin: 'local', - sort: '+updatedAt', - } }, - recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: { - origin: 'local', - state: 'alive', - sort: '+createdAt', - } }, - popularUsersF: { endpoint: 'users', limit: 10, params: { - state: 'alive', - origin: 'remote', - sort: '+follower', - } }, - recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, params: { - origin: 'combined', - sort: '+updatedAt', - } }, - recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, params: { - origin: 'combined', - sort: '+createdAt', - } }, - tagsLocal: [], - tagsRemote: [], - stats: null, - meta: null, - num: Vue.filter('number'), - faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket - }; - }, - - computed: { - tagUsers(): any { - return { - endpoint: 'hashtags/users', - limit: 30, - params: { - tag: this.tag, - origin: 'combined', - sort: '+follower', - } - }; - }, - }, - - watch: { - tag() { - if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); - } - }, - - created() { - this.$emit('init', { - title: this.$t('@.explore'), - icon: faHashtag - }); - this.$root.api('hashtags/list', { - sort: '+attachedLocalUsers', - attachedToLocalUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsLocal = tags; - }); - this.$root.api('hashtags/list', { - sort: '+attachedRemoteUsers', - attachedToRemoteUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsRemote = tags; - }); - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> - -<style lang="stylus" scoped> -.localfedi7 - overflow hidden - background var(--face) - color #fff - text-shadow 0 0 8px #000 - border-radius 6px - padding 16px - margin-top 16px - margin-bottom 16px - height 80px - background-position 50% - background-size cover - > header - font-size 20px - font-weight bold - > div - font-size 14px - opacity 0.8 - -.localfedi7:first-child - margin-top 0 - -.vxjfqztj - padding 16px - - > * - margin-right 16px - - &.local - font-weight bold -</style> diff --git a/src/client/app/common/views/pages/favorites.vue b/src/client/app/common/views/pages/favorites.vue deleted file mode 100644 index e396615a93..0000000000 --- a/src/client/app/common/views/pages/favorites.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div> - <component :is="notesComponent" :pagination="pagination" :extract="items => items.map(item => item.note)"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faStar } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -//import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n(), - - props: { - platform: { - type: String, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'i/favorites', - limit: 10, - }, - - notesComponent: - this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null - }; - }, - - created() { - this.$emit('init', { - title: this.$t('@.favorites'), - icon: faStar - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/featured.vue b/src/client/app/common/views/pages/featured.vue deleted file mode 100644 index c00361aa85..0000000000 --- a/src/client/app/common/views/pages/featured.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div> - <component :is="notesComponent" :pagination="pagination"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -//import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n(), - - props: { - platform: { - type: String, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'notes/featured', - limit: 29, - }, - - notesComponent: - this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null - }; - }, - - created() { - this.$emit('init', { - title: this.$t('@.featured-notes'), - icon: faNewspaper - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue deleted file mode 100644 index 07ff7b7d54..0000000000 --- a/src/client/app/common/views/pages/follow-requests.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header>{{ $t('received-follow-requests') }}</template> - <div v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25" tag="div"> - <div v-for="req in requests" class="mcbzkkaw"> - <router-link :key="req.id" :to="req.follower | userPage"> - <mk-user-name :user="req.follower"/> - </router-link> - <span> - <a @click="accept(req.follower)">{{ $t('accept') }}</a> | <a @click="reject(req.follower)">{{ $t('reject') }}</a> - </span> - </div> - </sequential-entrance> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../scripts/loading'; -import { faUserClock } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/follow-requests.vue'), - data() { - return { - fetching: true, - requests: [] - }; - }, - created() { - this.$emit('init', { - title: this.$t('received-follow-requests'), - icon: faUserClock - }); - }, - mounted() { - Progress.start(); - this.$root.api('following/requests/list').then(requests => { - this.fetching = false; - this.requests = requests; - Progress.done(); - }); - }, - methods: { - accept(user) { - this.$root.api('following/requests/accept', { userId: user.id }).then(() => { - this.requests = this.requests.filter(r => r.follower.id != user.id); - }); - }, - reject(user) { - this.$root.api('following/requests/reject', { userId: user.id }).then(() => { - this.requests = this.requests.filter(r => r.follower.id != user.id); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mcbzkkaw - display flex - padding 16px - border solid 1px var(--faceDivider) - border-radius 4px - - > span - margin 0 0 0 auto - color var(--text) - -</style> diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue deleted file mode 100644 index c7b07a5be2..0000000000 --- a/src/client/app/common/views/pages/follow.vue +++ /dev/null @@ -1,242 +0,0 @@ -<template> -<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching"> - <div class="signed-in-as"> - <mfm :text="$t('signed-in-as').replace('{}', myName)" :plain="true" :custom-emojis="$store.state.i.emojis"/> - </div> - <main> - <div class="banner" :style="bannerStyle"></div> - <mk-avatar class="avatar" :user="user" :disable-preview="true"/> - <div class="body"> - <router-link :to="user | userPage" class="name"> - <mk-user-name :user="user"/> - </router-link> - <span class="username">@{{ user | acct }}</span> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </div> - </div> - </main> - - <button - :class="{ wait: followWait, active: user.isFollowing || user.hasPendingFollowRequestFromYou }" - @click="onClick" - :disabled="followWait"> - <template v-if="!followWait"> - <template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template> - <template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="spinner"/> {{ $t('follow-processing') }}</template> - <template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template> - <template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template> - <template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template> - </template> - <template v-else><fa icon="spinner" pulse fixed-width/></template> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/follow.vue'), - data() { - return { - fetching: true, - user: null, - followWait: false - }; - }, - - computed: { - myName(): string { - return Vue.filter('userName')(this.$store.state.i); - }, - - bannerStyle(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - const acct = new URL(location.href).searchParams.get('acct'); - this.fetching = true; - Progress.start(); - if (acct.match(/^https?:/)) { - this.$root.api('ap/show', { - uri: acct - }).then((res: { type: string, object: any }) => { - if (res.type === 'User') { - this.user = res.object; - } else if (res.type === 'Note') { - this.$router.replace(`/notes/${res.object.id}`); - } else { - this.$root.dialog({ - type: 'error', - text: 'Not supported' - }); - } - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }).finally(() => { - this.fetching = false; - Progress.done(); - }); - } else { - this.$root.api('users/show', parseAcct(acct)).then((user: any) => { - this.user = user; - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }).finally(() => { - this.fetching = false; - Progress.done(); - }); - } - }, - - async onClick() { - this.followWait = true; - - try { - if (this.user.isFollowing) { - this.user = await this.$root.api('following/delete', { - userId: this.user.id - }); - } else { - if (this.user.hasPendingFollowRequestFromYou) { - this.user = await this.$root.api('following/requests/cancel', { - userId: this.user.id - }); - } else if (this.user.isLocked) { - this.user = await this.$root.api('following/create', { - userId: this.user.id - }); - } else { - this.user = await this.$root.api('following/create', { - userId: this.user.id - }); - } - } - } catch (e) { - console.error(e); - } finally { - this.followWait = false; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.syxhndwprovvuqhmyvveewmbqayniwkv - padding 32px - max-width 500px - margin 0 auto - text-align center - color var(--text) - - $bg = var(--face) - - @media (max-width 400px) - padding 16px - - > .signed-in-as - margin-bottom 16px - font-size 14px - font-weight bold - - > main - margin-bottom 16px - background $bg - border-radius 8px - box-shadow 0 4px 12px rgba(#000, 0.1) - overflow hidden - - > .banner - height 128px - background-position center - background-size cover - - > .avatar - display block - margin -50px auto 0 auto - width 100px - height 100px - border-radius 100% - border solid 4px $bg - - > .body - padding 4px 32px 32px 32px - - @media (max-width 400px) - padding 4px 16px 16px 16px - - > .name - font-size 20px - font-weight bold - - > .username - display block - opacity 0.7 - - > .description - margin-top 16px - - > button - display block - user-select none - cursor pointer - padding 10px 16px - margin 0 - width 100% - min-width 150px - font-size 14px - font-weight bold - color var(--primary) - background transparent - outline none - border solid 1px var(--primary) - border-radius 36px - - &:hover - background var(--primaryAlpha01) - - &:active - background var(--primaryAlpha02) - - &.active - color var(--primaryForeground) - background var(--primary) - - &:hover - background var(--primaryLighten10) - border-color var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - border-color var(--primaryDarken10) - - &.wait - cursor wait !important - opacity 0.7 - - * - pointer-events none - -</style> diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue deleted file mode 100644 index b546e69ae3..0000000000 --- a/src/client/app/common/views/pages/followers.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../misc/acct/parse'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - pagination: { - endpoint: 'users/followers', - limit: 30, - params: { - ...parseAcct(this.$route.params.user), - } - }, - }; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue deleted file mode 100644 index 4e584c19d9..0000000000 --- a/src/client/app/common/views/pages/following.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../misc/acct/parse'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - pagination: { - endpoint: 'users/following', - limit: 30, - params: { - ...parseAcct(this.$route.params.user), - } - }, - }; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/not-found.vue b/src/client/app/common/views/pages/not-found.vue deleted file mode 100644 index cb1b19687c..0000000000 --- a/src/client/app/common/views/pages/not-found.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<figure class="megtcxgu"> - <img :src="src" alt=""> - <figcaption> - <h1><span>Not found</span></h1> - <p><span>{{ $t('page-not-found') }}</span></p> - </figcaption> -</figure> -</template> - -<script lang="ts"> -import Vue from 'vue' -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/not-found.vue'), - data() { - return { - src: '' - } - }, - created() { - this.$root.getMeta().then(meta => { - if (meta.errorImageUrl) - this.src = meta.errorImageUrl; - }); - } -}) -</script> - -<style lang="stylus" scoped> -.megtcxgu - align-items center - bottom 0 - display flex - justify-content center - left 0 - margin auto - position fixed - right 0 - top 0 - - > img - width 500px - - > figcaption - margin 8px - - h1, - p - color var(--text) - display flex - flex-flow column - - * - position relative - width 100% - - @media (max-width: 767px) - flex-flow column - - > figcaption - text-align center - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue deleted file mode 100644 index 6a82b0eec9..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template> - - <section class="xfhsjczc"> - <ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input> - <ui-switch v-model="value.primary"><span>{{ $t('blocks._button.colored') }}</span></ui-switch> - <ui-select v-model="value.action"> - <template #label>{{ $t('blocks._button.action') }}</template> - <option value="dialog">{{ $t('blocks._button._action.dialog') }}</option> - <option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option> - <option value="pushEvent">{{ $t('blocks._button._action.pushEvent') }}</option> - </ui-select> - <template v-if="value.action === 'dialog'"> - <ui-input v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input> - </template> - <template v-else-if="value.action === 'pushEvent'"> - <ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input> - <ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input> - <ui-select v-model="value.var"> - <template #label>{{ $t('blocks._button._action._pushEvent.variable') }}</template> - <option :value="null">{{ $t('blocks._button._action._pushEvent.no-variable') }}</option> - <option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option> - </optgroup> - </ui-select> - </template> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - aiScript: { - required: true, - }, - }, - - data() { - return { - faBolt - }; - }, - - created() { - if (this.value.text == null) Vue.set(this.value, 'text', ''); - if (this.value.action == null) Vue.set(this.value, 'action', 'dialog'); - if (this.value.content == null) Vue.set(this.value, 'content', null); - if (this.value.event == null) Vue.set(this.value, 'event', null); - if (this.value.message == null) Vue.set(this.value, 'message', null); - if (this.value.primary == null) Vue.set(this.value, 'primary', false); - if (this.value.var == null) Vue.set(this.value, 'var', null); - }, -}); -</script> - -<style lang="stylus" scoped> -.xfhsjczc - padding 0 16px 0 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue deleted file mode 100644 index 4fc2aac8fc..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.counter') }}</template> - - <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._counter.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._counter.text') }}</span></ui-input> - <ui-input v-model="value.inc" type="number"><span>{{ $t('blocks._counter.inc') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue deleted file mode 100644 index a3743d89d6..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faQuestion"/> {{ $t('blocks.if') }}</template> - <template #func> - <button @click="add()"> - <fa :icon="faPlus"/> - </button> - </template> - - <section class="romcojzs"> - <ui-select v-model="value.var"> - <template #label>{{ $t('blocks._if.variable') }}</template> - <option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> - </optgroup> - </ui-select> - - <x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { v4 as uuid } from 'uuid'; -import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - inject: ['getPageBlockList'], - - props: { - value: { - required: true - }, - aiScript: { - required: true, - }, - }, - - data() { - return { - faPlus, faQuestion - }; - }, - - beforeCreate() { - this.$options.components.XBlocks = require('../page-editor.blocks.vue').default - }, - - created() { - if (this.value.children == null) Vue.set(this.value, 'children', []); - if (this.value.var === undefined) Vue.set(this.value, 'var', null); - }, - - methods: { - async add() { - const { canceled, result: type } = await this.$root.dialog({ - type: null, - title: this.$t('choose-block'), - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.value.children.push({ id, type }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.romcojzs - padding 0 16px 16px 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue deleted file mode 100644 index e2e72b04c2..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template> - <template #func> - <button @click="choose()"> - <fa :icon="faFolderOpen"/> - </button> - </template> - - <section class="oyyftmcf"> - <x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; -import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; -import XFileThumbnail from '../../../components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer, XFileThumbnail - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - file: null, - faPencilAlt, faImage, faFolderOpen - }; - }, - - created() { - if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null); - }, - - mounted() { - if (this.value.fileId == null) { - this.choose(); - } else { - this.$root.api('drive/files/show', { - fileId: this.value.fileId - }).then(file => { - this.file = file; - }); - } - }, - - methods: { - async choose() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - this.file = file; - this.value.fileId = file.id; - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.oyyftmcf - > .preview - height 150px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue deleted file mode 100644 index 30c3938111..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.numberInput') }}</template> - - <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._numberInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._numberInput.text') }}</span></ui-input> - <ui-input v-model="value.default" type="number"><span>{{ $t('blocks._numberInput.default') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue deleted file mode 100644 index fc2f5f9032..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faPaperPlane"/> {{ $t('blocks.post') }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <ui-textarea v-model="value.text">{{ $t('blocks._post.text') }}</ui-textarea> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faPaperPlane - }; - }, - - created() { - if (this.value.text == null) Vue.set(this.value, 'text', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue deleted file mode 100644 index 3401c46f47..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.radioButton') }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._radioButton.name') }}</span></ui-input> - <ui-input v-model="value.title"><span>{{ $t('blocks._radioButton.title') }}</span></ui-input> - <ui-textarea v-model="values"><span>{{ $t('blocks._radioButton.values') }}</span></ui-textarea> - <ui-input v-model="value.default"><span>{{ $t('blocks._radioButton.default') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - values: '', - faBolt, faMagic - }; - }, - - watch: { - values() { - Vue.set(this.value, 'values', this.values.split('\n')); - } - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - if (this.value.title == null) Vue.set(this.value, 'title', ''); - if (this.value.values == null) Vue.set(this.value, 'values', []); - this.values = this.value.values.join('\n'); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue deleted file mode 100644 index 0f8f850947..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faStickyNote"/> {{ value.title }}</template> - <template #func> - <button @click="rename()"> - <fa :icon="faPencilAlt"/> - </button> - <button @click="add()"> - <fa :icon="faPlus"/> - </button> - </template> - - <section class="ilrvjyvi"> - <x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { v4 as uuid } from 'uuid'; -import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; -import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - inject: ['getPageBlockList'], - - props: { - value: { - required: true - }, - aiScript: { - required: true, - }, - }, - - data() { - return { - faStickyNote, faPlus, faPencilAlt - }; - }, - - beforeCreate() { - this.$options.components.XBlocks = require('../page-editor.blocks.vue').default - }, - - created() { - if (this.value.title == null) Vue.set(this.value, 'title', null); - if (this.value.children == null) Vue.set(this.value, 'children', []); - }, - - mounted() { - if (this.value.title == null) { - this.rename(); - } - }, - - methods: { - async rename() { - const { canceled, result: title } = await this.$root.dialog({ - title: 'Enter title', - input: { - type: 'text', - default: this.value.title - }, - showCancelButton: true - }); - if (canceled) return; - this.value.title = title; - }, - - async add() { - const { canceled, result: type } = await this.$root.dialog({ - type: null, - title: this.$t('choose-block'), - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.value.children.push({ id, type }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.ilrvjyvi - > .children - padding 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue deleted file mode 100644 index 174a344640..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template> - - <section class="kjuadyyj"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input> - <ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> - -<style lang="stylus" scoped> -.kjuadyyj - padding 0 16px 16px 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue deleted file mode 100644 index 50f95fd205..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.textInput') }}</template> - - <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._textInput.text') }}</span></ui-input> - <ui-input v-model="value.default" type="text"><span>{{ $t('blocks._textInput.default') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue deleted file mode 100644 index c09f9cc1cf..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template> - - <section class="ihymsbbe"> - <textarea v-model="value.text"></textarea> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faAlignLeft, - }; - }, - - created() { - if (this.value.text == null) Vue.set(this.value, 'text', ''); - }, -}); -</script> - -<style lang="stylus" scoped> -.ihymsbbe - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - min-width 100% - min-height 150px - border none - box-shadow none - padding 16px - background transparent - color var(--text) - font-size 14px -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue deleted file mode 100644 index da3eead080..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.textareaInput') }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textareaInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._textareaInput.text') }}</span></ui-input> - <ui-textarea v-model="value.default"><span>{{ $t('blocks._textareaInput.default') }}</span></ui-textarea> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue deleted file mode 100644 index a0cc1966e8..0000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.textarea') }}</template> - - <section class="ihymsbbe"> - <textarea v-model="value.text"></textarea> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faAlignLeft, - }; - }, - - created() { - if (this.value.text == null) Vue.set(this.value, 'text', ''); - }, -}); -</script> - -<style lang="stylus" scoped> -.ihymsbbe - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - min-width 100% - min-height 150px - border none - box-shadow none - padding 16px - background transparent - color var(--text) - font-size 14px -</style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue b/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue deleted file mode 100644 index 4d7293231f..0000000000 --- a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> - <component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :ai-script="aiScript"/> -</x-draggable> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import XSection from './els/page-editor.el.section.vue'; -import XText from './els/page-editor.el.text.vue'; -import XTextarea from './els/page-editor.el.textarea.vue'; -import XImage from './els/page-editor.el.image.vue'; -import XButton from './els/page-editor.el.button.vue'; -import XTextInput from './els/page-editor.el.text-input.vue'; -import XTextareaInput from './els/page-editor.el.textarea-input.vue'; -import XNumberInput from './els/page-editor.el.number-input.vue'; -import XSwitch from './els/page-editor.el.switch.vue'; -import XIf from './els/page-editor.el.if.vue'; -import XPost from './els/page-editor.el.post.vue'; -import XCounter from './els/page-editor.el.counter.vue'; -import XRadioButton from './els/page-editor.el.radio-button.vue'; - -export default Vue.extend({ - components: { - XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton - }, - - props: { - value: { - type: Array, - required: true - }, - aiScript: { - required: true, - }, - }, - - computed: { - blocks() { - return this.value; - } - }, - - methods: { - updateItem(v) { - const i = this.blocks.findIndex(x => x.id === v.id); - const newValue = [ - ...this.blocks.slice(0, i), - v, - ...this.blocks.slice(i + 1) - ]; - this.$emit('input', newValue); - }, - - removeItem(el) { - const i = this.blocks.findIndex(x => x.id === el.id); - const newValue = [ - ...this.blocks.slice(0, i), - ...this.blocks.slice(i + 1) - ]; - this.$emit('input', newValue); - }, - } -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.container.vue b/src/client/app/common/views/pages/page-editor/page-editor.container.vue deleted file mode 100644 index a3a501afb4..0000000000 --- a/src/client/app/common/views/pages/page-editor/page-editor.container.vue +++ /dev/null @@ -1,146 +0,0 @@ -<template> -<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> - <header> - <div class="title"><slot name="header"></slot></div> - <div class="buttons"> - <slot name="func"></slot> - <button v-if="removable" @click="remove()"> - <fa :icon="faTrashAlt"/> - </button> - <button v-if="draggable" class="drag-handle"> - <fa :icon="faBars"/> - </button> - <button @click="toggleContent(!showBody)"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </div> - </header> - <p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> - <p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBars } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('pages'), - - props: { - expanded: { - type: Boolean, - default: true - }, - removable: { - type: Boolean, - default: true - }, - draggable: { - type: Boolean, - default: false - }, - error: { - required: false, - default: null - }, - warn: { - required: false, - default: null - } - }, - data() { - return { - showBody: this.expanded, - faTrashAlt, faBars - }; - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - this.$emit('toggle', show); - }, - remove() { - this.$emit('remove'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpjygsrt - overflow hidden - background var(--face) - border solid 2px var(--pageBlockBorder) - border-radius 6px - - &:hover - border solid 2px var(--pageBlockBorderHover) - - &.warn - border solid 2px #dec44c - - &.error - border solid 2px #f00 - - & + .cpjygsrt - margin-top 16px - - > header - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 1px rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &:empty - display none - - > .buttons - position absolute - z-index 2 - top 0 - right 0 - - > button - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - .drag-handle - cursor move - - > .warn - color #b19e49 - margin 0 - padding 16px 16px 0 16px - font-size 14px - - > .error - color #f00 - margin 0 - padding 16px 16px 0 16px - font-size 14px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue b/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue deleted file mode 100644 index cf76cc003e..0000000000 --- a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue +++ /dev/null @@ -1,271 +0,0 @@ -<template> -<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable"> - <template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> - <template #func> - <button @click="changeType()"> - <fa :icon="faPencilAlt"/> - </button> - </template> - - <section v-if="value.type === null" class="pbglfege" @click="changeType()"> - {{ $t('script.emptySlot') }} - </section> - <section v-else-if="value.type === 'text'" class="tbwccoaw"> - <input v-model="value.value"/> - </section> - <section v-else-if="value.type === 'multiLineText'" class="tbwccoaw"> - <textarea v-model="value.value"></textarea> - </section> - <section v-else-if="value.type === 'textList'" class="tbwccoaw"> - <textarea v-model="value.value" :placeholder="$t('script.blocks._textList.info')"></textarea> - </section> - <section v-else-if="value.type === 'number'" class="tbwccoaw"> - <input v-model="value.value" type="number"/> - </section> - <section v-else-if="value.type === 'ref'" class="hpdwcrvs"> - <select v-model="value.value"> - <option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.argVariables')"> - <option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> - </optgroup> - <optgroup :label="$t('script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> - </optgroup> - </select> - </section> - <section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> - <ui-textarea v-model="slots"> - <span>{{ $t('script.blocks._fn.slots') }}</span> - <template #desc>{{ $t('script.blocks._fn.slots-info') }}</template> - </ui-textarea> - <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> - </section> - <section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> - <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/> - </section> - <section v-else class="" style="padding:16px;"> - <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XContainer from './page-editor.container.vue'; -import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons'; -import { isLiteralBlock, funcDefs, blockDefs } from '../../../../../../misc/aiscript/index'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - inject: ['getScriptBlockList'], - - props: { - getExpectedType: { - required: false, - default: null - }, - value: { - required: true - }, - title: { - required: false - }, - removable: { - required: false, - default: false - }, - aiScript: { - required: true, - }, - name: { - required: true, - }, - fnSlots: { - required: false, - }, - draggable: { - required: false, - default: false - } - }, - - data() { - return { - error: null, - warn: null, - slots: '', - faPencilAlt - }; - }, - - computed: { - icon(): any { - if (this.value.type === null) return null; - if (this.value.type.startsWith('fn:')) return faPlug; - return blockDefs.find(x => x.type === this.value.type).icon; - }, - typeText(): any { - if (this.value.type === null) return null; - if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1]; - return this.$t(`script.blocks.${this.value.type}`); - }, - }, - - watch: { - slots() { - this.value.value.slots = this.slots.split('\n').map(x => ({ - name: x, - type: null - })); - } - }, - - beforeCreate() { - this.$options.components.XV = require('./page-editor.script-block.vue').default; - }, - - created() { - if (this.value.value == null) Vue.set(this.value, 'value', null); - - if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.map(x => x.name).join('\n'); - - this.$watch('value.type', (t) => { - this.warn = null; - - if (this.value.type === 'fn') { - const id = uuid(); - this.value.value = {}; - Vue.set(this.value.value, 'slots', []); - Vue.set(this.value.value, 'expression', { id, type: null }); - return; - } - - if (this.value.type && this.value.type.startsWith('fn:')) { - const fnName = this.value.type.split(':')[1]; - const fn = this.aiScript.getVarByName(fnName); - - const empties = []; - for (let i = 0; i < fn.value.slots.length; i++) { - const id = uuid(); - empties.push({ id, type: null }); - } - Vue.set(this.value, 'args', empties); - return; - } - - if (isLiteralBlock(this.value)) return; - - const empties = []; - for (let i = 0; i < funcDefs[this.value.type].in.length; i++) { - const id = uuid(); - empties.push({ id, type: null }); - } - Vue.set(this.value, 'args', empties); - - for (let i = 0; i < funcDefs[this.value.type].in.length; i++) { - const inType = funcDefs[this.value.type].in[i]; - if (typeof inType !== 'number') { - if (inType === 'number') this.value.args[i].type = 'number'; - if (inType === 'string') this.value.args[i].type = 'text'; - } - } - }); - - this.$watch('value.args', (args) => { - if (args == null) { - this.warn = null; - return; - } - const emptySlotIndex = args.findIndex(x => x.type === null); - if (emptySlotIndex !== -1 && emptySlotIndex < args.length) { - this.warn = { - slot: emptySlotIndex - }; - } else { - this.warn = null; - } - }, { - deep: true - }); - - this.$watch('aiScript.variables', () => { - if (this.type != null && this.value) { - this.error = this.aiScript.typeCheck(this.value); - } - }, { - deep: true - }); - }, - - methods: { - async changeType() { - const { canceled, result: type } = await this.$root.dialog({ - type: null, - title: this.$t('select-type'), - select: { - groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null) - }, - showCancelButton: true - }); - if (canceled) return; - this.value.type = type; - }, - - _getExpectedType(slot: number) { - return this.aiScript.getExpectedType(this.value, slot); - } - } -}); -</script> - -<style lang="stylus" scoped> -.turmquns - opacity 0.7 - -.pbglfege - opacity 0.5 - padding 16px - text-align center - cursor pointer - color var(--text) - -.tbwccoaw - > input - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - max-width 100% - min-width 100% - border none - box-shadow none - padding 16px - font-size 16px - background transparent - color var(--text) - - > textarea - min-height 100px - -.hpdwcrvs - padding 16px - - > select - display block - padding 4px - font-size 16px - width 100% - -</style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.vue b/src/client/app/common/views/pages/page-editor/page-editor.vue deleted file mode 100644 index cbe65ad6f0..0000000000 --- a/src/client/app/common/views/pages/page-editor/page-editor.vue +++ /dev/null @@ -1,508 +0,0 @@ -<template> -<div> - <div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header> - <div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('read-page') : pageId ? $t('edit-page') : $t('new-page') }}</div> - <div class="buttons"> - <button @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button> - <button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button> - <button @click="save()" v-if="!readonly"><fa :icon="faSave"/></button> - </div> - </header> - - <section> - <router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</router-link> - - <ui-input v-model="title"> - <span>{{ $t('title') }}</span> - </ui-input> - - <template v-if="showOptions"> - <ui-input v-model="summary"> - <span>{{ $t('summary') }}</span> - </ui-input> - - <ui-input v-model="name"> - <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> - <span>{{ $t('url') }}</span> - </ui-input> - - <ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch> - - <ui-select v-model="font"> - <template #label>{{ $t('font') }}</template> - <option value="serif">{{ $t('fontSerif') }}</option> - <option value="sans-serif">{{ $t('fontSansSerif') }}</option> - </ui-select> - - <ui-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</ui-switch> - - <div class="eyeCatch"> - <ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button> - <div v-else-if="eyeCatchingImage"> - <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/> - <ui-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</ui-button> - </div> - </div> - </template> - - <x-blocks class="content" v-model="content" :ai-script="aiScript"/> - - <ui-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></ui-button> - </section> - </div> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faMagic"/> {{ $t('variables') }}</template> - <div class="qmuvgica"> - <x-draggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> - <x-variable v-for="variable in variables" - :value="variable" - :removable="true" - @input="v => updateVariable(v)" - @remove="() => removeVariable(variable)" - :key="variable.name" - :ai-script="aiScript" - :name="variable.name" - :title="variable.name" - :draggable="true" - /> - </x-draggable> - - <ui-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></ui-button> - - <ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info> - - <template v-if="moreDetails"> - <ui-info><span v-html="$t('variables-info2')"></span></ui-info> - <ui-info><span v-html="$t('variables-info3')"></span></ui-info> - <ui-info><span v-html="$t('variables-info4')"></span></ui-info> - </template> - </div> - </ui-container> - - <ui-container :body-togglable="true" :expanded="false"> - <template #header><fa :icon="faCode"/> {{ $t('inspector') }}</template> - <div style="padding:0 32px 32px 32px;"> - <ui-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</ui-textarea> - <ui-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</ui-textarea> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; -import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../i18n'; -import XVariable from './page-editor.script-block.vue'; -import XBlocks from './page-editor.blocks.vue'; -import { v4 as uuid } from 'uuid'; -import { blockDefs } from '../../../../../../misc/aiscript/index'; -import { ASTypeChecker } from '../../../../../../misc/aiscript/type-checker'; -import { url } from '../../../../config'; -import { collectPageVars } from '../../../scripts/collect-page-vars'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XDraggable, XVariable, XBlocks - }, - - props: { - initPageId: { - type: String, - required: false - }, - initPageName: { - type: String, - required: false - }, - initUser: { - type: String, - required: false - }, - }, - - data() { - return { - author: this.$store.state.i, - readonly: false, - page: null, - pageId: null, - currentName: null, - title: '', - summary: null, - name: Date.now().toString(), - eyeCatchingImage: null, - eyeCatchingImageId: null, - font: 'sans-serif', - content: [], - alignCenter: false, - hideTitleWhenPinned: false, - variables: [], - aiScript: null, - showOptions: false, - moreDetails: false, - url, - faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode - }; - }, - - watch: { - async eyeCatchingImageId() { - if (this.eyeCatchingImageId == null) { - this.eyeCatchingImage = null; - } else { - this.eyeCatchingImage = await this.$root.api('drive/files/show', { - fileId: this.eyeCatchingImageId, - }); - } - }, - }, - - async created() { - this.aiScript = new ASTypeChecker(); - - this.$watch('variables', () => { - this.aiScript.variables = this.variables; - }, { deep: true }); - - this.$watch('content', () => { - this.aiScript.pageVars = collectPageVars(this.content); - }, { deep: true }); - - if (this.initPageId) { - this.page = await this.$root.api('pages/show', { - pageId: this.initPageId, - }); - } else if (this.initPageName && this.initUser) { - this.page = await this.$root.api('pages/show', { - name: this.initPageName, - username: this.initUser, - }); - this.readonly = true; - } - - if (this.page) { - this.author = this.page.user; - this.pageId = this.page.id; - this.title = this.page.title; - this.name = this.page.name; - this.currentName = this.page.name; - this.summary = this.page.summary; - this.font = this.page.font; - this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; - this.alignCenter = this.page.alignCenter; - this.content = this.page.content; - this.variables = this.page.variables; - this.eyeCatchingImageId = this.page.eyeCatchingImageId; - } else { - const id = uuid(); - this.content = [{ - id, - type: 'text', - text: 'Hello World!' - }]; - } - }, - - provide() { - return { - readonly: this.readonly, - getScriptBlockList: this.getScriptBlockList, - getPageBlockList: this.getPageBlockList - } - }, - - methods: { - save() { - const options = { - title: this.title.trim(), - name: this.name.trim(), - summary: this.summary, - font: this.font, - hideTitleWhenPinned: this.hideTitleWhenPinned, - alignCenter: this.alignCenter, - content: this.content, - variables: this.variables, - eyeCatchingImageId: this.eyeCatchingImageId, - }; - - const onError = err => { - if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { - if (err.info.param == 'name') { - this.$root.dialog({ - type: 'error', - title: this.$t('title-invalid-name'), - text: this.$t('text-invalid-name') - }); - } - } else if (err.code == 'NAME_ALREADY_EXISTS') { - this.$root.dialog({ - type: 'error', - text: this.$t('name-already-exists') - }); - } - }; - - if (this.pageId) { - options.pageId = this.pageId; - this.$root.api('pages/update', options) - .then(page => { - this.currentName = this.name.trim(); - this.$root.dialog({ - type: 'success', - text: this.$t('page-updated') - }); - }).catch(onError); - } else { - this.$root.api('pages/create', options) - .then(page => { - this.pageId = page.id; - this.currentName = this.name.trim(); - this.$root.dialog({ - type: 'success', - text: this.$t('page-created') - }); - this.$router.push(`/i/pages/edit/${this.pageId}`); - }).catch(onError); - } - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('are-you-sure-delete'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('pages/delete', { - pageId: this.pageId, - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('page-deleted') - }); - this.$router.push(`/i/pages`); - }); - }); - }, - - async add() { - const { canceled, result: type } = await this.$root.dialog({ - type: null, - title: this.$t('choose-block'), - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.content.push({ id, type }); - }, - - async addVariable() { - let { canceled, result: name } = await this.$root.dialog({ - title: this.$t('enter-variable-name'), - input: { - type: 'text', - }, - showCancelButton: true - }); - if (canceled) return; - - name = name.trim(); - - if (this.aiScript.isUsedName(name)) { - this.$root.dialog({ - type: 'error', - text: this.$t('the-variable-name-is-already-used') - }); - return; - } - - const id = uuid(); - this.variables.push({ id, name, type: null }); - }, - - removeVariable(v) { - const i = this.variables.findIndex(x => x.name === v.name); - const newValue = [ - ...this.variables.slice(0, i), - ...this.variables.slice(i + 1) - ]; - this.variables = newValue; - }, - - getPageBlockList() { - return [{ - label: this.$t('content-blocks'), - items: [ - { value: 'section', text: this.$t('blocks.section') }, - { value: 'text', text: this.$t('blocks.text') }, - { value: 'image', text: this.$t('blocks.image') }, - { value: 'textarea', text: this.$t('blocks.textarea') }, - ] - }, { - label: this.$t('input-blocks'), - items: [ - { value: 'button', text: this.$t('blocks.button') }, - { value: 'radioButton', text: this.$t('blocks.radioButton') }, - { value: 'textInput', text: this.$t('blocks.textInput') }, - { value: 'textareaInput', text: this.$t('blocks.textareaInput') }, - { value: 'numberInput', text: this.$t('blocks.numberInput') }, - { value: 'switch', text: this.$t('blocks.switch') }, - { value: 'counter', text: this.$t('blocks.counter') } - ] - }, { - label: this.$t('special-blocks'), - items: [ - { value: 'if', text: this.$t('blocks.if') }, - { value: 'post', text: this.$t('blocks.post') } - ] - }]; - }, - - getScriptBlockList(type: string = null) { - const list = []; - - const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); - - for (const block of blocks) { - const category = list.find(x => x.category === block.category); - if (category) { - category.items.push({ - value: block.type, - text: this.$t(`script.blocks.${block.type}`) - }); - } else { - list.push({ - category: block.category, - label: this.$t(`script.categories.${block.category}`), - items: [{ - value: block.type, - text: this.$t(`script.blocks.${block.type}`) - }] - }); - } - } - - const userFns = this.variables.filter(x => x.type === 'fn'); - if (userFns.length > 0) { - list.unshift({ - label: this.$t(`script.categories.fn`), - items: userFns.map(v => ({ - value: 'fn:' + v.name, - text: v.name - })) - }); - } - - return list; - }, - - setEyeCatchingImage() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - this.eyeCatchingImageId = file.id; - }); - }, - - removeEyeCatchingImage() { - this.eyeCatchingImageId = null; - } - } -}); -</script> - -<style lang="stylus" scoped> -.gwbmwxkm - overflow hidden - background var(--face) - margin-bottom 16px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > header - background var(--faceHeader) - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &:empty - display none - - > .buttons - position absolute - z-index 2 - top 0 - right 0 - - > button - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - > section - padding 0 32px 32px 32px - - @media (max-width 500px) - padding 0 16px 16px 16px - - > .view - display inline-block - margin 16px 0 0 0 - font-size 14px - - > .content - margin-bottom 16px - - > .eyeCatch - margin-bottom 16px - - > div - > img - max-width 100% - -.qmuvgica - padding 32px - - @media (max-width 500px) - padding 16px - - > .variables - margin-bottom 16px - - > .add - margin-bottom 16px - -</style> diff --git a/src/client/app/common/views/pages/page.vue b/src/client/app/common/views/pages/page.vue deleted file mode 100644 index d1c4c2be43..0000000000 --- a/src/client/app/common/views/pages/page.vue +++ /dev/null @@ -1,63 +0,0 @@ -<template> -<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; -import XPage from '../components/page/page.vue'; - -export default Vue.extend({ - components: { - XPage - }, - - props: { - pageName: { - type: String, - required: true - }, - username: { - type: String, - required: true - }, - }, - - data() { - return { - page: null, - }; - }, - - computed: { - path(): string { - return this.username + '/' + this.pageName; - } - }, - - watch: { - path() { - this.fetch(); - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('pages/show', { - name: this.pageName, - username: this.username, - }).then(page => { - this.page = page; - this.$emit('init', { - title: this.page.title, - icon: faStickyNote - }); - }); - }, - } -}); -</script> diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue deleted file mode 100644 index 236330db46..0000000000 --- a/src/client/app/common/views/pages/pages.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> - <div class="rknalgpo my"> - <ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button> - <ui-pagination :pagination="myPagesPagination" #default="{items}"> - <x-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> - </ui-pagination> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> - <div class="rknalgpo"> - <ui-pagination :pagination="likedPagesPagination" #default="{items}"> - <x-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> - </ui-pagination> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons'; -import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import XPagePreview from '../../views/components/page-preview.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - components: { - XPagePreview - }, - data() { - return { - myPagesPagination: { - endpoint: 'i/pages', - limit: 5, - }, - likedPagesPagination: { - endpoint: 'i/page-likes', - limit: 5, - }, - faStickyNote, faPlus, faEdit, faHeart - }; - }, - created() { - this.$emit('init', { - title: this.$t('@.pages'), - icon: faStickyNote - }); - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - create() { - this.$router.push(`/i/pages/new`); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rknalgpo - padding 16px - - &.my .ckltabjg:first-child - margin-top 16px - - .ckltabjg:not(:last-child) - margin-bottom 8px - - @media (min-width 500px) - .ckltabjg:not(:last-child) - margin-bottom 16px - -</style> diff --git a/src/client/app/common/views/pages/room/preview.vue b/src/client/app/common/views/pages/room/preview.vue deleted file mode 100644 index 94c13cee9f..0000000000 --- a/src/client/app/common/views/pages/room/preview.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<canvas width=224 height=128></canvas> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as THREE from 'three'; - -export default Vue.extend({ - data() { - return { - selected: null, - objectHeight: 0, - orbitRadius: 5 - }; - }, - - mounted() { - const canvas = this.$el; - - const width = canvas.width; - const height = canvas.height; - - const scene = new THREE.Scene(); - - const renderer = new THREE.WebGLRenderer({ - canvas: canvas, - antialias: true, - alpha: false - }); - renderer.setPixelRatio(window.devicePixelRatio); - renderer.setSize(width, height); - renderer.setClearColor(0x000000); - renderer.autoClear = false; - renderer.shadowMap.enabled = true; - renderer.shadowMap.cullFace = THREE.CullFaceBack; - - const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100); - camera.zoom = 10; - camera.position.x = 0; - camera.position.y = 2; - camera.position.z = 0; - camera.updateProjectionMatrix(); - scene.add(camera); - - const ambientLight = new THREE.AmbientLight(0xffffff, 1); - ambientLight.castShadow = false; - scene.add(ambientLight); - - const light = new THREE.PointLight(0xffffff, 1, 100); - light.position.set(3, 3, 3); - scene.add(light); - - const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222); - scene.add(grid); - - const render = () => { - const timer = Date.now() * 0.0004; - requestAnimationFrame(render); - - camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius; // Math.PI / 6 => 30deg - camera.position.z = Math.cos(timer) * this.orbitRadius; - camera.position.x = Math.sin(timer) * this.orbitRadius; - camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0)); - renderer.render(scene, camera); - }; - - this.selected = selected => { - const obj = selected.clone(); - - // Remove current object - const current = scene.getObjectByName('obj'); - if (current != null) { - scene.remove(current); - } - - // Add new object - obj.name = 'obj'; - obj.position.x = 0; - obj.position.y = 0; - obj.position.z = 0; - obj.rotation.x = 0; - obj.rotation.y = 0; - obj.rotation.z = 0; - obj.traverse(child => { - if (child instanceof THREE.Mesh) { - child.material = child.material.clone(); - return child.material.emissive.setHex(0x000000); - } - }); - const objectBoundingBox = new THREE.Box3().setFromObject(obj); - this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y; - - const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x; - const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z; - - const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect; - this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180)); - - scene.add(obj); - }; - - render(); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/room/room.vue b/src/client/app/common/views/pages/room/room.vue deleted file mode 100644 index 1e81920c22..0000000000 --- a/src/client/app/common/views/pages/room/room.vue +++ /dev/null @@ -1,310 +0,0 @@ -<template> -<div class="hveuntkp"> - <div class="controller" v-if="objectSelected"> - <section> - <p class="name">{{ selectedFurnitureName }}</p> - <x-preview ref="preview"/> - <template v-if="selectedFurnitureInfo.props"> - <div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k"> - <p>{{ k }}</p> - <template v-if="selectedFurnitureInfo.props[k] === 'image'"> - <ui-button @click="chooseImage(k)">{{ $t('chooseImage') }}</ui-button> - </template> - <template v-else-if="selectedFurnitureInfo.props[k] === 'color'"> - <input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/> - </template> - </div> - </template> - </section> - <section> - <ui-button @click="translate()" :primary="isTranslateMode"><fa :icon="faArrowsAlt"/> {{ $t('translate') }}</ui-button> - <ui-button @click="rotate()" :primary="isRotateMode"><fa :icon="faUndo"/> {{ $t('rotate') }}</ui-button> - <ui-button v-if="isTranslateMode || isRotateMode" @click="exit()"><fa :icon="faBan"/> {{ $t('exit') }}</ui-button> - </section> - <section> - <ui-button @click="remove()"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</ui-button> - </section> - </div> - - <div class="menu" v-if="isMyRoom"> - <section> - <ui-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('add-furniture') }}</ui-button> - </section> - <section> - <ui-select :value="roomType" @input="updateRoomType($event)"> - <template #label>{{ $t('room-type') }}</template> - <option value="default">{{ $t('rooms.default') }}</option> - <option value="washitsu">{{ $t('rooms.washitsu') }}</option> - </ui-select> - <label v-if="roomType === 'default'"> - <span>{{ $t('carpet-color') }}</span> - <input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/> - </label> - </section> - <section> - <ui-button :primary="changed" @click="save()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - <ui-button @click="clear()"><fa :icon="faBroom"/> {{ $t('clear') }}</ui-button> - </section> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { Room } from '../../../scripts/room/room'; -import parseAcct from '../../../../../../misc/acct/parse'; -import XPreview from './preview.vue'; -const storeItems = require('../../../scripts/room/furnitures.json5'); -import { faBoxOpen, faUndo, faArrowsAlt, faBan, faBroom } from '@fortawesome/free-solid-svg-icons'; -import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { query as urlQuery } from '../../../../../../prelude/url'; - -let room: Room; - -export default Vue.extend({ - i18n: i18n('room'), - - components: { - XPreview - }, - - props: { - acct: { - type: String, - required: true - }, - }, - - data() { - return { - objectSelected: false, - selectedFurnitureName: null, - selectedFurnitureInfo: null, - selectedFurnitureProps: null, - roomType: null, - carpetColor: null, - isTranslateMode: false, - isRotateMode: false, - isMyRoom: false, - changed: false, - faBoxOpen, faSave, faTrashAlt, faUndo, faArrowsAlt, faBan, faBroom, - }; - }, - - async mounted() { - window.addEventListener('beforeunload', this.beforeunload); - - const user = await this.$root.api('users/show', { - ...parseAcct(this.acct) - }); - - this.isMyRoom = this.$store.getters.isSignedIn && this.$store.state.i.id === user.id; - - const roomInfo = await this.$root.api('room/show', { - userId: user.id - }); - - this.roomType = roomInfo.roomType; - this.carpetColor = roomInfo.carpetColor; - - room = new Room(user, this.isMyRoom, roomInfo, this.$el, { - graphicsQuality: this.$store.state.device.roomGraphicsQuality, - onChangeSelect: obj => { - this.objectSelected = obj != null; - if (obj) { - const f = room.findFurnitureById(obj.name); - this.selectedFurnitureName = this.$t('furnitures.' + f.type); - this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type); - this.selectedFurnitureProps = f.props - ? JSON.parse(JSON.stringify(f.props)) // Disable reactivity - : null; - this.$nextTick(() => { - this.$refs.preview.selected(obj); - }); - } - }, - useOrthographicCamera: this.$store.state.device.roomUseOrthographicCamera - }); - }, - - beforeRouteLeave(to, from, next) { - if (this.changed) { - this.$root.dialog({ - type: 'warning', - text: this.$t('leave-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) { - next(false); - } else { - next(); - } - }); - } else { - next(); - } - }, - - beforeDestroy() { - room.destroy(); - window.removeEventListener('beforeunload', this.beforeunload); - }, - - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, - - async add() { - const { canceled, result: id } = await this.$root.dialog({ - type: null, - title: this.$t('add-furniture'), - select: { - items: storeItems.map(item => ({ - value: item.id, text: this.$t('furnitures.' + item.id) - })) - }, - showCancelButton: true - }); - if (canceled) return; - room.addFurniture(id); - this.changed = true; - }, - - remove() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.removeFurniture(); - this.changed = true; - }, - - save() { - this.$root.api('room/update', { - room: room.getRoomInfo() - }).then(() => { - this.changed = false; - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - clear() { - this.$root.dialog({ - type: 'warning', - text: this.$t('clear-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - room.removeAllFurnitures(); - this.changed = true; - }); - }, - - chooseImage(key) { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }); - }, - - updateColor(key, ev) { - room.updateProp(key, ev.target.value); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }, - - updateCarpetColor(ev) { - room.updateCarpetColor(ev.target.value); - this.carpetColor = ev.target.value; - this.changed = true; - }, - - updateRoomType(type) { - room.changeRoomType(type); - this.roomType = type; - this.changed = true; - }, - - translate() { - if (this.isTranslateMode) { - this.exit(); - } else { - this.isRotateMode = false; - this.isTranslateMode = true; - room.enterTransformMode('translate'); - } - this.changed = true; - }, - - rotate() { - if (this.isRotateMode) { - this.exit(); - } else { - this.isTranslateMode = false; - this.isRotateMode = true; - room.enterTransformMode('rotate'); - } - this.changed = true; - }, - - exit() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.exitTransformMode(); - this.changed = true; - } - } -}); -</script> - -<style lang="stylus" scoped> -.hveuntkp - > .controller - > .menu - position fixed - z-index 1 - padding 16px - background var(--face) - color var(--text) - - > section - padding 16px 0 - - &:first-child - padding-top 0 - - &:last-child - padding-bottom 0 - - &:not(:last-child) - border-bottom solid 1px var(--faceDivider) - - > .controller - top 16px - left 16px - width 256px - - > section - > .name - margin 0 - - > .menu - top 16px - right 16px - width 256px - -</style> diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue deleted file mode 100644 index 293a9bcfb5..0000000000 --- a/src/client/app/common/views/pages/share.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> -<div class="azibmfpleajagva420swmu4c3r7ni7iw"> - <h1>{{ $t('share-with', { name }) }}</h1> - <div> - <mk-signin v-if="!$store.getters.isSignedIn"/> - <x-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/> - <p v-if="posted" class="posted"><fa icon="check"/></p> - </div> - <ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/share.vue'), - components: { - XPostForm: () => import('../../../desktop/views/components/post-form.vue').then(m => m.default) - }, - data() { - return { - name: null, - posted: false, - text: new URLSearchParams(location.search).get('text'), - url: new URLSearchParams(location.search).get('url'), - title: new URLSearchParams(location.search).get('title'), - }; - }, - computed: { - template(): string { - let t = ''; - if (this.title && this.url) t += `【[${this.title}](${this.url})】\n`; - if (this.title && !this.url) t += `【${this.title}】\n`; - if (this.text) t += `${this.text}\n`; - if (!this.title && this.url) t += `${this.url}`; - return t.trim(); - } - }, - mounted() { - this.$root.getMeta().then(meta => { - this.name = meta.name || 'Misskey'; - }); - }, - methods: { - close() { - window.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.azibmfpleajagva420swmu4c3r7ni7iw - > h1 - margin 8px 0 - color #555 - font-size 20px - text-align center - - > div - max-width 500px - margin 0 auto - - > .posted - display block - margin 0 auto - padding 64px - text-align center - background #fff - border-radius 6px - width calc(100% - 32px) - - > .close - display block - margin 16px auto - width calc(100% - 32px) -</style> diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue deleted file mode 100644 index 9cc012af7a..0000000000 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ /dev/null @@ -1,256 +0,0 @@ -<template> -<div class="ivrbakop"> - <ui-container v-if="group"> - <template #header><fa :icon="faUsers"/> {{ group.name }}</template> - - <section> - <ui-margin> - <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> - <ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - <ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button> - </ui-margin> - </section> - </ui-container> - - <ui-container> - <template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> - - <section> - <ui-margin> - <ui-button @click="invite()"><fa :icon="faPlus"/> {{ $t('invite') }}</ui-button> - </ui-margin> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="kjlrfbes" v-for="user in users"> - <div> - <a :href="user | userPage"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div> - <header> - <b><mk-user-name :user="user"/></b> - <span class="is-owner" v-if="group.ownerId === user.id">owner</span> - <span class="username">@{{ user | acct }}</span> - </header> - <div v-if="group.ownerId !== user.id"> - <a @click="remove(user)">{{ $t('remove-user') }}</a> - </div> - </div> - </div> - </sequential-entrance> - </section> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faCrown, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-group-editor.vue'), - - props: { - groupId: { - required: true - } - }, - - data() { - return { - group: null, - users: [], - faCrown, faICursor, faTrashAlt, faUsers, faPlus - }; - }, - - created() { - this.$root.api('users/groups/show', { - groupId: this.groupId - }).then(group => { - this.group = group; - this.fetchUsers(); - this.$emit('init', { - title: this.group.name, - icon: faUsers - }); - }); - }, - - methods: { - fetchGroup() { - this.$root.api('users/groups/show', { - groupId: this.group.id - }).then(group => { - this.group = group; - }) - }, - - fetchUsers() { - this.$root.api('users/show', { - userIds: this.group.userIds - }).then(users => { - this.users = users; - }); - }, - - rename() { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.group.name - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('users/groups/update', { - groupId: this.group.id, - name: name - }).then(() => { - this.fetchGroup(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }) - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.group.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/groups/delete', { - groupId: this.group.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - }, - - remove(user: any) { - this.$root.api('users/groups/pull', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.fetchGroup(); - this.fetchUsers(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async invite() { - const t = this.$t('invited'); - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('users/groups/invite', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: t - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async transfer() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - - this.$root.dialog({ - type: 'warning', - text: this.$t('transfer-are-you-sure').replace('$1', this.group.name).replace('$2', user.username), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/groups/transfer', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('transferred') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ivrbakop - .kjlrfbes - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - color var(--text) - - > .is-owner - flex-shrink 0 - align-self center - margin-left 8px - padding 1px 6px - font-size 80% - background var(--groupUserListOwnerBg) - color var(--groupUserListOwnerFg) - border-radius 3px - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue deleted file mode 100644 index 6501a26061..0000000000 --- a/src/client/app/common/views/pages/user-groups.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button> - </ui-margin> - <div class="hwgkdrbl" v-for="group in ownedGroups" :key="group.id"> - <ui-hr/> - <ui-margin> - <router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link> - <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template> - <div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id"> - <ui-hr v-if="i != 0"/> - <ui-margin> - <div style="color:var(--text);">{{ group.name }}</div> - <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template> - <div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id"> - <ui-hr v-if="i != 0"/> - <ui-margin> - <div class="name" style="color:var(--text);">{{ invite.group.name }}</div> - <x-avatars :user-ids="invite.group.userIds" style="margin-top:8px;"/> - <ui-horizon-group> - <ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button> - <ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button> - </ui-horizon-group> - </ui-margin> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -import XAvatars from '../../views/components/avatars.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-groups.vue'), - components: { - XAvatars - }, - data() { - return { - ownedGroups: [], - joinedGroups: [], - invites: [], - faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText - }; - }, - mounted() { - document.title = this.$root.instanceName; - - this.$root.api('users/groups/owned').then(groups => { - this.ownedGroups = groups; - }); - - this.$root.api('users/groups/joined').then(groups => { - this.joinedGroups = groups; - }); - - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }); - - this.$emit('init', { - title: this.$t('user-groups'), - icon: faUsers - }); - }, - methods: { - add() { - this.$root.dialog({ - title: this.$t('group-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const group = await this.$root.api('users/groups/create', { - name - }); - - this.ownedGroups.push(group) - }); - }, - acceptInvite(invite) { - this.$root.api('users/groups/invitations/accept', { - inviteId: invite.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }).then(() => { - this.$root.api('users/groups/joined').then(groups => { - this.joinedGroups = groups; - }); - }); - }); - }, - rejectInvite(invite) { - this.$root.api('users/groups/invitations/reject', { - inviteId: invite.id - }).then(() => { - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.hwgkdrbl - display block - -</style> diff --git a/src/client/app/common/views/pages/user-list-editor.vue b/src/client/app/common/views/pages/user-list-editor.vue deleted file mode 100644 index 3bc5cca778..0000000000 --- a/src/client/app/common/views/pages/user-list-editor.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="cudqjmnl"> - <ui-container v-if="list"> - <template #header><fa :icon="faListUl"/> {{ list.name }}</template> - - <section class="fwvevrks"> - <ui-margin> - <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> - <ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - </ui-margin> - </section> - </ui-container> - - <ui-container> - <template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> - - <section> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button> - </ui-margin> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="phcqulfl" v-for="user in users"> - <div> - <a :href="user | userPage"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div> - <header> - <b><mk-user-name :user="user"/></b> - <span class="username">@{{ user | acct }}</span> - </header> - <div> - <a @click="remove(user)">{{ $t('remove-user') }}</a> - </div> - </div> - </div> - </sequential-entrance> - </section> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faListUl, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-list-editor.vue'), - - props: { - listId: { - required: true - } - }, - - data() { - return { - list: null, - users: [], - faListUl, faICursor, faTrashAlt, faUsers, faPlus - }; - }, - - created() { - this.$root.api('users/lists/show', { - listId: this.listId - }).then(list => { - this.list = list; - this.fetchUsers(); - this.$emit('init', { - title: this.list.name, - icon: faListUl - }); - }); - }, - - methods: { - fetchUsers() { - this.$root.api('users/show', { - userIds: this.list.userIds - }).then(users => { - this.users = users; - }); - }, - - rename() { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.list.name - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('users/lists/update', { - listId: this.list.id, - name: name - }); - }); - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.list.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/lists/delete', { - listId: this.list.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - }, - - remove(user: any) { - this.$root.api('users/lists/pull', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.fetchUsers(); - }); - }, - - async add() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('users/lists/push', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.fetchUsers(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cudqjmnl - .phcqulfl - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - color var(--text) - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/pages/user-lists.vue b/src/client/app/common/views/pages/user-lists.vue deleted file mode 100644 index 955eef993a..0000000000 --- a/src/client/app/common/views/pages/user-lists.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<ui-container> - <template #header><fa :icon="faListUl"/> {{ $t('user-lists') }}</template> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-list') }}</ui-button> - </ui-margin> - <div class="cpqqyrst" v-for="list in lists" :key="list.id"> - <ui-hr/> - <ui-margin> - <router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link> - <x-avatars :user-ids="list.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; -import XAvatars from '../../views/components/avatars.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-lists.vue'), - components: { - XAvatars - }, - data() { - return { - fetching: true, - lists: [], - faListUl, faPlus - }; - }, - mounted() { - document.title = this.$root.instanceName; - - this.$root.api('users/lists/list').then(lists => { - this.fetching = false; - this.lists = lists; - }); - - this.$emit('init', { - title: this.$t('user-lists'), - icon: faListUl - }); - }, - methods: { - add() { - this.$root.dialog({ - title: this.$t('list-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const list = await this.$root.api('users/lists/create', { - name - }); - - this.lists.push(list) - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.cpqqyrst - display block - -</style> diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue deleted file mode 100644 index bff01f89b5..0000000000 --- a/src/client/app/common/views/widgets/analog-clock.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<div class="mkw-analog-clock"> - <ui-container :naked="props.style % 2 === 0" :show-header="false"> - <div class="mkw-analog-clock--body"> - <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'analog-clock', - props: () => ({ - style: 0 - }) -}).extend({ - methods: { - func() { - this.props.style = (this.props.style + 1) % 4; - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-analog-clock - .mkw-analog-clock--body - padding 8px - -</style> diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue deleted file mode 100644 index 9423d25da8..0000000000 --- a/src/client/app/common/views/widgets/broadcast.vue +++ /dev/null @@ -1,205 +0,0 @@ -<template> -<div class="anltbovirfeutcigvwgmgxipejaeozxi"> - <ui-container :show-header="false" :naked="props.design === 1"> - <div class="anltbovirfeutcigvwgmgxipejaeozxi-body" - :data-found="announcements && announcements.length !== 0" - :data-melt="props.design == 1" - :data-mobile="platform == 'mobile'" - > - <div class="broadcast-left" v-show="announcements && announcements.length !== 0"> - <div class="icon"> - <svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> - <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path> - <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path> - <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path> - <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path> - <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path> - </svg> - </div> - <div class="broadcast-nav" v-show="announcements && announcements.length > 1"> - <mk-frac class="broadcast-page" :value="i + 1" :total="announcements.length"/> - <ui-button class="broadcast-prev" @click="prev" :title="$t('next')"><fa :icon="faAngleLeft"/></ui-button> - <ui-button class="broadcast-next" @click="next" :title="$t('prev')"><fa :icon="faAngleRight"/></ui-button> - </div> - </div> - <div class="broadcast-right"> - <p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p> - <h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1> - <p v-if="!fetching"> - <mfm v-if="announcements.length != 0" :text="announcements[i].text" :key="i"/> - <img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/> - <template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template> - </p> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; - -export default define({ - name: 'broadcast', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/broadcast.vue'), - data() { - return { - i: 0, - fetching: true, - announcements: [], - faAngleLeft, faAngleRight - }; - }, - mounted() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - this.fetching = false; - }); - }, - methods: { - next() { - if (this.i === this.announcements.length - 1) { - this.i = 0; - } else { - this.i++; - } - }, - prev() { - if (this.i === 0) { - this.i = this.announcements.length - 1; - } else { - this.i--; - } - }, - func() { - if (this.props.design === 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.anltbovirfeutcigvwgmgxipejaeozxi-body - display flex - padding 10px - background var(--announcementsBg) - - &[data-melt] - background transparent - - > .broadcast-left - width 32px - margin-right 8px - - > .icon - > svg - fill currentColor - color var(--announcementsTitle) - - > .wave - opacity 1 - - &.a - animation wave 20s ease-in-out 2.1s infinite - &.b - animation wave 20s ease-in-out 2s infinite - &.c - animation wave 20s ease-in-out 2s infinite - &.d - animation wave 20s ease-in-out 2.1s infinite - - @keyframes wave - 0% - opacity 1 - 1.5% - opacity 0 - 3.5% - opacity 0 - 5% - opacity 1 - 6.5% - opacity 0 - 8.5% - opacity 0 - 10% - opacity 1 - - > .broadcast-nav - display flex - flex-wrap wrap - padding 1px 0 2px - - > .broadcast-page - width 100% - color var(--announcementsTitle) - text-align center - font-size .6rem - - > .broadcast-prev, - > .broadcast-next - flex 1 - width 50% - display block - margin 0 - padding 0 - font-size .9rem - line-height 1.3em - color var(--link) - background transparent - cursor pointer - - &:focus - &:after - top -1px - right -1px - bottom -1px - left -1px - - &.round:focus:after - border-radius 5px - - > .broadcast-prev - padding-right 3px - - > .broadcast-next - padding-left 3px - - > .broadcast-right - flex 1 - word-break break-word - - > h1 - margin 0 - font-size .975em - font-weight normal - line-height 1.3em - color var(--announcementsTitle) - padding-bottom 2px - - > p - display block - z-index 1 - margin 0 - font-size .8em - color var(--announcementsText) - width 100% - - &.fetching - text-align center - - &[data-mobile] - > p - color #fff - -</style> diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue deleted file mode 100644 index 32ce1efeb7..0000000000 --- a/src/client/app/common/views/widgets/calendar.vue +++ /dev/null @@ -1,202 +0,0 @@ -<template> -<div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'"> - <ui-container :naked="props.design == 1" :show-header="false"> - <div class="mkw-calendar--body"> - <div class="calendar" :data-is-holiday="isHoliday"> - <p class="month-and-year"> - <span class="year">{{ this.$t('year').split('{}')[0] }}{{ year }}{{ this.$t('year').split('{}')[1] }}</span> - <span class="month">{{ this.$t('month').split('{}')[0] }}{{ month }}{{ this.$t('month').split('{}')[1] }}</span> - </p> - <p class="day">{{ this.$t('day').split('{}')[0] }}{{ day }}{{ this.$t('day').split('{}')[1] }}</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('this-month') }}<b>{{ monthP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${monthP}%` }"></div> - </div> - </div> - <div> - <p>{{ $t('this-year') }}<b>{{ yearP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${yearP}%` }"></div> - </div> - </div> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'calendar', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/calendar.vue'), - data() { - return { - now: new Date(), - year: null, - month: null, - day: null, - weekDay: null, - yearP: null, - dayP: null, - monthP: null, - isHoliday: null, - special: null, - clock: null - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - func() { - if (this.platform == 'mobile') return; - 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; - - this.special = - nm == 0 && nd == 1 ? 'on-new-years-day' : - false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-calendar - &[data-special='on-new-years-day'] - border-color #ef95a0 - - .mkw-calendar--body - padding 16px 0 - color var(--calendarDay) - - &:after - content "" - display block - clear both - - > .calendar - float left - width 60% - text-align center - - &[data-is-holiday] - > .day - color #ef95a0 - - > p - margin 0 - line-height 18px - font-size 14px - - > span - margin 0 4px - - > .day - margin 10px 0 - line-height 32px - font-size 28px - - > .info - display block - float left - width 40% - padding 0 16px 0 0 - - > div - margin-bottom 8px - - &:last-child - margin-bottom 4px - - > p - margin 0 0 2px 0 - font-size 12px - line-height 18px - color var(--text) - opacity 0.8 - - > b - margin-left 2px - - > .meter - width 100% - overflow hidden - background var(--materBg) - border-radius 8px - - > .val - height 4px - background var(--primary) - 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> diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue deleted file mode 100644 index b266d5f6e6..0000000000 --- a/src/client/app/common/views/widgets/hashtags.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<div class="mkw-hashtags"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="hashtag"/>{{ $t('title') }}</template> - - <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> - <mk-trends/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'hashtags', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('common/views/widgets/hashtags.vue'), - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> diff --git a/src/client/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts deleted file mode 100644 index d923a01941..0000000000 --- a/src/client/app/common/views/widgets/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Vue from 'vue'; - -import wAnalogClock from './analog-clock.vue'; -import wVersion from './version.vue'; -import wRss from './rss.vue'; -import wServer from './server.vue'; -import wPostsMonitor from './posts-monitor.vue'; -import wMemo from './memo.vue'; -import wBroadcast from './broadcast.vue'; -import wCalendar from './calendar.vue'; -import wPhotoStream from './photo-stream.vue'; -import wSlideshow from './slideshow.vue'; -import wTips from './tips.vue'; -import wNav from './nav.vue'; -import wHashtags from './hashtags.vue'; -import wInstance from './instance.vue'; -import wPostForm from './post-form.vue'; - -Vue.component('mkw-analog-clock', wAnalogClock); -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-broadcast', wBroadcast); -Vue.component('mkw-server', wServer); -Vue.component('mkw-posts-monitor', wPostsMonitor); -Vue.component('mkw-memo', wMemo); -Vue.component('mkw-rss', wRss); -Vue.component('mkw-version', wVersion); -Vue.component('mkw-hashtags', wHashtags); -Vue.component('mkw-instance', wInstance); -Vue.component('mkw-post-form', wPostForm); -Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default)); diff --git a/src/client/app/common/views/widgets/instance.vue b/src/client/app/common/views/widgets/instance.vue deleted file mode 100644 index 96d6184d1e..0000000000 --- a/src/client/app/common/views/widgets/instance.vue +++ /dev/null @@ -1,14 +0,0 @@ -<template> -<div class="mkw-instance"> - <ui-container> - <mk-instance/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'instance' -}); -</script> diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue deleted file mode 100644 index b3b668a9ad..0000000000 --- a/src/client/app/common/views/widgets/memo.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<div class="mkw-memo"> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="['far', 'sticky-note']"/>{{ $t('title') }}</template> - - <div class="mkw-memo--body"> - <textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> - <button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'memo', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('common/views/widgets/memo.vue'), - data() { - return { - text: null, - changed: false, - timeoutId: null - }; - }, - - 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="stylus" scoped> -.mkw-memo - .mkw-memo--body - 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 var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - - &:disabled - opacity 0.7 - cursor default - -</style> diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue deleted file mode 100644 index 2b8caa7be8..0000000000 --- a/src/client/app/common/views/widgets/nav.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<div class="mkw-nav"> - <ui-container> - <div class="mkw-nav--body"> - <mk-nav/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'nav' -}); -</script> - -<style lang="stylus" scoped> -.mkw-nav - .mkw-nav--body - padding 16px - font-size 12px - color var(--text) - background var(--face) - - a - color var(--text) - - i - color var(--text) - -</style> diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue deleted file mode 100644 index eae6d0a190..0000000000 --- a/src/client/app/common/views/widgets/photo-stream.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> -<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="camera"/>{{ $t('title') }}</template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div :class="$style.stream" v-if="!fetching && images.length > 0"> - <div v-for="(image, i) in images" :key="i" - :class="$style.img" - :style="`background-image: url(${thumbnail(image)})`" - draggable="true" - @dragstart="onDragstart(image, $event)" - ></div> - </div> - <p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import { getStaticImageUrl } from '../../scripts/get-static-image-url'; - -export default define({ - name: 'photo-stream', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/photo-stream.vue'), - - data() { - return { - images: [], - fetching: true, - connection: null - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('driveFileCreated', this.onDriveFileCreated); - - this.$root.api('drive/stream', { - type: 'image/*', - limit: 9 - }).then(images => { - this.images = images; - this.fetching = false; - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - 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++; - } - - this.save(); - }, - - onDragstart(file, e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_file', JSON.stringify(file)); - }, - - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - } -}); -</script> - -<style lang="stylus" module> -.root[data-melt] - .stream - padding 0 - - .img - border solid 4px transparent - border-radius 8px - -.stream - 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 var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue deleted file mode 100644 index 6680a11435..0000000000 --- a/src/client/app/common/views/widgets/post-form.vue +++ /dev/null @@ -1,294 +0,0 @@ -<template> -<div> - <ui-container :show-header="props.design == 0"> - <template #header><fa icon="pencil-alt"/>{{ $t('title') }}</template> - - <div class="lhcuptdmcdkfwmipgazeawoiuxpzaclc-body" - @dragover.stop="onDragover" - @drop.stop="onDrop" - > - <div class="textarea"> - <textarea - :disabled="posting" - v-model="text" - @keydown="onKeydown" - @paste="onPaste" - :placeholder="placeholder" - ref="text" - v-autocomplete="{ model: 'text' }" - ></textarea> - <button class="emoji" @click="emoji" ref="emoji" v-if="!$root.isMobile"> - <fa :icon="['far', 'laugh']"/> - </button> - </div> - <x-post-form-attaches class="files" :files="files" :detach-media-fn="detachMedia"/> - <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> - <mk-uploader ref="uploader" @uploaded="attachMedia"/> - <footer> - <button @click="chooseFile"><fa icon="upload"/></button> - <button @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button @click="post" :disabled="posting" class="post">{{ $t('note') }}</button> - </footer> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import insertTextAtCursor from 'insert-text-at-cursor'; -import { formatTimeString } from '../../../../../misc/format-time-string'; - -export default define({ - name: 'post-form', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('desktop/views/widgets/post-form.vue'), - - components: { - XPostFormAttaches: () => import('../components/post-form-attaches.vue').then(m => m.default) - }, - - data() { - return { - posting: false, - text: '', - files: [], - }; - }, - - computed: { - placeholder(): string { - const xs = [ - this.$t('@.note-placeholders.a'), - this.$t('@.note-placeholders.b'), - this.$t('@.note-placeholders.c'), - this.$t('@.note-placeholders.d'), - this.$t('@.note-placeholders.e'), - this.$t('@.note-placeholders.f') - ]; - return xs[Math.floor(Math.random() * xs.length)]; - } - }, - - methods: { - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - - chooseFile() { - (this.$refs.file as any).click(); - }, - - chooseFileFromDrive() { - this.$chooseDriveFile({ - multiple: true - }).then(files => { - for (const x of files) this.attachMedia(x); - }); - }, - - attachMedia(driveFile) { - this.files.push(driveFile); - this.$emit('change-attached-files', this.files); - }, - - detachMedia(id) { - this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-files', this.files); - }, - - onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post(); - }, - - async onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { - const file = item.getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - const name = this.$store.state.settings.pasteDialog - ? await this.$root.dialog({ - title: this.$t('@.post-form.enter-file-name'), - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); - } - } - }, - - onChangeFile() { - for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); - }, - - upload(file: File, name?: string) { - (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name); - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - if (isFile || isDriveFile) { - e.preventDefault(); - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDrop(e): void { - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) this.upload(x); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - e.preventDefault(); - } - //#endregion - }, - - async emoji() { - const Picker = await import('../../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default); - const button = this.$refs.emoji; - const rect = button.getBoundingClientRect(); - const vm = this.$root.new(Picker, { - x: button.offsetWidth + rect.left + window.pageXOffset, - y: rect.top + window.pageYOffset - }); - vm.$once('chosen', emoji => { - insertTextAtCursor(this.$refs.text, emoji); - }); - }, - - post() { - this.posting = true; - - let visibility = 'public'; - let localOnly = false; - - const m = this.$store.state.settings.defaultNoteVisibility.match(/^local-(.+)/); - if (m) { - visibility = m[1]; - localOnly = true; - } else { - visibility = this.$store.state.settings.defaultNoteVisibility; - } - - this.$root.api('notes/create', { - text: this.text == '' ? undefined : this.text, - fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, - visibility, - localOnly, - }).then(data => { - this.clear(); - }).catch(err => { - this.$root.dialog({ - type: 'error', - text: this.$t('something-happened') - }); - }).then(() => { - this.posting = false; - this.$nextTick(() => { - this.$refs.text.focus(); - }); - }); - }, - - clear() { - this.text = ''; - this.files = []; - } - } -}); -</script> - -<style lang="stylus" scoped> -.lhcuptdmcdkfwmipgazeawoiuxpzaclc-body - > .textarea - > .emoji - position absolute - top 0 - right 0 - padding 10px - font-size 18px - color var(--text) - opacity 0.5 - - &:hover - color var(--textHighlighted) - opacity 1 - - &:active - color var(--primary) - opacity 1 - - > textarea - display block - width 100% - max-width 100% - min-width 100% - padding 16px - color var(--desktopPostFormTextareaFg) - outline none - background var(--desktopPostFormTextareaBg) - border none - border-bottom solid 1px var(--faceDivider) - padding-right 30px - - &:focus - & + .emoji - opacity 0.7 - - > input[type=file] - display none - - > footer - display flex - padding 8px - - > button:not(.post) - color var(--text) - - &:hover - color var(--textHighlighted) - - > .post - display block - margin 0 0 0 auto - padding 0 10px - height 28px - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - -</style> diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue deleted file mode 100644 index 64c3b51540..0000000000 --- a/src/client/app/common/views/widgets/posts-monitor.vue +++ /dev/null @@ -1,203 +0,0 @@ -<template> -<div class="mkw-posts-monitor"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="chart-line"/>{{ $t('title') }}</template> - <template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template> - - <div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2"> - <defs> - <linearGradient :id="localGradientId" 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="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="localPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="localPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="localHeadX" - :cy="localHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/> - <text x="1" y="5">Local</text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1"> - <defs> - <linearGradient :id="fediGradientId" 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="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="fediPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="fediPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="fediHeadX" - :cy="fediHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/> - <text x="1" y="5">Fedi</text> - </svg> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import { v4 as uuid } from 'uuid'; - -export default define({ - name: 'posts-monitor', - props: () => ({ - design: 0, - view: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/posts-monitor.vue'), - - data() { - return { - connection: null, - viewBoxY: 30, - stats: [], - fediGradientId: uuid(), - fediMaskId: uuid(), - localGradientId: uuid(), - localMaskId: uuid(), - fediPolylinePoints: '', - localPolylinePoints: '', - fediPolygonPoints: '', - localPolygonPoints: '', - fediHeadX: null, - fediHeadY: null, - localHeadX: null, - localHeadY: null - }; - }, - computed: { - viewBoxX(): number { - return this.props.view == 0 ? 50 : 100; - } - }, - watch: { - viewBoxX() { - this.draw(); - } - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('notesStats'); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog',{ - id: Math.random().toString().substr(2, 8) - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - toggle() { - if (this.props.view == 2) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - draw() { - const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats; - const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1; - const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1; - - const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]); - const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]); - this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0]; - this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1]; - this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0]; - this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1]; - }, - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 100) this.stats.shift(); - this.draw(); - }, - onStatsLog(statsLog) { - for (const stats of statsLog) this.onStats(stats); - } - } -}); -</script> - -<style lang="stylus" scoped> -.qpdmibaztplkylerhdbllwcokyrfxeyj - &.dual - > svg - width 50% - float left - - &:first-child - padding-right 5px - - &:last-child - padding-left 5px - - > svg - display block - padding 10px - width 100% - - > text - font-size 5px - fill var(--chartCaption) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/queue.vue b/src/client/app/common/views/widgets/queue.vue deleted file mode 100644 index 6e49f1efb0..0000000000 --- a/src/client/app/common/views/widgets/queue.vue +++ /dev/null @@ -1,173 +0,0 @@ -<template> -<div> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="faTasks"/>Queue</template> - - <div class="mntrproz"> - <div> - <b>In</b> - <span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span> - <div ref="in"></div> - </div> - <div> - <b>Out</b> - <span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span> - <div ref="out"></div> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../define-widget'; -import { faTasks } from '@fortawesome/free-solid-svg-icons'; -import ApexCharts from 'apexcharts'; - -export default define({ - name: 'queue', - props: () => ({ - compact: false - }) -}).extend({ - data() { - return { - stats: [], - inChart: null, - outChart: null, - faTasks - }; - }, - - watch: { - stats(stats) { - this.inChart.updateSeries([{ - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) - }, { - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.inbox.active })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) - }]); - this.outChart.updateSeries([{ - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) - }, { - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.deliver.active })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) - }]); - } - }, - - computed: { - latestStats(): any { - return this.stats[this.stats.length - 1]; - } - }, - - mounted() { - const chartOpts = { - chart: { - type: 'area', - height: 70, - animations: { - dynamicAnimation: { - enabled: false - } - }, - sparkline: { - enabled: true, - } - }, - dataLabels: { - enabled: false - }, - tooltip: { - enabled: false - }, - stroke: { - curve: 'straight', - width: 1 - }, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any, - yaxis: { - min: 0, - } - }; - - this.inChart = new ApexCharts(this.$refs.in, chartOpts); - this.outChart = new ApexCharts(this.$refs.out, chartOpts); - - this.inChart.render(); - this.outChart.render(); - - const connection = this.$root.stream.useSharedConnection('queueStats'); - connection.on('stats', this.onStats); - connection.on('statsLog', this.onStatsLog); - connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 50 - }); - - this.$once('hook:beforeDestroy', () => { - connection.dispose(); - this.inChart.destroy(); - this.outChart.destroy(); - }); - }, - - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mntrproz - display flex - padding 4px - - > div - width 50% - padding 4px - - > b - display block - font-size 12px - color var(--text) - - > span - position absolute - top 4px - right 4px - opacity 0.7 - font-size 12px - color var(--text) - -</style> diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue deleted file mode 100644 index c1a66bfebb..0000000000 --- a/src/client/app/common/views/widgets/rss.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> -<div class="mkw-rss"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="rss-square"/>RSS</template> - <template #func><button title="設定" @click="setting"><fa icon="cog"/></button></template> - - <div class="mkw-rss--body" :data-mobile="platform == 'mobile'"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <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> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'rss', - props: () => ({ - compact: false, - url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews' - }) -}).extend({ - i18n: i18n(), - data() { - return { - 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; - 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="stylus" scoped> -.mkw-rss - .mkw-rss--body - .feed - padding 12px 16px - font-size 0.9em - - > a - display block - padding 4px 0 - color var(--text) - border-bottom dashed var(--lineWidth) var(--faceDivider) - white-space nowrap - text-overflow ellipsis - overflow hidden - - &:last-child - border-bottom none - - .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - - &[data-mobile] - background var(--face) - - .feed - padding 0 - - > a - padding 8px 16px - border-bottom none - - &:nth-child(even) - background rgba(#000, 0.05) - -</style> diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue deleted file mode 100644 index 799773a70c..0000000000 --- a/src/client/app/common/views/widgets/server.cpu-memory.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="cpu-memory"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <defs> - <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="cpuPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="cpuPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="cpuHeadX" - :cy="cpuHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> - <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <defs> - <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="memPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="memPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="memHeadX" - :cy="memHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> - <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> - </svg> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - props: ['connection'], - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - cpuGradientId: uuid(), - cpuMaskId: uuid(), - memGradientId: uuid(), - memMaskId: uuid(), - cpuPolylinePoints: '', - memPolylinePoints: '', - cpuPolygonPoints: '', - memPolygonPoints: '', - cpuHeadX: null, - cpuHeadY: null, - memHeadX: null, - memHeadY: null, - cpuP: '', - memP: '' - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]); - const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]); - this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - - this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; - this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; - this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0]; - this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1]; - - this.cpuP = (stats.cpu_usage * 100).toFixed(0); - this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - }, - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) this.onStats(stats); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpu-memory - > svg - display block - padding 10px - width 50% - float left - - &:first-child - padding-right 5px - - &:last-child - padding-left 5px - - > text - font-size 5px - fill var(--chartCaption) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue deleted file mode 100644 index c08971e11c..0000000000 --- a/src/client/app/common/views/widgets/server.cpu.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="cpu"> - <x-pie class="pie" :value="usage"/> - <div> - <p><fa icon="microchip"/>CPU</p> - <p>{{ meta.cpu.cores }} Logical cores</p> - <p>{{ meta.cpu.model }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection', 'meta'], - data() { - return { - usage: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.cpu_usage; - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpu - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue deleted file mode 100644 index 039c4f5c29..0000000000 --- a/src/client/app/common/views/widgets/server.disk.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="disk"> - <x-pie class="pie" :value="usage"/> - <div> - <p><fa :icon="['far', 'hdd']"/>Storage</p> - <p>Total: {{ total | bytes(1) }}</p> - <p>Free: {{ available | bytes(1) }}</p> - <p>Used: {{ used | bytes(1) }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection'], - data() { - return { - usage: 0, - total: 0, - used: 0, - available: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - stats.disk.used = stats.disk.total - stats.disk.free; - this.usage = stats.disk.used / stats.disk.total; - this.total = stats.disk.total; - this.used = stats.disk.used; - this.available = stats.disk.available; - } - } -}); -</script> - -<style lang="stylus" scoped> -.disk - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue deleted file mode 100644 index c6e0d68b11..0000000000 --- a/src/client/app/common/views/widgets/server.info.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div class="info"> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - <p>Machine: {{ meta.machine }}</p> - <p>Node: {{ meta.node }}</p> - <p>PSQL: {{ meta.psql }}</p> - <p>Redis: {{ meta.redis }}</p> - <p>Version: {{ meta.version }} </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['meta'] -}); -</script> - -<style lang="stylus" scoped> -.info - padding 10px 14px - - > p - margin 0 - font-size 12px - color var(--text) -</style> diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue deleted file mode 100644 index c3b2f3a101..0000000000 --- a/src/client/app/common/views/widgets/server.memory.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="memory"> - <x-pie class="pie" :value="usage"/> - <div> - <p><fa icon="memory"/>Memory</p> - <p>Total: {{ total | bytes(1) }}</p> - <p>Used: {{ used | bytes(1) }}</p> - <p>Free: {{ free | bytes(1) }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection'], - data() { - return { - usage: 0, - total: 0, - used: 0, - free: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - stats.mem.free = stats.mem.total - stats.mem.used; - this.usage = stats.mem.used / stats.mem.total; - this.total = stats.mem.total; - this.used = stats.mem.used; - this.free = stats.mem.free; - } - } -}); -</script> - -<style lang="stylus" scoped> -.memory - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue deleted file mode 100644 index ce342fd41b..0000000000 --- a/src/client/app/common/views/widgets/server.pie.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<svg viewBox="0 0 1 1" preserveAspectRatio="none"> - <circle - :r="r" - cx="50%" cy="50%" - fill="none" - stroke-width="0.1" - stroke="rgba(0, 0, 0, 0.05)"/> - <circle - :r="r" - cx="50%" cy="50%" - :stroke-dasharray="Math.PI * (r * 2)" - :stroke-dashoffset="strokeDashoffset" - fill="none" - stroke-width="0.1" - :stroke="color"/> - <text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - type: Number, - required: true - } - }, - data() { - return { - r: 0.4 - }; - }, - computed: { - color(): string { - return `hsl(${180 - (this.value * 180)}, 80%, 70%)`; - }, - strokeDashoffset(): number { - return (1 - this.value) * (Math.PI * (this.r * 2)); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - height 100% - - > circle - transform-origin center - transform rotate(-90deg) - transition stroke-dashoffset 0.5s ease - - > text - font-size 0.15px - fill var(--chartCaption) - -</style> diff --git a/src/client/app/common/views/widgets/server.uptimes.vue b/src/client/app/common/views/widgets/server.uptimes.vue deleted file mode 100644 index 0da5c4ec50..0000000000 --- a/src/client/app/common/views/widgets/server.uptimes.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="uptimes"> - <p>Uptimes</p> - <p>Process: {{ process }}</p> - <p>OS: {{ os }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import formatUptime from '../../scripts/format-uptime'; - -export default Vue.extend({ - props: ['connection'], - data() { - return { - process: 0, - os: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.process = formatUptime(stats.process_uptime); - this.os = formatUptime(stats.os_uptime); - } - } -}); -</script> - -<style lang="stylus" scoped> -.uptimes - padding 10px 14px - - > p - margin 0 - font-size 12px - color var(--text) - - &:first-child - font-weight bold -</style> diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue deleted file mode 100644 index 90a0f0171b..0000000000 --- a/src/client/app/common/views/widgets/server.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<div class="mkw-server"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="server"/>{{ $t('title') }}</template> - <template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.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> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -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({ - i18n: i18n('common/views/widgets/server.vue'), - - components: { - XCpuMemory, - XCpu, - XMemory, - XDisk, - XUptimes, - XInfo - }, - data() { - return { - fetching: true, - meta: null, - connection: null - }; - }, - mounted() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.fetching = false; - }); - - this.connection = this.$root.stream.useSharedConnection('serverStats'); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - toggle() { - if (this.props.view == 5) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue deleted file mode 100644 index 23ccb9da6b..0000000000 --- a/src/client/app/common/views/widgets/slideshow.vue +++ /dev/null @@ -1,165 +0,0 @@ -<template> -<div class="mkw-slideshow" :data-mobile="platform == 'mobile'"> - <div @click="choose"> - <p v-if="props.folder === undefined"> - <template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template> - <template v-else>{{ $t('folder') }}</template> - </p> - <p v-if="props.folder !== undefined && images.length == 0 && !fetching">{{ $t('no-image') }}</p> - <div ref="slideA" class="slide a"></div> - <div ref="slideB" class="slide b"></div> - </div> -</div> -</template> - -<script lang="ts"> -import anime from 'animejs'; -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'slideshow', - props: () => ({ - folder: undefined, - size: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/slideshow.vue'), - - data() { - return { - images: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.$nextTick(() => { - this.applySize(); - }); - - if (this.props.folder !== undefined) { - this.fetch(); - } - - this.clock = setInterval(this.change, 10000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - func() { - this.resize(); - }, - applySize() { - let h; - - if (this.props.size == 1) { - h = 250; - } else { - h = 170; - } - - this.$el.style.height = `${h}px`; - }, - resize() { - if (this.props.size == 1) { - this.props.size = 0; - } else { - this.props.size++; - } - this.save(); - - this.applySize(); - }, - change() { - if (this.images.length == 0) return; - - const index = Math.floor(Math.random() * this.images.length); - const img = `url(${ this.images[index].url })`; - - (this.$refs.slideB as any).style.backgroundImage = img; - - anime({ - targets: this.$refs.slideB, - opacity: 1, - duration: 1000, - easing: 'linear', - complete: () => { - // 既にこのウィジェットがunmountされていたら要素がない - if ((this.$refs.slideA as any) == null) return; - - (this.$refs.slideA as any).style.backgroundImage = img; - anime({ - targets: this.$refs.slideB, - opacity: 0, - duration: 0 - }); - } - }); - }, - fetch() { - this.fetching = true; - - this.$root.api('drive/files', { - folderId: this.props.folder, - type: 'image/*', - limit: 100 - }).then(images => { - this.images = images; - this.fetching = false; - (this.$refs.slideA as any).style.backgroundImage = ''; - (this.$refs.slideB as any).style.backgroundImage = ''; - this.change(); - }); - }, - choose() { - this.$chooseDriveFolder().then(folder => { - this.props.folder = folder ? folder.id : null; - this.save(); - this.fetch(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-slideshow - overflow hidden - background #fff - border solid 1px rgba(#000, 0.075) - border-radius 6px - - &[data-mobile] - border none - border-radius 8px - box-shadow 0 0 0 1px rgba(#000, 0.2) - - > div - width 100% - height 100% - cursor pointer - - > p - display block - margin 1em - text-align center - color #888 - - > * - pointer-events none - - > .slide - position absolute - top 0 - left 0 - width 100% - height 100% - background-size cover - background-position center - - &.b - opacity 0 - -</style> diff --git a/src/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue deleted file mode 100644 index 9e047ef47c..0000000000 --- a/src/client/app/common/views/widgets/tips.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="mkw-tips"> - <p ref="tip"><fa :icon="['far', 'lightbulb']"/><span v-html="tip"></span></p> -</div> -</template> - -<script lang="ts"> -import anime from 'animejs'; -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'tips' -}).extend({ - i18n: i18n('common/views/widgets/tips.vue'), - - data() { - return { - tips: [], - tip: null, - clock: null - }; - }, - created() { - this.tips = [ - this.$t('tips-line1'), - this.$t('tips-line2'), - this.$t('tips-line3'), - this.$t('tips-line4'), - this.$t('tips-line5'), - this.$t('tips-line6'), - this.$t('tips-line7'), - this.$t('tips-line8'), - this.$t('tips-line9'), - this.$t('tips-line10'), - this.$t('tips-line11'), - this.$t('tips-line13'), - this.$t('tips-line14'), - this.$t('tips-line17'), - this.$t('tips-line19'), - this.$t('tips-line20'), - this.$t('tips-line21'), - this.$t('tips-line23'), - this.$t('tips-line24'), - this.$t('tips-line25') - ]; - }, - mounted() { - this.$nextTick(() => { - this.set(); - }); - - this.clock = setInterval(this.change, 20000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - set() { - this.tip = this.tips[Math.floor(Math.random() * this.tips.length)]; - }, - change() { - anime({ - targets: this.$refs.tip, - opacity: 0, - duration: 500, - easing: 'linear', - complete: this.set - }); - - setTimeout(() => { - anime({ - targets: this.$refs.tip, - opacity: 1, - duration: 500, - easing: 'linear' - }); - }, 500); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-tips - overflow visible !important - opacity 0.8 - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color var(--text) - - > [data-icon] - margin-right 4px - - kbd - display inline - padding 0 6px - margin 0 2px - font-size 1em - font-family inherit - border solid 1px var(--text) - border-radius 2px - -</style> diff --git a/src/client/app/common/views/widgets/version.vue b/src/client/app/common/views/widgets/version.vue deleted file mode 100644 index e8f6c08f34..0000000000 --- a/src/client/app/common/views/widgets/version.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<p>ver {{ version }} ({{ codename }})</p> -</template> - -<script lang="ts"> -import { version, codename } from '../../../config'; -import define from '../../../common/define-widget'; -export default define({ - name: 'version' -}).extend({ - data() { - return { - version, - codename - }; - } -}); -</script> - -<style lang="stylus" scoped> -p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color var(--text) - opacity 0.8 - -</style> diff --git a/src/client/app/config.ts b/src/client/app/config.ts deleted file mode 100644 index 55c0c6b3a5..0000000000 --- a/src/client/app/config.ts +++ /dev/null @@ -1,20 +0,0 @@ -declare const _LANGS_: string[]; -declare const _COPYRIGHT_: string; -declare const _VERSION_: string; -declare const _CODENAME_: string; -declare const _ENV_: string; - -const address = new URL(location.href); - -export const host = address.host; -export const hostname = address.hostname; -export const url = address.origin; -export const apiUrl = url + '/api'; -export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; -export const lang = localStorage.getItem('lang') || window.lang; // windowは後方互換性のため -export const langs = _LANGS_; -export const locale = JSON.parse(localStorage.getItem('locale')); -export const copyright = _COPYRIGHT_; -export const version = _VERSION_; -export const codename = _CODENAME_; -export const env = _ENV_; diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts deleted file mode 100644 index 6b88b51ef1..0000000000 --- a/src/client/app/desktop/api/update-avatar.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { apiUrl, locale } from '../../config'; -import ProgressDialog from '../views/components/progress-dialog.vue'; - -export default ($root: any) => { - - const cropImage = file => new Promise(async (resolve, reject) => { - const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default); - const w = $root.new(CropWindow, { - image: file, - title: locale['desktop']['avatar-crop-title'], - aspectRatio: 1 / 1 - }); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', $root.$store.state.i.token); - data.append('file', blob, file.name + '.cropped.png'); - - $root.api('drive/folders/find', { - name: locale['desktop']['avatar'] - }).then(avatarFolder => { - if (avatarFolder.length === 0) { - $root.api('drive/folders/create', { - name: locale['desktop']['avatar'] - }).then(iconFolder => { - resolve(upload(data, iconFolder)); - }); - } else { - resolve(upload(data, avatarFolder[0])); - } - }); - }); - - w.$once('skipped', () => { - resolve(file); - }); - - w.$once('cancelled', reject); - - document.body.appendChild(w.$el); - }); - - const upload = (data, folder) => new Promise((resolve, reject) => { - const dialog = $root.new(ProgressDialog, { - title: locale['desktop']['uploading-avatar'] - }); - document.body.appendChild(dialog.$el); - - if (folder) data.append('folderId', folder.id); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = e => { - const file = JSON.parse((e.target as any).response); - (dialog as any).close(); - resolve(file); - }; - xhr.onerror = reject; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); - }; - - xhr.send(data); - }); - - const setAvatar = file => { - return $root.api('i/update', { - avatarId: file.id - }).then(i => { - $root.$store.commit('updateIKeyValue', { - key: 'avatarId', - value: i.avatarId - }); - $root.$store.commit('updateIKeyValue', { - key: 'avatarUrl', - value: i.avatarUrl - }); - - $root.dialog({ - title: locale['desktop']['avatar-updated'], - text: null - }); - - return i; - }).catch(err => { - switch (err.id) { - case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191': - $root.dialog({ - type: 'error', - title: locale['desktop']['unable-to-process'], - text: locale['desktop']['invalid-filetype'] - }); - break; - default: - $root.dialog({ - type: 'error', - text: locale['desktop']['unable-to-process'] - }); - } - }); - }; - - return (file = null) => { - const selectedFile = file - ? Promise.resolve(file) - : $root.$chooseDriveFile({ - multiple: false, - type: 'image/*', - title: locale['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 deleted file mode 100644 index 09632b1941..0000000000 --- a/src/client/app/desktop/api/update-banner.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { apiUrl, locale } from '../../config'; -import ProgressDialog from '../views/components/progress-dialog.vue'; - -export default ($root: any) => { - - const cropImage = file => new Promise(async (resolve, reject) => { - const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default); - const w = $root.new(CropWindow, { - image: file, - title: locale['desktop']['banner-crop-title'], - aspectRatio: 16 / 9 - }); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', $root.$store.state.i.token); - data.append('file', blob, file.name + '.cropped.png'); - - $root.api('drive/folders/find', { - name: locale['desktop']['banner'] - }).then(bannerFolder => { - if (bannerFolder.length === 0) { - $root.api('drive/folders/create', { - name: locale['desktop']['banner'] - }).then(iconFolder => { - resolve(upload(data, iconFolder)); - }); - } else { - resolve(upload(data, bannerFolder[0])); - } - }); - }); - - w.$once('skipped', () => { - resolve(file); - }); - - w.$once('cancelled', reject); - - document.body.appendChild(w.$el); - }); - - const upload = (data, folder) => new Promise((resolve, reject) => { - const dialog = $root.new(ProgressDialog, { - title: locale['desktop']['uploading-banner'] - }); - document.body.appendChild(dialog.$el); - - if (folder) data.append('folderId', folder.id); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = e => { - const file = JSON.parse((e.target as any).response); - (dialog as any).close(); - resolve(file); - }; - xhr.onerror = reject; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); - }; - - xhr.send(data); - }); - - const setBanner = file => { - return $root.api('i/update', { - bannerId: file.id - }).then(i => { - $root.$store.commit('updateIKeyValue', { - key: 'bannerId', - value: i.bannerId - }); - $root.$store.commit('updateIKeyValue', { - key: 'bannerUrl', - value: i.bannerUrl - }); - - $root.dialog({ - title: locale['desktop']['banner-updated'], - text: null - }); - - return i; - }).catch(err => { - switch (err.id) { - case '75aedb19-2afd-4e6d-87fc-67941256fa60': - $root.dialog({ - type: 'error', - title: locale['desktop']['unable-to-process'], - text: locale['desktop']['invalid-filetype'] - }); - break; - default: - $root.dialog({ - type: 'error', - text: locale['desktop']['unable-to-process'] - }); - } - }); - }; - - return (file = null) => { - const selectedFile = file - ? Promise.resolve(file) - : $root.$chooseDriveFile({ - multiple: false, - type: 'image/*', - title: locale['desktop']['choose-banner'] - }); - - return selectedFile - .then(cropImage) - .then(setBanner) - .catch(err => err && console.warn(err)); - }; -}; diff --git a/src/client/app/desktop/assets/grid.svg b/src/client/app/desktop/assets/grid.svg deleted file mode 100644 index d1d72cd8ce..0000000000 --- a/src/client/app/desktop/assets/grid.svg +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="32" - height="32" - viewBox="0 0 8.4666665 8.4666669" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="grid.svg"> - <defs - id="defs2" /> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="22.4" - inkscape:cx="14.687499" - inkscape:cy="14.558219" - inkscape:document-units="px" - inkscape:current-layer="layer1" - showgrid="true" - units="px" - showguides="true" - inkscape:window-width="1920" - inkscape:window-height="1017" - inkscape:window-x="-8" - inkscape:window-y="1072" - inkscape:window-maximized="1"> - <inkscape:grid - type="xygrid" - id="grid3680" - empspacing="8" - empcolor="#ff3fff" - empopacity="0.41176471" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="レイヤー 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-288.53331)"> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 0,296.99998 v -8.46667 h 8.4666666 l 10e-8,0.26458 H 0.26458333 l 0,8.20209 z" - id="path3684" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,292.23748 h 0.2645833 v 0.52916 h 0.5291667 l 0,0.26459 H 4.4979167 v 0.52917 H 4.2333334 v -0.52917 H 3.7041667 l 0,-0.26459 h 0.5291667 z" - id="path4491" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 3.4395833,292.76664 0,0.26459 H 2.38125 l 0,-0.26459 z" - id="path4493" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 6.3499999,292.76664 10e-8,0.26459 H 5.2916667 l -1e-7,-0.26459 z" - id="path4493-2" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 7.6729167,292.76664 v 0.26459 H 6.6145834 v -0.26459 z" - id="path4493-6" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 2.1166666,292.76664 1e-7,0.26459 H 1.0583334 l -1e-7,-0.26459 z" - id="path4493-1" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333333,291.97289 0.2645834,0 v -1.05833 l -0.2645834,0 z" - id="path4522" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,290.64997 0.2645833,1e-5 v -1.05833 l -0.2645833,-1e-5 z" - id="path4522-7" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,294.88331 h 0.2645833 v -1.05833 H 4.2333334 Z" - id="path4522-5" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333333,296.20622 h 0.2645833 v -1.05833 H 4.2333333 Z" - id="path4522-74" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,289.32706 0.2645834,10e-6 -10e-8,-0.52918 -0.2645834,-10e-6 z" - id="path4522-7-4" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333332,296.99998 h 0.2645835 l 0,-0.52917 H 4.2333333 Z" - id="path4522-7-4-4" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 0.79375,292.76664 -3e-8,0.26459 -0.52916667,0 3e-8,-0.26459 z" - id="path4493-1-7" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 8.4666667,292.76664 v 0.26459 l -0.5291667,0 v -0.26459 z" - id="path4493-1-7-2" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - </g> -</svg> diff --git a/src/client/app/desktop/assets/header-icon.svg b/src/client/app/desktop/assets/header-icon.svg deleted file mode 100644 index d677d2d163..0000000000 --- a/src/client/app/desktop/assets/header-icon.svg +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="512" - height="512" - viewBox="0 0 135.46667 135.46667" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="header-icon.dark.svg" - inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png" - inkscape:export-xdpi="6" - inkscape:export-ydpi="6"> - <defs - id="defs2"> - <inkscape:path-effect - effect="simplify" - id="path-effect5115" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - <inkscape:path-effect - effect="simplify" - id="path-effect5111" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - <inkscape:path-effect - effect="simplify" - id="path-effect5104" - is_visible="true" - steps="1" - threshold="0.000408163" - smooth_angles="360" - helper_size="0" - simplify_individual_paths="false" - simplify_just_coalesce="false" - simplifyindividualpaths="false" - simplifyJustCoalesce="false" /> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="1.4142136" - inkscape:cx="114.309" - inkscape:cy="251.50613" - inkscape:document-units="px" - inkscape:current-layer="g4502" - showgrid="true" - units="px" - inkscape:snap-bbox="true" - inkscape:bbox-nodes="true" - inkscape:snap-bbox-edge-midpoints="false" - inkscape:snap-smooth-nodes="true" - inkscape:snap-center="true" - inkscape:snap-page="true" - inkscape:window-width="1920" - inkscape:window-height="1027" - inkscape:window-x="-8" - inkscape:window-y="1072" - inkscape:window-maximized="1" - inkscape:snap-object-midpoints="true" - inkscape:snap-midpoints="true" - inkscape:object-paths="true" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" - objecttolerance="1" - guidetolerance="1" - inkscape:snap-nodes="false" - inkscape:snap-others="false"> - <inkscape:grid - type="xygrid" - id="grid4504" - spacingx="4.2333334" - spacingy="4.2333334" - empcolor="#ff3fff" - empopacity="0.25098039" - empspacing="4" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="レイヤー 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-30.809093,-111.78601)"> - <g - id="g4502" - transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)"> - <g - style="fill-opacity:1" - transform="translate(-1.3333333e-6,-1.3439941e-6)" - id="g5125"> - <g - transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" - id="text4489" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - aria-label="Mi"> - <path - sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" - inkscape:connector-curvature="0" - id="path5210" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px" - d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> - <path - inkscape:connector-curvature="0" - id="path5212" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px" - d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> - </g> - </g> - </g> - </g> -</svg> diff --git a/src/client/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg Binary files differdeleted file mode 100644 index c054188159..0000000000 --- a/src/client/app/desktop/assets/index.jpg +++ /dev/null diff --git a/src/client/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png Binary files differdeleted file mode 100644 index c2e222a0fc..0000000000 --- a/src/client/app/desktop/assets/remove.png +++ /dev/null diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts deleted file mode 100644 index 914e162c9a..0000000000 --- a/src/client/app/desktop/script.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Desktop Client - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import composeNotification from '../common/scripts/compose-notification'; - -import MkHome from './views/home/home.vue'; -import MkSelectDrive from './views/pages/selectdrive.vue'; -import MkDrive from './views/pages/drive.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkReversi from './views/pages/games/reversi.vue'; -import MkShare from '../common/views/pages/share.vue'; -import MkFollow from '../common/views/pages/follow.vue'; -import MkNotFound from '../common/views/pages/not-found.vue'; -import MkSettings from './views/pages/settings.vue'; -import DeckColumn from '../common/views/deck/deck.column-template.vue'; - -import Ctx from './views/components/context-menu.vue'; -import RenoteFormWindow from './views/components/renote-form-window.vue'; -import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue'; -import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue'; -import MkHomeTimeline from './views/home/timeline.vue'; -import Notification from './views/components/ui-notification.vue'; - -import { url } from '../config'; -import MiOS from '../mios'; - -/** - * init - */ -init(async (launch, os) => { - Vue.mixin({ - methods: { - $contextmenu(e, menu, opts?) { - const o = opts || {}; - const vm = this.$root.new(Ctx, { - menu, - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset, - }); - vm.$once('closed', () => { - if (o.closed) o.closed(); - }); - }, - - $post(opts) { - const o = opts || {}; - if (o.renote) { - const vm = this.$root.new(RenoteFormWindow, { - note: o.renote, - animation: o.animation == null ? true : o.animation - }); - if (o.cb) vm.$once('closed', o.cb); - } else { - this.$root.newAsync(() => import('./views/components/post-form-window.vue').then(m => m.default), { - reply: o.reply, - mention: o.mention, - animation: o.animation == null ? true : o.animation, - initialText: o.initialText, - instant: o.instant, - initialNote: o.initialNote, - }).then(vm => { - if (o.cb) vm.$once('closed', o.cb); - }); - } - }, - - $chooseDriveFile(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - - if (document.body.clientWidth > 800) { - const w = this.$root.new(MkChooseFileFromDriveWindow, { - title: o.title, - type: o.type, - multiple: o.multiple, - initFolder: o.currentFolder - }); - w.$once('selected', file => { - res(file); - }); - } else { - window['cb'] = file => { - res(file); - }; - - window.open(url + `/selectdrive?multiple=${o.multiple}`, - 'choose_drive_window', - 'height=500, width=800'); - } - }); - }, - - $chooseDriveFolder(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const w = this.$root.new(MkChooseFolderFromDriveWindow, { - title: o.title, - initFolder: o.currentFolder - }); - w.$once('selected', folder => { - res(folder); - }); - }); - }, - - $notify(message) { - this.$root.new(Notification, { - message - }); - } - } - }); - - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - os.store.state.device.inDeckMode - ? { path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ - { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, - { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, - { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, - { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) }, - ]} - : { path: '/', component: MkHome, children: [ - { path: '', name: 'index', component: MkHomeTimeline }, - { path: '/@:user', component: () => import('./views/home/user/index.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('./views/home/user/user.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) }, - { path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: () => import('../common/views/pages/featured.vue').then(m => m.default), props: { platform: 'desktop' } }, - { path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, - { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, - { path: '/i/favorites', component: () => import('../common/views/pages/favorites.vue').then(m => m.default), props: { platform: 'desktop' } }, - { path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, - { path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }, - { path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) }, - { path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }, - { path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) }, - { path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }, - { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, - { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/@:user/pages/:page', component: () => import('../common/views/pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - ]}, - { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, - { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/i/messaging/group/:group', component: MkMessagingRoom }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/settings', redirect: '/i/settings/profile' }, - { path: '/i/settings/:page', component: MkSettings }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) }, - { path: '/share', component: MkShare }, - { path: '/games/reversi/:game?', component: MkReversi }, - { path: '/authorize-follow', component: MkFollow }, - { path: '/deck', redirect: '/' }, - { path: '*', component: MkNotFound } - ], - scrollBehavior(to, from, savedPosition) { - return { x: 0, y: 0 }; - } - }); - - // Launch the app - const [app, _] = launch(router); - - /** - * Init Notification - */ - if ('Notification' in window && os.store.getters.isSignedIn) { - // 許可を得ていなかったらリクエスト - if ((Notification as any).permission == 'default') { - await Notification.requestPermission(); - } - - if ((Notification as any).permission == 'granted') { - registerNotifications(os); - } - } -}, true); - -function registerNotifications(os: MiOS) { - const stream = os.stream; - - if (stream == null) return; - - const connection = stream.useSharedConnection('main'); - - connection.on('notification', notification => { - const _n = composeNotification('notification', notification); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('driveFileCreated', file => { - const _n = composeNotification('driveFileCreated', file); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 5000); - }); - - connection.on('unreadMessagingMessage', message => { - const _n = composeNotification('unreadMessagingMessage', message); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - n.onclick = () => { - n.close(); - /*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { - user: message.user - });*/ - }; - setTimeout(n.close.bind(n), 7000); - }); - - connection.on('reversiInvited', matching => { - const _n = composeNotification('reversiInvited', matching); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - }); -} diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl deleted file mode 100644 index 249d3db2ed..0000000000 --- a/src/client/app/desktop/style.styl +++ /dev/null @@ -1,40 +0,0 @@ -@import "../app" -@import "../reset" - -*::input-placeholder - color #D8CBC5 - -*:focus - outline none - -html - height 100% - background var(--bg) - - &, div, textarea - scrollbar-width thin - - &, * - scrollbar-color var(--scrollbarHandle) var(--scrollbarTrack) - - &:hover - scrollbar-color var(--scrollbarHandleHover) var(--scrollbarTrack) - - &:active - scrollbar-color var(--primary) var(--scrollbarTrack) - - &::-webkit-scrollbar - width 6px - height 6px - - &::-webkit-scrollbar-track - background var(--scrollbarTrack) - - &::-webkit-scrollbar-thumb - background var(--scrollbarHandle) - - &:hover - background var(--scrollbarHandleHover) - - &:active - background var(--primary) diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue deleted file mode 100644 index da74a97f68..0000000000 --- a/src/client/app/desktop/views/components/activity.calendar.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<svg viewBox="0 0 21 7"> - <rect v-for="record in data" class="day" - width="1" height="1" - :x="record.x" :y="record.date.weekday" - rx="1" ry="1" - fill="transparent"> - <title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title> - </rect> - <rect v-for="record in data" class="day" - :width="record.v" :height="record.v" - :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" - rx="1" ry="1" - :fill="record.color" - style="pointer-events: none;"/> - <rect class="today" - width="1" height="1" - :x="data[0].x" :y="data[0].date.weekday" - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['data'], - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - - let x = 20; - this.data.slice().forEach((d, i) => { - d.x = x; - - const date = new Date(year, month, day - i); - d.date = { - year: date.getFullYear(), - month: date.getMonth(), - day: date.getDate(), - weekday: date.getDay() - }; - - d.v = peak == 0 ? 0 : d.total / (peak / 2); - if (d.v > 1) d.v = 1; - const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - - if (d.date.weekday == 0) x--; - }); - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - - > rect - transform-origin center - - &.day - &:hover - fill rgba(#000, 0.05) - -</style> diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue deleted file mode 100644 index 648b64a3fe..0000000000 --- a/src/client/app/desktop/views/components/activity.chart.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown"> - <title>{{ $t('total') }}<br/>{{ $t('notes') }}<br/>{{ $t('replies') }}<br/>{{ $t('renotes') }}</title> - <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'; -import i18n from '../../../i18n'; - -function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/activity.chart.vue'), - props: ['data'], - data() { - return { - viewBoxX: 147, - viewBoxY: 60, - zoom: 1, - pos: 0, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - - this.render(); - }, - methods: { - render() { - const peak = Math.max.apply(null, this.data.map(d => d.total)); - if (peak != 0) { - const data = this.data.slice().reverse(); - this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - }, - onMousedown(e) { - const clickX = e.clientX; - const clickY = e.clientY; - const baseZoom = this.zoom; - const basePos = this.pos; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; - - this.zoom = baseZoom + (-moveTop / 20); - this.pos = basePos + moveLeft; - if (this.zoom < 1) this.zoom = 1; - if (this.pos > 0) this.pos = 0; - if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); - - this.render(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - cursor all-scroll - -</style> diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue deleted file mode 100644 index 2cac125041..0000000000 --- a/src/client/app/desktop/views/components/activity.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="mk-activity"> - <ui-container :show-header="design == 0" :naked="design == 2"> - <template #header><fa icon="chart-bar"/>{{ $t('title') }}</template> - <template #func><button :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button></template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <template v-else> - <x-calendar v-show="view == 0" :data="[].concat(activity)"/> - <x-chart v-show="view == 1" :data="[].concat(activity)"/> - </template> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XCalendar from './activity.calendar.vue'; -import XChart from './activity.chart.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/activity.vue'), - components: { - XCalendar, - XChart - }, - props: { - design: { - default: 0 - }, - initView: { - default: 0 - }, - user: { - type: Object, - required: true - } - }, - data() { - return { - fetching: true, - activity: null, - view: this.initView - }; - }, - mounted() { - this.$root.api('charts/user/notes', { - userId: this.user.id, - span: 'day', - limit: 7 * 21 - }).then(activity => { - this.activity = activity.diffs.normal.map((_, i) => ({ - total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i], - notes: activity.diffs.normal[i], - replies: activity.diffs.reply[i], - renotes: activity.diffs.renote[i] - })); - this.fetching = false; - }); - }, - methods: { - toggle() { - if (this.view == 1) { - this.view = 0; - this.$emit('viewChanged', this.view); - } else { - this.view++; - this.$emit('viewChanged', this.view); - } - } - } -}); -</script> - -<style lang="stylus" module> -.fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue deleted file mode 100644 index cdeac51638..0000000000 --- a/src/client/app/desktop/views/components/calendar.vue +++ /dev/null @@ -1,252 +0,0 @@ -<template> -<div class="mk-calendar" :data-melt="design == 4 || design == 5" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <template v-if="design == 0 || design == 1"> - <button @click="prev" :title="$t('prev')"><fa icon="chevron-circle-left"/></button> - <p class="title">{{ $t('title', { year, month }) }}</p> - <button @click="next" :title="$t('next')"><fa icon="chevron-circle-right"/></button> - </template> - - <div class="calendar"> - <template v-if="design == 0 || design == 2 || design == 4"> - <div class="weekday" - v-for="(day, i) in Array(7).fill(0)" - :data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i" - :data-is-weekend="i == 0 || i == 6" - >{{ weekdayText[i] }}</div> - </template> - <div v-for="n in paddingDays"></div> - <div class="day" v-for="(day, i) in days" - :data-today="isToday(i + 1)" - :data-selected="isSelected(i + 1)" - :data-is-out-of-range="isOutOfRange(i + 1)" - :data-is-weekend="isWeekend(i + 1)" - @click="go(i + 1)" - :title="isOutOfRange(i + 1) ? null : $t('go')" - > - <div>{{ i + 1 }}</div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - -function isLeapYear(year) { - return !(year & (year % 25 ? 3 : 15)); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/calendar.vue'), - props: { - design: { - default: 0 - }, - start: { - type: Date, - required: false - } - }, - data() { - return { - today: new Date(), - year: new Date().getFullYear(), - month: new Date().getMonth() + 1, - selected: new Date(), - weekdayText: [ - this.$t('@.weekday-short.sunday'), - this.$t('@.weekday-short.monday'), - this.$t('@.weekday-short.tuesday'), - this.$t('@.weekday-short.wednesday'), - this.$t('@.weekday-short.thursday'), - this.$t('@.weekday-short.friday'), - this.$t('@.weekday-short.saturday') - ] - }; - }, - computed: { - paddingDays(): number { - const date = new Date(this.year, this.month - 1, 1); - return date.getDay(); - }, - days(): number { - let days = eachMonthDays[this.month - 1]; - - // うるう年なら+1日 - if (this.month == 2 && isLeapYear(this.year)) days++; - - return days; - } - }, - methods: { - isToday(day) { - return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate(); - }, - - isSelected(day) { - return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate(); - }, - - isOutOfRange(day) { - const test = (new Date(this.year, this.month - 1, day)).getTime(); - return test > this.today.getTime() || - (this.start ? test < (this.start as any).getTime() : false); - }, - - isWeekend(day) { - const weekday = (new Date(this.year, this.month - 1, day)).getDay(); - return weekday == 0 || weekday == 6; - }, - - prev() { - if (this.month == 1) { - this.year = this.year - 1; - this.month = 12; - } else { - this.month--; - } - }, - - next() { - if (this.month == 12) { - this.year = this.year + 1; - this.month = 1; - } else { - this.month++; - } - }, - - go(day) { - if (this.isOutOfRange(day)) return; - const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999); - this.selected = date; - this.$emit('chosen', this.selected); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-calendar - color var(--calendarDay) - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &[data-melt] - background transparent !important - border none !important - - > .title - z-index 1 - margin 0 - padding 0 16px - text-align center - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - background var(--faceHeader) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - &:first-of-type - left 0 - - &:last-of-type - right 0 - - > .calendar - display flex - flex-wrap wrap - padding 16px - - * - user-select none - - > div - width calc(100% * (1/7)) - text-align center - line-height 32px - font-size 14px - - &.weekday - color var(--calendarWeek) - - &[data-is-weekend] - color var(--calendarSaturdayOrSunday) - - &[data-today] - box-shadow 0 0 0 var(--lineWidth) var(--calendarWeek) inset - border-radius 6px - - &[data-is-weekend] - box-shadow 0 0 0 var(--lineWidth) var(--calendarSaturdayOrSunday) inset - - &.day - cursor pointer - color var(--calendarDay) - - > div - border-radius 6px - - &:hover > div - background var(--faceClearButtonHover) - - &:active > div - background var(--faceClearButtonActive) - - &[data-is-weekend] - color var(--calendarSaturdayOrSunday) - - &[data-is-out-of-range] - cursor default - opacity 0.5 - - &[data-selected] - font-weight bold - - > div - background var(--faceClearButtonHover) - - &:active > div - background var(--faceClearButtonActive) - - &[data-today] - > div - color var(--primaryForeground) - background var(--primary) - - &:hover > div - background var(--primaryLighten10) - - &:active > div - background var(--primaryDarken10) - -</style> diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue deleted file mode 100644 index 71c430edeb..0000000000 --- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> - <template #header> - <span class="jqiaciqv"> - <span class="title">{{ $t('choose-prompt') }}</span> - <span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span> - </span> - </template> - - <div class="rqsvbumu"> - <x-drive - ref="browser" - class="browser" - :type="type" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <div class="footer"> - <button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button> - <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button> - <ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button> - </div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/choose-file-from-drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: { - type: { - type: String, - required: false, - default: undefined - }, - multiple: { - default: false - } - }, - data() { - return { - files: [] - }; - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - ok() { - this.$emit('selected', this.multiple ? this.files : this.files[0]); - (this.$refs.window as any).close(); - }, - cancel() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.jqiaciqv - .title - > [data-icon] - margin-right 4px - - .count - margin-left 8px - opacity 0.7 - -.rqsvbumu - display flex - flex-direction column - height 100% - - .browser - flex 1 - overflow auto - - .footer - padding 16px - background var(--desktopPostFormBg) - text-align right - - .upload - display inline-block - position absolute - top 8px - left 16px - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color var(--primaryAlpha05) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background transparent - border-color var(--primaryAlpha05) - //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - -</style> diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue deleted file mode 100644 index fe76436544..0000000000 --- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> - <template #header> - <span>{{ $t('choose-prompt') }}</span> - </template> - - <div class="hllkpxxu"> - <x-drive - ref="browser" - class="browser" - :multiple="false" - /> - <div class="footer"> - <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button> - <ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button> - </div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/choose-folder-from-drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - methods: { - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - (this.$refs.window as any).close(); - }, - cancel() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.hllkpxxu - display flex - flex-direction column - height 100% - - .browser - flex 1 - overflow auto - - .footer - padding 16px - background var(--desktopPostFormBg) - text-align right - -</style> diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue deleted file mode 100644 index f2bb3bec23..0000000000 --- a/src/client/app/desktop/views/components/context-menu.menu.vue +++ /dev/null @@ -1,121 +0,0 @@ -<template> -<ul class="menu"> - <li v-for="(item, i) in menu" :class="item ? item.type : item === null ? 'divider' : null"> - <template v-if="item"> - <template v-if="item.type == null || item.type == 'item'"> - <p @click="click(item)"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</p> - </template> - <template v-else-if="item.type == 'link'"> - <a :href="item.href" :target="item.target" @click="click(item)" :download="item.download"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</a> - </template> - <template v-else-if="item.type == 'nest'"> - <p><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}...<span class="caret"><fa icon="caret-right"/></span></p> - <me-nu :menu="item.menu" @x="click"/> - </template> - </template> - </li> -</ul> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - name: 'me-nu', - props: ['menu'], - methods: { - click(item) { - this.$emit('x', item); - } - } -}); -</script> - -<style lang="stylus" scoped> -.menu - $width = 240px - $item-height = 38px - $padding = 10px - - margin 0 - padding $padding 0 - list-style none - - li - display block - - &.divider - margin-top $padding - padding-top $padding - border-top solid var(--lineWidth) var(--faceDivider) - - &.nest - > p - cursor default - - > .caret - position absolute - top 0 - right 8px - - > * - line-height $item-height - width 28px - text-align center - - &:hover > ul - visibility visible - - &:active - > p, a - background var(--primary) - - > p, a - display block - z-index 1 - margin 0 - padding 0 32px 0 38px - line-height $item-height - color var(--text) - text-decoration none - cursor pointer - - &:hover - text-decoration none - - * - pointer-events none - - &:hover - > p, a - text-decoration none - background var(--primary) - color var(--primaryForeground) - - &:active - > p, a - text-decoration none - background var(--primaryDarken10) - color var(--primaryForeground) - - li > ul - visibility hidden - position absolute - top 0 - left $width - margin-top -($padding) - width $width - background var(--popupBg) - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(#000, 0.2) - transition visibility 0s linear 0.2s - -</style> - -<style lang="stylus" module> -.icon - display inline-block - width 28px - margin-left -28px - text-align center -</style> - diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue deleted file mode 100644 index e79536fc0f..0000000000 --- a/src/client/app/desktop/views/components/context-menu.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<div class="context-menu" @contextmenu.prevent="() => {}"> - <x-menu :menu="menu" @x="click"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import contains from '../../../common/scripts/contains'; -import XMenu from './context-menu.menu.vue'; - -export default Vue.extend({ - components: { - XMenu - }, - props: ['x', 'y', 'menu'], - mounted() { - this.$nextTick(() => { - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; - - let x = this.x; - let y = this.y; - - if (x + width - window.pageXOffset > window.innerWidth) { - x = window.innerWidth - width + window.pageXOffset; - } - - if (y + height - window.pageYOffset > window.innerHeight) { - y = window.innerHeight - height + window.pageYOffset; - } - - this.$el.style.left = x + 'px'; - this.$el.style.top = y + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - - this.$el.style.display = 'block'; - - anime({ - targets: this.$el, - opacity: [0, 1], - duration: 100, - easing: 'linear' - }); - }); - }, - methods: { - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - return false; - }, - click(item) { - if (item.action) item.action(); - this.close(); - }, - close() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.context-menu - $width = 240px - $item-height = 38px - $padding = 10px - - position fixed - top 0 - left 0 - z-index 4096 - width $width - font-size 0.8em - background var(--popupBg) - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(#000, 0.2) - opacity 0 - -</style> diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue deleted file mode 100644 index 856f889b02..0000000000 --- a/src/client/app/desktop/views/components/crop-window.vue +++ /dev/null @@ -1,189 +0,0 @@ -<template> - <mk-window ref="window" is-modal width="800px" :can-close="false"> - <template #header><fa icon="crop"/>{{ title }}</template> - <div class="body"> - <vue-cropper ref="cropper" - :src="imageUrl" - :view-mode="1" - :aspect-ratio="aspectRatio" - :container-style="{ width: '100%', 'max-height': '400px' }" - /> - </div> - <div :class="$style.actions"> - <button :class="$style.skip" @click="skip">{{ $t('skip') }}</button> - <button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button> - <button :class="$style.ok" @click="ok">{{ $t('ok') }}</button> - </div> - </mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import VueCropper from 'vue-cropperjs'; -import 'cropperjs/dist/cropper.css'; -import * as url from '../../../../../prelude/url'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/crop-window.vue'), - components: { - VueCropper - }, - props: { - image: { - type: Object, - required: true - }, - title: { - type: String, - required: true - }, - aspectRatio: { - type: Number, - required: true - } - }, - computed: { - imageUrl() { - return `/proxy/?${url.query({ - url: this.image.url - })}`; - }, - }, - methods: { - ok() { - (this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => { - this.$emit('cropped', blob); - (this.$refs.window as any).close(); - }); - }, - - skip() { - this.$emit('skipped'); - (this.$refs.window as any).close(); - }, - - cancel() { - this.$emit('canceled'); - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> - - -.header - > [data-icon] - margin-right 4px - -.img - width 100% - max-height 400px - -.actions - height 72px - background var(--primaryLighten95) - -.ok -.cancel -.skip - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok -.cancel - width 120px - -.ok - right 16px - color var(--primaryForeground) - background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%) - border solid 1px var(--primaryLighten15) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%) - border-color var(--primary) - - &:active:not(:disabled) - background var(--primary) - border-color var(--primary) - -.cancel -.skip - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -.cancel - right 148px - -.skip - left 16px - width 150px - -</style> - -<style lang="stylus"> -.cropper-modal { - opacity: 0.8; -} - -.cropper-view-box { - outline-color: var(--primary); -} - -.cropper-line, .cropper-point { - background-color: var(--primary); -} - -.cropper-bg { - animation: cropper-bg 0.5s linear infinite; -} - -@keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } -} -</style> diff --git a/src/client/app/desktop/views/components/detail-notes.vue b/src/client/app/desktop/views/components/detail-notes.vue deleted file mode 100644 index e50dda7c6f..0000000000 --- a/src/client/app/desktop/views/components/detail-notes.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div class="ecsvsegy" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - <div class="more" v-if="more"> - <ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.ecsvsegy - margin 0 auto - - > * > .post - margin-bottom 16px - - > .more - margin 32px 16px 16px 16px - text-align center - -</style> diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue deleted file mode 100644 index 5f8a9316f3..0000000000 --- a/src/client/app/desktop/views/components/drive-window.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout"> - <template #header> - <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> {{ $t('used') }}</p> - <span :class="$style.title"><fa icon="cloud"/>{{ $t('@.drive') }}</span> - </template> - <x-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: ['folder'], - data() { - return { - usage: null - }; - }, - mounted() { - this.$root.api('drive').then(info => { - this.usage = info.usage / info.capacity * 100; - }); - }, - methods: { - popout() { - const folder = (this.$refs.browser as any) ? (this.$refs.browser as any).folder : null; - if (folder) { - return `${url}/i/drive/folder/${folder.id}`; - } else { - return `${url}/i/drive`; - } - } - } -}); -</script> - -<style lang="stylus" module> -.title - > [data-icon] - margin-right 4px - -.info - position absolute - top 0 - left 16px - margin 0 - font-size 80% - -.browser - height 100% - -</style> - diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue deleted file mode 100644 index e34fdff423..0000000000 --- a/src/client/app/desktop/views/components/drive.file.vue +++ /dev/null @@ -1,339 +0,0 @@ -<template> -<div class="gvfdktuvdgwhmztnuekzkswkjygptfcv" - :data-is-selected="isSelected" - :data-is-contextmenu-showing="isContextmenuShowing" - @click="onClick" - draggable="true" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" - :title="title" -> - <div class="label" v-if="$store.state.i.avatarId == file.id"> - <img src="/assets/label.svg"/> - <p>{{ $t('avatar') }}</p> - </div> - <div class="label" v-if="$store.state.i.bannerId == file.id"> - <img src="/assets/label.svg"/> - <p>{{ $t('banner') }}</p> - </div> - <div class="label red" v-if="file.isSensitive"> - <img src="/assets/label-red.svg"/> - <p>{{ $t('nsfw') }}</p> - </div> - - <x-file-thumbnail class="thumbnail" :file="file" fit="contain"/> - - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; -import updateAvatar from '../../api/update-avatar'; -import updateBanner from '../../api/update-banner'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.file.vue'), - props: ['file'], - components: { - XFileThumbnail - }, - data() { - return { - isContextmenuShowing: false, - isDragging: false - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - isSelected(): boolean { - return this.browser.selectedFiles.some(f => f.id == this.file.id); - }, - title(): string { - return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`; - } - }, - methods: { - onClick() { - this.browser.chooseFile(this.file); - }, - - onContextmenu(e) { - this.isContextmenuShowing = true; - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.rename'), - icon: 'i-cursor', - action: this.rename - }, { - type: 'item', - text: this.file.isSensitive ? this.$t('contextmenu.unmark-as-sensitive') : this.$t('contextmenu.mark-as-sensitive'), - icon: this.file.isSensitive ? ['far', 'eye'] : ['far', 'eye-slash'], - action: this.toggleSensitive - }, null, { - type: 'item', - text: this.$t('contextmenu.copy-url'), - icon: 'link', - action: this.copyUrl - }, { - type: 'link', - href: this.file.url, - target: '_blank', - text: this.$t('contextmenu.download'), - icon: 'download', - download: this.file.name - }, null, { - type: 'item', - text: this.$t('@.delete'), - icon: ['far', 'trash-alt'], - action: this.deleteFile - }, null, { - type: 'nest', - text: this.$t('contextmenu.else-files'), - menu: [{ - type: 'item', - text: this.$t('contextmenu.set-as-avatar'), - action: this.setAsAvatar - }, { - type: 'item', - text: this.$t('contextmenu.set-as-banner'), - action: this.setAsBanner - }] - }, /*{ - type: 'nest', - text: this.$t('contextmenu.open-in-app'), - menu: [{ - type: 'item', - text: '%i18n:@contextmenu.add-app%...', - action: this.addApp - }] - }*/], { - closed: () => { - this.isContextmenuShowing = false; - } - }); - }, - - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file)); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, - - onDragend(e) { - this.isDragging = false; - this.browser.isDragSource = false; - }, - - onThumbnailLoaded() { - if (this.file.properties.avgColor) { - anime({ - targets: this.$refs.thumbnail, - backgroundColor: 'transparent', // TODO fade - duration: 100, - easing: 'linear' - }); - } - }, - - rename() { - this.$root.dialog({ - title: this.$t('contextmenu.rename-file'), - input: { - placeholder: this.$t('contextmenu.input-new-file-name'), - default: this.file.name, - allowEmpty: false - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('drive/files/update', { - fileId: this.file.id, - name: name - }); - }); - }, - - toggleSensitive() { - this.$root.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive - }); - }, - - copyUrl() { - copyToClipboard(this.file.url); - this.$root.dialog({ - title: this.$t('contextmenu.copied'), - text: this.$t('contextmenu.copied-url-to-clipboard') - }); - }, - - setAsAvatar() { - updateAvatar(this.$root)(this.file); - }, - - setAsBanner() { - updateBanner(this.$root)(this.file); - }, - - addApp() { - alert('not implemented yet'); - }, - - deleteFile() { - this.$root.api('drive/files/delete', { - fileId: this.file.id - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gvfdktuvdgwhmztnuekzkswkjygptfcv - padding 8px 0 0 0 - min-height 180px - border-radius 4px - - &, * - cursor pointer - - &:hover - background rgba(#000, 0.05) - - > .label - &:before - &:after - background #0b65a5 - - &.red - &:before - &:after - background #c12113 - - &:active - background rgba(#000, 0.1) - - > .label - &:before - &:after - background #0b588c - - &.red - &:before - &:after - background #ce2212 - - &[data-is-selected] - background var(--primary) - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > .label - &:before - &:after - display none - - > .name - color var(--primaryForeground) - - > .thumbnail - color var(--primaryForeground) - - &[data-is-contextmenu-showing] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed var(--primaryAlpha03) - border-radius 4px - - > .label - position absolute - top 0 - left 0 - pointer-events none - - &:before - &:after - content "" - display block - position absolute - z-index 1 - background #0c7ac9 - - &:before - top 0 - left 57px - width 28px - height 8px - - &:after - top 57px - left 0 - width 8px - height 28px - - &.red - &:before - &:after - background #c12113 - - > img - position absolute - z-index 2 - top 0 - left 0 - - > p - position absolute - z-index 3 - top 19px - left -28px - width 120px - margin 0 - text-align center - line-height 28px - color #fff - transform rotate(-45deg) - - > .thumbnail - width 128px - height 128px - margin auto - color var(--driveFileIcon) - - > .name - display block - margin 4px 0 0 0 - font-size 0.8em - text-align center - word-break break-all - color var(--text) - overflow hidden - - > .ext - opacity 0.5 - -</style> diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue deleted file mode 100644 index cf59d51b01..0000000000 --- a/src/client/app/desktop/views/components/drive.folder.vue +++ /dev/null @@ -1,313 +0,0 @@ -<template> -<div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd" - :data-is-contextmenu-showing="isContextmenuShowing" - :data-draghover="draghover" - @click="onClick" - @mouseover="onMouseover" - @mouseout="onMouseout" - @dragover.prevent.stop="onDragover" - @dragenter.prevent="onDragenter" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - draggable="true" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" - :title="title" -> - <p class="name"> - <template v-if="hover"><fa :icon="['far', 'folder-open']" fixed-width/></template> - <template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template> - {{ folder.name }} - </p> - <p class="upload" v-if="$store.state.settings.uploadFolder == folder.id"> - {{ $t('upload-folder') }} - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.folder.vue'), - props: ['folder'], - data() { - return { - hover: false, - draghover: false, - isDragging: false, - isContextmenuShowing: false - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - title(): string { - return this.folder.name; - } - }, - methods: { - onClick() { - this.browser.move(this.folder); - }, - - onContextmenu(e) { - this.isContextmenuShowing = true; - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.move-to-this-folder'), - icon: 'arrow-right', - action: this.go - }, { - type: 'item', - text: this.$t('contextmenu.show-in-new-window'), - icon: ['far', 'window-restore'], - action: this.newWindow - }, null, { - type: 'item', - text: this.$t('contextmenu.rename'), - icon: 'i-cursor', - action: this.rename - }, null, { - type: 'item', - text: this.$t('@.delete'), - icon: ['far', 'trash-alt'], - action: this.deleteFolder - }, null, { - type: 'nest', - text: this.$t('contextmenu.else-folders'), - menu: [{ - type: 'item', - text: this.$t('contextmenu.set-as-upload-folder'), - action: this.setAsUploadFolder - }] - }], { - closed: () => { - this.isContextmenuShowing = false; - } - }); - }, - - onMouseover() { - this.hover = true; - }, - - onMouseout() { - this.hover = false - }, - - onDragover(e) { - // 自分自身がドラッグされている場合 - if (this.isDragging) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, - - onDragenter() { - if (!this.isDragging) this.draghover = true; - }, - - onDragleave() { - this.draghover = false; - }, - - onDrop(e) { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.browser.upload(file, this.folder); - } - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - this.$root.api('drive/files/update', { - fileId: file.id, - folderId: this.folder.id - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - - // 移動先が自分自身ならreject - if (folder.id == this.folder.id) return; - - this.browser.removeFolder(folder.id); - this.$root.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder.id - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - this.$root.dialog({ - title: this.$t('unable-to-process'), - text: this.$t('circular-reference-detected') - }); - break; - default: - this.$root.dialog({ - type: 'error', - text: this.$t('unhandled-error') - }); - } - }); - } - //#endregion - }, - - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder)); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, - - onDragend() { - this.isDragging = false; - this.browser.isDragSource = false; - }, - - go() { - this.browser.move(this.folder.id); - }, - - newWindow() { - this.browser.newWindow(this.folder); - }, - - rename() { - this.$root.dialog({ - title: this.$t('contextmenu.rename-folder'), - input: { - placeholder: this.$t('contextmenu.input-new-folder-name'), - default: this.folder.name - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('drive/folders/update', { - folderId: this.folder.id, - name: name - }); - }); - }, - - deleteFolder() { - this.$root.api('drive/folders/delete', { - folderId: this.folder.id - }).then(() => { - if (this.$store.state.settings.uploadFolder === this.folder.id) { - this.$store.dispatch('settings/set', { - key: 'uploadFolder', - value: null - }); - } - }).catch(err => { - switch(err.id) { - case 'b0fc8a17-963c-405d-bfbc-859a487295e1': - this.$root.dialog({ - type: 'error', - title: this.$t('unable-to-delete'), - text: this.$t('has-child-files-or-folders') - }); - break; - default: - this.$root.dialog({ - type: 'error', - text: this.$t('unable-to-delete') - }); - } - }); - }, - - setAsUploadFolder() { - this.$store.dispatch('settings/set', { - key: 'uploadFolder', - value: this.folder.id - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.ynntpczxvnusfwdyxsfuhvcmuypqopdd - padding 8px - height 64px - background var(--desktopDriveFolderBg) - border-radius 4px - - &, * - cursor pointer - - * - pointer-events none - - &:hover - background var(--desktopDriveFolderHoverBg) - - &:active - background var(--desktopDriveFolderActiveBg) - - &[data-is-contextmenu-showing] - &[data-draghover] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed var(--primaryAlpha03) - border-radius 4px - - &[data-draghover] - background var(--desktopDriveFolderActiveBg) - - > .name - margin 0 - font-size 0.9em - color var(--desktopDriveFolderFg) - - > [data-icon] - margin-right 4px - margin-left 2px - text-align left - - > .upload - margin 4px 4px - font-size 0.8em - text-align right - color var(--desktopDriveFolderFg) - -</style> diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue deleted file mode 100644 index 14ab467642..0000000000 --- a/src/client/app/desktop/views/components/drive.nav-folder.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<div class="root nav-folder" - :data-draghover="draghover" - @click="onClick" - @dragover.prevent.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <i v-if="folder == null" class="cloud"><fa icon="cloud"/></i> - <span>{{ folder == null ? $t('@.drive') : folder.name }}</span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n(), - props: ['folder'], - data() { - return { - hover: false, - draghover: false - }; - }, - computed: { - browser(): any { - return this.$parent; - } - }, - methods: { - onClick() { - this.browser.move(this.folder); - }, - onMouseover() { - this.hover = true; - }, - onMouseout() { - this.hover = false; - }, - onDragover(e) { - // このフォルダがルートかつカレントディレクトリならドロップ禁止 - if (this.folder == null && this.browser.folder == null) { - e.dataTransfer.dropEffect = 'none'; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - - return false; - }, - onDragenter() { - if (this.folder || this.browser.folder) this.draghover = true; - }, - onDragleave() { - if (this.folder || this.browser.folder) this.draghover = false; - }, - onDrop(e) { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.browser.upload(file, this.folder); - } - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - this.$root.api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return; - this.browser.removeFolder(folder.id); - this.$root.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }); - } - //#endregion - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.nav-folder - > * - pointer-events none - - &[data-draghover] - background #eee - - i.cloud - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue deleted file mode 100644 index ff4ff18e6e..0000000000 --- a/src/client/app/desktop/views/components/drive.vue +++ /dev/null @@ -1,760 +0,0 @@ -<template> -<div class="mk-drive"> - <nav> - <div class="path" @contextmenu.prevent.stop="() => {}"> - <x-nav-folder :class="{ current: folder == null }"/> - <template v-for="folder in hierarchyFolders"> - <span class="separator"><fa icon="angle-right"/></span> - <x-nav-folder :folder="folder" :key="folder.id"/> - </template> - <span class="separator" v-if="folder != null"><fa icon="angle-right"/></span> - <span class="folder current" v-if="folder != null">{{ folder.name }}</span> - </div> - </nav> - <div class="main" :class="{ uploading: uploadings.length > 0, fetching }" - ref="main" - @mousedown="onMousedown" - @dragover.prevent.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - @contextmenu.prevent.stop="onContextmenu" - > - <div class="selection" ref="selection"></div> - <div class="contents" ref="contents"> - <div class="folders" ref="foldersContainer" v-if="folders.length > 0 || moreFolders"> - <x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/> - <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div class="padding" v-for="n in 16"></div> - <ui-button v-if="moreFolders">{{ $t('@.load-more') }}</ui-button> - </div> - <div class="files" ref="filesContainer" v-if="files.length > 0 || moreFiles"> - <x-file v-for="file in files" :key="file.id" class="file" :file="file"/> - <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div class="padding" v-for="n in 16"></div> - <ui-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</ui-button> - </div> - <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching"> - <p v-if="draghover">{{ $t('empty-draghover') }}</p> - <p v-if="!draghover && folder == null"><strong>{{ $t('empty-drive') }}</strong><br/>{{ $t('empty-drive-description') }}</p> - <p v-if="!draghover && folder != null">{{ $t('empty-folder') }}</p> - </div> - </div> - <div class="fetching" v-if="fetching"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - </div> - <div class="dropzone" v-if="draghover"></div> - <mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> - <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkDriveWindow from './drive-window.vue'; -import XNavFolder from './drive.nav-folder.vue'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import contains from '../../../common/scripts/contains'; -import { url } from '../../../config'; -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.vue'), - components: { - XNavFolder, - XFolder, - XFile - }, - props: { - initFolder: { - type: Object, - required: false - }, - type: { - type: String, - required: false, - default: undefined - }, - multiple: { - type: Boolean, - default: false - } - }, - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, - - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - uploadings: [], - connection: null, - - /** - * ドロップされようとしているか - */ - draghover: false, - - /** - * 自信の所有するアイテムがドラッグをスタートさせたか - * (自分自身の階層にドロップできないようにするためのフラグ) - */ - isDragSource: false, - - fetching: true - }; - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('drive'); - - this.connection.on('fileCreated', this.onStreamDriveFileCreated); - this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); - this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); - this.connection.on('folderCreated', this.onStreamDriveFolderCreated); - this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated); - this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted); - - if (this.initFolder) { - this.move(this.initFolder); - } else { - this.fetch(); - } - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - onContextmenu(e) { - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.create-folder'), - icon: ['far', 'folder'], - action: this.createFolder - }, { - type: 'item', - text: this.$t('contextmenu.upload'), - icon: 'upload', - action: this.selectLocalFile - }, { - type: 'item', - text: this.$t('contextmenu.url-upload'), - icon: faCloudUploadAlt, - action: this.urlUpload - }]); - }, - - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, - - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, - - onStreamDriveFileDeleted(fileId) { - this.removeFile(fileId); - }, - - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, - - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, - - onStreamDriveFolderDeleted(folderId) { - this.removeFolder(folderId); - }, - - onChangeUploaderUploads(uploads) { - this.uploadings = uploads; - }, - - onUploaderUploaded(file) { - this.addFile(file, true); - }, - - onMousedown(e): any { - if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true; - - const main = this.$refs.main as any; - const selection = this.$refs.selection as any; - - const rect = main.getBoundingClientRect(); - - const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset - const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset - - const move = e => { - selection.style.display = 'block'; - - const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset; - const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset; - const w = cursorX - left; - const h = cursorY - top; - - if (w > 0) { - selection.style.width = w + 'px'; - selection.style.left = left + 'px'; - } else { - selection.style.width = -w + 'px'; - selection.style.left = cursorX + 'px'; - } - - if (h > 0) { - selection.style.height = h + 'px'; - selection.style.top = top + 'px'; - } else { - selection.style.height = -h + 'px'; - selection.style.top = cursorY + 'px'; - } - }; - - const up = e => { - document.documentElement.removeEventListener('mousemove', move); - document.documentElement.removeEventListener('mouseup', up); - - selection.style.display = 'none'; - }; - - document.documentElement.addEventListener('mousemove', move); - document.documentElement.addEventListener('mouseup', up); - }, - - onDragover(e): any { - // ドラッグ元が自分自身の所有するアイテムだったら - if (this.isDragSource) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - - return false; - }, - - onDragenter(e) { - if (!this.isDragSource) this.draghover = true; - }, - - onDragleave(e) { - this.draghover = false; - }, - - onDrop(e): any { - this.draghover = false; - - // ドロップされてきたものがファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.upload(file, this.folder); - } - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - if (this.files.some(f => f.id == file.id)) return; - this.removeFile(file.id); - this.$root.api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return false; - if (this.folders.some(f => f.id == folder.id)) return false; - this.removeFolder(folder.id); - this.$root.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - this.$root.dialog({ - title: this.$t('unable-to-process'), - text: this.$t('circular-reference-detected') - }); - break; - default: - this.$root.dialog({ - type: 'error', - text: this.$t('unhandled-error') - }); - } - }); - } - //#endregion - }, - - selectLocalFile() { - (this.$refs.fileInput as any).click(); - }, - - urlUpload() { - this.$root.dialog({ - title: this.$t('url-upload'), - input: { - placeholder: this.$t('url-of-file') - } - }).then(({ canceled, result: url }) => { - if (canceled) return; - this.$root.api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - - this.$root.dialog({ - title: this.$t('url-upload-requested'), - text: this.$t('may-take-time') - }); - }); - }, - - createFolder() { - this.$root.dialog({ - title: this.$t('create-folder'), - input: { - placeholder: this.$t('folder-name') - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }); - }, - - onChangeFileInput() { - for (const file of Array.from((this.$refs.fileInput as any).files)) { - this.upload(file, this.folder); - } - }, - - upload(file, folder) { - if (folder && typeof folder == 'object') folder = folder.id; - (this.$refs.uploader as any).upload(file, folder); - }, - - chooseFile(file) { - const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); - if (this.multiple) { - if (isAlreadySelected) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - if (isAlreadySelected) { - this.$emit('selected', file); - } else { - this.selectedFiles = [file]; - this.$emit('change-selection', [file]); - } - } - }, - - newWindow(folder) { - if (document.body.clientWidth > 800) { - this.$root.new(MkDriveWindow, { - folder: folder - }); - } else { - window.open(`${url}/i/drive/folder/${folder.id}`, - 'drive_window', - 'height=500, width=800'); - } - }, - - move(target) { - if (target == null) { - this.goRoot(); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.fetching = true; - - this.$root.api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - const dive = folder => { - this.hierarchyFolders.unshift(folder); - if (folder.parent) dive(folder.parent); - }; - - if (folder.parent) dive(folder.parent); - - this.$emit('open-folder', folder); - this.fetch(); - }); - }, - - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) return; - - if (this.folders.some(f => f.id == folder.id)) { - const exist = this.folders.map(f => f.id).indexOf(folder.id); - Vue.set(this.folders, exist, folder); - return; - } - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, - - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - Vue.set(this.files, exist, file); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, - - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, - - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, - - appendFile(file) { - this.addFile(file); - }, - - appendFolder(folder) { - this.addFolder(folder); - }, - - prependFile(file) { - this.addFile(file, true); - }, - - prependFolder(folder) { - this.addFolder(folder, true); - }, - - goRoot() { - // 既にrootにいるなら何もしない - if (this.folder == null) return; - - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root'); - this.fetch(); - }, - - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 30; - const filesMax = 30; - - // フォルダ一覧取得 - this.$root.api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - type: this.type, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - for (const x of fetchedFolders) this.appendFolder(x); - for (const x of fetchedFiles) this.appendFile(x); - this.fetching = false; - } else { - flag = true; - } - }; - }, - - fetchMoreFiles() { - this.fetching = true; - - const max = 30; - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - type: this.type, - untilId: this.files[this.files.length - 1].id, - limit: max + 1 - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - for (const x of files) this.appendFile(x); - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive - > nav - display block - z-index 2 - width 100% - overflow auto - font-size 0.9em - color var(--text) - background var(--face) - box-shadow 0 1px 0 rgba(#000, 0.05) - - &, * - user-select none - - > .path - display inline-block - vertical-align bottom - margin 0 - padding 0 8px - width calc(100% - 200px) - line-height 38px - white-space nowrap - - > * - display inline-block - margin 0 - padding 0 8px - line-height 38px - cursor pointer - - * - pointer-events none - - &:hover - text-decoration underline - - &.current - font-weight bold - cursor default - - &:hover - text-decoration none - - &.separator - margin 0 - padding 0 - opacity 0.5 - cursor default - - > [data-icon] - margin 0 - - > .main - padding 8px - height calc(100% - 38px) - overflow auto - background var(--desktopDriveBg) - - &, * - user-select none - - &.fetching - cursor wait !important - - * - pointer-events none - - > .contents - opacity 0.5 - - &.uploading - height calc(100% - 38px - 100px) - - > .selection - display none - position absolute - z-index 128 - top 0 - left 0 - border solid 1px var(--primary) - background var(--primaryAlpha05) - pointer-events none - - > .contents - - > .folders - > .files - display flex - flex-wrap wrap - - > .folder - > .file - flex-grow 1 - width 144px - margin 4px - - > .padding - flex-grow 1 - pointer-events none - width 144px + 8px // 8px is margin - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background-color rgba(#000, 0.3) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { - 100% { - transform: rotate(360deg); - } - } - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } - 50% { - transform: scale(1.0); - } - } - - > .dropzone - position absolute - left 0 - top 38px - width 100% - height calc(100% - 38px) - border dashed 2px var(--primaryAlpha05) - pointer-events none - - > .mk-uploader - height 100px - padding 16px - - > input - display none - -</style> diff --git a/src/client/app/desktop/views/components/emoji-picker-dialog.vue b/src/client/app/desktop/views/components/emoji-picker-dialog.vue deleted file mode 100644 index 4ea0f441a9..0000000000 --- a/src/client/app/desktop/views/components/emoji-picker-dialog.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<div class="gcafiosrssbtbnbzqupfmglvzgiaipyv"> - <x-picker @chosen="chosen"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - components: { - XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default) - }, - - props: { - x: { - type: Number, - required: true - }, - y: { - type: Number, - required: true - } - }, - - mounted() { - this.$nextTick(() => { - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; - - let x = this.x; - let y = this.y; - - if (x + width - window.pageXOffset > window.innerWidth) { - x = window.innerWidth - width + window.pageXOffset; - } - - if (y + height - window.pageYOffset > window.innerHeight) { - y = window.innerHeight - height + window.pageYOffset; - } - - this.$el.style.left = x + 'px'; - this.$el.style.top = y + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }); - }, - - methods: { - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - return false; - }, - - chosen(emoji) { - this.$emit('chosen', emoji); - this.close(); - }, - - close() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gcafiosrssbtbnbzqupfmglvzgiaipyv - position absolute - top 0 - left 0 - z-index 3000 - box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3) - -</style> diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue deleted file mode 100644 index 3dba4c3af4..0000000000 --- a/src/client/app/desktop/views/components/game-window.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> - <template #header><fa icon="gamepad"/> {{ $t('game') }}</template> - <x-reversi :class="$style.content" @gamed="g => game = g"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/game-window.vue'), - components: { - XReversi: () => import('../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - data() { - return { - game: null - }; - }, - computed: { - popout(): string { - return this.game - ? `${url}/games/reversi/${this.game.id}` - : `${url}/games/reversi`; - } - } -}); -</script> - -<style lang="stylus" module> -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts deleted file mode 100644 index 0cc44e1bbd..0000000000 --- a/src/client/app/desktop/views/components/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import uiNotification from './ui-notification.vue'; -import note from './note.vue'; -import notes from './notes.vue'; -import subNoteContent from './sub-note-content.vue'; -import window from './window.vue'; -import renoteFormWindow from './renote-form-window.vue'; -import mediaVideo from './media-video.vue'; -import notifications from './notifications.vue'; -import renoteForm from './renote-form.vue'; -import notePreview from './note-preview.vue'; -import noteDetail from './note-detail.vue'; -import calendar from './calendar.vue'; -import activity from './activity.vue'; -import userListTimeline from './user-list-timeline.vue'; -import uiContainer from './ui-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-ui-notification', uiNotification); -Vue.component('mk-note', note); -Vue.component('mk-notes', notes); -Vue.component('mk-sub-note-content', subNoteContent); -Vue.component('mk-window', window); -Vue.component('mk-renote-form-window', renoteFormWindow); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-notifications', notifications); -Vue.component('mk-renote-form', renoteForm); -Vue.component('mk-note-preview', notePreview); -Vue.component('mk-note-detail', noteDetail); -Vue.component('mk-calendar', calendar); -Vue.component('mk-activity', activity); -Vue.component('mk-user-list-timeline', userListTimeline); -Vue.component('ui-container', uiContainer); diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue deleted file mode 100644 index 9d2d0527ef..0000000000 --- a/src/client/app/desktop/views/components/media-video-dialog.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<ui-modal v-hotkey.global="keymap"> - <video :src="video.url" :title="video.name" controls autoplay ref="video" @volumechange="volumechange" /> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['video', 'start'], - mounted() { - const videoTag = this.$refs.video as HTMLVideoElement; - if (this.start) videoTag.currentTime = this.start - videoTag.volume = this.$store.state.device.mediaVolume; - }, - computed: { - keymap(): any { - return { - 'esc': this.close, - }; - } - }, - methods: { - close() { - }, - volumechange() { - const videoTag = this.$refs.video as HTMLVideoElement; - this.$store.commit('device/set', { key: 'mediaVolume', value: videoTag.volume }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -video - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 80vw - max-height 80vh - margin auto - -</style> diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue deleted file mode 100644 index c53da0f49e..0000000000 --- a/src/client/app/desktop/views/components/media-video.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else> - <a class="thumbnail" - :href="video.url" - :style="imageStyle" - @click.prevent="onClick" - :title="video.name" - > - <fa :icon="['far', 'play-circle']"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMediaVideoDialog from './media-video-dialog.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/media-video.vue'), - props: { - video: { - type: Object, - required: true - }, - inlinePlayable: { - default: false - } - }, - data() { - return { - hide: true - }; - }, - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.thumbnailUrl})` - }; - } - }, - methods: { - onClick() { - const videoTag = this.$refs.video as (HTMLVideoElement | null) - var start = 0 - if (videoTag) { - start = videoTag.currentTime - videoTag.pause() - } - const viewer = this.$root.new(MkMediaVideoDialog, { - video: this.video, - start, - }); - this.$once('hook:beforeDestroy', () => { - viewer.close(); - }); - } - } -}) -</script> - -<style lang="stylus" scoped> -.vwxdhznewyashiknzolsoihtlpicqepe - .video - display block - width 100% - height 100% - border-radius 4px - - .thumbnail - display flex - justify-content center - align-items center - font-size 3.5em - cursor zoom-in - overflow hidden - background-position center - background-size cover - width 100% - height 100% - -.uofhebxjdgksfmltszlxurtjnjjsvioh - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > b - display block -</style> diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue deleted file mode 100644 index 6c1708b59f..0000000000 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> - <template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name v-if="user" :user="user"/><span v-else>{{ group.name }}</span></template> - <x-messaging-room :user="user" :group="group" :class="$style.content"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - props: ['user', 'group'], - computed: { - popout(): string { - if (this.user) { - return `${url}/i/messaging/${getAcct(this.user)}`; - } else if (this.group) { - return `${url}/i/messaging/group/${this.group.id}`; - } - } - } -}); -</script> - -<style lang="stylus" module> -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue deleted file mode 100644 index 7cec9484d6..0000000000 --- a/src/client/app/desktop/views/components/messaging-window.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> - <template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template> - <x-messaging :class="$style.content" @navigate="navigate" @navigateGroup="navigateGroup"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMessagingRoomWindow from './messaging-room-window.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - methods: { - navigate(user) { - this.$root.new(MkMessagingRoomWindow, { - user: user - }); - }, - navigateGroup(group) { - this.$root.new(MkMessagingRoomWindow, { - group: group - }); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue deleted file mode 100644 index e0ce5ce1c6..0000000000 --- a/src/client/app/desktop/views/components/note-detail.vue +++ /dev/null @@ -1,356 +0,0 @@ -<template> -<div class="mk-note-detail" :title="title" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <button - class="read-more" - v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" - :title="$t('title')" - @click="fetchConversation" - :disabled="conversationFetching" - > - <template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> - <template v-if="conversationFetching"><fa icon="spinner" pulse/></template> - </button> - <div class="conversation"> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - </div> - <div class="reply-to" v-if="appearNote.reply"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article> - <mk-avatar class="avatar" :user="appearNote.user"/> - <header> - <router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.user.id"> - <mk-user-name :user="appearNote.user"/> - </router-link> - <span class="username"><mk-acct :user="appearNote.user"/></span> - <div class="info"> - <router-link class="time" :to="appearNote | notePage"> - <mk-time :time="appearNote.createdAt"/> - </router-link> - <div class="visibility-info"> - <span class="visibility" v-if="appearNote.visibility != 'public'"> - <fa v-if="appearNote.visibility == 'home'" icon="home"/> - <fa v-if="appearNote.visibility == 'followers'" icon="unlock"/> - <fa v-if="appearNote.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span> - </div> - </div> - </header> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <span v-if="appearNote.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files" :raw="true"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="map" v-if="appearNote.geo" ref="map"></div> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote"/> - </div> - </div> - </div> - <footer> - <span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span> - <mk-reactions-viewer :note="appearNote"/> - <button class="replyButton" @click="reply()" :title="$t('reply')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton" @click="renote()" :title="$t('renote')"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="inhibitedButton"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton" :title="$t('add-reaction')"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> - <fa icon="minus"/> - </button> - <button @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSub from './note.sub.vue'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; -import noteMixin from '../../../common/scripts/note-mixin'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/note-detail.vue'), - - components: { - XSub - }, - - mixins: [noteMixin(), noteSubscriber('note')], - - props: { - note: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - - data() { - return { - conversation: [], - conversationFetching: false, - replies: [] - }; - }, - - mounted() { - // Get replies - if (!this.compact) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - } - }, - - methods: { - fetchConversation() { - this.conversationFetching = true; - - // Fetch conversation - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversationFetching = false; - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-detail - overflow hidden - text-align left - background var(--face) - - &.round - border-radius 6px - - > .read-more - border-radius 6px 6px 0 0 - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > .read-more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background var(--subNoteBg) - outline none - border none - border-bottom solid 1px var(--faceDivider) - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - &:disabled - cursor wait - - > .conversation - > * - border-bottom 1px solid var(--faceDivider) - - > .renote + article - padding-top 8px - - > .reply-to - border-bottom 1px solid var(--faceDivider) - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > footer > button - color var(--noteActionsHighlighted) - - > .avatar - width 60px - height 60px - border-radius 8px - - > header - position absolute - top 28px - left 108px - width calc(100% - 108px) - - > .name - display inline-block - margin 0 - line-height 24px - color var(--noteHeaderName) - font-size 18px - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color var(--noteHeaderAcct) - - > .info - position absolute - top 0 - right 32px - font-size 1em - - > .time - color var(--noteHeaderInfo) - - > .visibility-info - text-align: right - color var(--noteHeaderInfo) - - > .localOnly - margin-left 4px - - > .body - padding 8px 0 - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.5em - color var(--noteText) - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 300px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > footer - font-size 1.2em - - > .app - display block - font-size 0.8em - margin-left 0.5em - color var(--noteHeaderInfo) - - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - font-size 1em - color var(--noteActions) - cursor pointer - - &:hover - color var(--noteActionsHover) - - &.replyButton:hover - color var(--noteActionsReplyHover) - - &.renoteButton:hover - color var(--noteActionsRenoteHover) - - &.reactionButton:hover - color var(--noteActionsReactionHover) - - &.inhibitedButton - cursor not-allowed - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted, &.reacted:hover - color var(--noteActionsReactionHover) - - > .replies - > * - border-top 1px solid var(--faceDivider) - -</style> diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue deleted file mode 100644 index 3b1e71e168..0000000000 --- a/src/client/app/desktop/views/components/note-preview.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title"> - <mk-avatar class="avatar" :user="note.user" v-if="!narrow"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - }, - - computed: { - title(): string { - return new Date(this.note.createdAt).toLocaleString(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.qiziqtywpuaucsgarwajitwaakggnisj - display flex - overflow hidden - font-size 0.9em - - > .avatar - flex-shrink 0 - display block - margin 0 12px 0 0 - width 48px - height 48px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - -</style> diff --git a/src/client/app/desktop/views/components/note.sub.vue b/src/client/app/desktop/views/components/note.sub.vue deleted file mode 100644 index bfecef3eb2..0000000000 --- a/src/client/app/desktop/views/components/note.sub.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="tkfdzaxtkdeianobciwadajxzbddorql" :class="{ mini: narrow }" :title="title"> - <mk-avatar class="avatar" :user="note.user"/> - <div class="main"> - <mk-note-header class="header" :note="note"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - }, - - computed: { - title(): string { - return new Date(this.note.createdAt).toLocaleString(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.tkfdzaxtkdeianobciwadajxzbddorql - display flex - padding 16px 32px - font-size 0.9em - background var(--subNoteBg) - - &.mini - padding 16px - font-size 10px - - > .avatar - margin 0 8px 0 0 - width 38px - height 38px - - > .avatar - flex-shrink 0 - display block - margin 0 12px 0 0 - width 48px - height 48px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - font-size calc(1em + var(--fontSize)) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue deleted file mode 100644 index 1c00faed39..0000000000 --- a/src/client/app/desktop/views/components/note.vue +++ /dev/null @@ -1,323 +0,0 @@ -<template> -<div - class="note" - :class="{ mini: narrow }" - v-show="(this.$store.state.settings.remainDeletedNote || appearNote.deletedAt == null) && !hideThisNote" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - v-hotkey="keymap" - :title="title" -> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article class="article"> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="narrow"/> - <div class="body" v-if="appearNote.deletedAt == null"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - <a class="rp" v-if="appearNote.renote">RN:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> 位置情報</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/> - </div> - </div> - <footer v-if="appearNote.deletedAt == null" class="footer"> - <span class="app" v-if="appearNote.app && narrow && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button class="replyButton button" @click="reply()" :title="$t('reply')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton button" @click="renote()" :title="$t('renote')"> - <fa icon="retweet"/> - <p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="inhibitedButton button"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')"> - <fa icon="plus"/> - <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> - <fa icon="minus"/> - <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> - </button> - <button @click="menu()" ref="menuButton" class="button"> - <fa icon="ellipsis-h"/> - </button> - </footer> - <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> - </div> - </article> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -import XSub from './note.sub.vue'; -import noteMixin from '../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/note.vue'), - - components: { - XSub - }, - - mixins: [ - noteMixin(), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - compact: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - conversation: [], - replies: [] - }; - }, - - created() { - if (this.detail) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - margin 0 - padding 0 - overflow hidden - background var(--face) - border-bottom solid var(--lineWidth) var(--faceDivider) - - &.mini - font-size 13px - - > .renote - padding 8px 16px 0 16px - - .avatar - width 20px - height 20px - - > .article - padding 16px 16px 4px - - > .avatar - margin 0 10px 8px 0 - width 42px - height 42px - - &:last-of-type - border-bottom none - - &:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 2px - right 2px - bottom 2px - left 2px - border 2px solid var(--primaryAlpha03) - border-radius 4px - - > .renote + article - padding-top 8px - - > .article - display flex - padding 28px 32px 18px 32px - - &:hover - > .main > footer > button - color var(--noteActionsHighlighted) - - > .avatar - flex-shrink 0 - display block - margin 0 16px 10px 0 - width 58px - height 58px - border-radius 8px - //position -webkit-sticky - //position sticky - //top 74px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 4px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - font-size calc(1em + var(--fontSize)) - - > .reply - margin-right 8px - color var(--text) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 300px - - &:empty - display none - - .mk-url-preview - margin-top 8px - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed var(--lineWidth) var(--quoteBorder) - border-radius 8px - - > .footer - > .app - display block - margin-top 0.5em - margin-left 0.5em - color var(--noteHeaderInfo) - font-size 0.8em - - > .button - margin 0 28px 0 0 - padding 0 8px - line-height 32px - font-size 1em - color var(--noteActions) - background transparent - border none - cursor pointer - - &:last-child - margin-right 0 - - &:hover - color var(--noteActionsHover) - - &.replyButton:hover - color var(--noteActionsReplyHover) - - &.renoteButton:hover - color var(--noteActionsRenoteHover) - - &.reactionButton:hover - color var(--noteActionsReactionHover) - - &.inhibitedButton - cursor not-allowed - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted, &.reacted:hover - color var(--noteActionsReactionHover) - - > .deleted - color var(--noteText) - opacity 0.7 - -</style> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue deleted file mode 100644 index 0820d5d80c..0000000000 --- a/src/client/app/desktop/views/components/notes.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="mk-notes" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <slot name="header"></slot> - - <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> - - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes"> - <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id" :compact="true" ref="note"/> - <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as config from '../../../config'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - - onQueueChanged: (self, x) => { - if (x.length > 0) { - self.$store.commit('indicate', true); - } else { - self.$store.commit('indicate', false); - } - }, - - onPrepend: (self, note, silent) => { - // 弾く - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - - if (self.isScrollTop()) { - // サウンドを再生する - if (self.$store.state.device.enableSounds && !silent) { - const sound = new Audio(`${config.url}/assets/post.mp3`); - sound.volume = self.$store.state.device.soundVolume; - sound.play(); - } - } - }, - - onInited: (self) => { - self.$emit('loaded'); - } - }), - ], - - props: { - pagination: { - required: true - }, - }, - - computed: { - _notes(): any[] { - return (this.items as any).map(item => { - const date = new Date(item.createdAt).getDate(); - const month = new Date(item.createdAt).getMonth() + 1; - item._date = date; - item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return item; - }); - } - }, - - methods: { - focus() { - (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notes - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .empty - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 32px - opacity 0.3 - - > .notes - > .date - display block - margin 0 - line-height 32px - font-size 14px - text-align center - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .newer-indicator - position -webkit-sticky - position sticky - z-index 100 - height 3px - background var(--primary) - - > footer - > button - display block - margin 0 - padding 16px - width 100% - text-align center - color #ccc - background var(--face) - border-top solid var(--lineWidth) var(--faceDivider) - border-bottom-left-radius 6px - border-bottom-right-radius 6px - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - -</style> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue deleted file mode 100644 index a2504abe66..0000000000 --- a/src/client/app/desktop/views/components/notifications.vue +++ /dev/null @@ -1,379 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <div class="notifications" v-if="!empty"> - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> - <template v-for="(notification, i) in _notifications"> - <div class="notification" :class="notification.type" :key="notification.id"> - <template v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <mk-reaction-icon :reaction="notification.reaction" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="retweet" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="quote-left" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="user-plus" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </template> - - <template v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="user-clock" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="reply" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="at" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="chart-pie" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - </div> - - <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - </div> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import getNoteSummary from '../../../../../misc/get-note-summary'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - isContainer: true - }), - ], - - props: { - type: { - type: String, - required: false - } - }, - - data() { - return { - connection: null, - getNoteSummary, - pagination: { - endpoint: 'i/notifications', - limit: 10, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .placeholder - padding 16px - opacity 0.3 - - > .notifications - > div - > .notification - margin 0 - padding 16px - overflow-wrap break-word - font-size 12px - border-bottom solid var(--lineWidth) var(--faceDivider) - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - position -webkit-sticky - position sticky - top 16px - width 36px - height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - > .icon - margin-right 4px - - > .name - overflow hidden - text-overflow ellipsis - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - .note-preview - color var(--noteText) - display inline-block - word-break break-word - - .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - .text header - align-items normal - - &.renote, &.quote - .text header [data-icon] - color #77B255 - - &.follow - .text header [data-icon] - color #53c7ce - - &.receiveFollowRequest - .text header [data-icon] - color #888 - - &.reply, &.mention - .text header [data-icon] - color #555 - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - -</style> diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue deleted file mode 100644 index ff6f24b6e1..0000000000 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ /dev/null @@ -1,140 +0,0 @@ -<template> -<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation"> - <template #header> - <span class="mk-post-form-window--header"> - <span class="icon" v-if="geo"><fa icon="map-marker-alt"/></span> - <span v-if="!reply">{{ $t('note') }}</span> - <span v-if="reply">{{ $t('reply') }}</span> - <span class="count" v-if="files.length != 0">{{ $t('attaches').replace('{}', files.length) }}</span> - <span class="count" v-if="uploadings.length != 0">{{ $t('uploading-media').replace('{}', uploadings.length) }}<mk-ellipsis/></span> - </span> - </template> - - <div class="mk-post-form-window--body" :style="{ maxHeight: `${maxHeight}px` }"> - <mk-note-preview v-if="reply" class="notePreview" :note="reply"/> - <x-post-form ref="form" - :reply="reply" - :mention="mention" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - - @posted="onPosted" - @change-uploadings="onChangeUploadings" - @change-attached-files="onChangeFiles" - @geo-attached="onGeoAttached" - @geo-dettached="onGeoDettached"/> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XPostForm from './post-form.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/post-form-window.vue'), - - components: { - XPostForm - }, - - props: { - reply: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - - animation: { - type: Boolean, - required: false, - default: true - }, - - initialText: { - type: String, - required: false - }, - - initialNote: { - type: Object, - required: false - }, - - instant: { - type: Boolean, - required: false, - default: false - }, - }, - - data() { - return { - uploadings: [], - files: [], - geo: null - }; - }, - - computed: { - maxHeight() { - return window.innerHeight - 50; - }, - }, - - mounted() { - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - - methods: { - onChangeUploadings(files) { - this.uploadings = files; - }, - onChangeFiles(files) { - this.files = files; - }, - onGeoAttached(geo) { - this.geo = geo; - }, - onGeoDettached() { - this.geo = null; - }, - onPosted() { - (this.$refs.window as any).close(); - }, - onWindowClosed() { - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-post-form-window - .mk-post-form-window--header - .icon - margin-right 8px - - .count - margin-left 8px - opacity 0.8 - - &:before - content '(' - - &:after - content ')' - - .mk-post-form-window--body - .notePreview - margin 16px 22px - -</style> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue deleted file mode 100644 index b9c0624bd7..0000000000 --- a/src/client/app/desktop/views/components/post-form.vue +++ /dev/null @@ -1,331 +0,0 @@ -<template> -<div class="gjisdzwh" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <div class="content"> - <div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags"> - <b>{{ $t('@.post-form.recent-tags') }}:</b> - <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('@.post-form.click-to-tagging')">#{{ tag }}</a> - </div> - <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }} - <div class="visibleUsers"> - <span v-for="u in visibleUsers"> - <mk-user-name :user="u"/> - <button @click="removeVisibleUser(u)"><fa icon="times"/></button> - </span> - <button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button> - </div> - </div> - <div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div> - <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }"> - <div class="textarea"> - <textarea :class="{ with: (files.length != 0 || poll) }" - ref="text" v-model="text" :disabled="posting" - @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" - v-autocomplete="{ model: 'text' }" - ></textarea> - <button class="emoji" @click="emoji" ref="emoji"> - <fa :icon="['far', 'laugh']"/> - </button> - <x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/> - <x-poll-editor class="poll-editor" v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> - </div> - </div> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <button class="upload" :title="$t('@.post-form.attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button> - <button class="drive" :title="$t('@.post-form.attach-media-from-drive')" @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button class="kao" :title="$t('@.post-form.insert-a-kao')" @click="kao"><fa :icon="['far', 'smile']"/></button> - <button class="poll" :title="$t('@.post-form.create-poll')" @click="poll = !poll"><fa icon="chart-pie"/></button> - <button class="cw" :title="$t('@.post-form.hide-contents')" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> - <button class="geo" :title="$t('@.post-form.attach-location-information')" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> - <button class="visibility" :title="$t('@.post-form.visibility')" @click="setVisibility" ref="visibilityButton"> - <span v-if="visibility === 'public'"><fa icon="globe"/></span> - <span v-if="visibility === 'home'"><fa icon="home"/></span> - <span v-if="visibility === 'followers'"><fa icon="unlock"/></span> - <span v-if="visibility === 'specified'"><fa icon="envelope"/></span> - </button> - <p class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</p> - <ui-button primary :wait="posting" class="submit" :disabled="!canPost" @click="post"> - {{ posting ? $t('@.post-form.posting') : submitText }}<mk-ellipsis v-if="posting"/> - </ui-button> - <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> - <div class="dropzone" v-if="draghover"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import form from '../../../common/scripts/post-form'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/post-form.vue'), - - mixins: [ - form({ - onSuccess: self => { - self.$notify(self.renote - ? self.$t('reposted') - : self.reply - ? self.$t('replied') - : self.$t('posted')); - }, - onFailure: self => { - self.$notify(self.renote - ? self.$t('renote-failed') - : self.reply - ? self.$t('reply-failed') - : self.$t('note-failed')); - } - }), - ], -}); -</script> - -<style lang="stylus" scoped> -.gjisdzwh - display block - padding 16px - background var(--desktopPostFormBg) - overflow hidden - - &:after - content "" - display block - clear both - - > .content - > input - > .textarea > textarea - display block - width 100% - padding 12px - font-size 16px - color var(--desktopPostFormTextareaFg) - background var(--desktopPostFormTextareaBg) - outline none - border solid 1px var(--primaryAlpha01) - border-radius 4px - transition border-color .2s ease - padding-right 30px - - &:hover - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - border-color var(--primaryAlpha05) - transition border-color 0s ease - - &:disabled - opacity 0.5 - - &::-webkit-input-placeholder - color var(--primaryAlpha03) - - > input - margin-bottom 8px - - > .textarea - > .emoji - position absolute - top 0 - right 0 - padding 10px - font-size 18px - color var(--text) - opacity 0.5 - - &:hover - color var(--textHighlighted) - opacity 1 - - &:active - color var(--primary) - opacity 1 - - > textarea - margin 0 - max-width 100% - min-width 100% - min-height 84px - - &:hover - & + * + * - & + * + * + * - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - & + * + * - & + * + * + * - border-color var(--primaryAlpha05) - transition border-color 0s ease - - & + .emoji - opacity 0.7 - - &.with - border-bottom solid 1px var(--primaryAlpha01) !important - border-radius 4px 4px 0 0 - - > .files - margin 0 - padding 0 - background var(--desktopPostFormTextareaBg) - border solid 1px var(--primaryAlpha01) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - &.with - border-bottom solid 1px var(--primaryAlpha01) !important - border-radius 0 - - > .poll-editor - background var(--desktopPostFormTextareaBg) - border solid 1px var(--primaryAlpha01) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - > .hashtags - margin 0 0 8px 0 - overflow hidden - white-space nowrap - font-size 14px - - > b - color var(--primary) - - > * - margin-right 8px - white-space nowrap - - > .with-quote - margin 0 0 8px 0 - color var(--primary) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .to-specified - margin 0 0 8px 0 - color var(--primary) - - > .visibleUsers - display inline - top -1px - font-size 14px - - > span - margin-left 14px - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .local-only - margin 0 0 8px 0 - color var(--primary) - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - border solid 1px var(--primaryAlpha02) - border-radius 4px - - input[type='file'] - display none - - .submit - display block - position absolute - bottom 16px - right 16px - width 110px - height 40px - - > .text-count - pointer-events none - display block - position absolute - bottom 16px - right 138px - margin 0 - line-height 40px - color var(--primaryAlpha05) - - &.over - color #ec3828 - - > .upload - > .drive - > .kao - > .poll - > .cw - > .geo - > .visibility - display inline-block - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color var(--desktopPostFormTransparentButtonFg) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background linear-gradient(to bottom, var(--desktopPostFormTransparentButtonActiveGradientStart) 0%, var(--desktopPostFormTransparentButtonActiveGradientEnd) 100%) - border-color var(--primaryAlpha05) - box-shadow 0 2px 4px rgba(#000, 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - - > .dropzone - position absolute - left 0 - top 0 - width 100% - height 100% - border dashed 2px var(--primaryAlpha05) - pointer-events none - -</style> diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue deleted file mode 100644 index 28b35dbd97..0000000000 --- a/src/client/app/desktop/views/components/progress-dialog.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom"> - <template #header>{{ title }}<mk-ellipsis/></template> - <div :class="$style.body"> - <p :class="$style.init" v-if="isNaN(value)">{{ $t('waiting') }}<mk-ellipsis/></p> - <p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p> - <progress :class="$style.progress" - v-if="!isNaN(value) && value < max" - :value="isNaN(value) ? 0 : value" - :max="max" - ></progress> - <div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/progress-dialog.vue'), - props: ['title', 'initValue', 'initMax'], - data() { - return { - value: this.initValue, - max: this.initMax - }; - }, - methods: { - update(value, max) { - this.value = parseInt(value, 10); - this.max = parseInt(max, 10); - }, - close() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> - - -.body - padding 18px 24px 24px 24px - -.init - display block - margin 0 - text-align center - color rgba(#000, 0.7) - -.percentage - display block - margin 0 0 4px 0 - text-align center - line-height 16px - color var(--primaryAlpha07) - - &:after - content '%' - -.progress - display block - margin 0 - width 100% - height 10px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background var(--primary) - - &::-webkit-progress-bar - background var(--primaryAlpha01) - -.waiting - background linear-gradient( - 45deg, - var(--primaryLighten30) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryLighten30) 50%, - var(--primaryLighten30) 75%, - var(--primary) 75%, - var(--primary) - ) - background-size 32px 32px - animation progress-dialog-tag-progress-waiting 1.5s linear infinite - - @keyframes progress-dialog-tag-progress-waiting - from {background-position: 0 0;} - to {background-position: -64px 32px;} - -</style> diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue deleted file mode 100644 index 0ca347b530..0000000000 --- a/src/client/app/desktop/views/components/renote-form-window.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation"> - <template #header :class="$style.header"><fa icon="retweet"/>{{ $t('title') }}</template> - <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/renote-form-window.vue'), - props: { - note: { - type: Object, - required: true - }, - - animation: { - type: Boolean, - required: false, - default: true - } - }, - - computed: { - keymap(): any { - return { - 'esc': this.close, - 'enter': this.post, - 'q': this.quote, - }; - } - }, - - methods: { - post() { - (this.$refs.form as any).ok(); - }, - quote() { - (this.$refs.form as any).onQuote(); - }, - close() { - (this.$refs.window as any).close(); - }, - onPosted() { - (this.$refs.window as any).close(); - }, - onCanceled() { - (this.$refs.window as any).close(); - }, - onWindowClosed() { - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue deleted file mode 100644 index 53fbf0ff30..0000000000 --- a/src/client/app/desktop/views/components/renote-form.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<div class="mk-renote-form"> - <mk-note-preview class="preview" :note="note"/> - <template v-if="!quote"> - <footer> - <a class="quote" v-if="!quote" @click="onQuote">{{ $t('quote') }}</a> - <ui-button class="button cancel" inline @click="cancel">{{ $t('cancel') }}</ui-button> - <ui-button class="button home" inline :primary="visibility != 'public'" @click="ok('home')" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote-home') }}</ui-button> - <ui-button class="button ok" inline :primary="visibility == 'public'" @click="ok('public')" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote') }}</ui-button> - </footer> - </template> - <template v-if="quote"> - <x-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/renote-form.vue'), - - components: { - XPostForm: () => import('./post-form.vue').then(m => m.default) - }, - - props: { - note: { - type: Object, - required: true - } - }, - - data() { - return { - wait: false, - quote: false, - visibility: this.$store.state.settings.defaultNoteVisibility - }; - }, - - methods: { - ok(v: string) { - this.wait = true; - this.$root.api('notes/create', { - renoteId: this.note.id, - visibility: v || this.visibility - }).then(data => { - this.$emit('posted'); - this.$notify(this.$t('success')); - }).catch(err => { - this.$notify(this.$t('failure')); - }).then(() => { - this.wait = false; - }); - }, - - cancel() { - this.$emit('canceled'); - }, - - onQuote() { - this.quote = true; - - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - - onChildFormPosted() { - this.$emit('posted'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-renote-form - > .preview - margin 16px 22px - - > footer - height 72px - background var(--desktopRenoteFormFooter) - - > .quote - position absolute - bottom 16px - left 28px - line-height 40px - - > .button - display block - position absolute - bottom 16px - width 120px - height 40px - - &.cancel - right 280px - - &.home - right 148px - font-size 13px - - &.ok - right 16px - -</style> diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue deleted file mode 100644 index 9bfd5a14c7..0000000000 --- a/src/client/app/desktop/views/components/settings-window.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom"> - <template #header :class="$style.header"><fa icon="cog"/>{{ $t('@.settings') }}</template> - <x-settings :initial-page="initialPage" @done="close"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings-window.vue'), - - components: { - XSettings: () => import('./settings.vue').then(m => m.default) - }, - - props: { - initialPage: { - type: String, - required: false - } - }, - methods: { - close() { - (this as any).$refs.window.close(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue deleted file mode 100644 index 65701cd5f3..0000000000 --- a/src/client/app/desktop/views/components/settings.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="mk-settings"> - <div class="nav" :class="{ inWindow }"> - <router-link to="/i/settings/profile" active-class="active"><fa icon="user" fixed-width/>{{ $t('@._settings.profile') }}</router-link> - <router-link to="/i/settings/appearance" active-class="active"><fa icon="palette" fixed-width/>{{ $t('@._settings.appearance') }}</router-link> - <router-link to="/i/settings/behavior" active-class="active"><fa icon="desktop" fixed-width/>{{ $t('@._settings.behavior') }}</router-link> - <router-link to="/i/settings/notification" active-class="active"><fa :icon="['far', 'bell']" fixed-width/>{{ $t('@._settings.notification') }}</router-link> - <router-link to="/i/settings/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link> - <router-link to="/i/settings/hashtags" active-class="active"><fa icon="hashtag" fixed-width/>{{ $t('@._settings.tags') }}</router-link> - <router-link to="/i/settings/muteAndBlock" active-class="active"><fa icon="ban" fixed-width/>{{ $t('@._settings.mute-and-block') }}</router-link> - <router-link to="/i/settings/apps" active-class="active"><fa icon="puzzle-piece" fixed-width/>{{ $t('@._settings.apps') }}</router-link> - <router-link to="/i/settings/security" active-class="active"><fa icon="unlock-alt" fixed-width/>{{ $t('@._settings.security') }}</router-link> - <router-link to="/i/settings/api" active-class="active"><fa icon="key" fixed-width/>API</router-link> - <router-link to="/i/settings/other" active-class="active"><fa icon="cogs" fixed-width/>{{ $t('@._settings.other') }}</router-link> - </div> - <div class="pages"> - <x-settings :page="page"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSettings from '../../../common/views/components/settings/settings.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XSettings, - }, - props: { - page: { - type: String, - required: true, - }, - inWindow: { - type: Boolean, - required: false, - default: true - } - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-settings - display flex - width 100% - height 100% - - > .nav - flex 0 0 200px - width 100% - height 100% - padding 16px 0 0 0 - overflow auto - z-index 1 - font-size 15px - - > a - display block - padding 10px 16px - margin 0 - color var(--desktopSettingsNavItem) - cursor pointer - user-select none - transition margin-left 0.2s ease - - > [data-icon] - margin-right 4px - - &:hover - color var(--desktopSettingsNavItemHover) - - &.active - margin-left 8px - color var(--primary) !important - - > .pages - width 100% - height 100% - flex auto - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue deleted file mode 100644 index 78f9a6034b..0000000000 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div class="mk-sub-note-content"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span> - <a class="reply" v-if="note.replyId"><fa icon="reply"/></a> - <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> - <a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ this.$t('media-count').replace('{}', note.files.length) }})</summary> - <mk-media-list :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $t('poll') }}</summary> - <mk-poll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/sub-note-content.vue'), - props: ['note'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-note-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - mk-poll - font-size 80% - -</style> diff --git a/src/client/app/desktop/views/components/ui-container.vue b/src/client/app/desktop/views/components/ui-container.vue deleted file mode 100644 index 59954fee8e..0000000000 --- a/src/client/app/desktop/views/components/ui-container.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<div class="kedshtep" :class="{ naked, inNakedDeckColumn, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header v-if="showHeader" :class="{ bodyTogglable }" @click="toggleContent(!showBody)"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - <button v-if="bodyTogglable"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </header> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - }, - bodyTogglable: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - inject: { - inNakedDeckColumn: { - default: false - } - }, - data() { - return { - showBody: this.expanded - }; - }, - methods: { - toggleContent(show: boolean) { - if (!this.bodyTogglable) return; - this.showBody = show; - this.$emit('toggle', show); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kedshtep - overflow hidden - - &:not(.inNakedDeckColumn) - background var(--face) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - & + .kedshtep - margin-top 16px - - &.naked - background transparent !important - box-shadow none !important - - > header - background var(--faceHeader) - - &.bodyTogglable - cursor pointer - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &: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 var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - &.inNakedDeckColumn - background var(--face) - - > header - margin 0 - padding 8px 16px - font-size 12px - color var(--text) - background var(--deckColumnBg) - - &.bodyTogglable - cursor pointer - - > button - position absolute - top 0 - right 8px - padding 8px 6px - font-size 14px - color var(--text) - -</style> diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue deleted file mode 100644 index 52e8e1d6cb..0000000000 --- a/src/client/app/desktop/views/components/ui-notification.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="mk-ui-notification"> - <p>{{ message }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: ['message'], - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - opacity: 1, - translateY: [-64, 0], - easing: 'easeOutElastic', - duration: 500 - }); - - setTimeout(() => { - anime({ - targets: this.$el, - opacity: 0, - translateY: -64, - duration: 500, - easing: 'easeInElastic', - complete: () => this.destroyDom() - }); - }, 5000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui-notification - display block - position fixed - z-index 10000 - top -128px - left 0 - right 0 - margin 0 auto - padding 128px 0 0 0 - width 500px - color var(--desktopNotificationFg) - background var(--desktopNotificationBg) - border-radius 0 0 8px 8px - box-shadow 0 2px 4px var(--desktopNotificationShadow) - transform translateY(-64px) - opacity 0 - pointer-events none - - > p - margin 0 - line-height 64px - text-align center - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue deleted file mode 100644 index 690f3a5587..0000000000 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ /dev/null @@ -1,343 +0,0 @@ -<template> -<div class="account" v-hotkey.global="keymap"> - <button class="header" :data-active="isOpen" @click="toggle"> - <span class="username">{{ $store.state.i.username }}<template v-if="!isOpen"><fa icon="angle-down"/></template><template v-if="isOpen"><fa icon="angle-up"/></template></span> - <mk-avatar class="avatar" :user="$store.state.i"/> - </button> - <transition name="zoom-in-top"> - <div class="menu" v-if="isOpen"> - <ul> - <li> - <router-link :to="`/@${ $store.state.i.username }`"> - <i><fa icon="user" fixed-width/></i> - <span>{{ $t('profile') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li @click="drive"> - <p> - <i><fa icon="cloud" fixed-width/></i> - <span>{{ $t('@.drive') }}</span> - <i><fa icon="angle-right"/></i> - </p> - </li> - <li> - <router-link to="/i/favorites"> - <i><fa icon="star" fixed-width/></i> - <span>{{ $t('@.favorites') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/lists"> - <i><fa icon="list" fixed-width/></i> - <span>{{ $t('lists') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/groups"> - <i><fa :icon="faUsers" fixed-width/></i> - <span>{{ $t('groups') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/pages"> - <i><fa :icon="faStickyNote" fixed-width/></i> - <span>{{ $t('@.pages') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> - <router-link to="/i/follow-requests"> - <i><fa :icon="['far', 'envelope']" fixed-width/></i> - <span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link :to="`/@${ $store.state.i.username }/room`"> - <i><fa :icon="faDoorOpen" fixed-width/></i> - <span>{{ $t('room') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - </ul> - <ul> - <li> - <router-link to="/i/settings"> - <i><fa icon="cog" fixed-width/></i> - <span>{{ $t('@.settings') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li v-if="$store.state.i.isAdmin || $store.state.i.isModerator"> - <a href="/admin"> - <i><fa icon="terminal" fixed-width/></i> - <span>{{ $t('admin') }}</span> - <i><fa icon="angle-right"/></i> - </a> - </li> - </ul> - <ul> - <li @click="toggleDeckMode"> - <p> - <template v-if="$store.state.device.inDeckMode"><span>{{ $t('@.home') }}</span><i><fa :icon="faHome"/></i></template> - <template v-else><span>{{ $t('@.deck') }}</span><i><fa :icon="faColumns"/></i></template> - </p> - </li> - <li @click="dark"> - <p> - <span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span> - <template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon"/></i></template> - </p> - </li> - </ul> - <ul> - <li @click="signout"> - <p class="signout"> - <i><fa icon="power-off" fixed-width/></i> - <span>{{ $t('@.signout') }}</span> - </p> - </li> - </ul> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -// import MkSettingsWindow from './settings-window.vue'; -import MkDriveWindow from './drive-window.vue'; -import contains from '../../../common/scripts/contains'; -import { faHome, faColumns, faUsers, faDoorOpen } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.account.vue'), - data() { - return { - isOpen: false, - faHome, faColumns, faMoon, faSun, faStickyNote, faUsers, faDoorOpen - }; - }, - computed: { - keymap(): any { - return { - 'a|m': this.toggle - }; - } - }, - beforeDestroy() { - this.close(); - }, - methods: { - toggle() { - this.isOpen ? this.close() : this.open(); - }, - open() { - this.isOpen = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - close() { - this.isOpen = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); - return false; - }, - drive() { - this.close(); - this.$root.new(MkDriveWindow); - }, - signout() { - this.$root.signout(); - }, - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - toggleDeckMode() { - this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); - location.replace('/'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.account - > .header - display block - margin 0 - padding 0 - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > .avatar - filter saturate(150%) - - > .username - display block - float left - margin 0 12px 0 16px - max-width 16em - line-height 48px - font-weight bold - text-decoration none - - @media (max-width 1100px) - display none - - [data-icon] - margin-left 8px - - > .avatar - display block - float left - min-width 32px - max-width 32px - min-height 32px - max-height 32px - margin 8px 8px 8px 0 - border-radius 4px - transition filter 100ms ease - - @media (max-width 1100px) - margin-left 8px - - > .menu - $bgcolor = var(--face) - display block - position absolute - top 56px - right -2px - width 230px - font-size 0.8em - background $bgcolor - border-radius 4px - box-shadow 0 var(--lineWidth) 4px rgba(#000, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(#000, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px $bgcolor - border-left solid 14px transparent - - ul - display block - margin 10px 0 - padding 0 - list-style none - - & + ul - padding-top 10px - border-top solid var(--lineWidth) var(--faceDivider) - - > li - display block - margin 0 - padding 0 - - > a - > p - display block - z-index 1 - padding 0 28px - margin 0 - line-height 40px - color var(--text) - cursor pointer - - * - pointer-events none - - > span:first-child - padding-left 22px - - > span:nth-child(2) - > i - margin-left 4px - padding 2px 8px - font-size 90% - font-style normal - background var(--primary) - color var(--primaryForeground) - border-radius 8px - - > i:first-child - margin-right 6px - width 16px - - > i:last-child - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height 40px - - &:hover, &:active - text-decoration none - background var(--primary) - color var(--primaryForeground) - - &:active - background var(--primaryDarken10) - - &.signout - $color = #e64137 - - &:hover, &:active - background $color - color #fff - - &:active - background darken($color, 10%) - -.zoom-in-top-enter-active, -.zoom-in-top-leave-active { - transform-origin: center -16px; -} - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue deleted file mode 100644 index b8b638bc41..0000000000 --- a/src/client/app/desktop/views/components/ui.header.clock.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="clock"> - <div class="header"> - <time ref="time"> - <span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span> - <br> - <span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span> - </time> - </div> - <div class="content"> - <mk-analog-clock :dark="true"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - now: new Date(), - clock: null - }; - }, - computed: { - yyyy(): number { - return this.now.getFullYear(); - }, - mm(): string { - return ('0' + (this.now.getMonth() + 1)).slice(-2); - }, - dd(): string { - return ('0' + this.now.getDate()).slice(-2); - }, - hh(): string { - return ('0' + this.now.getHours()).slice(-2); - }, - nn(): string { - return ('0' + this.now.getMinutes()).slice(-2); - } - }, - mounted() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - tick() { - this.now = new Date(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.clock - display inline-block - overflow visible - - > .header - padding 0 12px - text-align center - font-size 10px - - &, * - cursor: default - - &:hover - background #899492 - - & + .content - visibility visible - - > time - color #fff !important - - * - color #fff !important - - &:after - content "" - display block - clear both - - > time - display table-cell - vertical-align middle - height 48px - color var(--desktopHeaderFg) - - > .yyyymmdd - opacity 0.7 - - > .content - visibility hidden - display block - position absolute - top auto - right 0 - z-index 3 - margin 0 - padding 0 - width 256px - background #899492 - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.messaging.vue b/src/client/app/desktop/views/components/ui.header.messaging.vue deleted file mode 100644 index c5d1da3a3d..0000000000 --- a/src/client/app/desktop/views/components/ui.header.messaging.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="toltmoik"> - <button @click="open()" :title="$t('@.messaging')"> - <i class="bell"><fa :icon="faComments"/></i> - <i class="circle" v-if="hasUnreadMessagingMessage"><fa icon="circle"/></i> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMessagingWindow from './messaging-window.vue'; -import { faComments } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - faComments - }; - }, - - computed: { - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - - methods: { - open() { - this.$root.new(MkMessagingWindow); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.toltmoik - > button - display block - margin 0 - padding 0 - width 32px - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > i.bell - font-size 1.2em - line-height 48px - - > i.circle - margin-left -5px - vertical-align super - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue deleted file mode 100644 index 2bd3cf8772..0000000000 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="nav"> - <ul> - <li class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop"> - <router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link> - </li> - <li class="featured" :class="{ active: $route.name == 'featured' }"> - <router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link> - </li> - <li class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }"> - <router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link> - </li> - <li class="game"> - <a @click="game"> - <fa icon="gamepad"/> - <p>{{ $t('game') }}</p> - <template v-if="hasGameInvitations"><fa icon="circle"/></template> - </a> - </li> - </ul> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkGameWindow from './game-window.vue'; -import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.nav.vue'), - data() { - return { - hasGameInvitations: false, - connection: null, - faNewspaper, faHashtag - }; - }, - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - methods: { - onReversiInvited() { - this.hasGameInvitations = true; - }, - - onReversiNoInvites() { - this.hasGameInvitations = false; - }, - - game() { - this.$root.new(MkGameWindow); - }, - - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nav - display inline-block - margin 0 - padding 0 - line-height 3rem - vertical-align top - - > ul - display inline-block - margin 0 - padding 0 - vertical-align top - line-height 3rem - list-style none - - > li - display inline-block - vertical-align top - height 48px - line-height 48px - - &.active - > a - border-bottom solid 3px var(--primary) - - > a - display inline-block - z-index 1 - height 100% - padding 0 20px - font-size 13px - font-variant small-caps - color var(--desktopHeaderFg) - text-decoration none - transition none - cursor pointer - - * - pointer-events none - - &:hover - color var(--desktopHeaderHoverFg) - text-decoration none - - > [data-icon]:first-child - margin-right 8px - - > [data-icon]:last-child - margin-left 5px - font-size 10px - color var(--notificationIndicator) - - @media (max-width 1100px) - margin-left -5px - - > p - display inline - margin 0 - - @media (max-width 1100px) - display none - - @media (max-width 700px) - padding 0 12px - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue deleted file mode 100644 index d3316d6a89..0000000000 --- a/src/client/app/desktop/views/components/ui.header.notifications.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<div class="notifications" v-hotkey.global="keymap"> - <button :data-active="isOpen" @click="toggle" :title="$t('title')"> - <i class="bell"><fa :icon="['far', 'bell']"/></i> - <i class="circle" v-if="hasUnreadNotification"><fa icon="circle"/></i> - </button> - <div class="pop" v-if="isOpen"> - <mk-notifications/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.notifications.vue'), - data() { - return { - isOpen: false - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - keymap(): any { - return { - 'shift+n': this.toggle - }; - } - }, - - methods: { - toggle() { - this.isOpen ? this.close() : this.open(); - }, - - open() { - this.isOpen = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - - close() { - this.isOpen = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); - return false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.notifications - > button - display block - margin 0 - padding 0 - width 32px - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > i.bell - font-size 1.2em - line-height 48px - - > i.circle - margin-left -5px - vertical-align super - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - - > .pop - $bgcolor = var(--face) - display block - position absolute - top 56px - right -72px - width 300px - background $bgcolor - border-radius 4px - box-shadow 0 1px 4px rgba(#000, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(#000, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px $bgcolor - border-left solid 14px transparent - - > .mk-notifications - max-height 350px - font-size 1rem - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue deleted file mode 100644 index b273ad8d4d..0000000000 --- a/src/client/app/desktop/views/components/ui.header.post.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="note"> - <button @click="post" :title="$t('post')"><fa icon="pencil-alt"/></button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.post.vue'), - methods: { - post() { - this.$post(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - display inline-block - padding 8px - height 100% - vertical-align top - - > button - display inline-block - margin 0 - padding 0 10px - height 100% - font-size 1.2em - font-weight normal - text-decoration none - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue deleted file mode 100644 index 0cf5ca6f32..0000000000 --- a/src/client/app/desktop/views/components/ui.header.search.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<form class="wlvfdpkp" @submit.prevent="onSubmit"> - <i><fa icon="search"/></i> - <input v-model="q" type="search" :placeholder="$t('placeholder')" v-autocomplete="{ model: 'q' }"/> - <div class="result"></div> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { search } from '../../../common/scripts/search'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.search.vue'), - data() { - return { - q: '', - wait: false - }; - }, - methods: { - async onSubmit() { - if (this.wait) return; - - this.wait = true; - search(this, this.q).finally(() => { - this.wait = false; - this.q = ''; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wlvfdpkp - @media (max-width 800px) - display none !important - - > i - display block - position absolute - top 0 - left 0 - width 48px - text-align center - line-height 48px - color var(--desktopHeaderFg) - pointer-events none - - > * - vertical-align middle - - > input - user-select text - cursor auto - margin 8px 0 0 0 - padding 6px 18px 6px 36px - width 14em - height 32px - font-size 1em - background var(--desktopHeaderSearchBg) - outline none - border none - border-radius 16px - transition color 0.5s ease, border 0.5s ease - color var(--desktopHeaderSearchFg) - - @media (max-width 1000px) - width 10em - - &::placeholder - color var(--desktopHeaderFg) - - &:hover - background var(--desktopHeaderSearchHoverBg) - - &:focus - box-shadow 0 0 0 2px var(--primaryAlpha05) !important - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue deleted file mode 100644 index 14a7321552..0000000000 --- a/src/client/app/desktop/views/components/ui.header.vue +++ /dev/null @@ -1,161 +0,0 @@ -<template> -<div class="header" :style="style"> - <p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }} <a href="/assets/flush.html?force">Flush</a></p> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="main"> - <div class="container" ref="mainContainer"> - <div class="left"> - <x-nav/> - </div> - <div class="center"> - <div class="icon" @click="goToTop"> - <img svg-inline src="../../assets/header-icon.svg"/> - </div> - </div> - <div class="right"> - <x-search/> - <x-account v-if="$store.getters.isSignedIn"/> - <x-messaging v-if="$store.getters.isSignedIn"/> - <x-notifications v-if="$store.getters.isSignedIn"/> - <x-post v-if="$store.getters.isSignedIn"/> - <x-clock v-if="$store.state.settings.showClockOnHeader" class="clock"/> - </div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { env } from '../../../config'; - -import XNav from './ui.header.nav.vue'; -import XSearch from './ui.header.search.vue'; -import XAccount from './ui.header.account.vue'; -import XNotifications from './ui.header.notifications.vue'; -import XPost from './ui.header.post.vue'; -import XClock from './ui.header.clock.vue'; -import XMessaging from './ui.header.messaging.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XNav, - XSearch, - XAccount, - XNotifications, - XMessaging, - XPost, - XClock - }, - - data() { - return { - env: env - }; - }, - - computed: { - style(): any { - return { - 'box-shadow': this.$store.state.device.useShadow ? '0 0px 8px rgba(0, 0, 0, 0.2)' : 'none' - }; - } - }, - - mounted() { - this.$store.commit('setUiHeaderHeight', this.$el.offsetHeight); - }, - - methods: { - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - }, -}); -</script> - -<style lang="stylus" scoped> -.header - position fixed - top 0 - z-index 1000 - width 100% - - > .warn - display block - margin 0 - padding 4px - text-align center - font-size 12px - background #f00 - color #fff - - > .main - height 48px - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height 48px - background var(--desktopHeaderBg) - - > .main - z-index 1001 - margin 0 - padding 0 - background-clip content-box - font-size 0.9rem - user-select none - - > .container - display flex - width 100% - max-width 1208px - margin 0 auto - - > * - position absolute - height 48px - - > .center - right 0 - - > .icon - margin auto - display block - width 48px - text-align center - cursor pointer - opacity 0.5 - - > svg - width 24px - height 48px - vertical-align top - fill var(--desktopHeaderFg) - - > .left, - > .center - left 0 - - > .right - right 0 - - > * - display inline-block - vertical-align top - - @media (max-width 1100px) - > .clock - display none - -</style> diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue deleted file mode 100644 index d1ceec5198..0000000000 --- a/src/client/app/desktop/views/components/ui.sidebar.vue +++ /dev/null @@ -1,363 +0,0 @@ -<template> -<div class="header" :class="navbar" :data-shadow="$store.state.device.useShadow"> - <div class="body"> - <div class="post"> - <button @click="post" :title="$t('title')"><fa icon="pencil-alt"/></button> - </div> - - <div class="nav" v-if="$store.getters.isSignedIn"> - <div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop"> - <router-link to="/"><fa icon="home"/></router-link> - </div> - <div class="featured" :class="{ active: $route.name == 'featured' }"> - <router-link to="/featured"><fa :icon="faNewspaper"/></router-link> - </div> - <div class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }"> - <router-link to="/explore"><fa :icon="faHashtag"/></router-link> - </div> - <div class="game"> - <a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a> - </div> - </div> - - <div class="nav bottom" v-if="$store.getters.isSignedIn"> - <div> - <a @click="drive"><fa icon="cloud"/></a> - </div> - <div ref="notificationsButton" :class="{ active: showNotifications }"> - <a @click="notifications"><fa :icon="['far', 'bell']"/></a> - </div> - <div class="messaging"> - <a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a> - </div> - <div> - <a @click="settings"><fa icon="cog"/></a> - </div> - <div class="signout"> - <a @click="signout"><fa icon="power-off"/></a> - </div> - <div> - <router-link to="/i/favorites"><fa icon="star"/></router-link> - </div> - <div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> - <a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a> - </div> - <div class="account"> - <router-link :to="`/@${ $store.state.i.username }`"> - <mk-avatar class="avatar" :user="$store.state.i"/> - </router-link> - </div> - <div> - <template v-if="$store.state.device.inDeckMode"> - <a @click="toggleDeckMode(false)"><fa icon="home"/></a> - </template> - <template v-else> - <a @click="toggleDeckMode(true)"><fa icon="columns"/></a> - </template> - </div> - <div> - <a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a> - </div> - </div> - </div> - - <transition :name="`slide-${navbar}`"> - <div class="notifications" v-if="showNotifications" ref="notifications" :class="navbar" :data-shadow="$store.state.device.useShadow"> - <mk-notifications/> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkSettingsWindow from './settings-window.vue'; -import MkDriveWindow from './drive-window.vue'; -import MkMessagingWindow from './messaging-window.vue'; -import MkGameWindow from './game-window.vue'; -import contains from '../../../common/scripts/contains'; -import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.sidebar.vue'), - data() { - return { - hasGameInvitations: false, - connection: null, - showNotifications: false, - faNewspaper, faHashtag - }; - }, - - computed: { - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - }, - - navbar(): string { - return this.$store.state.device.navbar; - }, - }, - - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - toggleDeckMode(deck) { - this.$store.commit('device/set', { key: 'deckMode', value: deck }); - location.replace('/'); - }, - - onReversiInvited() { - this.hasGameInvitations = true; - }, - - onReversiNoInvites() { - this.hasGameInvitations = false; - }, - - messaging() { - this.$root.new(MkMessagingWindow); - }, - - game() { - this.$root.new(MkGameWindow); - }, - - post() { - this.$post(); - }, - - drive() { - this.$root.new(MkDriveWindow); - }, - - list() { - this.$root.new(MkUserListsWindow); - }, - - followRequests() { - this.$root.new(MkFollowRequestsWindow); - }, - - settings() { - this.$root.new(MkSettingsWindow); - }, - - signout() { - this.$root.signout(); - }, - - notifications() { - this.showNotifications ? this.closeNotifications() : this.openNotifications(); - }, - - openNotifications() { - this.showNotifications = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - - closeNotifications() { - this.showNotifications = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - - onMousedown(e) { - e.preventDefault(); - if ( - !contains(this.$refs.notifications, e.target) && - this.$refs.notifications != e.target && - !contains(this.$refs.notificationsButton, e.target) && - this.$refs.notificationsButton != e.target - ) { - this.closeNotifications(); - } - return false; - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.header - $width = 68px - - position fixed - top 0 - z-index 1000 - width $width - height 100% - - &.left - left 0 - - &[data-shadow] - box-shadow 4px 0 4px rgba(0, 0, 0, 0.1) - - &.right - right 0 - - &[data-shadow] - box-shadow -4px 0 4px rgba(0, 0, 0, 0.1) - - > .body - position fixed - top 0 - z-index 1 - width $width - height 100% - background var(--desktopHeaderBg) - - > .post - width $width - height $width - padding 12px - - > button - display inline-block - margin 0 - padding 0 - height 100% - width 100% - font-size 1.2em - font-weight normal - text-decoration none - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 100% - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - - > .nav.bottom - position absolute - bottom 0 - left 0 - - > .account - width $width - height $width - padding 14px - - > * - display block - width 100% - height 100% - - > .avatar - pointer-events none - width 100% - height 100% - - > .notifications - position fixed - top 0 - width 350px - height 100% - overflow auto - background var(--face) - - &.left - left $width - - &[data-shadow] - box-shadow 4px 0 4px rgba(0, 0, 0, 0.1) - - &.right - right $width - - &[data-shadow] - box-shadow -4px 0 4px rgba(0, 0, 0, 0.1) - - .nav - > * - > * - display block - width $width - line-height 52px - text-align center - font-size 18px - color var(--desktopHeaderFg) - - &:hover - background rgba(0, 0, 0, 0.05) - color var(--desktopHeaderHoverFg) - text-decoration none - - &:active - background rgba(0, 0, 0, 0.1) - - &.left - .nav - > * - &.active - box-shadow -4px 0 var(--primary) inset - - &.right - .nav - > * - &.active - box-shadow 4px 0 var(--primary) inset - -.slide-left-enter-active, -.slide-left-leave-active { - transition: all 0.2s ease; -} - -.slide-left-enter, .slide-left-leave-to { - transform: translateX(-16px); - opacity: 0; -} - -.slide-right-enter-active, -.slide-right-leave-active { - transition: all 0.2s ease; -} - -.slide-right-enter, .slide-right-leave-to { - transform: translateX(16px); - opacity: 0; -} -</style> diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue deleted file mode 100644 index f7961d5083..0000000000 --- a/src/client/app/desktop/views/components/ui.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<div class="mk-ui" v-hotkey.global="keymap"> - <div class="bg" v-if="$store.getters.isSignedIn && $store.state.settings.wallpaper" :style="style"></div> - <x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/> - <x-sidebar class="sidebar" v-if="navbar != 'top'" v-show="!zenMode" ref="sidebar"/> - <div class="content" :class="[{ sidebar: navbar != 'top', zen: zenMode }, navbar]"> - <slot></slot> - </div> - <mk-stream-indicator v-if="$store.getters.isSignedIn"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XHeader from './ui.header.vue'; -import XSidebar from './ui.sidebar.vue'; - -export default Vue.extend({ - components: { - XHeader, - XSidebar - }, - - data() { - return { - zenMode: false - }; - }, - - computed: { - navbar(): string { - return this.$store.state.device.navbar; - }, - - style(): any { - if (!this.$store.getters.isSignedIn || this.$store.state.settings.wallpaper == null) return {}; - return { - backgroundImage: `url(${ this.$store.state.settings.wallpaper })` - }; - }, - - keymap(): any { - return { - 'p': this.post, - 'n': this.post, - 'z': this.toggleZenMode - }; - } - }, - - watch: { - '$store.state.uiHeaderHeight'() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - }, - - navbar() { - if (this.navbar != 'top') { - this.$store.commit('setUiHeaderHeight', 0); - } - } - }, - - mounted() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - }, - - methods: { - post() { - this.$post(); - }, - - toggleZenMode() { - this.zenMode = !this.zenMode; - this.$nextTick(() => { - if (this.$refs.header) { - this.$store.commit('setUiHeaderHeight', this.$refs.header.$el.offsetHeight); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - min-height 100vh - padding-top 48px - - > .bg - position fixed - top 0 - left 0 - width 100% - height 100vh - background-size cover - background-position center - background-attachment fixed - - > .content.sidebar.left - padding-left 68px - - > .content.sidebar.right - padding-right 68px - - > .content.zen - padding 0 !important - -</style> diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue deleted file mode 100644 index dae282ec5c..0000000000 --- a/src/client/app/desktop/views/components/user-list-timeline.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <slot></slot> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['list'], - data() { - return { - connection: null, - date: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - watch: { - $route: 'init' - }, - mounted() { - this.init(); - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - init() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - onNote(note) { - (this.$refs.timeline as any).prepend(note); - }, - onUserAdded() { - (this.$refs.timeline as any).reload(); - }, - onUserRemoved() { - (this.$refs.timeline as any).reload(); - }, - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue deleted file mode 100644 index 9328648ccb..0000000000 --- a/src/client/app/desktop/views/components/user-preview.vue +++ /dev/null @@ -1,164 +0,0 @@ -<template> -<div class="mk-user-preview"> - <template v-if="u != null"> - <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div> - <mk-avatar class="avatar" :user="u" :disable-preview="true"/> - <div class="title"> - <router-link class="name" :to="u | userPage"><mk-user-name :user="u" :nowrap="false"/></router-link> - <p class="username"><mk-acct :user="u"/></p> - </div> - <div class="description"> - <mfm v-if="u.description" :text="u.description" :author="u" :i="$store.state.i" :custom-emojis="u.emojis"/> - </div> - <div class="status"> - <div> - <p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span> - </div> - <div> - <p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span> - </div> - <div> - <p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span> - </div> - </div> - <mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; -import parseAcct from '../../../../../misc/acct/parse'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/user-preview.vue'), - props: { - user: { - type: [Object, String], - required: true - } - }, - data() { - return { - u: null - }; - }, - mounted() { - if (typeof this.user == 'object') { - this.u = this.user; - this.$nextTick(() => { - this.open(); - }); - } else { - const query = this.user.startsWith('@') ? - parseAcct(this.user.substr(1)) : - { userId: this.user }; - - this.$root.api('users/show', query).then(user => { - this.u = user; - this.open(); - }); - } - }, - methods: { - open() { - anime({ - targets: this.$el, - opacity: 1, - 'margin-top': 0, - duration: 200, - easing: 'easeOutQuad' - }); - }, - close() { - anime({ - targets: this.$el, - opacity: 0, - 'margin-top': '-8px', - duration: 200, - easing: 'easeOutQuad', - complete: () => this.destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-user-preview - position absolute - z-index 2048 - margin-top -8px - width 250px - background var(--face) - background-clip content-box - border solid 1px rgba(#000, 0.1) - border-radius 4px - overflow hidden - opacity 0 - - > .banner - height 84px - background-color rgba(0, 0, 0, 0.1) - background-size cover - background-position center - - > .avatar - display block - position absolute - top 62px - left 13px - z-index 2 - width 58px - height 58px - border solid 3px var(--face) - border-radius 8px - - > .title - display block - padding 8px 0 8px 82px - - > .name - display inline-block - margin 0 - font-weight bold - line-height 16px - color var(--text) - - > .username - display block - margin 0 - line-height 16px - font-size 0.8em - color var(--text) - opacity 0.7 - - > .description - padding 0 16px - font-size 0.7em - color var(--text) - - > .status - padding 8px 16px - - > div - display inline-block - width 33% - - > p - margin 0 - font-size 0.7em - color var(--text) - - > span - font-size 1em - color var(--primary) - - > .koudoku-button - position absolute - top 8px - right 8px - -</style> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue deleted file mode 100644 index 499f4e7c91..0000000000 --- a/src/client/app/desktop/views/components/window.vue +++ /dev/null @@ -1,620 +0,0 @@ -<template> -<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover"> - <div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div> - <div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }"> - <div class="body"> - <header ref="header" - @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown" - > - <h1><slot name="header"></slot></h1> - <div> - <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" :title="$t('popout')"> - <i><fa :icon="['far', 'window-restore']"/></i> - </button> - <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" :title="$t('close')"> - <i><fa icon="times"/></i> - </button> - </div> - </header> - <div class="content"> - <slot></slot> - </div> - </div> - <template v-if="canResize"> - <div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div> - <div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div> - <div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div> - <div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div> - <div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div> - <div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div> - <div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div> - <div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; -import contains from '../../../common/scripts/contains'; - -const minHeight = 40; -const minWidth = 200; - -function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/window.vue'), - props: { - isModal: { - type: Boolean, - default: false - }, - canClose: { - type: Boolean, - default: true - }, - width: { - type: String, - default: '530px' - }, - height: { - type: String, - default: 'auto' - }, - popoutUrl: { - type: [String, Function], - default: null - }, - name: { - type: String, - default: null - }, - animation: { - type: Boolean, - required: false, - default: true - } - }, - - computed: { - isFlexible(): boolean { - return this.height == 'auto'; - }, - canResize(): boolean { - return !this.isFlexible; - } - }, - - created() { - // ウィンドウをウィンドウシステムに登録 - this.$root.os.windows.add(this); - }, - - mounted() { - this.$nextTick(() => { - const main = this.$refs.main as any; - main.style.top = '15%'; - main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px'; - - window.addEventListener('resize', this.onBrowserResize); - - this.open(); - }); - }, - - destroyed() { - // ウィンドウをウィンドウシステムから削除 - this.$root.os.windows.remove(this); - - window.removeEventListener('resize', this.onBrowserResize); - }, - - methods: { - open() { - this.$emit('opening'); - - this.top(); - - const bg = this.$refs.bg as any; - const main = this.$refs.main as any; - - if (this.isModal) { - bg.style.pointerEvents = 'auto'; - anime({ - targets: bg, - opacity: 1, - duration: this.animation ? 100 : 0, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'auto'; - anime({ - targets: main, - opacity: 1, - scale: [1.1, 1], - duration: this.animation ? 200 : 0, - easing: 'easeOutQuad' - }); - - if (focus) main.focus(); - - setTimeout(() => { - this.$emit('opened'); - }, this.animation ? 300 : 0); - }, - - close() { - this.$emit('before-close'); - - const bg = this.$refs.bg as any; - const main = this.$refs.main as any; - - if (this.isModal) { - bg.style.pointerEvents = 'none'; - anime({ - targets: bg, - opacity: 0, - duration: this.animation ? 300 : 0, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'none'; - - anime({ - targets: main, - opacity: 0, - scale: 0.8, - duration: this.animation ? 300 : 0, - easing: 'cubicBezier(0.5, -0.5, 1, 0.5)' - }); - - setTimeout(() => { - this.$emit('closed'); - this.destroyDom(); - }, this.animation ? 300 : 0); - }, - - popout() { - const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl; - - const main = this.$refs.main as any; - - if (main) { - const position = main.getBoundingClientRect(); - - const width = parseInt(getComputedStyle(main, '').width, 10); - const height = parseInt(getComputedStyle(main, '').height, 10); - const x = window.screenX + position.left; - const y = window.screenY + position.top; - - window.open(url, url, - `width=${width}, height=${height}, top=${y}, left=${x}`); - - this.close(); - } else { - const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2); - const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2); - window.open(url, url, - `width=${this.width}, height=${this.height}, top=${x}, left=${y}`); - } - }, - - // 最前面へ移動 - top() { - let z = 0; - - const ws = Array.from(this.$root.os.windows.getAll()).filter(w => w != this); - for (const w of ws) { - const m = w.$refs.main; - const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex); - if (mz > z) z = mz; - } - - if (z > 0) { - (this.$refs.main as any).style.zIndex = z + 1; - if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1; - } - }, - - onBgClick() { - if (this.canClose) this.close(); - }, - - onBodyMousedown() { - this.top(); - }, - - onHeaderMousedown(e) { - const main = this.$refs.main as any; - - if (!contains(main, document.activeElement)) main.focus(); - - const position = main.getBoundingClientRect(); - - const clickX = e.clientX; - const clickY = e.clientY; - const moveBaseX = clickX - position.left; - const moveBaseY = clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - moveBaseX; - let moveTop = me.clientY - moveBaseY; - - // 下はみ出し - if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - - // 左はみ出し - if (moveLeft < 0) moveLeft = 0; - - // 上はみ出し - if (moveTop < 0) moveTop = 0; - - // 右はみ出し - if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - - main.style.left = moveLeft + 'px'; - main.style.top = moveTop + 'px'; - }); - }, - - // 上ハンドル掴み時 - onTopHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + move > 0) { - if (height + -move > minHeight) { - this.applyTransformHeight(height + -move); - this.applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - this.applyTransformTop(top + (height - minHeight)); - } - } else { // 上のはみ出し時 - this.applyTransformHeight(top + height); - this.applyTransformTop(0); - } - }); - }, - - // 右ハンドル掴み時 - onRightHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - const browserWidth = window.innerWidth; - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + width + move < browserWidth) { - if (width + move > minWidth) { - this.applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - } - } else { // 右のはみ出し時 - this.applyTransformWidth(browserWidth - left); - } - }); - }, - - // 下ハンドル掴み時 - onBottomHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - const browserHeight = window.innerHeight; - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + height + move < browserHeight) { - if (height + move > minHeight) { - this.applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - } - } else { // 下のはみ出し時 - this.applyTransformHeight(browserHeight - top); - } - }); - }, - - // 左ハンドル掴み時 - onLeftHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + move > 0) { - if (width + -move > minWidth) { - this.applyTransformWidth(width + -move); - this.applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - this.applyTransformLeft(left + (width - minWidth)); - } - } else { // 左のはみ出し時 - this.applyTransformWidth(left + width); - this.applyTransformLeft(0); - } - }); - }, - - // 左上ハンドル掴み時 - onTopLeftHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 右上ハンドル掴み時 - onTopRightHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 右下ハンドル掴み時 - onBottomRightHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 左下ハンドル掴み時 - onBottomLeftHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 高さを適用 - applyTransformHeight(height) { - (this.$refs.main as any).style.height = height + 'px'; - }, - - // 幅を適用 - applyTransformWidth(width) { - (this.$refs.main as any).style.width = width + 'px'; - }, - - // Y座標を適用 - applyTransformTop(top) { - (this.$refs.main as any).style.top = top + 'px'; - }, - - // X座標を適用 - applyTransformLeft(left) { - (this.$refs.main as any).style.left = left + 'px'; - }, - - onDragover(e) { - e.dataTransfer.dropEffect = 'none'; - }, - - onKeydown(e) { - if (e.which == 27) { // Esc - if (this.canClose) { - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - } - }, - - onBrowserResize() { - const main = this.$refs.main as any; - const position = main.getBoundingClientRect(); - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - if (position.left < 0) main.style.left = 0; // 左はみ出し - if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し - if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し - if (position.top < 0) main.style.top = 0; // 上はみ出し - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-window - display block - - > .bg - display block - position fixed - z-index 2000 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.7) - opacity 0 - pointer-events none - - > .main - display block - position fixed - z-index 2000 - top 15% - left 0 - margin 0 - opacity 0 - pointer-events none - - &:focus - &:not([data-is-modal]) - > .body - box-shadow 0 0 0 1px var(--primaryAlpha05), 0 2px 12px 0 var(--desktopWindowShadow) - - > .handle - $size = 8px - - position absolute - - &.top - top -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.right - top 0 - right -($size) - width $size - height 100% - cursor ew-resize - - &.bottom - bottom -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.left - top 0 - left -($size) - width $size - height 100% - cursor ew-resize - - &.top-left - top -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.top-right - top -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - &.bottom-right - bottom -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.bottom-left - bottom -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - > .body - height 100% - overflow hidden - background var(--face) - border-radius 6px - box-shadow 0 2px 12px 0 rgba(#000, 0.5) - - > header - $header-height = 40px - - z-index 1001 - height $header-height - overflow hidden - white-space nowrap - cursor move - background var(--faceHeader) - border-radius 6px 6px 0 0 - box-shadow 0 1px 0 rgba(#000, 0.1) - - &, * - user-select none - - > h1 - pointer-events none - display block - margin 0 auto - overflow hidden - height $header-height - text-overflow ellipsis - text-align center - font-size 1em - line-height $header-height - font-weight normal - color var(--desktopWindowTitle) - - > div:last-child - position absolute - top 0 - right 0 - display block - z-index 1 - - > * - display inline-block - margin 0 - padding 0 - cursor pointer - font-size 1em - color var(--faceTextButton) - border none - outline none - background transparent - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - > i - display inline-block - padding 0 - width $header-height - line-height $header-height - text-align center - - > .content - height 100% - overflow auto - - &:not([flexible]) - > .main > .body > .content - height calc(100% - 40px) - -</style> diff --git a/src/client/app/desktop/views/directives/index.ts b/src/client/app/desktop/views/directives/index.ts deleted file mode 100644 index 324e07596d..0000000000 --- a/src/client/app/desktop/views/directives/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; - -import userPreview from './user-preview'; - -Vue.directive('userPreview', userPreview); -Vue.directive('user-preview', userPreview); diff --git a/src/client/app/desktop/views/directives/user-preview.ts b/src/client/app/desktop/views/directives/user-preview.ts deleted file mode 100644 index 8a4035881a..0000000000 --- a/src/client/app/desktop/views/directives/user-preview.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * マウスオーバーするとユーザーがプレビューされる要素を設定します - */ - -import MkUserPreview from '../components/user-preview.vue'; - -export default { - bind(el, binding, vn) { - const self = el._userPreviewDirective_ = {} as any; - - self.user = binding.value; - self.tag = null; - self.showTimer = null; - self.hideTimer = null; - - self.close = () => { - if (self.tag) { - self.tag.close(); - self.tag = null; - } - }; - - const show = () => { - if (self.tag) return; - - self.tag = new MkUserPreview({ - parent: vn.context, - propsData: { - user: self.user - } - }).$mount(); - - const preview = self.tag.$el; - const rect = el.getBoundingClientRect(); - const x = rect.left + el.offsetWidth + window.pageXOffset; - const y = rect.top + window.pageYOffset; - - preview.style.top = y + 'px'; - preview.style.left = x + 'px'; - - preview.addEventListener('mouseover', () => { - clearTimeout(self.hideTimer); - }); - - preview.addEventListener('mouseleave', () => { - clearTimeout(self.showTimer); - self.hideTimer = setTimeout(self.close, 500); - }); - - document.body.appendChild(preview); - }; - - el.addEventListener('mouseover', () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.showTimer = setTimeout(show, 500); - }); - - el.addEventListener('mouseleave', () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.hideTimer = setTimeout(self.close, 500); - }); - }, - - unbind(el, binding, vn) { - const self = el._userPreviewDirective_; - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.close(); - } -}; diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue deleted file mode 100644 index ec166d42c8..0000000000 --- a/src/client/app/desktop/views/home/home.vue +++ /dev/null @@ -1,406 +0,0 @@ -<template> -<component :is="customize ? 'mk-dummy' : 'mk-ui'" v-hotkey.global="keymap" v-if="$store.getters.isSignedIn || $route.name != 'index'"> - <div class="wqsofvpm" :data-customize="customize"> - <div class="customize" v-if="customize"> - <a @click="done()"><fa icon="check"/>{{ $t('done') }}</a> - <div> - <div class="adder"> - <p>{{ $t('add-widget') }}</p> - <select v-model="widgetAdderSelected"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="trends">{{ $t('@.widgets.trends') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> - <option value="notifications">{{ $t('@.widgets.notifications') }}</option> - <option value="users">{{ $t('@.widgets.users') }}</option> - <option value="polls">{{ $t('@.widgets.polls') }}</option> - <option value="post-form">{{ $t('@.widgets.post-form') }}</option> - <option value="messaging">{{ $t('@.messaging') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - <button @click="addWidget">{{ $t('add') }}</button> - </div> - <div class="trash"> - <x-draggable v-model="trash" group="x" @add="onTrash"></x-draggable> - <p>{{ $t('@.trash') }}</p> - </div> - </div> - </div> - <div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }"> - <template v-if="customize"> - <x-draggable v-for="place in ['left', 'right']" - :list="widgets[place]" - :class="place" - :data-place="place" - group="x" - animation="150" - @sort="onWidgetSort" - :key="place" - > - <div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/> - </div> - </x-draggable> - <div class="main"> - <a @click="hint">{{ $t('@.customization-tips.title') }}</a> - <div> - <x-timeline/> - </div> - </div> - </template> - <template v-else> - <div v-for="place in ['left', 'right']" :class="place" :key="place"> - <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/> - </div> - <div class="main"> - <router-view ref="content"></router-view> - </div> - </template> - </div> - </div> -</component> -<x-welcome v-else/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; -import XWelcome from '../pages/welcome.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/home.vue'), - - components: { - XDraggable, - XWelcome - }, - - data() { - return { - customize: window.location.search == '?customize', - connection: null, - widgetAdderSelected: null, - trash: [], - view: null - }; - }, - - computed: { - home(): any[] { - if (this.$store.getters.isSignedIn) { - return this.$store.getters.home || []; - } else { - return [{ - name: 'instance', - place: 'right' - }, { - name: 'broadcast', - place: 'right', - data: {} - }, { - name: 'hashtags', - place: 'right', - data: {} - }]; - } - }, - left(): any[] { - return this.home.filter(w => w.place == 'left'); - }, - right(): any[] { - return this.home.filter(w => w.place == 'right'); - }, - widgets(): any { - return { - left: this.left, - right: this.right - }; - }, - keymap(): any { - return { - 't': this.focus - }; - } - }, - - created() { - if (!this.$store.getters.isSignedIn) return; - - if (this.$store.getters.home == null) { - const defaultDesktopHomeWidgets = { - left: [ - 'profile', - 'calendar', - 'activity', - 'rss', - 'hashtags', - 'photo-stream', - 'version' - ], - right: [ - 'customize', - 'broadcast', - 'notifications', - 'users', - 'polls', - 'server', - 'nav', - 'tips' - ] - }; - - //#region Construct home data - const _defaultDesktopHomeWidgets = []; - - for (const widget of defaultDesktopHomeWidgets.left) { - _defaultDesktopHomeWidgets.push({ - name: widget, - id: uuid(), - place: 'left', - data: {} - }); - } - - for (const widget of defaultDesktopHomeWidgets.right) { - _defaultDesktopHomeWidgets.push({ - name: widget, - id: uuid(), - place: 'right', - data: {} - }); - } - //#endregion - - this.$store.commit('setHome', _defaultDesktopHomeWidgets); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - hint() { - this.$root.dialog({ - title: this.$t('@.customization-tips.title'), - text: this.$t('@.customization-tips.paragraph') - }); - }, - - onTlLoaded() { - this.$emit('loaded'); - }, - - onWidgetContextmenu(widgetId) { - const w = (this.$refs[widgetId] as any)[0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveHome(); - }, - - onTrash(evt) { - this.saveHome(); - }, - - addWidget() { - if(this.widgetAdderSelected == null) return; - - this.$store.commit('addHomeWidget', { - name: this.widgetAdderSelected, - id: uuid(), - place: 'left', - data: {} - }); - }, - - saveHome() { - const left = this.widgets.left; - const right = this.widgets.right; - this.$store.commit('setHome', left.concat(right)); - for (const w of left) w.place = 'left'; - for (const w of right) w.place = 'right'; - }, - - done() { - location.href = '/'; - }, - - focus() { - (this.$refs.content as any).focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wqsofvpm - display block - - &[data-customize] - padding-top 48px - background-image url('/assets/desktop/grid.svg') - - > .main > .main - > a - display block - margin-bottom 8px - text-align center - - > div - cursor not-allowed !important - - > * - pointer-events none - - &:not([data-customize]) - > .main > *:not(.main):empty - display none - - > .customize - position fixed - z-index 1000 - top 0 - left 0 - width 100% - height 48px - color var(--text) - background var(--desktopHeaderBg) - box-shadow 0 1px 1px rgba(#000, 0.075) - - > a - display block - position absolute - z-index 1001 - top 0 - right 0 - padding 0 16px - line-height 48px - text-decoration none - color var(--primaryForeground) - background var(--primary) - transition background 0.1s ease - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - transition background 0s ease - - > [data-icon] - margin-right 8px - - > div - display flex - margin 0 auto - max-width 1220px - 32px - - > div - width 50% - - &.adder - > p - display inline - line-height 48px - - &.trash - border-left solid 1px var(--faceDivider) - - > div - width 100% - height 100% - - > p - position absolute - top 0 - left 0 - width 100% - line-height 48px - margin 0 - text-align center - pointer-events none - - > .main - display flex - justify-content center - margin 0 auto - max-width 1240px - - > * - .customize-container - cursor move - border-radius 6px - - &:hover - box-shadow 0 0 8px rgba(64, 120, 200, 0.3) - - > * - pointer-events none - - > .main - padding 16px - width calc(100% - 280px * 2) - order 2 - - &.side - > .main - width calc(100% - 280px) - max-width 680px - - > *:not(.main) - width 280px - padding 16px 0 16px 0 - - > *:not(:last-child) - margin-bottom 16px - - > .left - padding-left 16px - order 1 - - > .right - padding-right 16px - order 3 - - &.side - @media (max-width 1000px) - > *:not(.main) - display none - - > .main - width 100% - max-width 700px - margin 0 auto - - &:not(.side) - @media (max-width 1100px) - > *:not(.main) - display none - - > .main - width 100% - max-width 700px - margin 0 auto - -</style> diff --git a/src/client/app/desktop/views/home/note.vue b/src/client/app/desktop/views/home/note.vue deleted file mode 100644 index c19f58cd2b..0000000000 --- a/src/client/app/desktop/views/home/note.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div v-if="!fetching" class="kcthdwmv"> - <mk-note-detail :note="note" :key="note.id"/> - <footer> - <router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link> - <router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/note.vue'), - data() { - return { - fetching: true, - note: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kcthdwmv - text-align center - - > footer - margin-top 16px - - > a - display inline-block - margin 0 16px - -</style> diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue deleted file mode 100644 index 06b354b133..0000000000 --- a/src/client/app/desktop/views/home/search.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"> - <template #header> - <header class="oxgbmvii"> - <span><fa icon="search"/> {{ q }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/search.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll, { passive: true }); - Progress.start(); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - inited() { - Progress.done(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.oxgbmvii - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > span - padding 0 8px - font-size 0.9em - line-height 42px - color var(--text) -</style> diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue deleted file mode 100644 index 343b4ce951..0000000000 --- a/src/client/app/desktop/views/home/tag.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @loaded="inited"> - <template #header> - <header class="wqraeznr"> - <span><fa icon="hashtag"/> {{ $route.params.tag }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/tag.vue'), - computed: { - pagination() { - return { - endpoint: 'notes/search-by-tag', - limit: 20, - params: { - tag: this.$route.params.tag - } - }; - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - Progress.start(); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - inited() { - Progress.done(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wqraeznr - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > span - padding 0 8px - font-size 0.9em - line-height 42px - color var(--text) -</style> diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue deleted file mode 100644 index aae7dbc60e..0000000000 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ /dev/null @@ -1,144 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <slot></slot> - <div v-if="src == 'home' && alone" class="ibpylqas"> - <p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p> - <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> - </div> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/timeline.core.vue'), - - props: { - src: { - type: String, - required: true - }, - tagTl: { - required: false - } - }, - - data() { - return { - connection: null, - date: null, - baseQuery: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }, - query: {}, - endpoint: null, - pagination: null - }; - }, - - computed: { - alone(): boolean { - return this.$store.state.i.followingCount == 0; - } - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - this.connection.dispose(); - }); - - const prepend = note => { - (this.$refs.timeline as any).prepend(note); - }; - - if (this.src == 'tag') { - this.endpoint = 'notes/search-by-tag'; - this.query = { - query: this.tagTl.query - }; - this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); - this.connection.on('note', prepend); - } else if (this.src == 'home') { - this.endpoint = 'notes/timeline'; - const onChangeFollowing = () => { - this.fetch(); - }; - this.connection = this.$root.stream.useSharedConnection('homeTimeline'); - this.connection.on('note', prepend); - this.connection.on('follow', onChangeFollowing); - this.connection.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - this.endpoint = 'notes/local-timeline'; - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - this.endpoint = 'notes/global-timeline'; - this.connection = this.$root.stream.useSharedConnection('globalTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'mentions') { - this.endpoint = 'notes/mentions'; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', prepend); - } else if (this.src == 'messages') { - this.endpoint = 'notes/mentions'; - this.query = { - visibility: 'specified' - }; - const onNote = note => { - if (note.visibility == 'specified') { - prepend(note); - } - }; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', onNote); - } - - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - ...this.baseQuery, ...this.query - }) - }; - }, - - methods: { - focus() { - (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ibpylqas - padding 16px - text-align center - color var(--text) - border-bottom solid var(--lineWidth) var(--faceDivider) - font-size 14px - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue deleted file mode 100644 index 224b937997..0000000000 --- a/src/client/app/desktop/views/home/timeline.vue +++ /dev/null @@ -1,278 +0,0 @@ -<template> -<div class="pwbzawku"> - <x-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/> - <div class="main"> - <component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options"> - <header class="zahtxcqi"> - <div :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</div> - <div :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</div> - <div :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</div> - <div :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</div> - <div :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</div> - <div :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</div> - <div class="buttons"> - <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> - <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> - <button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> - <button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> - </div> - </header> - </component> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XCore from './timeline.core.vue'; -import Menu from '../../../common/views/components/menu.vue'; -import MkSettingsWindow from '../components/settings-window.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/timeline.vue'), - - components: { - XCore, - XPostForm: () => import('../components/post-form.vue').then(m => m.default) - }, - - data() { - return { - src: 'home', - list: null, - tagTl: null, - enableLocalTimeline: false, - enableGlobalTimeline: false, - }; - }, - - computed: { - options(): any { - return { - ...(this.src == 'list' ? { list: this.list } : { src: this.src }), - ...(this.src == 'tag' ? { tagTl: this.tagTl } : {}), - key: this.src == 'list' ? this.list.id : this.src - } - } - }, - - watch: { - src() { - this.saveSrc(); - }, - - list(x) { - this.saveSrc(); - if (x != null) this.tagTl = null; - }, - - tagTl(x) { - this.saveSrc(); - if (x != null) this.list = null; - } - }, - - created() { - this.$root.getMeta().then((meta: Record<string, any>) => { - if (!( - this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && this.src === 'global') this.src = 'local'; - if (!( - this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; - }); - - if (this.$store.state.device.tl) { - this.src = this.$store.state.device.tl.src; - if (this.src == 'list') { - this.list = this.$store.state.device.tl.arg; - } else if (this.src == 'tag') { - this.tagTl = this.$store.state.device.tl.arg; - } - } - }, - - mounted() { - document.title = this.$root.instanceName; - - (this.$refs.tl as any).$once('loaded', () => { - this.$emit('loaded'); - }); - }, - - methods: { - saveSrc() { - this.$store.commit('device/setTl', { - src: this.src, - arg: this.src == 'list' ? this.list : this.tagTl - }); - }, - - focus() { - (this.$refs.tl as any).focus(); - }, - - warp(date) { - (this.$refs.tl as any).warp(date); - }, - - async chooseList() { - const lists = await this.$root.api('users/lists/list'); - - let menu = [{ - icon: 'plus', - text: this.$t('add-list'), - action: () => { - this.$root.dialog({ - title: this.$t('list-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const list = await this.$root.api('users/lists/create', { - name - }); - - this.list = list; - this.src = 'list'; - }); - } - }]; - - if (lists.length > 0) { - menu.push(null); - } - - menu = menu.concat(lists.map(list => ({ - icon: 'list', - text: list.name, - action: () => { - this.list = list; - this.src = 'list'; - } - }))); - - this.$root.new(Menu, { - source: this.$refs.listButton, - items: menu - }); - }, - - chooseTag() { - let menu = [{ - icon: 'plus', - text: this.$t('add-tag-timeline'), - action: () => { - this.$root.new(MkSettingsWindow, { - initialPage: 'hashtags' - }); - } - }]; - - if (this.$store.state.settings.tagTimelines.length > 0) { - menu.push(null); - } - - menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ - icon: 'hashtag', - text: t.title, - action: () => { - this.tagTl = t; - this.src = 'tag'; - } - }))); - - this.$root.new(Menu, { - source: this.$refs.tagButton, - items: menu - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.pwbzawku - > .form - margin-bottom 16px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - header.zahtxcqi - display flex - flex-wrap wrap - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > * - flex-shrink 0 - - > .buttons - margin-left auto - - > button - padding 0 8px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - > .indicator - position absolute - top -4px - right 4px - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - - &:hover - color var(--faceTextButtonHover) - - &[data-active] - color var(--primary) - cursor default - - &:before - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 2px - background var(--primary) - - > div:not(.buttons) - padding 0 10px - line-height 42px - font-size 12px - user-select none - - &[data-active] - color var(--primary) - cursor default - font-weight bold - - &:before - content "" - display block - position absolute - bottom 0 - left -8px - width calc(100% + 16px) - height 2px - background var(--primary) - - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer - - &:hover - color var(--desktopTimelineSrcHover) - -</style> diff --git a/src/client/app/desktop/views/home/user/index.vue b/src/client/app/desktop/views/home/user/index.vue deleted file mode 100644 index 98ad165d93..0000000000 --- a/src/client/app/desktop/views/home/user/index.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<div class="omechnps" v-if="!fetching"> - <div class="is-suspended" v-if="user.isSuspended" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }} - </div> - <div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </div> - <div class="main"> - <x-header class="header" :user="user"/> - <router-view :user="user"></router-view> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import parseAcct from '../../../../../../misc/acct/parse'; -import Progress from '../../../../common/scripts/loading'; -import XHeader from './user.header.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XHeader - }, - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - Progress.start(); - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - Progress.done(); - }); - }, - - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.omechnps - width 100% - margin 0 auto - - > .is-suspended - > .is-remote - margin-bottom 16px - padding 14px 16px - font-size 14px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &.is-suspended - color var(--suspendedInfoFg) - background var(--suspendedInfoBg) - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - - > .main - > .header - margin-bottom 16px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue deleted file mode 100644 index c8e7779678..0000000000 --- a/src/client/app/desktop/views/home/user/user.header.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> -<div class="header" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="banner-container" :style="style"> - <div class="banner" ref="banner" :style="style"></div> - <div class="fade"></div> - <div class="title"> - <p class="name"> - <mk-user-name :user="user" :nowrap="false"/> - </p> - <div> - <span class="username"><mk-acct :user="user" :detail="true" /></span> - <span v-if="user.isBot" :title="$t('is-bot')"><fa icon="robot"/></span> - </div> - </div> - <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('follows-you') }}</span> - <div class="actions" v-if="$store.getters.isSignedIn"> - <button @click="menu" class="menu" ref="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="follow"/> - </div> - </div> - <mk-avatar class="avatar" :user="user" :disable-preview="true"/> - <div class="body"> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - <p v-else class="empty">{{ $t('no-description') }}</p> - <x-integrations :user="user" style="margin-top:16px;"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="info"> - <span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span> - <span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> - </div> - <div class="status"> - <router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> - <router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link> - <router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as age from 's-age'; -import XUserMenu from '../../../../common/views/components/user-menu.vue'; -import XIntegrations from '../../../../common/views/components/integrations.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.header.vue'), - components: { - XIntegrations - }, - props: ['user'], - computed: { - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - }, - - age(): number { - return age(this.user.birthday); - } - }, - mounted() { - if (this.user.bannerUrl) { - //window.addEventListener('load', this.onScroll); - //window.addEventListener('scroll', this.onScroll, { passive: true }); - //window.addEventListener('resize', this.onScroll); - } - }, - beforeDestroy() { - if (this.user.bannerUrl) { - //window.removeEventListener('load', this.onScroll); - //window.removeEventListener('scroll', this.onScroll); - //window.removeEventListener('resize', this.onScroll); - } - }, - methods: { - mention() { - this.$post({ mention: this.user }); - }, - onScroll() { - const banner = this.$refs.banner as any; - - const top = window.scrollY; - - const z = 1.25; // 奥行き(小さいほど奥) - const pos = -(top / z); - banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; - - const blur = top / 32 - if (blur <= 10) banner.style.filter = `blur(${blur}px)`; - }, - - menu() { - const w = this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.header - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > .banner-container - height 250px - overflow hidden - background-size cover - background-position center - - > .banner - height 100% - background-color #383838 - background-size cover - background-position center - box-shadow 0 0 128px rgba(0, 0, 0, 0.5) inset - - > .fade - position absolute - bottom 0 - left 0 - width 100% - height 78px - background linear-gradient(transparent, rgba(#000, 0.7)) - - > .followed - position absolute - top 12px - left 12px - padding 4px 6px - color #fff - background rgba(0, 0, 0, 0.7) - font-size 12px - - > .actions - position absolute - top 12px - right 12px - - > .menu - height 100% - padding 0 14px - color #fff - text-shadow 0 0 8px #000 - font-size 16px - - > .title - position absolute - bottom 0 - left 0 - width 100% - padding 0 0 8px 154px - color #fff - - > .name - display block - margin 0 - line-height 32px - font-weight bold - font-size 1.8em - text-shadow 0 0 8px #000 - - > div - > * - display inline-block - margin-right 16px - line-height 20px - opacity 0.8 - - &.username - font-weight bold - - > .avatar - display block - position absolute - top 170px - left 16px - z-index 2 - width 120px - height 120px - box-shadow 1px 1px 3px rgba(#000, 0.2) - - > &.cat::before, - > &.cat::after - border-width 8px - - > .body - padding 16px 16px 16px 154px - color var(--text) - - > .description - font-size 15px - - > .empty - margin 0 - opacity 0.5 - - > .fields - margin-top 16px - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - border-right solid 1px var(--faceDivider) - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - text-align center - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .info - margin-top 16px - padding-top 16px - border-top solid 1px var(--faceDivider) - font-size 15px - - &:empty - display none - - > * - margin-right 16px - - > .status - margin-top 16px - padding-top 16px - border-top solid 1px var(--faceDivider) - font-size 80% - - > * - display inline-block - padding-right 16px - margin-right 16px - color inherit - - &:not(:last-child) - border-right solid 1px var(--faceDivider) - - &.clickable - cursor pointer - - &:hover - color var(--faceTextButtonHover) - - > b - margin-right 4px - font-size 1rem - font-weight bold - color var(--primary) - -</style> diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue deleted file mode 100644 index c47e0a0771..0000000000 --- a/src/client/app/desktop/views/home/user/user.home.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="lnctpgve"> - <x-page v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> - <!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>--> - <div class="activity"> - <ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersActivity" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <x-activity :user="user" :limit="35" style="padding: 16px;"/> - </ui-container> - </div> - <x-photos :user="user"/> - <x-timeline ref="tl" :user="user"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XTimeline from './user.timeline.vue'; -import XPhotos from './user.photos.vue'; -import XActivity from '../../../../common/views/components/activity.vue'; -import XPage from '../../../../common/views/components/page/page.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XTimeline, - XPhotos, - XActivity, - XPage, - }, - props: { - user: { - type: Object, - required: true - } - }, - methods: { - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.lnctpgve - > * - margin-bottom 16px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.photos.vue b/src/client/app/desktop/views/home/user/user.photos.vue deleted file mode 100644 index 03abcf865c..0000000000 --- a/src/client/app/desktop/views/home/user/user.photos.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersPhotos" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })"> - <template #header><fa icon="camera"/> {{ $t('title') }}</template> - - <div class="dzsuvbsrrrwobdxifudxuefculdfiaxd"> - <p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <router-link v-for="image in images" class="img" - :style="`background-image: url(${image.thumbnailUrl})`" - :key="`${image.id}:${image._note.id}`" - :to="image._note | notePage" - :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`" - ></router-link> - </div> - <p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; -import { concat } from '../../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.photos.vue'), - props: ['user'], - data() { - return { - images: [], - fetching: true - }; - }, - mounted() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - file._note = note; - } - } - const files = concat(notes.map((n: any): any[] => n.files)); - this.images = files.filter(f => image.includes(f.type)).slice(0, 9); - this.fetching = false; - }); - }, - methods: { - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.dzsuvbsrrrwobdxifudxuefculdfiaxd - > .stream - display grid - grid-template-columns 1fr 1fr 1fr - gap 8px - padding 16px - background var(--face) - - > * - height 120px - background-position center center - background-size cover - background-clip content-box - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.timeline.vue b/src/client/app/desktop/views/home/user/user.timeline.vue deleted file mode 100644 index 2a97f2c96e..0000000000 --- a/src/client/app/desktop/views/home/user/user.timeline.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <header class="kugajpep"> - <span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span> - <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span> - <span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> - <span :data-active="mode == 'my-posts'" @click="mode = 'my-posts'"><fa icon="user"/> {{ $t('my-posts') }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.timeline.vue'), - - props: ['user'], - - data() { - return { - fetching: true, - mode: 'default', - unreadCount: 0, - date: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeReplies: this.mode == 'with-replies', - includeMyRenotes: this.mode != 'my-posts', - withFiles: this.mode == 'with-media', - }) - } - }; - }, - - watch: { - mode() { - (this.$refs.timeline as any).reload(); - } - }, - - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - document.removeEventListener('keydown', this.onDocumentKeydown); - }); - }, - - methods: { - onDocumentKeydown(e) { - if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { - if (e.which == 84) { // [t] - (this.$refs.timeline as any).focus(); - } - } - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kugajpep - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 1px var(--desktopTimelineHeaderShadow) - - > span - display inline-block - padding 0 10px - line-height 42px - font-size 12px - user-select none - - &[data-active] - color var(--primary) - cursor default - font-weight bold - - &:before - content "" - display block - position absolute - bottom 0 - left -8px - width calc(100% + 16px) - height 2px - background var(--primary) - - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer - - &:hover - color var(--desktopTimelineSrcHover) - -</style> diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue deleted file mode 100644 index b389392ec8..0000000000 --- a/src/client/app/desktop/views/pages/drive.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="mk-drive-page"> - <x-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/drive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - folder: null - }; - }, - created() { - this.folder = this.$route.params.folder; - }, - mounted() { - document.title = this.$t('title'); - }, - methods: { - onMoveRoot() { - const title = this.$t('title'); - - // Rewrite URL - history.pushState(null, title, '/i/drive'); - - document.title = title; - }, - onOpenFolder(folder) { - const title = `${folder.name} | ${this.$t('title')}`; - - // Rewrite URL - history.pushState(null, title, `/i/drive/folder/${folder.id}`); - - document.title = title; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-page - position fixed - width 100% - height 100% - background #fff - - > .mk-drive - height 100% -</style> diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue deleted file mode 100644 index b859b95d7f..0000000000 --- a/src/client/app/desktop/views/pages/games/reversi.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<component :is="ui ? 'mk-ui' : 'div'"> - <x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - props: { - ui: { - default: false - } - }, - methods: { - nav(game, actualNav) { - if (actualNav) { - this.$router.push(`/games/reversi/${game.id}`); - } else { - // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push(`/games/reversi/${game.id}`); - } - } - } -}); -</script> diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue deleted file mode 100644 index c725074b7d..0000000000 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<div class="mk-messaging-room-page"> - <x-messaging-room v-if="user || group" :user="user" :group="group" :is-naked="true"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../misc/acct/parse'; -import getUserName from '../../../../../misc/get-user-name'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - data() { - return { - fetching: true, - user: null, - group: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - const applyBg = v => - document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); - - applyBg(this.$store.state.device.darkmode); - - this.unwatchDarkmode = this.$store.watch(s => { - return s.device.darkmode; - }, applyBg); - - this.fetch(); - }, - beforeDestroy() { - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); // for safari's bug - this.unwatchDarkmode(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - if (this.$route.params.user) { - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); - - Progress.done(); - }); - } else { - this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { - this.group = group; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + this.group.name; - - Progress.done(); - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging-room-page - display flex - flex 1 - flex-direction column - min-height 100% - -</style> diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue deleted file mode 100644 index 7e6a8e1937..0000000000 --- a/src/client/app/desktop/views/pages/selectdrive.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="mkp-selectdrive"> - <x-drive ref="browser" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <footer> - <button class="upload" :title="$t('upload')" @click="upload"><fa icon="upload"/></button> - <button class="cancel" @click="close">{{ $t('cancel') }}</button> - <button class="ok" @click="ok">{{ $t('ok') }}</button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/selectdrive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = this.$t('title'); - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - close() { - window.close(); - }, - ok() { - window.opener.cb(this.multiple ? this.files : this.files[0]); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> - - -.mkp-selectdrive - display block - position fixed - width 100% - height 100% - background #fff - - > .mk-drive - height calc(100% - 72px) - - > footer - position fixed - bottom 0 - left 0 - width 100% - height 72px - background var(--primaryLighten95) - - .upload - display inline-block - position absolute - top 8px - left 16px - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color var(--primaryAlpha05) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background transparent - border-color var(--primaryAlpha05) - //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - - .ok - .cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - right 16px - color var(--primaryForeground) - background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%) - border solid 1px var(--primaryLighten15) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%) - border-color var(--primary) - - &:active:not(:disabled) - background var(--primary) - border-color var(--primary) - - .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -</style> diff --git a/src/client/app/desktop/views/pages/settings.vue b/src/client/app/desktop/views/pages/settings.vue deleted file mode 100644 index 826fae2529..0000000000 --- a/src/client/app/desktop/views/pages/settings.vue +++ /dev/null @@ -1,27 +0,0 @@ -<template> -<mk-ui> - <main> - <x-settings :in-window="false" :page="$route.params.page" /> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XSettings: () => import('../components/settings.vue').then(m => m.default) - }, - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> - -<style lang="stylus" scoped> -main - margin 0 auto - max-width 900px - -</style> diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue deleted file mode 100644 index 511e1548e5..0000000000 --- a/src/client/app/desktop/views/pages/welcome.vue +++ /dev/null @@ -1,509 +0,0 @@ -<template> -<div class="mk-welcome"> - <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> - - <button @click="dark"> - <template v-if="$store.state.device.darkmode"><fa icon="moon"/></template> - <template v-else><fa :icon="['far', 'moon']"/></template> - </button> - - <mk-forkit class="forkit"/> - - <main> - <div class="body"> - <div class="main block"> - <div> - <h1 v-if="name != null && name != ''">{{ name }}</h1> - <h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1> - - <div class="info"> - <span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span> - <span class="stats" v-if="stats"> - <span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span> - <span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span> - </span> - </div> - - <div class="desc"> - <span class="desc" v-html="description || $t('@.about')"></span> - <a class="about" @click="about">{{ $t('about') }}</a> - </div> - - <p class="sign"> - <span class="signup" @click="signup">{{ $t('@.signup') }}</span> - <span class="divider">|</span> - <span class="signin" @click="signin">{{ $t('@.signin') }}</span> - </p> - - <img v-if="meta" :src="meta.mascotImageUrl" alt="" title="藍" class="char"> - </div> - </div> - - <div class="announcements block"> - <header><fa icon="broadcast-tower"/> {{ $t('announcements') }}</header> - <div v-if="announcements && announcements.length > 0"> - <div v-for="announcement in announcements"> - <h1 v-html="announcement.title"></h1> - <mfm :text="announcement.text"/> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/> - </div> - </div> - </div> - - <div class="photos block"> - <header><fa :icon="['far', 'images']"/> {{ $t('photos') }}</header> - <div> - <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> - </div> - </div> - - <div class="tag-cloud block"> - <div> - <mk-tag-cloud/> - </div> - </div> - - <div class="nav block"> - <div> - <mk-nav class="nav"/> - </div> - </div> - - <div class="side"> - <div class="trends block"> - <div> - <mk-trends/> - </div> - </div> - - <div class="tl block"> - <header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</header> - <div> - <mk-welcome-timeline class="tl" :max="20"/> - </div> - </div> - - <div class="info block"> - <header><fa icon="info-circle"/> {{ $t('info') }}</header> - <div> - <div v-if="meta" class="body"> - <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - </div> - </div> - </div> - </div> - </div> - </main> - - <modal name="about" class="about modal" width="800px" height="auto" scrollable> - <article class="fpdezooorhntlzyeszemrsqdlgbysvxq"> - <h1>{{ $t('@.intro.title') }}</h1> - <p v-html="this.$t('@.intro.about')"></p> - <section> - <h2>{{ $t('@.intro.features') }}</h2> - <section> - <div class="body"> - <h3>{{ $t('@.intro.rich-contents') }}</h3> - <p v-html="this.$t('@.intro.rich-contents-desc')"></p> - </div> - <div class="image"><img src="/assets/about/post.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.reaction') }}</h3> - <p v-html="this.$t('@.intro.reaction-desc')"></p> - </div> - <div class="image"><img src="/assets/about/reaction.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.ui') }}</h3> - <p v-html="this.$t('@.intro.ui-desc')"></p> - </div> - <div class="image"><img src="/assets/about/ui.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.drive') }}</h3> - <p v-html="this.$t('@.intro.drive-desc')"></p> - </div> - <div class="image"><img src="/assets/about/drive.png" alt=""></div> - </section> - </section> - <p v-html="this.$t('@.intro.outro')"></p> - </article> - </modal> - - <modal name="signup" class="modal" width="450px" height="auto" scrollable> - <header class="formHeader">{{ $t('@.signup') }}</header> - <mk-signup class="form"/> - </modal> - - <modal name="signin" class="modal" width="450px" height="auto" scrollable> - <header class="formHeader">{{ $t('@.signin') }}</header> - <mk-signin class="form"/> - </modal> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { host, copyright } from '../../../config'; -import { concat } from '../../../../../prelude/array'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/welcome.vue'), - data() { - return { - meta: null, - stats: null, - banner: null, - copyright, - host: toUnicode(host), - name: null, - description: '', - announcements: [], - photos: [] - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.name = meta.name; - this.description = meta.description; - this.announcements = meta.announcements; - this.banner = meta.bannerUrl; - }); - - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('notes/local-timeline', { - fileType: image, - excludeNsfw: true, - limit: 6 - }).then((notes: any[]) => { - const files = concat(notes.map((n: any): any[] => n.files)); - this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); - }); - }, - - methods: { - about() { - this.$modal.show('about'); - }, - - signup() { - this.$modal.show('signup'); - }, - - signin() { - this.$modal.show('signin'); - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - } - } -}); -</script> - -<style lang="stylus"> -#wait - right auto - left 15px - -.v--modal-overlay - background rgba(0, 0, 0, 0.6) - -.modal - .form - padding 24px 48px 48px 48px - - .formHeader - text-align center - padding 48px 0 12px 0 - margin 0 48px - font-size 1.5em - - .v--modal-box - background var(--face) - color var(--text) - - .formHeader - border-bottom solid 1px rgba(#000, 0.2) - -.v--modal-overlay.about - .v--modal-box.v--modal - margin 32px 0 - -.fpdezooorhntlzyeszemrsqdlgbysvxq - padding 64px - - > p:last-child - margin-bottom 0 - - > h1 - margin-top 0 - - > section - > h2 - border-bottom 1px solid var(--faceDivider) - - > section - display grid - grid-template-rows 1fr - grid-template-columns 180px 1fr - gap 32px - margin-bottom 32px - padding-bottom 32px - border-bottom 1px solid var(--faceDivider) - - &:nth-child(odd) - grid-template-columns 1fr 180px - - > .body - grid-column 1 - - > .image - grid-column 2 - - > .body - grid-row 1 - grid-column 2 - - > .image - grid-row 1 - grid-column 1 - - > img - display block - width 100% - height 100% - object-fit cover -</style> - -<style lang="stylus" scoped> -.mk-welcome - display flex - min-height 100vh - - > .banner - position absolute - top 0 - left 0 - width 100% - height 400px - background-position center - background-size cover - opacity 0.7 - - &:after - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 100px - background linear-gradient(transparent, var(--bg)) - - > .forkit - position absolute - top 0 - right 0 - - > button - position fixed - z-index 1 - bottom 16px - left 16px - padding 16px - font-size 18px - color var(--text) - - > main - margin 0 auto - padding 64px - width 100% - max-width 1200px - - .block - color var(--text) - background var(--face) - overflow auto - - > header - z-index 1 - padding 0 16px - line-height 48px - background var(--faceHeader) - box-shadow 0 1px 0 rgba(0, 0, 0, 0.1) - - & + div - max-height calc(100% - 48px) - - > div - overflow auto - - > .body - display grid - grid-template-rows 390px 1fr 256px 64px - grid-template-columns 1fr 1fr 350px - gap 16px - height 1150px - - > .main - grid-row 1 - grid-column 1 / 3 - - > div - padding 32px - min-height 100% - - > h1 - margin 0 - - > svg - margin -8px 0 0 -16px - width 280px - height 100px - fill currentColor - - > .info - margin 0 auto 16px auto - width $width - font-size 14px - - > .stats - margin-left 16px - padding-left 16px - border-left solid 1px var(--faceDivider) - - > * - margin-right 16px - - > .desc - max-width calc(100% - 150px) - - > .sign - font-size 120% - margin-bottom 0 - - > .divider - margin 0 16px - - > .signin - > .signup - cursor pointer - - &:hover - color var(--primary) - - > .char - display block - position absolute - right 16px - bottom 0 - height 320px - opacity 0.7 - - > *:not(.char) - z-index 1 - - > .announcements - grid-row 2 - grid-column 1 - - > div - padding 32px - - > div - padding 0 0 16px 0 - margin 0 0 16px 0 - border-bottom 1px solid var(--faceDivider) - - > h1 - margin 0 - font-size 1.25em - - > .photos - grid-row 2 - grid-column 2 - - > div - display grid - grid-template-rows 1fr 1fr 1fr - grid-template-columns 1fr 1fr - gap 8px - height 100% - padding 16px - - > div - //border-radius 4px - background-position center center - background-size cover - - > .tag-cloud - grid-row 3 - grid-column 1 / 3 - - > div - height 256px - padding 32px - - > .nav - display flex - justify-content center - align-items center - grid-row 4 - grid-column 1 / 3 - font-size 14px - - > .side - display grid - grid-row 1 / 5 - grid-column 3 - grid-template-rows 1fr 350px - grid-template-columns 1fr - gap 16px - - > .tl - grid-row 1 - grid-column 1 - overflow auto - - > .trends - grid-row 2 - grid-column 1 - padding 8px - - > .info - grid-row 3 - grid-column 1 - - > div - padding 16px - - > .body - > p - display block - margin 0 - -</style> diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue deleted file mode 100644 index 73c6d0ef64..0000000000 --- a/src/client/app/desktop/views/widgets/activity.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<mk-activity - :design="props.design" - :init-view="props.view" - :user="$store.state.i" - @view-changed="viewChanged"/> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'activity', - props: () => ({ - design: 0, - view: 0 - }) -}).extend({ - methods: { - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - viewChanged(view) { - this.props.view = view; - this.save(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/customize.vue b/src/client/app/desktop/views/widgets/customize.vue deleted file mode 100644 index eb71910382..0000000000 --- a/src/client/app/desktop/views/widgets/customize.vue +++ /dev/null @@ -1,21 +0,0 @@ -<template> -<div class="mkw-customize"> - <ui-button @click="customize()">{{ $t('@.customize-home') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'customize', -}).extend({ - i18n: i18n(), - methods: { - customize(date) { - location.href = '/?customize'; - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts deleted file mode 100644 index c00cd2ff4d..0000000000 --- a/src/client/app/desktop/views/widgets/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Vue from 'vue'; - -import wNotifications from './notifications.vue'; -import wTimemachine from './timemachine.vue'; -import wActivity from './activity.vue'; -import wTrends from './trends.vue'; -import wUsers from './users.vue'; -import wPolls from './polls.vue'; -import wMessaging from './messaging.vue'; -import wProfile from './profile.vue'; -import wCustomize from './customize.vue'; - -Vue.component('mkw-notifications', wNotifications); -Vue.component('mkw-timemachine', wTimemachine); -Vue.component('mkw-activity', wActivity); -Vue.component('mkw-trends', wTrends); -Vue.component('mkw-users', wUsers); -Vue.component('mkw-polls', wPolls); -Vue.component('mkw-messaging', wMessaging); -Vue.component('mkw-profile', wProfile); -Vue.component('mkw-customize', wCustomize); diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue deleted file mode 100644 index e94e745c19..0000000000 --- a/src/client/app/desktop/views/widgets/messaging.vue +++ /dev/null @@ -1,60 +0,0 @@ -<template> -<div class="mkw-messaging"> - <ui-container :show-header="props.design == 0"> - <template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template> - <template #func><button @click="add"><fa icon="plus"/></button></template> - - <x-messaging ref="index" compact @navigate="navigate" @navigateGroup="navigateGroup"/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import MkMessagingRoomWindow from '../components/messaging-room-window.vue'; -import MkMessagingWindow from '../components/messaging-window.vue'; - -export default define({ - name: 'messaging', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n(''), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - methods: { - navigate(user) { - this.$root.new(MkMessagingRoomWindow, { - user: user - }); - }, - navigateGroup(group) { - this.$root.new(MkMessagingRoomWindow, { - group: group - }); - }, - add() { - this.$root.new(MkMessagingWindow); - }, - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-messaging - .mk-messaging - max-height 250px - overflow auto - -</style> diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue deleted file mode 100644 index 5a84f7b371..0000000000 --- a/src/client/app/desktop/views/widgets/notifications.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<div class="mkw-notifications"> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="['far', 'bell']"/>{{ props.type === 'all' ? $t('title') : $t('@.notification-types.' + props.type) }}</template> - <template #func><button :title="$t('@.notification-type')" @click="settings"><fa icon="cog"/></button></template> - - <mk-notifications :class="$style.notifications" :type="props.type === 'all' ? null : props.type"/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'notifications', - props: () => ({ - compact: false, - type: 'all' - }) -}).extend({ - i18n: i18n('desktop/views/widgets/notifications.vue'), - methods: { - settings() { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.props.type, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.props.type = type; - this.save(); - }); - }, - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.notifications - max-height 300px - overflow auto - -</style> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue deleted file mode 100644 index c77762ecdf..0000000000 --- a/src/client/app/desktop/views/widgets/polls.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div class="mkw-polls"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="chart-pie"/>{{ $t('title') }}</template> - <template #func> - <button :title="$t('title')" @click="fetch"> - <fa v-if="!fetching && more" icon="arrow-right"/> - <fa v-if="!fetching && !more" icon="sync"/> - </button> - </template> - - <div class="mkw-polls--body"> - <div class="poll" v-if="!fetching && poll != null"> - <p v-if="poll.text"><router-link :to="poll | notePage"> - <mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/> - </router-link></p> - <p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p> - <mk-poll :note="poll"/> - </div> - <p class="empty" v-if="!fetching && poll == null">{{ $t('nothing') }}</p> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'polls', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/polls.vue'), - data() { - return { - poll: null, - fetching: true, - more: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.poll = null; - - this.$root.api('notes/polls/recommendation', { - limit: 1, - offset: this.offset - }).then(notes => { - const poll = notes ? notes[0] : null; - if (poll == null) { - this.more = false; - this.offset = 0; - } else { - this.more = true; - this.offset++; - } - this.poll = poll; - this.fetching = false; - }).catch(() => { - this.poll = null; - this.fetching = false; - this.more = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-polls--body - > .poll - padding 16px - font-size 12px - color var(--text) - - > p - margin 0 0 8px 0 - - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue deleted file mode 100644 index bad1925f69..0000000000 --- a/src/client/app/desktop/views/widgets/profile.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> -<div class="egwyvoaaryotefqhqtmiyawwefemjfsd"> - <ui-container :show-header="false" :naked="props.design == 2"> - <div class="egwyvoaaryotefqhqtmiyawwefemjfsd-body" - :data-compact="props.design == 1 || props.design == 2" - :data-melt="props.design == 2" - > - <div class="banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" - :title="$t('update-banner')" - @click="updateBanner()" - ></div> - <mk-avatar class="avatar" :user="$store.state.i" - :disable-link="true" - @click="updateAvatar()" - :title="$t('update-avatar')" - /> - <router-link class="name" :to="$store.state.i | userPage"><mk-user-name :user="$store.state.i"/></router-link> - <p class="username">@{{ $store.state.i | acct }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import updateAvatar from '../../api/update-avatar'; -import updateBanner from '../../api/update-banner'; - -export default define({ - name: 'profile', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('desktop/views/widgets/profile.vue'), - methods: { - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - updateAvatar() { - updateAvatar(this.$root)(); - }, - updateBanner() { - updateBanner(this.$root)(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.egwyvoaaryotefqhqtmiyawwefemjfsd-body - &[data-compact] - > .banner:before - content "" - display block - width 100% - height 100% - background rgba(#000, 0.5) - - > .avatar - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(#000, 0.5) - - > .name - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - text-shadow 0 0 8px rgba(#000, 0.5) - - > .username - display none - - &[data-melt] - > .banner - visibility hidden - - > .avatar - box-shadow none - - > .name - color #666 - text-shadow none - - > .banner - height 100px - background-color var(--primaryAlpha01) - background-size cover - background-position center - cursor pointer - - > .avatar - display block - position absolute - top 76px - left 16px - width 58px - height 58px - border solid 3px var(--face) - border-radius 8px - cursor pointer - - > .name - display block - margin 10px 0 0 84px - line-height 16px - font-weight bold - color var(--text) - overflow hidden - text-overflow ellipsis - - > .username - display block - margin 4px 0 8px 84px - line-height 16px - font-size 0.9em - color var(--text) - opacity 0.7 - -</style> diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue deleted file mode 100644 index 854b01c13e..0000000000 --- a/src/client/app/desktop/views/widgets/timemachine.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> -<div class="mkw-timemachine"> - <mk-calendar :design="props.design" @chosen="chosen"/> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'timemachine', - props: () => ({ - design: 0 - }) -}).extend({ - methods: { - chosen(date) { - this.$root.$emit('warp', date); - }, - func() { - if (this.props.design == 5) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue deleted file mode 100644 index c512945895..0000000000 --- a/src/client/app/desktop/views/widgets/trends.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="mkw-trends"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="fire"/>{{ $t('title') }}</template> - <template #func><button :title="$t('title')" @click="fetch"><fa icon="sync"/></button></template> - - <div class="mkw-trends--body"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div class="note" v-else-if="note != null"> - <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> - <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> - </div> - <p class="empty" v-else>{{ $t('nothing') }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'trends', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/trends.vue'), - data() { - return { - note: null, - fetching: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.note = null; - - this.$root.api('notes/trend', { - limit: 1, - offset: this.offset, - renote: false, - reply: false, - file: false, - poll: false - }).then(notes => { - const note = notes ? notes[0] : null; - if (note == null) { - this.offset = 0; - } else { - this.offset++; - } - this.note = note; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-trends - .mkw-trends--body - > .note - padding 16px - font-size 12px - font-style oblique - color #555 - - > p - margin 0 - - > .text, - > .author - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue deleted file mode 100644 index 85902fc20c..0000000000 --- a/src/client/app/desktop/views/widgets/users.vue +++ /dev/null @@ -1,145 +0,0 @@ -<template> -<div class="mkw-users"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="users"/>{{ $t('title') }}</template> - <template #func> - <button :title="$t('title')" @click="refresh"> - <fa v-if="!fetching && more" icon="arrow-right"/> - <fa v-if="!fetching && !more" icon="sync"/> - </button> - </template> - - <div class="mkw-users--body"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <template v-else-if="users.length != 0"> - <div class="user" v-for="_user in users"> - <mk-avatar class="avatar" :user="_user"/> - <div class="body"> - <router-link class="name" :to="_user | userPage" v-user-preview="_user.id"><mk-user-name :user="_user"/></router-link> - <p class="username">@{{ _user | acct }}</p> - </div> - </div> - </template> - <p class="empty" v-else>{{ $t('no-one') }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -const limit = 3; - -export default define({ - name: 'users', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/users.vue'), - data() { - return { - users: [], - fetching: true, - more: true, - page: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.users = []; - - this.$root.api('users/recommendation', { - limit: limit, - offset: limit * this.page - }).then(users => { - this.users = users; - this.fetching = false; - }).catch(() => { - this.users = []; - this.fetching = false; - this.more = false; - this.page = 0; - }); - }, - refresh() { - if (this.users.length < limit) { - this.more = false; - this.page = 0; - } else { - this.more = true; - this.page++; - } - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-users - .mkw-users--body - > .user - padding 16px - border-bottom solid 1px var(--faceDivider) - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - margin 0 12px 0 0 - width 42px - height 42px - border-radius 8px - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color var(--text) - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color var(--text) - opacity 0.7 - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/dev/script.ts b/src/client/app/dev/script.ts deleted file mode 100644 index 9adcb84d7c..0000000000 --- a/src/client/app/dev/script.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Developer Center - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import BootstrapVue from 'bootstrap-vue'; -import 'bootstrap/dist/css/bootstrap.css'; -import 'bootstrap-vue/dist/bootstrap-vue.css'; - -// Style -import './style.styl'; - -import init from '../init'; - -import Index from './views/index.vue'; -import Apps from './views/apps.vue'; -import AppNew from './views/new-app.vue'; -import App from './views/app.vue'; -import ui from './views/ui.vue'; -import NotFound from '../common/views/pages/not-found.vue'; - -Vue.use(BootstrapVue); - -Vue.component('mk-ui', ui); - -/** - * init - */ -init(launch => { - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/dev/', - routes: [ - { path: '/', component: Index }, - { path: '/apps', component: Apps }, - { path: '/app/new', component: AppNew }, - { path: '/app/:id', component: App }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/dev/style.styl b/src/client/app/dev/style.styl deleted file mode 100644 index e635897b17..0000000000 --- a/src/client/app/dev/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -// Bootstrapのデザインを崩すので: -* - position initial - background-clip initial !important - -html - background-color #fff diff --git a/src/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue deleted file mode 100644 index 2379d71aa5..0000000000 --- a/src/client/app/dev/views/app.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<mk-ui> - <p v-if="fetching">{{ $t('@.loading') }}</p> - <b-card v-if="!fetching" :header="app.name"> - <b-form-group label="App Secret"> - <b-input :value="app.secret" readonly/> - </b-form-group> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n(), - data() { - return { - fetching: true, - app: null - }; - }, - watch: { - $route: 'fetch' - }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - this.$root.api('app/show', { - appId: this.$route.params.id - }).then(app => { - this.app = app; - this.fetching = false; - }); - } - } -}); -</script> diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue deleted file mode 100644 index b99ccdf576..0000000000 --- a/src/client/app/dev/views/apps.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<mk-ui> - <b-card :header="$t('manage-apps')"> - <b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button> - <hr> - <div class="apps"> - <p v-if="fetching">{{ $t('@.loading') }}</p> - <template v-if="!fetching"> - <b-alert v-if="apps.length == 0">{{ $t('app-missing') }}</b-alert> - <b-list-group v-else> - <b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`"> - {{ app.name }} - </b-list-group-item> - </b-list-group> - </template> - </div> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n('dev/views/apps.vue'), - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - this.$root.api('my/apps').then(apps => { - this.apps = apps; - this.fetching = false; - }); - } -}); -</script> diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue deleted file mode 100644 index db0e4d57c2..0000000000 --- a/src/client/app/dev/views/index.vue +++ /dev/null @@ -1,13 +0,0 @@ -<template> -<mk-ui> - <b-button to="/apps" variant="primary">{{ $t('manage-apps') }}</b-button> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n('dev/views/index.vue') -}); -</script> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue deleted file mode 100644 index dbb41211cc..0000000000 --- a/src/client/app/dev/views/new-app.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<mk-ui> - <b-card :header="$t('new-app')"> - <b-alert show variant="info"><fa icon="info-circle"/> {{ $t('new-app-info') }}</b-alert> - <b-form @submit.prevent="onSubmit" autocomplete="off"> - <b-form-group :label="$t('app-name')" :description="$t('app-name-desc')"> - <b-form-input v-model="name" type="text" :placeholder="$t('app-name-placeholder')" autocomplete="off" required/> - </b-form-group> - <b-form-group :label="$t('app-overview')" :description="$t('app-overview-desc')"> - <b-textarea v-model="description" :placeholder="$t('app-overview-placeholder')" autocomplete="off" required></b-textarea> - </b-form-group> - <b-form-group :label="$t('callback-url')" :description="$t('callback-url-desc')"> - <b-input v-model="cb" type="url" :placeholder="$t('callback-url-placeholder')" autocomplete="off"/> - </b-form-group> - <b-card :header="$t('authority')"> - <b-form-group :description="$t('authority-desc')"> - <b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> - <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox v-for="v in permissionsList" :value="v" :key="v">{{ $t(`@.permissions.${v}`) }} ({{ v }})</b-form-checkbox> - </b-form-checkbox-group> - </b-form-group> - </b-card> - <hr> - <b-button type="submit" variant="primary">{{ $t('create-app') }}</b-button> - </b-form> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { kinds } from '../../../../server/api/kinds'; - -export default Vue.extend({ - i18n: i18n('dev/views/new-app.vue'), - data() { - return { - name: '', - description: '', - cb: '', - nidState: null, - permission: [], - permissionsList: kinds - }; - }, - methods: { - onSubmit() { - this.$root.api('app/create', { - name: this.name, - description: this.description, - callbackUrl: this.cb, - permission: this.permission - }).then(() => { - location.href = '/dev/apps'; - }).catch(() => { - alert(this.$t('@.dev.failed-to-create')); - }); - } - } -}); -</script> diff --git a/src/client/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue deleted file mode 100644 index f1e001909f..0000000000 --- a/src/client/app/dev/views/ui.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> -<div> - <b-navbar toggleable="md" type="dark" variant="info"> - <b-navbar-brand>Developers</b-navbar-brand> - <b-navbar-nav> - <b-nav-item to="/">Home</b-nav-item> - <b-nav-item to="/apps">Apps</b-nav-item> - </b-navbar-nav> - </b-navbar> - <main> - <slot></slot> - </main> -</div> -</template> - -<style lang="stylus" scoped> -main - padding 32px - max-width 700px -</style> diff --git a/src/client/app/i18n.ts b/src/client/app/i18n.ts deleted file mode 100644 index 2d0d9ba550..0000000000 --- a/src/client/app/i18n.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { lang, locale } from './config'; - -export default function(scope?: string) { - const texts = scope ? locale[scope] || {} : {}; - texts['@'] = locale['common']; - texts['@deck'] = locale['deck']; - return { - sync: false, - locale: lang, - messages: { - [lang]: texts - } - }; -} diff --git a/src/client/app/init.css b/src/client/app/init.css deleted file mode 100644 index db5e23c56d..0000000000 --- a/src/client/app/init.css +++ /dev/null @@ -1,57 +0,0 @@ -@charset "utf-8"; - -/** - * Boot screen style - */ - -html { - font-family: Roboto, HelveticaNeue, Arial, sans-serif; -} - -body > noscript { - position: fixed; - z-index: 2; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - background: #fff; -} - body > noscript > p { - display: block; - margin: 32px; - font-size: 2em; - color: #555; - } - -#ini { - position: fixed; - z-index: 1; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--bg); - cursor: wait; -} - #ini > svg { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 64px; - height: 64px; - animation: ini 0.6s infinite linear; - } - -@keyframes ini { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/client/app/init.ts b/src/client/app/init.ts deleted file mode 100644 index 6fd11b9b75..0000000000 --- a/src/client/app/init.ts +++ /dev/null @@ -1,512 +0,0 @@ -/** - * App initializer - */ - -import Vue from 'vue'; -import Vuex from 'vuex'; -import VueRouter from 'vue-router'; -import VAnimateCss from 'v-animate-css'; -import VModal from 'vue-js-modal'; -import VueI18n from 'vue-i18n'; -import SequentialEntrance from 'vue-sequential-entrance'; - -import VueHotkey from './common/hotkey'; -import VueSize from './common/size'; -import App from './app.vue'; -import checkForUpdate from './common/scripts/check-for-update'; -import MiOS from './mios'; -import { version, codename, lang, locale } from './config'; -import { builtinThemes, applyTheme, futureTheme } from './theme'; -import Dialog from './common/views/components/dialog.vue'; - -if (localStorage.getItem('theme') == null) { - applyTheme(futureTheme); -} - -//#region FontAwesome -import { library } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; - -import { - faRetweet, - faPlus, - faUser, - faCog, - faCheck, - faStar, - faReply, - faEllipsisH, - faQuoteLeft, - faQuoteRight, - faAngleUp, - faAngleDown, - faAt, - faHashtag, - faHome, - faGlobe, - faCircle, - faList, - faHeart, - faUnlock, - faRssSquare, - faSort, - faChartPie, - faChartBar, - faPencilAlt, - faColumns, - faComments, - faGamepad, - faCloud, - faPowerOff, - faChevronCircleLeft, - faChevronCircleRight, - faShareAlt, - faTimes, - faThumbtack, - faSearch, - faAngleRight, - faWrench, - faTerminal, - faMoon, - faPalette, - faSlidersH, - faDesktop, - faVolumeUp, - faLanguage, - faInfoCircle, - faExclamationTriangle, - faKey, - faBan, - faCogs, - faUnlockAlt, - faPuzzlePiece, - faMobileAlt, - faSignInAlt, - faSyncAlt, - faPaperPlane, - faUpload, - faMapMarkerAlt, - faEnvelope, - faLock, - faFolderOpen, - faBirthdayCake, - faImage, - faEye, - faDownload, - faFileImport, - faLink, - faArrowRight, - faICursor, - faCaretRight, - faReplyAll, - faCamera, - faMinus, - faCaretDown, - faCalculator, - faUsers, - faBars, - faFileImage, - faPollH, - faFolder, - faMicrochip, - faMemory, - faServer, - faExclamationCircle, - faSpinner, - faBroadcastTower, - faChartLine, - faEllipsisV, - faStickyNote, - faUserClock, - faUserPlus, - faExternalLinkSquareAlt, - faSync, - faArrowLeft, - faMapMarker, - faRobot, - faHourglassHalf, - faGavel, - faUndoAlt, -} from '@fortawesome/free-solid-svg-icons'; - -import { - faBell as farBell, - faEnvelope as farEnvelope, - faComments as farComments, - faTrashAlt as farTrashAlt, - faWindowRestore as farWindowRestore, - faFolder as farFolder, - faLaugh as farLaugh, - faSmile as farSmile, - faEyeSlash as farEyeSlash, - faFolderOpen as farFolderOpen, - faSave as farSave, - faImages as farImages, - faChartBar as farChartBar, - faCommentAlt as farCommentAlt, - faClock as farClock, - faCalendarAlt as farCalendarAlt, - faHdd as farHdd, - faMoon as farMoon, - faPlayCircle as farPlayCircle, - faLightbulb as farLightbulb, - faStickyNote as farStickyNote, -} from '@fortawesome/free-regular-svg-icons'; - -import { - faTwitter as fabTwitter, - faGithub as fabGithub, - faDiscord as fabDiscord -} from '@fortawesome/free-brands-svg-icons'; -import i18n from './i18n'; - -library.add( - faRetweet, - faPlus, - faUser, - faCog, - faCheck, - faStar, - faReply, - faEllipsisH, - faQuoteLeft, - faQuoteRight, - faAngleUp, - faAngleDown, - faAt, - faHashtag, - faHome, - faGlobe, - faCircle, - faList, - faHeart, - faUnlock, - faRssSquare, - faSort, - faChartPie, - faChartBar, - faPencilAlt, - faColumns, - faComments, - faGamepad, - faCloud, - faPowerOff, - faChevronCircleLeft, - faChevronCircleRight, - faShareAlt, - faTimes, - faThumbtack, - faSearch, - faAngleRight, - faWrench, - faTerminal, - faMoon, - faPalette, - faSlidersH, - faDesktop, - faVolumeUp, - faLanguage, - faInfoCircle, - faExclamationTriangle, - faKey, - faBan, - faCogs, - faUnlockAlt, - faPuzzlePiece, - faMobileAlt, - faSignInAlt, - faSyncAlt, - faPaperPlane, - faUpload, - faMapMarkerAlt, - faEnvelope, - faLock, - faFolderOpen, - faBirthdayCake, - faImage, - faEye, - faDownload, - faFileImport, - faLink, - faArrowRight, - faICursor, - faCaretRight, - faReplyAll, - faCamera, - faMinus, - faCaretDown, - faCalculator, - faUsers, - faBars, - faFileImage, - faPollH, - faFolder, - faMicrochip, - faMemory, - faServer, - faExclamationCircle, - faSpinner, - faBroadcastTower, - faChartLine, - faEllipsisV, - faStickyNote, - faUserClock, - faUserPlus, - faExternalLinkSquareAlt, - faSync, - faArrowLeft, - faMapMarker, - faRobot, - faHourglassHalf, - faGavel, - faUndoAlt, - - farBell, - farEnvelope, - farComments, - farTrashAlt, - farWindowRestore, - farFolder, - farLaugh, - farSmile, - farEyeSlash, - farFolderOpen, - farSave, - farImages, - farChartBar, - farCommentAlt, - farClock, - farCalendarAlt, - farHdd, - farMoon, - farPlayCircle, - farLightbulb, - farStickyNote, - - fabTwitter, - fabGithub, - fabDiscord -); -//#endregion - -Vue.use(Vuex); -Vue.use(VueRouter); -Vue.use(VAnimateCss); -Vue.use(VModal); -Vue.use(VueHotkey); -Vue.use(VueSize); -Vue.use(VueI18n); -Vue.use(SequentialEntrance); - -Vue.component('fa', FontAwesomeIcon); - -// Register global directives -require('./common/views/directives'); - -// Register global components -require('./common/views/components'); -require('./common/views/widgets'); - -// Register global filters -require('./common/views/filters'); - -Vue.mixin({ - methods: { - destroyDom() { - this.$destroy(); - - if (this.$el.parentNode) { - this.$el.parentNode.removeChild(this.$el); - } - } - } -}); - -/** - * APP ENTRY POINT! - */ - -console.info(`Misskey v${version} (${codename})`); -console.info( - `%c${locale['common']['do-not-copy-paste']}`, - 'color: red; background: yellow; font-size: 16px; font-weight: bold;'); - -// BootTimer解除 -window.clearTimeout((window as any).mkBootTimer); -delete (window as any).mkBootTimer; - -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#endregion - -// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする -try { - localStorage.setItem('kyoppie', 'yuppie'); -} catch (e) { - Storage.prototype.setItem = () => { }; // noop -} - -// クライアントを更新すべきならする -if (localStorage.getItem('should-refresh') == 'true') { - localStorage.removeItem('should-refresh'); - location.reload(true); -} - -// MiOSを初期化してコールバックする -export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS) => void, sw = false) => { - const os = new MiOS(sw); - - os.init(() => { - // アプリ基底要素マウント - document.body.innerHTML = '<div id="app"></div>'; - - const launch = (router: VueRouter) => { - //#region theme - os.store.watch(s => { - return s.device.darkmode; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const dark = themes.find(t => t.id == os.store.state.device.darkTheme); - const light = themes.find(t => t.id == os.store.state.device.lightTheme); - applyTheme(v ? dark : light); - }); - os.store.watch(s => { - return s.device.lightTheme; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == v); - if (!os.store.state.device.darkmode) { - applyTheme(theme); - } - }); - os.store.watch(s => { - return s.device.darkTheme; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == v); - if (os.store.state.device.darkmode) { - applyTheme(theme); - } - }); - //#endregion - - /*// Reapply current theme - try { - const themeName = os.store.state.device.darkmode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme; - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == themeName); - if (theme) { - applyTheme(theme); - } - } catch (e) { - console.log(`Cannot reapply theme. ${e}`); - }*/ - - //#region line width - document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`); - os.store.watch(s => { - return s.device.lineWidth; - }, v => { - document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`); - }); - //#endregion - - //#region fontSize - document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`); - os.store.watch(s => { - return s.device.fontSize; - }, v => { - document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`); - }); - //#endregion - - document.addEventListener('visibilitychange', () => { - if (!document.hidden) { - os.store.commit('clearBehindNotes'); - } - }, false); - - window.addEventListener('scroll', () => { - if (window.scrollY <= 8) { - os.store.commit('clearBehindNotes'); - } - }, { passive: true }); - - const app = new Vue({ - i18n: i18n(), - store: os.store, - data() { - return { - os: { - windows: os.windows - }, - stream: os.stream, - instanceName: os.instanceName - }; - }, - methods: { - api: os.api, - getMeta: os.getMeta, - getMetaSync: os.getMetaSync, - signout: os.signout, - new(vm, props) { - const x = new vm({ - parent: this, - propsData: props - }).$mount(); - document.body.appendChild(x.$el); - return x; - }, - newAsync(vm, props) { - return new Promise((res) => { - vm().then(vm => { - const x = new vm({ - parent: this, - propsData: props - }).$mount(); - document.body.appendChild(x.$el); - res(x); - }); - }); - }, - dialog(opts) { - const vm = this.new(Dialog, opts); - const p: any = new Promise((res) => { - vm.$once('ok', result => res({ canceled: false, result })); - vm.$once('cancel', () => res({ canceled: true })); - }); - p.close = () => { - vm.close(); - }; - return p; - } - }, - router, - render: createEl => createEl(App) - }); - - os.app = app; - - // マウント - app.$mount('#app'); - - //#region 更新チェック - setTimeout(() => { - checkForUpdate(app); - }, 3000); - //#endregion - - return [app, os] as [Vue, MiOS]; - }; - - // Deck mode - os.store.commit('device/set', { - key: 'inDeckMode', - value: os.store.getters.isSignedIn && os.store.state.device.deckMode - && (document.location.pathname === '/' || window.performance.navigation.type === 1) - }); - - callback(launch, os); - }); -}; diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts deleted file mode 100644 index 2c62f120ea..0000000000 --- a/src/client/app/mios.ts +++ /dev/null @@ -1,522 +0,0 @@ -import autobind from 'autobind-decorator'; -import Vue from 'vue'; -import { EventEmitter } from 'eventemitter3'; -import { v4 as uuid } from 'uuid'; - -import initStore from './store'; -import { apiUrl, version, locale } from './config'; -import Progress from './common/scripts/loading'; - -import Err from './common/views/components/connect-failed.vue'; -import Stream from './common/scripts/stream'; - -//#region api requests -let spinner = null; -let pending = 0; -//#endregion - -/** - * Misskey Operating System - */ -export default class MiOS extends EventEmitter { - /** - * Misskeyの /meta で取得できるメタ情報 - */ - private meta: { - data: { [x: string]: any }; - chachedAt: Date; - }; - - public get instanceName() { - const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement; - if (siteName && siteName.content) { - return siteName.content; - } - - return 'Misskey'; - } - - private isMetaFetching = false; - - public app: Vue; - - /** - * Whether is debug mode - */ - public get debug() { - return this.store ? this.store.state.device.debug : false; - } - - public store: ReturnType<typeof initStore>; - - /** - * A connection manager of home stream - */ - public stream: Stream; - - /** - * A registration of service worker - */ - private swRegistration: ServiceWorkerRegistration = null; - - /** - * Whether should register ServiceWorker - */ - private shouldRegisterSw: boolean; - - /** - * ウィンドウシステム - */ - public windows = new WindowSystem(); - - /** - * MiOSインスタンスを作成します - * @param shouldRegisterSw ServiceWorkerを登録するかどうか - */ - constructor(shouldRegisterSw = false) { - super(); - - this.shouldRegisterSw = shouldRegisterSw; - - if (this.debug) { - (window as any).os = this; - } - } - - @autobind - public log(...args) { - if (!this.debug) return; - console.log.apply(null, args); - } - - @autobind - public logInfo(...args) { - if (!this.debug) return; - console.info.apply(null, args); - } - - @autobind - public logWarn(...args) { - if (!this.debug) return; - console.warn.apply(null, args); - } - - @autobind - public logError(...args) { - if (!this.debug) return; - console.error.apply(null, args); - } - - @autobind - public signout() { - this.store.dispatch('logout'); - location.href = '/'; - } - - /** - * Initialize MiOS (boot) - * @param callback A function that call when initialized - */ - @autobind - public async init(callback) { - this.store = initStore(this); - - // ユーザーをフェッチしてコールバックする - const fetchme = (token, cb) => { - let me = null; - - // Return when not signed in - if (token == null) { - return done(); - } - - // Fetch user - fetch(`${apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token - }) - }) - // When success - .then(res => { - // When failed to authenticate user - if (res.status !== 200 && res.status < 500) { - return this.signout(); - } - - // Parse response - res.json().then(i => { - me = i; - me.token = token; - done(); - }); - }) - // When failure - .catch(() => { - // Render the error screen - document.body.innerHTML = '<div id="err"></div>'; - new Vue({ - render: createEl => createEl(Err) - }).$mount('#err'); - - Progress.done(); - }); - - function done() { - if (cb) cb(me); - } - }; - - // フェッチが完了したとき - const fetched = () => { - this.emit('signedin'); - - this.initStream(); - - // Finish init - callback(); - - // Init service worker - if (this.shouldRegisterSw) { - this.getMeta().then(data => { - if (data.swPublickey) this.registerSw(data.swPublickey); - }); - } - }; - - // キャッシュがあったとき - if (this.store.state.i != null) { - if (this.store.state.i.token == null) { - this.signout(); - return; - } - - // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、 - fetched(); - - // 後から新鮮なデータをフェッチ - fetchme(this.store.state.i.token, freshData => { - this.store.dispatch('mergeMe', freshData); - }); - } else { - // Get token from cookie or localStorage - const i = (document.cookie.match(/i=(\w+)/) || [null, null])[1] || localStorage.getItem('i'); - - fetchme(i, me => { - if (me) { - this.store.dispatch('login', me); - fetched(); - } else { - this.initStream(); - - // Finish init - callback(); - } - }); - } - } - - @autobind - private initStream() { - this.stream = new Stream(this); - - if (this.store.getters.isSignedIn) { - const main = this.stream.useSharedConnection('main'); - - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - this.store.dispatch('mergeMe', i); - }); - - main.on('readAllNotifications', () => { - this.store.dispatch('mergeMe', { - hasUnreadNotification: false - }); - }); - - main.on('unreadNotification', () => { - this.store.dispatch('mergeMe', { - hasUnreadNotification: true - }); - }); - - main.on('readAllMessagingMessages', () => { - this.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: false - }); - }); - - main.on('unreadMessagingMessage', () => { - this.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: true - }); - }); - - main.on('unreadMention', () => { - this.store.dispatch('mergeMe', { - hasUnreadMentions: true - }); - }); - - main.on('readAllUnreadMentions', () => { - this.store.dispatch('mergeMe', { - hasUnreadMentions: false - }); - }); - - main.on('unreadSpecifiedNote', () => { - this.store.dispatch('mergeMe', { - hasUnreadSpecifiedNotes: true - }); - }); - - main.on('readAllUnreadSpecifiedNotes', () => { - this.store.dispatch('mergeMe', { - hasUnreadSpecifiedNotes: false - }); - }); - - main.on('clientSettingUpdated', x => { - this.store.commit('settings/set', { - key: x.key, - value: x.value - }); - }); - - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - alert(locale['common']['my-token-regenerated']); - this.signout(); - }); - } - } - - /** - * Register service worker - */ - @autobind - private registerSw(swPublickey: string) { - // Check whether service worker and push manager supported - const isSwSupported = - ('serviceWorker' in navigator) && ('PushManager' in window); - - // Reject when browser not service worker supported - if (!isSwSupported) return; - - // Reject when not signed in to Misskey - if (!this.store.getters.isSignedIn) return; - - // When service worker activated - navigator.serviceWorker.ready.then(registration => { - this.log('[sw] ready: ', registration); - - this.swRegistration = registration; - - // Options of pushManager.subscribe - // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters - const opts = { - // A boolean indicating that the returned push subscription - // will only be used for messages whose effect is made visible to the user. - userVisibleOnly: true, - - // A public key your push server will use to send - // messages to client apps via a push server. - applicationServerKey: urlBase64ToUint8Array(swPublickey) - }; - - // Subscribe push notification - this.swRegistration.pushManager.subscribe(opts).then(subscription => { - this.log('[sw] Subscribe OK:', subscription); - - function encode(buffer: ArrayBuffer) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); - } - - // Register - this.api('sw/register', { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey('auth')), - publickey: encode(subscription.getKey('p256dh')) - }); - }) - // When subscribe failed - .catch(async (err: Error) => { - this.logError('[sw] Subscribe Error:', err); - - // 通知が許可されていなかったとき - if (err.name == 'NotAllowedError') { - this.logError('[sw] Subscribe failed due to notification not allowed'); - return; - } - - // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが - // 既に存在していることが原因でエラーになった可能性があるので、 - // そのサブスクリプションを解除しておく - const subscription = await this.swRegistration.pushManager.getSubscription(); - if (subscription) subscription.unsubscribe(); - }); - }); - - // The path of service worker script - const sw = `/sw.${version}.js`; - - // Register service worker - navigator.serviceWorker.register(sw).then(registration => { - // 登録成功 - this.logInfo('[sw] Registration successful with scope: ', registration.scope); - }).catch(err => { - // 登録失敗 :( - this.logError('[sw] Registration failed: ', err); - }); - } - - public requests = []; - - /** - * Misskey APIにリクエストします - * @param endpoint エンドポイント名 - * @param data パラメータ - */ - @autobind - public api(endpoint: string, data: { [x: string]: any } = {}, silent = false): Promise<{ [x: string]: any }> { - if (!silent) { - if (++pending === 1) { - spinner = document.createElement('div'); - spinner.setAttribute('id', 'wait'); - document.body.appendChild(spinner); - } - } - - const onFinally = () => { - if (!silent) { - if (--pending === 0) spinner.parentNode.removeChild(spinner); - } - }; - - const promise = new Promise((resolve, reject) => { - // Append a credential - if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token; - - const req = { - id: uuid(), - date: new Date(), - name: endpoint, - data, - res: null, - status: null - }; - - if (this.debug) { - this.requests.push(req); - } - - // Send request - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: endpoint === 'signin' ? 'include' : 'omit', - cache: 'no-cache' - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (this.debug) { - req.status = res.status; - req.res = body; - } - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; - } - - /** - * Misskeyのメタ情報を取得します - */ - @autobind - public getMetaSync() { - return this.meta ? this.meta.data : null; - } - - /** - * Misskeyのメタ情報を取得します - * @param force キャッシュを無視するか否か - */ - @autobind - public getMeta(force = false) { - return new Promise<{ [x: string]: any }>(async (res, rej) => { - if (this.isMetaFetching) { - this.once('_meta_fetched_', () => { - res(this.meta.data); - }); - return; - } - - const expire = 1000 * 60; // 1min - - // forceが有効, meta情報を保持していない or 期限切れ - if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) { - this.isMetaFetching = true; - const meta = await this.api('meta', { - detail: false - }); - this.meta = { - data: meta, - chachedAt: new Date() - }; - this.isMetaFetching = false; - this.emit('_meta_fetched_'); - res(meta); - } else { - res(this.meta.data); - } - }); - } -} - -class WindowSystem extends EventEmitter { - public windows = new Set(); - - public add(window) { - this.windows.add(window); - this.emit('added', window); - } - - public remove(window) { - this.windows.delete(window); - this.emit('removed', window); - } - - public getAll() { - return this.windows; - } -} - -/** - * Convert the URL safe base64 string to a Uint8Array - * @param base64String base64 string - */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/-/g, '+') - .replace(/_/g, '/'); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts deleted file mode 100644 index 26ef740811..0000000000 --- a/src/client/app/mobile/script.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Mobile Client - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; - -import MkIndex from './views/pages/index.vue'; -import MkSignup from './views/pages/signup.vue'; -import MkSelectDrive from './views/pages/selectdrive.vue'; -import MkDrive from './views/pages/drive.vue'; -import MkNotifications from './views/pages/notifications.vue'; -import MkMessaging from './views/pages/messaging.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkNote from './views/pages/note.vue'; -import MkSearch from './views/pages/search.vue'; -import UI from './views/pages/ui.vue'; -import MkReversi from './views/pages/games/reversi.vue'; -import MkTag from './views/pages/tag.vue'; -import MkShare from '../common/views/pages/share.vue'; -import MkFollow from '../common/views/pages/follow.vue'; -import MkNotFound from '../common/views/pages/not-found.vue'; -import DeckColumn from '../common/views/deck/deck.column-template.vue'; -import PostFormDialog from './views/components/post-form-dialog.vue'; - -import FileChooser from './views/components/drive-file-chooser.vue'; -import FolderChooser from './views/components/drive-folder-chooser.vue'; - -/** - * init - */ -init((launch, os) => { - Vue.mixin({ - data() { - return { - isMobile: true - }; - }, - - methods: { - $post(opts) { - const o = opts || {}; - - document.documentElement.style.overflow = 'hidden'; - - function recover() { - document.documentElement.style.overflow = 'auto'; - } - - const vm = this.$root.new(PostFormDialog, { - reply: o.reply, - mention: o.mention, - renote: o.renote, - initialText: o.initialText, - instant: o.instant, - initialNote: o.initialNote, - }); - vm.$once('cancel', recover); - vm.$once('posted', recover); - if (o.cb) vm.$once('closed', o.cb); - (vm as any).focus(); - }, - - $chooseDriveFile(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FileChooser, { - title: o.title, - multiple: o.multiple, - initFolder: o.currentFolder - }); - vm.$once('selected', file => { - res(file); - }); - }); - }, - - $chooseDriveFolder(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FolderChooser, { - title: o.title, - initFolder: o.currentFolder - }); - vm.$once('selected', folder => { - res(folder); - }); - }); - }, - - $notify(message) { - alert(message); - } - } - }); - - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // http://qiita.com/junya/items/3ff380878f26ca447f85 - document.body.setAttribute('ontouchstart', ''); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - ...(os.store.state.device.inDeckMode - ? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ - { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, - { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, - { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, - { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) }, - ]}] - : [ - { path: '/', name: 'index', component: MkIndex }, - ]), - { path: '/signup', name: 'signup', component: MkSignup }, - { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, - { path: '/i/settings/:page', redirect: '/i/settings' }, - { path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, - { path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, - { path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) }, - { path: '/i/notifications', name: 'notifications', component: MkNotifications }, - { path: '/i/messaging', name: 'messaging', component: MkMessaging }, - { path: '/i/messaging/group/:group', component: MkMessagingRoom }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', name: 'drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/drive/file/:file', component: MkDrive }, - { path: '/i/pages/new', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }) }, - { path: '/i/pages/edit/:pageId', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initPageId: route.params.pageId }) }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/search', component: MkSearch }, - { path: '/tags/:tag', component: MkTag }, - { path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/share', component: MkShare }, - { path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, - { path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) }, - { path: '/notes/:note', component: MkNote }, - { path: '/authorize-follow', component: MkFollow }, - { path: '*', component: MkNotFound } - ] - }); - - // Launch the app - launch(router); -}, true); diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl deleted file mode 100644 index 3a4fc9c0c6..0000000000 --- a/src/client/app/mobile/style.styl +++ /dev/null @@ -1,23 +0,0 @@ -@import "../app" -@import "../reset" - -#wait - top auto - bottom 15px - left 15px - -html - height 100% - background var(--bg) - -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px diff --git a/src/client/app/mobile/views/components/detail-notes.vue b/src/client/app/mobile/views/components/detail-notes.vue deleted file mode 100644 index bab7949534..0000000000 --- a/src/client/app/mobile/views/components/detail-notes.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div class="fdcvngpy"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - <ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.fdcvngpy - > * > .post - margin-bottom 8px - - @media (min-width 500px) - > * > .post - margin-bottom 16px - -</style> diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue deleted file mode 100644 index 8795102f97..0000000000 --- a/src/client/app/mobile/views/components/drive-file-chooser.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb"> - <div class="body"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive class="drive" ref="browser" - :select-file="true" - :type="type" - :multiple="multiple" - @change-selection="onChangeSelection" - @selected="onSelected" - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-file-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: ['type', 'multiple'], - data() { - return { - files: [] - }; - }, - methods: { - onChangeSelection(files) { - this.files = files; - }, - onSelected(file) { - this.$emit('selected', file); - this.destroyDom(); - }, - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', this.files); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cdxzvcfawjxdyxsekbxbfgtplebnoneb - position fixed - z-index 20000 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(#000, 0.2) - - > .body - width 100% - height 100% - background var(--faceHeader) - - > header - border-bottom solid 1px var(--faceDivider) - color var(--text) - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue deleted file mode 100644 index 250a7aca2c..0000000000 --- a/src/client/app/mobile/views/components/drive-folder-chooser.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div class="mk-drive-folder-chooser"> - <div class="body"> - <header> - <h1>{{ $t('select-folder') }}</h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" - select-folder - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-folder-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - methods: { - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-folder-chooser - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(#000, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue deleted file mode 100644 index 328982a16b..0000000000 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ /dev/null @@ -1,253 +0,0 @@ -<template> -<div class="pyvicwrksnfyhpfgkjwqknuururpaztw"> - <div class="preview"> - <x-file-thumbnail class="preview" :file="file" :detail="true"/> - <template v-if="kind != 'image'"><fa icon="file"/></template> - <footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height"> - <span class="size"> - <span class="width">{{ file.properties.width }}</span> - <span class="time">×</span> - <span class="height">{{ file.properties.height }}</span> - <span class="px">px</span> - </span> - <span class="separator"></span> - <span class="aspect-ratio"> - <span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span> - <span class="colon">:</span> - <span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span> - </span> - </footer> - </div> - <div class="info"> - <div> - <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </div> - </div> - <div class="menu"> - <div> - <ui-input readonly :value="file.url">URL</ui-input> - <ui-button link :href="dlUrl" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button> - <ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button> - <ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button> - <ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button> - <ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> {{ $t('delete') }}</ui-button> - </div> - </div> - <div class="hash"> - <div> - <p> - <fa icon="hashtag"/>{{ $t('hash') }} - </p> - <code>{{ file.md5 }}</code> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { gcd } from '../../../../../prelude/math'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file-detail.vue'), - props: ['file'], - - components: { - XFileThumbnail - }, - - data() { - return { - gcd, - exif: null - }; - }, - - computed: { - browser(): any { - return this.$parent; - }, - - kind(): string { - return this.file.type.split('/')[0]; - }, - - style(): any { - return this.file.properties.avgColor ? { - 'background-color': this.file.properties.avgColor - } : {}; - }, - - dlUrl(): string { - return this.file.url; - } - }, - - methods: { - rename() { - const name = window.prompt(this.$t('rename'), this.file.name); - if (name == null || name == '' || name == this.file.name) return; - this.$root.api('drive/files/update', { - fileId: this.file.id, - name: name - }).then(() => { - this.browser.cf(this.file, true); - }); - }, - - move() { - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/files/update', { - fileId: this.file.id, - folderId: folder == null ? null : folder.id - }).then(() => { - this.browser.cf(this.file, true); - }); - }); - }, - - del() { - this.$root.api('drive/files/delete', { - fileId: this.file.id - }).then(() => { - this.browser.cd(this.file.folderId); - }); - }, - - toggleSensitive() { - this.$root.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive - }); - - this.file.isSensitive = !this.file.isSensitive; - }, - - showCreatedAt() { - this.$root.dialog({ - type: 'info', - text: (new Date(this.file.createdAt)).toLocaleString() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.pyvicwrksnfyhpfgkjwqknuururpaztw - > .preview - padding 8px - background var(--bg) - - > .preview - width fit-content - width -moz-fit-content - max-width 100% - margin 0 auto - box-shadow 1px 1px 4px rgba(#000, 0.2) - overflow hidden - color var(--driveFileIcon) - justify-content center - - > footer - padding 8px 8px 0 8px - text-align center - font-size 0.8em - color var(--text) - opacity 0.7 - - > .separator - display inline - padding 0 4px - - > .size - display inline - - .time - margin 0 2px - - .px - margin-left 4px - - > .aspect-ratio - display inline - opacity 0.7 - - &:before - content "(" - - &:after - content ")" - - > .info - padding 14px - font-size 0.8em - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - color var(--text) - - > .separator - padding 0 4px - - > .created-at - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - > .menu - padding 0 14px 14px 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > .hash - padding 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color var(--text) - font-size 0.9em - - > [data-icon] - margin-right 4px - - > code - display block - width 100% - margin 6px 0 0 0 - padding 8px - white-space nowrap - overflow auto - font-size 0.8em - color #222 - border solid 1px #dfdfdf - border-radius 2px - background #f5f5f5 - -</style> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue deleted file mode 100644 index ed95537f9c..0000000000 --- a/src/client/app/mobile/views/components/drive.file.vue +++ /dev/null @@ -1,155 +0,0 @@ -<template> -<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> - <div class="container"> - <x-file-thumbnail class="thumbnail" :file="file" fit="cover"/> - <div class="body"> - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> - <footer> - <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </footer> - </div> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file.vue'), - props: ['file'], - components: { - XFileThumbnail - }, - data() { - return { - isSelected: false - }; - }, - computed: { - browser(): any { - return this.$parent; - } - }, - created() { - this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id) - - this.browser.$on('change-selection', this.onBrowserChangeSelection); - }, - beforeDestroy() { - this.browser.$off('change-selection', this.onBrowserChangeSelection); - }, - methods: { - onBrowserChangeSelection(selections) { - this.isSelected = selections.some(f => f.id == this.file.id); - }, - onClick() { - this.browser.chooseFile(this.file); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vupkuhvjnjyqaqhsiogfbywvjxynrgsm - display block - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - display grid - max-width 500px - margin 0 auto - padding 16px - grid-template-columns 64px 1fr - grid-column-gap 10px - - &:after - content "" - display block - clear both - - > .thumbnail - width 64px - height 64px - color var(--driveFileIcon) - - > .body - display block - word-break break-all - - > .name - display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color var(--text) - word-break break-word - - > .ext - opacity 0.5 - - > .tags - display block - margin 4px 0 0 0 - padding 0 - list-style none - font-size 0.5em - - > .tag - display inline-block - margin 0 5px 0 0 - padding 1px 5px - border-radius 2px - - > footer - display block - margin 4px 0 0 0 - font-size 0.7em - color var(--text) - - > .separator - padding 0 4px - - > .type - opacity 0.7 - - > .mk-file-type-icon - margin-right 4px - - > .data-size - opacity 0.7 - - > .created-at - opacity 0.7 - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - &[data-is-selected] - background var(--primary) - - &, * - color var(--primaryForeground) !important - -</style> diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue deleted file mode 100644 index 0959c1e7d4..0000000000 --- a/src/client/app/mobile/views/components/drive.folder.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> - <div class="container"> - <p class="name"><fa icon="folder"/>{{ folder.name }}</p><fa icon="angle-right"/> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['folder'], - computed: { - browser(): any { - return this.$parent; - } - }, - methods: { - onClick() { - this.browser.cd(this.folder); - } - } -}); -</script> - -<style lang="stylus" scoped> -.jvwxssxsytqlqvrpiymarjlzlsxskqsr - display block - color var(--text) - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - > .name - display block - margin 0 - padding 0 - - > [data-icon] - margin-right 6px - - > [data-icon] - position absolute - top 0 - bottom 0 - right 20px - height 100% - -</style> diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue deleted file mode 100644 index fe193f311a..0000000000 --- a/src/client/app/mobile/views/components/drive.vue +++ /dev/null @@ -1,618 +0,0 @@ -<template> -<div class="kmmwchoexgckptowjmjgfsygeltxfeqs"> - <nav ref="nav"> - <a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>{{ $t('@.drive') }}</a> - <template v-for="folder in hierarchyFolders"> - <span :key="folder.id + '>'"><fa icon="angle-right"/></span> - <a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a> - </template> - <template v-if="folder != null"> - <span><fa icon="angle-right"/></span> - <p>{{ folder.name }}</p> - </template> - <template v-if="file != null"> - <span><fa icon="angle-right"/></span> - <p>{{ file.name }}</p> - </template> - </nav> - <mk-uploader ref="uploader"/> - <div class="browser" :class="{ fetching }" v-if="file == null"> - <div class="info" v-if="info"> - <p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% {{ $t('used') }}</p> - <p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)"> - <template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} {{ $t('folder-count') }}</template> - <template v-if="folder.foldersCount > 0 && folder.filesCount > 0">{{ $t('count-separator') }}</template> - <template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template> - </p> - </div> - <div class="folders" v-if="folders.length > 0 || moreFolders"> - <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/> - <p v-if="moreFolders">{{ $t('@.load-more') }}</p> - </div> - <div class="files" v-if="files.length > 0 || moreFiles"> - <x-file class="file" v-for="file in files" :key="file.id" :file="file"/> - <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> - {{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }} - </button> - </div> - <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching"> - <p v-if="folder == null">{{ $t('nothing-in-drive') }}</p> - <p v-if="folder != null">{{ $t('folder-is-empty') }}</p> - </div> - </div> - <div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/> - <x-file-detail v-if="file != null" :file="file"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import XFileDetail from './drive.file-detail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.vue'), - components: { - XFolder, - XFile, - XFileDetail - }, - props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'], - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, - - file: null, - - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - info: null, - connection: null, - - fetching: true, - fetchingMoreFiles: false, - fetchingMoreFolders: false - }; - }, - computed: { - isFileSelectMode(): boolean { - return this.selectFile; - } - }, - watch: { - top() { - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - } - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('drive'); - - this.connection.on('fileCreated', this.onStreamDriveFileCreated); - this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); - this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); - this.connection.on('folderCreated', this.onStreamDriveFolderCreated); - this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated); - - if (this.initFolder) { - this.cd(this.initFolder, true); - } else if (this.initFile) { - this.cf(this.initFile, true); - } else { - this.fetch(); - } - - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, - - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, - - onStreamDriveFileDeleted(fileId) { - this.removeFile(fileId); - }, - - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, - - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, - - dive(folder) { - this.hierarchyFolders.unshift(folder); - if (folder.parent) this.dive(folder.parent); - }, - - cd(target, silent = false) { - if (target == null) { - this.goRoot(silent); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.file = null; - this.fetching = true; - - this.$root.api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - if (folder.parent) this.dive(folder.parent); - - this.$emit('open-folder', this.folder, silent); - this.fetch(); - }); - }, - - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断 - if (current != folder.parentId) return; - - // 追加しようとしているフォルダを既に所有してたら中断 - if (this.folders.some(f => f.id == folder.id)) return; - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, - - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断 - if (current != file.folderId) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - Vue.set(this.files, exist, file); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, - - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, - - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, - - appendFile(file) { - this.addFile(file); - }, - appendFolder(folder) { - this.addFolder(folder); - }, - prependFile(file) { - this.addFile(file, true); - }, - prependFolder(folder) { - this.addFolder(folder, true); - }, - - goRoot(silent = false) { - // すでにrootにいるなら何もしない - if (this.folder == null && this.file == null) return; - - this.file = null; - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root', silent); - this.fetch(); - }, - - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; - - this.$emit('begin-fetch'); - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 20; - const filesMax = 20; - - // フォルダ一覧取得 - this.$root.api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - for (const x of fetchedFolders) this.appendFolder(x); - for (const x of fetchedFiles) this.appendFile(x); - this.fetching = false; - - // 一連の読み込みが完了したイベントを発行 - this.$emit('fetched'); - } else { - flag = true; - // 一連の読み込みが半分完了したイベントを発行 - this.$emit('fetch-mid'); - } - }; - - if (this.folder == null) { - // Fetch addtional drive info - this.$root.api('drive').then(info => { - this.info = info; - }); - } - }, - - fetchMoreFiles() { - this.fetching = true; - this.fetchingMoreFiles = true; - - const max = 30; - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: max + 1, - untilId: this.files[this.files.length - 1].id - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - for (const x of files) this.appendFile(x); - this.fetching = false; - this.fetchingMoreFiles = false; - }); - }, - - chooseFile(file) { - if (this.isFileSelectMode) { - if (this.multiple) { - if (this.selectedFiles.some(f => f.id == file.id)) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - this.$emit('selected', file); - } - } else { - this.cf(file); - } - }, - - cf(file, silent = false) { - if (typeof file == 'object') file = file.id; - - this.fetching = true; - - this.$root.api('drive/files/show', { - fileId: file - }).then(file => { - this.file = file; - this.folder = null; - this.hierarchyFolders = []; - - if (file.folder) this.dive(file.folder); - - this.fetching = false; - - this.$emit('open-file', this.file, silent); - }); - }, - - selectLocalFile() { - (this.$refs.file as any).click(); - }, - - createFolder() { - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('folder-name-cannot-empty') - }); - return; - } - this.$root.api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }); - }, - - renameFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('cannot-empty') - }); - return; - } - this.$root.api('drive/folders/update', { - name: name, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - moveFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/folders/update', { - parentId: folder ? folder.id : null, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - urlUpload() { - const url = window.prompt(this.$t('url-prompt')); - if (url == null || url == '') return; - this.$root.api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - this.$root.dialog({ - type: 'info', - text: this.$t('uploading') - }); - }, - - onChangeLocalFile() { - for (const f of Array.from((this.$refs.file as any).files)) { - (this.$refs.uploader as any).upload(f, this.folder); - } - }, - - deleteFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.api('drive/folders/delete', { - folderId: this.folder.id - }).then(folder => { - this.cd(this.folder.parentId); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kmmwchoexgckptowjmjgfsygeltxfeqs - background var(--face) - - > nav - display block - position sticky - position -webkit-sticky - top 0 - z-index 1 - width 100% - padding 10px 12px - overflow auto - white-space nowrap - font-size 0.9em - color var(--text) - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileDriveNavBg) - border-bottom solid 1px rgba(#000, 0.13) - - > p - > a - display inline - margin 0 - padding 0 - text-decoration none !important - color inherit - - &:last-child - font-weight bold - - > [data-icon] - margin-right 4px - - > span - margin 0 8px - opacity 0.5 - - > .browser - &.fetching - opacity 0.5 - - > .info - border-bottom solid 1px var(--faceDivider) - - &:empty - display none - - > p - display block - max-width 500px - margin 0 auto - padding 4px 16px - font-size 10px - color var(--text) - - > .folders - > .folder - border-bottom solid 1px var(--faceDivider) - - > .files - > .file - border-bottom solid 1px var(--faceDivider) - - > .more - display block - width 100% - padding 16px - font-size 16px - color #555 - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background rgba(#000, 0.2) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { - 100% { - transform: rotate(360deg); - } - } - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } - 50% { - transform: scale(1.0); - } - } - - > .file - display none - -</style> diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts deleted file mode 100644 index 4e10d80f92..0000000000 --- a/src/client/app/mobile/views/components/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import note from './note.vue'; -import notes from './notes.vue'; -import mediaVideo from './media-video.vue'; -import notePreview from './note-preview.vue'; -import subNoteContent from './sub-note-content.vue'; -import noteCard from './note-card.vue'; -import noteDetail from './note-detail.vue'; -import notification from './notification.vue'; -import notifications from './notifications.vue'; -import notificationPreview from './notification-preview.vue'; -import userTimeline from './user-timeline.vue'; -import userListTimeline from './user-list-timeline.vue'; -import uiContainer from './ui-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-note', note); -Vue.component('mk-notes', notes); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-note-preview', notePreview); -Vue.component('mk-sub-note-content', subNoteContent); -Vue.component('mk-note-card', noteCard); -Vue.component('mk-note-detail', noteDetail); -Vue.component('mk-notification', notification); -Vue.component('mk-notifications', notifications); -Vue.component('mk-notification-preview', notificationPreview); -Vue.component('mk-user-timeline', userTimeline); -Vue.component('mk-user-list-timeline', userListTimeline); -Vue.component('ui-container', uiContainer); diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue deleted file mode 100644 index 044bb4c106..0000000000 --- a/src/client/app/mobile/views/components/media-video.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else - :href="video.url" - rel="nofollow noopener" - target="_blank" - :style="imageStyle" - :title="video.name" -> - <fa :icon="['far', 'play-circle']"/> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/media-video.vue'), - props: { - video: { - type: Object, - required: true - } - }, - data() { - return { - hide: true - }; - }, - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.thumbnailUrl})` - }; - } - } -}); -</script> - -<style lang="stylus" scoped> -.kkjnbbplepmiyuadieoenjgutgcmtsvu - display flex - justify-content center - align-items center - - font-size 3.5em - overflow hidden - background-position center - background-size cover - width 100% - height 100% - -.icozogqfvdetwohsdglrbswgrejoxbdj - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > b - display block - -</style> diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue deleted file mode 100644 index 2704820318..0000000000 --- a/src/client/app/mobile/views/components/note-card.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="mk-note-card"> - <a :href="note | notePage"> - <header> - <img :src="avator" alt="avatar"/> - <h3><mk-user-name :user="note.user"/></h3> - </header> - <div> - {{ text }} - </div> - <mk-time :time="note.createdAt"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import summary from '../../../../../misc/get-note-summary'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - props: ['note'], - computed: { - text(): string { - return summary(this.note); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.note.user.avatarUrl) - : this.note.user.avatarUrl; - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-card - display inline-block - width 150px - //height 120px - font-size 12px - background var(--face) - border-radius 4px - box-shadow 0 2px 8px rgba(0, 0, 0, 0.2) - - > a - display block - color var(--noteText) - - &:hover - text-decoration none - - > header - > img - position absolute - top 8px - left 8px - width 28px - height 28px - border-radius 6px - - > h3 - display inline-block - overflow hidden - width calc(100% - 45px) - margin 8px 0 0 42px - line-height 28px - white-space nowrap - text-overflow ellipsis - font-size 12px - - > div - padding 2px 8px 8px 8px - height 60px - overflow hidden - white-space normal - - &:after - content "" - display block - position absolute - top 40px - left 0 - width 100% - height 20px - background linear-gradient(to bottom, transparent 0%, var(--face) 100%) - - > .mk-time - display inline-block - padding 8px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue deleted file mode 100644 index 358b827a5c..0000000000 --- a/src/client/app/mobile/views/components/note-detail.vue +++ /dev/null @@ -1,351 +0,0 @@ -<template> -<div class="mk-note-detail" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <button - class="more" - v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" - @click="fetchConversation" - :disabled="conversationFetching" - > - <template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> - <template v-if="conversationFetching"><fa icon="spinner" pulse/></template> - </button> - <div class="conversation"> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - </div> - <div class="reply-to" v-if="appearNote.reply"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note" mini/> - <article> - <header> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div> - <router-link class="name" :to="appearNote.user | userPage"><mk-user-name :user="appearNote.user"/></router-link> - <span class="username"><mk-acct :user="appearNote.user"/></span> - </div> - </header> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files" :raw="true"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="map" v-if="appearNote.geo" ref="map"></div> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote"/> - </div> - </div> - </div> - <router-link class="time" :to="appearNote | notePage"> - <mk-time :time="appearNote.createdAt" mode="detail"/> - </router-link> - <div class="visibility-info"> - <span class="visibility" v-if="appearNote.visibility != 'public'"> - <fa v-if="appearNote.visibility == 'home'" icon="home"/> - <fa v-if="appearNote.visibility == 'followers'" icon="unlock"/> - <fa v-if="appearNote.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span> - </div> - <footer> - <mk-reactions-viewer :note="appearNote"/> - <button @click="reply()" :title="$t('title')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSub from './note.sub.vue'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; -import noteMixin from '../../../common/scripts/note-mixin'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note-detail.vue'), - - components: { - XSub - }, - - mixins: [noteMixin(), noteSubscriber('note')], - - props: { - note: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - - data() { - return { - conversation: [], - conversationFetching: false, - replies: [] - }; - }, - - watch: { - note() { - this.fetchReplies(); - } - }, - - mounted() { - this.fetchReplies(); - }, - - methods: { - fetchReplies() { - if (this.compact) return; - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - }, - - fetchConversation() { - this.conversationFetching = true; - - // Fetch conversation - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversationFetching = false; - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-detail - overflow hidden - width 100% - text-align left - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .fetching - padding 64px 0 - - > .more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color var(--text) - cursor pointer - background var(--subNoteBg) - outline none - border none - border-bottom solid 1px var(--faceDivider) - border-radius 6px 6px 0 0 - box-shadow none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .conversation - > * - border-bottom 1px solid var(--faceDivider) - - > .renote + article - padding-top 8px - - > .reply-to - border-bottom 1px solid var(--faceDivider) - - > article - padding 14px 16px 9px 16px - - @media (min-width 500px) - padding 28px 32px 18px 32px - - > header - display flex - line-height 1.1em - - > .avatar - display block - margin 0 12px 0 0 - width 54px - height 54px - border-radius 8px - - @media (min-width 500px) - width 60px - height 60px - - > div - min-width 0 - - > .name - display inline-block - margin .4em 0 - color var(--noteHeaderName) - font-size 16px - font-weight bold - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color var(--noteHeaderAcct) - - > .body - padding 8px 0 - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color var(--noteText) - - @media (min-width 500px) - font-size 24px - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color var(--text) - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .time - font-size 16px - color var(--noteHeaderInfo) - - > .visibility-info - color var(--noteHeaderInfo) - - > .localOnly - margin-left 4px - - > footer - font-size 1.2em - - > button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .replies - > * - border-top 1px solid var(--faceDivider) - -</style> diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue deleted file mode 100644 index 1dbbddaa62..0000000000 --- a/src/client/app/mobile/views/components/note-preview.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart' && !narrow"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.yohlumlkhizgfkvvscwfcrcggkotpvry - display flex - margin 0 - padding 0 - overflow hidden - font-size 10px - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - > .avatar - - @media (min-width 350px) - margin 0 10px 0 0 - width 44px - height 44px - - @media (min-width 500px) - margin 0 12px 0 0 - width 48px - height 48px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 10px 0 0 - width 40px - height 40px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - -</style> diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue deleted file mode 100644 index b951947f2a..0000000000 --- a/src/client/app/mobile/views/components/note.sub.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - // TODO - truncate: { - type: Boolean, - default: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.zlrxdaqttccpwhpaagdmkawtzklsccam - display flex - padding 16px - font-size 10px - background var(--subNoteBg) - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - - @media (min-width 350px) - margin-right 10px - width 42px - height 42px - - @media (min-width 500px) - margin-right 14px - width 50px - height 50px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 8px 0 0 - width 38px - height 38px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - margin 0 - padding 0 - color var(--subNoteText) - font-size calc(1em + var(--fontSize)) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue deleted file mode 100644 index 01514f05fc..0000000000 --- a/src/client/app/mobile/views/components/note.vue +++ /dev/null @@ -1,302 +0,0 @@ -<template> -<div - class="note" - v-show="appearNote.deletedAt == null && !hideThisNote" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart', mini: narrow }" - v-hotkey="keymap" -> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article class="article"> - <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="true"/> - <div class="body" v-if="appearNote.deletedAt == null"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - <a class="rp" v-if="appearNote.renote != null">RN:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - </div> - <span class="app" v-if="appearNote.app && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span> - </div> - <footer v-if="appearNote.deletedAt == null" class="footer"> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button @click="reply()" class="button"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote" class="button"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="button"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="button" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="button reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button class="button" @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> - </div> - </article> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -import XSub from './note.sub.vue'; -import noteMixin from '../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note.vue'), - components: { - XSub - }, - - mixins: [ - noteMixin({ - mobile: true - }), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - conversation: [], - replies: [] - }; - }, - - created() { - if (this.detail) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - overflow hidden - font-size 13px - border-bottom solid var(--lineWidth) var(--faceDivider) - - &:last-of-type - border-bottom none - - &:not(.mini) - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - > .article - @media (min-width 600px) - padding 32px 32px 22px - - > .avatar - @media (min-width 350px) - width 48px - height 48px - border-radius 6px - - @media (min-width 500px) - margin-right 16px - width 58px - height 58px - border-radius 8px - - > .main - > .header - @media (min-width 500px) - margin-bottom 2px - - > .body - @media (min-width 700px) - font-size 1.1em - - &.smart - > .article - > .main - > header - align-items center - margin-bottom 4px - - > .renote + .article - padding-top 8px - - > .article - display flex - padding 16px 16px 9px - - > .avatar - flex-shrink 0 - display block - margin 0 10px 8px 0 - width 42px - height 42px - border-radius 6px - //position -webkit-sticky - //position sticky - //top 62px - - > .main - flex 1 - min-width 0 - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - font-size calc(1em + var(--fontSize)) - - > .reply - margin-right 8px - color var(--noteText) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed var(--lineWidth) var(--quoteBorder) - border-radius 8px - - > .app - font-size 12px - color #ccc - - > .footer - > .button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .deleted - color var(--noteText) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue deleted file mode 100644 index 1a0cd5cc24..0000000000 --- a/src/client/app/mobile/views/components/notes.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> - <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id"/> - <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - - onQueueChanged: (self, x) => { - if (x.length > 0) { - self.$store.commit('indicate', true); - } else { - self.$store.commit('indicate', false); - } - }, - - onPrepend: (self, note) => { - // 弾く - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - }, - - onInited: (self) => { - self.$emit('loaded'); - } - }), - ], - - props: { - pagination: { - required: true - }, - }, - - computed: { - _notes(): any[] { - return (this.items as any).map(item => { - const date = new Date(item.createdAt).getDate(); - const month = new Date(item.createdAt).getMonth() + 1; - item._date = date; - item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return item; - }); - } - }, -}); -</script> - -<style lang="stylus" scoped> -.ivaojijs - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .empty - padding 16px - text-align center - color var(--text) - - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .placeholder - padding 16px - opacity 0.3 - - @media (min-width 500px) - padding 32px - - > .empty - margin 0 auto - padding 32px - max-width 400px - text-align center - color var(--text) - - > footer - text-align center - border-top solid var(--lineWidth) var(--faceDivider) - - &:empty - display none - - > button - margin 0 - padding 16px - width 100% - color var(--text) - - @media (min-width 500px) - padding 20px - - &:disabled - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue deleted file mode 100644 index 8422c73420..0000000000 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> -<div class="mk-notification-preview" :class="notification.type"> - <template v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="retweet"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note.renote) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="quote-left"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-plus"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-clock"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="reply"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="at"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notification-preview - margin 0 - padding 8px - color #fff - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - [data-icon], mk-reaction-icon - margin-right 4px - - .note-ref - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.renote, &.quote - .text p [data-icon] - color #77B255 - - &.follow - .text p [data-icon] - color #53c7ce - - &.receiveFollowRequest - .text p [data-icon] - color #888 - - &.reply, &.mention - .text p [data-icon] - color #fff - -</style> - diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue deleted file mode 100644 index 2defef4777..0000000000 --- a/src/client/app/mobile/views/components/notification.vue +++ /dev/null @@ -1,199 +0,0 @@ -<template> -<div class="mk-notification"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="retweet"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-plus"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-clock"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification pollVote" v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="chart-pie"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-notification - - &.wide - > .notification - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - @media (min-width 500px) - width 42px - height 42px - - > div - @media (min-width 500px) - width calc(100% - 42px) - - > .notification - padding 16px - font-size 12px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > div - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - [data-icon], .mk-reaction-icon - margin-right 4px - - > .name - text-overflow ellipsis - white-space nowrap - min-width 0 - overflow hidden - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - > .note-preview - color var(--noteText) - - > .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - > div > header - align-items normal - - &.renote - > div > header [data-icon] - color #77B255 - - &.follow - > div > header [data-icon] - color #53c7ce - - &.receiveFollowRequest - > div > header [data-icon] - color #888 - -</style> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue deleted file mode 100644 index ca6a8beca3..0000000000 --- a/src/client/app/mobile/views/components/notifications.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> - <template v-for="(notification, i) in _notifications"> - <mk-notification :notification="notification" :key="notification.id" :class="{ wide: wide }"/> - <p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - {{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/notifications.vue'), - - mixins: [ - paging({ - beforeInit: (self) => { - self.$emit('beforeInit'); - }, - onInited: (self) => { - self.$emit('inited'); - } - }), - ], - - props: { - type: { - type: String, - required: false - }, - wide: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'i/notifications', - limit: 15, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .notifications - - > .mk-notification:not(:last-child) - border-bottom solid var(--lineWidth) var(--faceDivider) - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 32px - opacity 0.3 - -</style> diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue deleted file mode 100644 index c6e1df0fde..0000000000 --- a/src/client/app/mobile/views/components/notify.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<div class="mk-notify" :class="pos"> - <div> - <mk-notification-preview :notification="notification"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: ['notification'], - computed: { - pos() { - return this.$store.state.device.mobileNotificationPosition; - } - }, - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - [this.pos]: '0px', - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$el, - [this.pos]: `-${this.$el.offsetHeight}px`, - duration: 500, - easing: 'easeOutQuad', - complete: () => this.destroyDom() - }); - }, 6000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notify - $height = 78px - - position fixed - z-index 10000 - left 0 - right 0 - width 100% - max-width 500px - height $height - margin 0 auto - padding 8px - pointer-events none - font-size 80% - - &.bottom - bottom -($height) - - &.top - top -($height) - - > div - height 100% - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) - border-radius 7px - overflow hidden - -</style> diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue deleted file mode 100644 index 4ae79dbd7b..0000000000 --- a/src/client/app/mobile/views/components/post-form-dialog.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> -<ui-modal - ref="modal" - :close-on-bg-click="false" - :close-anime-duration="300" - @before-close="onBeforeClose"> - <div class="main" ref="main"> - <x-post-form ref="form" - :reply="reply" - :renote="renote" - :mention="mention" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - @posted="onPosted" - @cancel="onCanceled"/> - </div> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import XPostForm from './post-form.vue'; - -export default Vue.extend({ - components: { - XPostForm - }, - - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - instant: { - type: Boolean, - required: false, - default: false - } - }, - - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$refs.main, - opacity: 1, - translateY: [-16, 0], - duration: 300, - easing: 'easeOutQuad' - }); - }); - }, - - methods: { - focus() { - this.$refs.form.focus(); - }, - - onBeforeClose() { - (this.$refs.main as any).style.pointerEvents = 'none'; - - anime({ - targets: this.$refs.main, - opacity: 0, - translateY: 16, - duration: 300, - easing: 'easeOutQuad' - }); - }, - - close() { - (this.$refs.modal as any).close(); - }, - - onPosted() { - this.$emit('posted'); - this.close(); - }, - - onCanceled() { - this.$emit('cancel'); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> - -.main - display block - position fixed - z-index 10000 - top 0 - left 0 - right 0 - height 100% - overflow auto - margin 0 auto 0 auto - opacity 0 - transform translateY(-16px) - -</style> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue deleted file mode 100644 index 38c6a42dd5..0000000000 --- a/src/client/app/mobile/views/components/post-form.vue +++ /dev/null @@ -1,244 +0,0 @@ -<template> -<div class="gafaadew"> - <div class="form" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" - > - <header> - <button class="cancel" @click="cancel"><fa icon="times"/></button> - <div> - <span class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</span> - <span class="geo" v-if="geo"><fa icon="map-marker-alt"/></span> - <button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> - </div> - </header> - <div class="form"> - <mk-note-preview class="preview" v-if="reply" :note="reply"/> - <mk-note-preview class="preview" v-if="renote" :note="renote"/> - <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }} - <div class="visibleUsers"> - <span v-for="u in visibleUsers"> - <mk-user-name :user="u"/> - <button @click="removeVisibleUser(u)"><fa icon="times"/></button> - </span> - <button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button> - </div> - </div> - <div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div> - <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }"> - <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @paste="onPaste"></textarea> - <x-post-form-attaches class="attaches" :files="files"/> - <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <footer> - <button class="upload" @click="chooseFile"><fa icon="upload"/></button> - <button class="drive" @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button class="kao" @click="kao"><fa :icon="['far', 'smile']"/></button> - <button class="poll" @click="poll = true"><fa icon="chart-pie"/></button> - <button class="poll" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> - <button class="geo" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> - <button class="visibility" @click="setVisibility" ref="visibilityButton"> - <span v-if="visibility === 'public'"><fa icon="globe"/></span> - <span v-if="visibility === 'home'"><fa icon="home"/></span> - <span v-if="visibility === 'followers'"><fa icon="unlock"/></span> - <span v-if="visibility === 'specified'"><fa icon="envelope"/></span> - </button> - </footer> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/> - </div> - </div> - <div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags"> - <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import form from '../../../common/scripts/post-form'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - form({ - mobile: true - }), - ], - - methods: { - cancel() { - this.$emit('cancel'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.gafaadew - max-width 500px - width calc(100% - 16px) - margin 8px auto - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > .form - box-shadow 0 8px 32px rgba(#000, 0.1) - - @media (min-width 600px) - margin 32px auto - - > .form - background var(--face) - border-radius 8px - box-shadow 0 0 2px rgba(#000, 0.1) - - > header - z-index 1000 - height 50px - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - > .cancel - padding 0 - width 50px - line-height 50px - font-size 24px - color var(--text) - - > div - position absolute - top 0 - right 0 - color var(--text) - - > .text-count - line-height 50px - - > .geo - margin 0 8px - line-height 50px - - > .submit - margin 8px - padding 0 16px - line-height 34px - vertical-align bottom - color var(--primaryForeground) - background var(--primary) - border-radius 4px - - &:disabled - opacity 0.7 - - > .form - max-width 500px - margin 0 auto - - > .preview - padding 16px - - > .with-quote - margin 0 0 8px 0 - color var(--primary) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .to-specified - margin 0 0 8px 0 - color var(--primary) - - > .visibleUsers - display inline - top -1px - font-size 14px - - > span - margin-left 14px - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .local-only - margin 0 0 8px 0 - color var(--primary) - - > input - z-index 1 - - > input - > textarea - display block - padding 12px - margin 0 - width 100% - font-size 16px - color var(--inputText) - background var(--mobilePostFormTextareaBg) - border none - border-radius 0 - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - &:disabled - opacity 0.5 - - > textarea - max-width 100% - min-width 100% - min-height 80px - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - - > .file - display none - - > footer - white-space nowrap - overflow auto - -webkit-overflow-scrolling touch - overflow-scrolling touch - - > * - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color var(--mobilePostFormButton) - background transparent - outline none - border none - border-radius 0 - box-shadow none - - > .hashtags - margin 8px - - > * - margin-right 8px - -</style> diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue deleted file mode 100644 index 66dbb90ebb..0000000000 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="mk-sub-note-content"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <a class="reply" v-if="note.replyId"><fa icon="reply"/></a> - <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> - <a class="rp" v-if="note.renoteId">RN: ...</a> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ $t('media-count').replace('{}', note.files.length) }})</summary> - <mk-media-list :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $t('poll') }}</summary> - <mk-poll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/sub-note-content.vue'), - props: ['note'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-note-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - mk-poll - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/ui-container.vue b/src/client/app/mobile/views/components/ui-container.vue deleted file mode 100644 index 08af7035f9..0000000000 --- a/src/client/app/mobile/views/components/ui-container.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div class="ukygtjoj" :class="{ naked, inNakedDeckColumn, hideHeader: !showHeader, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header v-if="showHeader" @click="() => showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - <button v-if="bodyTogglable"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </header> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - }, - bodyTogglable: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - inject: { - inNakedDeckColumn: { - default: false - } - }, - data() { - return { - showBody: this.expanded - }; - }, - methods: { - toggleContent(show: boolean) { - if (!this.bodyTogglable) return; - this.showBody = show; - } - } -}); -</script> - -<style lang="stylus" scoped> -.ukygtjoj - overflow hidden - - &:not(.inNakedDeckColumn) - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - & + .ukygtjoj - margin-top 16px - - @media (max-width 500px) - margin-top 8px - - &.naked - background transparent !important - box-shadow none !important - - > header - > .title - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color var(--faceHeaderText) - background var(--faceHeader) - - > [data-icon] - 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 var(--faceTextButton) - - > div - color var(--text) - - &.inNakedDeckColumn - background var(--face) - - > header - margin 0 - padding 8px 16px - font-size 12px - color var(--text) - background var(--deckColumnBg) - - > button - position absolute - top 0 - right 8px - padding 8px 6px - font-size 14px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue deleted file mode 100644 index f20f64e7ff..0000000000 --- a/src/client/app/mobile/views/components/ui.header.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }"> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="content" ref="mainContainer"> - <button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button> - <i v-if="$parent.indicate" class="circle"><fa icon="circle"/></i> - <h1> - <slot>{{ $root.instanceName }}</slot> - </h1> - <slot name="func"></slot> - </div> - </div> - <div class="indicator" v-show="$store.state.indicate"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { env } from '../../../config'; - -export default Vue.extend({ - i18n: i18n(), - props: ['func'], - - data() { - return { - env: env - }; - }, - - mounted() { - this.$store.commit('setUiHeaderHeight', 48); - }, -}); -</script> - -<style lang="stylus" scoped> -.header - $height = 48px - - position fixed - top 0 - left -8px - z-index 1024 - width calc(100% + 16px) - padding 0 8px - - &.shadow - box-shadow 0 0 8px rgba(0, 0, 0, 0.25) - - &, * - user-select none - - > .indicator - height 3px - background var(--primary) - - > .warn - display block - margin 0 - padding 4px - text-align center - font-size 12px - background #f00 - color #fff - - > .main - color var(--mobileHeaderFg) - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height $height - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileHeaderBg) - - > .content - z-index 1001 - - > h1 - display block - margin 0 auto - padding 0 - width 100% - max-width calc(100% - 112px) - text-align center - font-size 1.1em - font-weight normal - line-height $height - white-space nowrap - overflow hidden - text-overflow ellipsis - - > img - display inline-block - vertical-align bottom - width ($height - 16px) - height ($height - 16px) - margin 8px - border-radius 6px - - > .nav - display block - position absolute - top 0 - left 0 - padding 0 - width $height - font-size 1.4em - line-height $height - border-right solid 1px rgba(#000, 0.1) - - > [data-icon] - transition all 0.2s ease - - > i.circle - position absolute - top 8px - left 8px - pointer-events none - font-size 10px - color var(--notificationIndicator) - - > button:last-child - display block - position absolute - top 0 - right 0 - padding 0 - width $height - text-align center - font-size 1.4em - color inherit - line-height $height - border-left solid 1px rgba(#000, 0.1) - -</style> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue deleted file mode 100644 index db250ec6f8..0000000000 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ /dev/null @@ -1,346 +0,0 @@ -<template> -<div class="fquwcbxs"> - <transition name="back"> - <div class="backdrop" - v-if="isOpen" - @click="$parent.isDrawerOpening = false" - @touchstart="$parent.isDrawerOpening = false" - ></div> - </transition> - <transition name="nav"> - <div class="body" :class="{ notifications: showNotifications }" v-if="isOpen"> - <div class="nav" v-show="!showNotifications"> - <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </router-link> - <div class="links"> - <ul> - <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.state.device.enableMobileQuickNotificationView"><p @click="showNotifications = true"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> - <li v-else><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/groups" :data-active="$route.name == 'user-groups'"><i><fa :icon="faUsers" fixed-width/></i>{{ $t('user-groups') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> - <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('@.settings') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> - </ul> - <ul> - <li @click="toggleDeckMode"><p><i><fa :icon="$store.state.device.inDeckMode ? faHome : faColumns" fixed-width/></i><span>{{ $store.state.device.inDeckMode ? $t('@.home') : $t('@.deck') }}</span></p></li> - <li @click="dark"><p><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li> - </ul> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span v-html="announcement.title" class="title"></span> - <div><mfm :text="announcement.text"/></div> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a> - </div> - <div class="notifications" v-if="showNotifications"> - <header> - <button @click="showNotifications = false"><fa icon="times"/></button> - <i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i> - </header> - <mk-notifications/> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { lang } from '../../../config'; -import { faNewspaper, faHashtag, faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun, faStickyNote, faBell } from '@fortawesome/free-regular-svg-icons'; -import { search } from '../../../common/scripts/search'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/ui.nav.vue'), - - props: ['isOpen'], - - provide: { - narrow: true - }, - - data() { - return { - hasGameInvitation: false, - connection: null, - aboutUrl: `/docs/${lang}/about`, - announcements: [], - searching: false, - showNotifications: false, - faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote, faUsers, faBell, - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - - watch: { - isOpen() { - this.showNotifications = false; - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - }); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - search() { - if (this.searching) return; - - this.$root.dialog({ - title: this.$t('search'), - input: true - }).then(async ({ canceled, result: query }) => { - if (canceled) return; - - this.searching = true; - search(this, query).finally(() => { - this.searching = false; - }); - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - - toggleDeckMode() { - this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); - location.replace('/'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.fquwcbxs - $color = var(--text) - - .backdrop - position fixed - top 0 - left 0 - z-index 1025 - width 100% - height 100% - background var(--mobileNavBackdrop) - - .body - position fixed - top 0 - left 0 - z-index 1026 - width 240px - height 100% - overflow auto - -webkit-overflow-scrolling touch - background var(--secondary) - font-size 15px - - &.notifications - width 330px - - > .notifications - padding-top 42px - - > header - position fixed - top 0 - left 0 - z-index 1000 - width 330px - line-height 42px - background var(--secondary) - - > button - display block - padding 0 14px - font-size 20px - line-height 42px - color var(--text) - - > i - position absolute - top 0 - right 16px - font-size 12px - color var(--notificationIndicator) - - > .nav - - > .me - display block - margin 0 - padding 16px - - .avatar - display inline - max-width 64px - border-radius 32px - vertical-align middle - - .name - display block - margin 0 16px - position absolute - top 0 - left 80px - padding 0 - width calc(100% - 112px) - color $color - line-height 96px - overflow hidden - text-overflow ellipsis - white-space nowrap - - ul - display block - margin 16px 0 - padding 0 - list-style none - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > li - display block - font-size 1em - line-height 1em - - a, p - display block - margin 0 - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color $color - text-decoration none - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - > i:last-child - color var(--primaryForeground) - - > i:first-child - margin-right 0.5em - width 20px - text-align center - - > i.circle - margin-left 6px - font-size 10px - color var(--notificationIndicator) - - > i:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color $color - opacity 0.5 - - .announcements - > article - background var(--mobileAnnouncement) - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - .about - margin 0 0 8px 0 - padding 1em 0 - text-align center - font-size 0.8em - color $color - opacity 0.5 - -.nav-enter-active, -.nav-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.nav-enter, -.nav-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.back-enter-active, -.back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.back-enter, -.back-leave-active { - opacity: 0; -} - -</style> diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue deleted file mode 100644 index 05c886a497..0000000000 --- a/src/client/app/mobile/views/components/ui.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<div class="mk-ui" :class="{ deck: $store.state.device.inDeckMode }"> - <x-header v-if="!$store.state.device.inDeckMode"> - <template #func><slot name="func"></slot></template> - <slot name="header"></slot> - </x-header> - <x-nav :is-open="isDrawerOpening"/> - <div class="content"> - <slot></slot> - </div> - <mk-stream-indicator v-if="$store.getters.isSignedIn"/> - <button class="nav button" v-if="$store.state.device.inDeckMode" @click="isDrawerOpening = !isDrawerOpening"><fa icon="bars"/><i v-if="indicate"><fa icon="circle"/></i></button> - <button class="post button" v-if="$store.state.device.inDeckMode" @click="$post()"><fa icon="pencil-alt"/></button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkNotify from './notify.vue'; -import XHeader from './ui.header.vue'; -import XNav from './ui.nav.vue'; - -export default Vue.extend({ - components: { - XHeader, - XNav - }, - - props: ['title'], - - data() { - return { - hasGameInvitation: false, - isDrawerOpening: false, - connection: null - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - }, - - indicate(): boolean { - return this.hasUnreadNotification || this.hasUnreadMessagingMessage || this.hasGameInvitation; - } - }, - - watch: { - '$store.state.uiHeaderHeight'() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - } - }, - - mounted() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('notification', this.onNotification); - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.$root.new(MkNotify, { - notification - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - &:not(.deck) - padding-top 48px - - > .button - position fixed - z-index 1000 - bottom 28px - padding 0 - width 64px - height 64px - border-radius 100% - box-shadow 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12) - - > * - font-size 24px - - &.nav - left 28px - background var(--secondary) - color var(--text) - - > i - position absolute - top 0 - left 0 - color var(--notificationIndicator) - font-size 16px - animation blink 1s infinite - - &.post - right 28px - background var(--primary) - color var(--primaryForeground) - -</style> diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue deleted file mode 100644 index d9aa1dad8a..0000000000 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['list'], - - data() { - return { - connection: null, - date: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - watch: { - $route: 'init' - }, - - mounted() { - this.init(); - - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - init() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onUserAdded() { - (this.$refs.timeline as any).reload(); - }, - - onUserRemoved() { - (this.$refs.timeline as any).reload(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue deleted file mode 100644 index 3b6baa76be..0000000000 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/user-timeline.vue'), - - props: ['user', 'withMedia'], - - data() { - return { - date: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - withFiles: this.withMedia, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - }) - } - }; - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - methods: { - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts deleted file mode 100644 index 324e07596d..0000000000 --- a/src/client/app/mobile/views/directives/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; - -import userPreview from './user-preview'; - -Vue.directive('userPreview', userPreview); -Vue.directive('user-preview', userPreview); diff --git a/src/client/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts deleted file mode 100644 index 1a54abc20d..0000000000 --- a/src/client/app/mobile/views/directives/user-preview.ts +++ /dev/null @@ -1,2 +0,0 @@ -// nope -export default {}; diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue deleted file mode 100644 index 05163c6ed9..0000000000 --- a/src/client/app/mobile/views/pages/drive.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="folder"><span style="margin-right:4px;"><fa :icon="['far', 'folder-open']"/></span>{{ folder.name }}</template> - <template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template> - <template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template> - </template> - <template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template> - <x-drive - ref="browser" - :init-folder="initFolder" - :init-file="initFile" - :is-naked="true" - :top="$store.state.uiHeaderHeight" - @begin-fetch="Progress.start()" - @fetched-mid="Progress.set(0.5)" - @fetched="Progress.done()" - @move-root="onMoveRoot" - @open-folder="onOpenFolder" - @open-file="onOpenFile" - /> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XMenu from '../../../common/views/components/menu.vue'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/drive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - Progress, - folder: null, - file: null, - initFolder: null, - initFile: null - }; - }, - created() { - this.initFolder = this.$route.params.folder; - this.initFile = this.$route.params.file; - - window.addEventListener('popstate', this.onPopState); - }, - mounted() { - document.title = `${this.$root.instanceName} Drive`; - }, - beforeDestroy() { - window.removeEventListener('popstate', this.onPopState); - }, - methods: { - onPopState() { - if (this.$route.params.folder) { - (this.$refs as any).browser.cd(this.$route.params.folder, true); - } else if (this.$route.params.file) { - (this.$refs as any).browser.cf(this.$route.params.file, true); - } else { - (this.$refs as any).browser.goRoot(true); - } - }, - onMoveRoot(silent) { - const title = `${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive'); - } - - document.title = title; - - this.file = null; - this.folder = null; - }, - onOpenFolder(folder, silent) { - const title = `${folder.name} | ${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, `/i/drive/folder/${folder.id}`); - } - - document.title = title; - - this.file = null; - this.folder = folder; - }, - onOpenFile(file, silent) { - const title = `${file.name} | ${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, `/i/drive/file/${file.id}`); - } - - document.title = title; - - this.file = file; - this.folder = null; - }, - openContextMenu() { - this.$root.new(XMenu, { - items: [{ - type: 'item', - text: this.$t('contextmenu.upload'), - icon: 'upload', - action: this.$refs.browser.selectLocalFile - }, { - type: 'item', - text: this.$t('contextmenu.url-upload'), - icon: faCloudUploadAlt, - action: this.$refs.browser.urlUpload - }, { - type: 'item', - text: this.$t('contextmenu.create-folder'), - icon: ['far', 'folder'], - action: this.$refs.browser.createFolder - }, ...(this.folder ? [{ - type: 'item', - text: this.$t('contextmenu.rename-folder'), - icon: 'i-cursor', - action: this.$refs.browser.renameFolder - }, { - type: 'item', - text: this.$t('contextmenu.move-folder'), - icon: ['far', 'folder-open'], - action: this.$refs.browser.moveFolder - }, { - type: 'item', - text: this.$t('contextmenu.delete-folder'), - icon: faTrashAlt, - action: this.$refs.browser.deleteFolder - }] : [])], - source: this.$refs.contextSource, - }); - } - } -}); -</script> - diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue deleted file mode 100644 index 69b7bdffb4..0000000000 --- a/src/client/app/mobile/views/pages/games/reversi.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="gamepad"/></span>{{ $t('reversi') }}</template> - <x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/games/reversi.vue'), - components: { - XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} | ${this.$t('reversi')}`; - }, - methods: { - nav(game, actualNav) { - if (actualNav) { - this.$router.push(`/games/reversi/${game.id}`); - } else { - // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push(`/games/reversi/${game.id}`); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue deleted file mode 100644 index f115458092..0000000000 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ /dev/null @@ -1,143 +0,0 @@ -<template> -<div> - <ui-container v-if="src == 'home' && alone" :show-header="false" style="margin-bottom:8px;"> - <div class="zrzngnxs"> - <p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p> - <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> - </div> - </ui-container> - - <mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.timeline.vue'), - - props: { - src: { - type: String, - required: true - }, - tagTl: { - required: false - } - }, - - data() { - return { - streamManager: null, - connection: null, - unreadCount: 0, - date: null, - baseQuery: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }, - query: {}, - endpoint: null, - pagination: null - }; - }, - - computed: { - alone(): boolean { - return this.$store.state.i.followingCount == 0; - } - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - this.connection.dispose(); - }); - - const prepend = note => { - (this.$refs.timeline as any).prepend(note); - }; - - if (this.src == 'tag') { - this.endpoint = 'notes/search-by-tag'; - this.query = { - query: this.tagTl.query - }; - this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); - this.connection.on('note', prepend); - } else if (this.src == 'home') { - this.endpoint = 'notes/timeline'; - const onChangeFollowing = () => { - this.fetch(); - }; - this.connection = this.$root.stream.useSharedConnection('homeTimeline'); - this.connection.on('note', prepend); - this.connection.on('follow', onChangeFollowing); - this.connection.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - this.endpoint = 'notes/local-timeline'; - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - this.endpoint = 'notes/global-timeline'; - this.connection = this.$root.stream.useSharedConnection('globalTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'mentions') { - this.endpoint = 'notes/mentions'; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', prepend); - } else if (this.src == 'messages') { - this.endpoint = 'notes/mentions'; - this.query = { - visibility: 'specified' - }; - const onNote = note => { - if (note.visibility == 'specified') { - prepend(note); - } - }; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', onNote); - } - - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - ...this.baseQuery, ...this.query - }) - }; - }, - - methods: { - focus() { - (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zrzngnxs - padding 16px - text-align center - font-size 14px - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue deleted file mode 100644 index 0d110bf2ee..0000000000 --- a/src/client/app/mobile/views/pages/home.vue +++ /dev/null @@ -1,249 +0,0 @@ -<template> -<mk-ui> - <template #header> - <span @click="showNav = true"> - <span :class="$style.title"> - <span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span> - <span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span> - <span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span> - <span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> - <span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> - <span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> - <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> - <span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> - </span> - <span style="margin-left:8px"> - <template v-if="!showNav"><fa icon="angle-down"/></template> - <template v-else><fa icon="angle-up"/></template> - </span> - <i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i> - </span> - </template> - - <template #func> - <button @click="fn"><fa icon="pencil-alt"/></button> - </template> - - <main> - <div class="nav" v-if="showNav"> - <div class="bg" @click="showNav = false"></div> - <div class="pointer"></div> - <div class="body"> - <div> - <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> - <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> - <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> - <div class="hr"></div> - <span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span> - <span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> {{ $t('messages') }}<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span> - <template v-if="lists"> - <div class="hr" v-if="lists.length > 0"></div> - <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id"><fa icon="list"/> {{ l.name }}</span> - </template> - <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> - <span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id"><fa icon="hashtag"/> {{ tl.title }}</span> - </div> - </div> - </div> - - <div class="tl"> - <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> - <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> - <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> - <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> - <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> - <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> - <x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> - <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> - </div> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XTl from './home.timeline.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.vue'), - - components: { - XTl - }, - - data() { - return { - src: 'home', - list: null, - lists: null, - tagTl: null, - showNav: false, - enableLocalTimeline: false, - enableGlobalTimeline: false, - }; - }, - - watch: { - src() { - this.showNav = false; - this.saveSrc(); - }, - - list(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.tagTl = null; - }, - - tagTl(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.list = null; - }, - - showNav(v) { - if (v && this.lists === null) { - this.$root.api('users/lists/list').then(lists => { - this.lists = lists; - }); - } - } - }, - - created() { - this.$root.getMeta().then((meta: Record<string, any>) => { - if (!( - this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && this.src === 'global') this.src = 'local'; - if (!( - this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; - }); - - if (this.$store.state.device.tl) { - this.src = this.$store.state.device.tl.src; - if (this.src == 'list') { - this.list = this.$store.state.device.tl.arg; - } else if (this.src == 'tag') { - this.tagTl = this.$store.state.device.tl.arg; - } - } - }, - - mounted() { - document.title = this.$root.instanceName; - - Progress.start(); - - (this.$refs.tl as any).$once('loaded', () => { - Progress.done(); - }); - }, - - methods: { - fn() { - this.$post(); - }, - - saveSrc() { - this.$store.commit('device/setTl', { - src: this.src, - arg: this.src == 'list' ? this.list : this.tagTl - }); - }, - - warp() { - - } - } -}); -</script> - -<style lang="stylus" scoped> -main - > .nav - > .pointer - position fixed - z-index 10002 - top 56px - left 0 - right 0 - - $size = 16px - - &:after - content "" - display block - position absolute - top -($size * 2) - left s('calc(50% - %s)', $size) - border-top solid $size transparent - border-left solid $size transparent - border-right solid $size transparent - border-bottom solid $size var(--popupBg) - - > .bg - position fixed - z-index 10000 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.5) - - > .body - position fixed - z-index 10001 - top 56px - left 0 - right 0 - width 300px - max-height calc(100% - 70px) - margin 0 auto - overflow auto - -webkit-overflow-scrolling touch - background var(--popupBg) - border-radius 8px - box-shadow 0 0 16px rgba(#000, 0.1) - - > div - padding 8px 0 - - > .hr - margin 8px 0 - border-top solid 1px var(--faceDivider) - - > *:not(.hr) - display block - padding 8px 16px - color var(--text) - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - &:not([data-active]):hover - background var(--mobileHomeTlItemHover) - - > .badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - -</style> - -<style lang="stylus" module> -.title - [data-icon] - margin-right 4px - -.badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - vertical-align middle - -</style> diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/app/mobile/views/pages/index.vue deleted file mode 100644 index 5d11fc5423..0000000000 --- a/src/client/app/mobile/views/pages/index.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'"></component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Home from './home.vue'; -import Welcome from './welcome.vue'; - -export default Vue.extend({ - components: { - Home, - Welcome - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue deleted file mode 100644 index 7872847127..0000000000 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template> - <template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template> - <template v-else><mk-ellipsis/></template> - </template> - <x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - data() { - return { - fetching: true, - user: null, - group: null, - unwatchDarkmode: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - const applyBg = v => - document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); - - applyBg(this.$store.state.device.darkmode); - - this.unwatchDarkmode = this.$store.watch(s => { - return s.device.darkmode; - }, applyBg); - - this.fetch(); - }, - beforeDestroy() { - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); // for safari's bug - this.unwatchDarkmode(); - }, - methods: { - fetch() { - this.fetching = true; - if (this.$route.params.user) { - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - } else { - this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { - this.group = group; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + this.group.name; - }); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue deleted file mode 100644 index ff66ae06e6..0000000000 --- a/src/client/app/mobile/views/pages/messaging.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template> - <x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} ${this.$t('@.messaging')}`; - }, - methods: { - navigate(user) { - (this as any).$router.push(`/i/messaging/${getAcct(user)}`); - }, - navigateGroup(group) { - (this as any).$router.push(`/i/messaging/group/${group.id}`); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue deleted file mode 100644 index 090851fc4e..0000000000 --- a/src/client/app/mobile/views/pages/note.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>{{ $t('title') }}</template> - <main v-if="!fetching"> - <div> - <mk-note-detail :note="note" :key="note.id"/> - </div> - <footer> - <router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> {{ $t('prev') }}</router-link> - <router-link v-if="note.next" :to="note.next">{{ $t('next') }} <fa icon="angle-right"/></router-link> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/note.vue'), - data() { - return { - fetching: true, - note: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - text-align center - - > footer - margin-top 16px - - > a - display inline-block - margin 0 16px - -</style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue deleted file mode 100644 index 24f8f79ccc..0000000000 --- a/src/client/app/mobile/views/pages/notifications.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header><fa :icon="faBell"/> {{ $t('notifications') }}</template> - <template #func> - <button @click="filter()"><fa icon="cog"/></button> - </template> - - <main> - <mk-notifications @before-init="beforeInit()" @inited="inited()" :type="type === 'all' ? null : type" :wide="true" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBell } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/notifications.vue'), - data() { - return { - type: 'all', - faBell, - }; - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - beforeInit() { - Progress.start(); - }, - inited() { - Progress.done(); - }, - filter() { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.type, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.type = type; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main > * - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) -</style> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue deleted file mode 100644 index dca1ffd40a..0000000000 --- a/src/client/app/mobile/views/pages/search.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<mk-ui> - <template #header><fa icon="search"/> {{ q }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/search.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - mounted() { - document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`; - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue deleted file mode 100644 index 095c19cf2c..0000000000 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<div class="mk-selectdrive"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="upload" @click="upload"><fa icon="upload"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/selectdrive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = this.$t('title'); - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - close() { - window.close(); - }, - ok() { - window.opener.cb(this.multiple ? this.files : this.files[0]); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-selectdrive - width 100% - height 100% - background #fff - - > header - position fixed - top 0 - left 0 - width 100% - z-index 1000 - background #fff - box-shadow 0 1px rgba(#000, 0.1) - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .upload - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - top 42px - -</style> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue deleted file mode 100644 index c24a56be7b..0000000000 --- a/src/client/app/mobile/views/pages/settings.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template> - <main> - <div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/> - </div> - - <x-settings/> - - <div class="signout" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" @click="signout">{{ $t('@.signout') }}</div> - - <footer> - <small>ver {{ version }} ({{ codename }})</small> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSettings from '../../../common/views/components/settings/settings.vue'; -import { version, codename } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/settings.vue'), - components: { - XSettings, - }, - data() { - return { - version, - codename, - }; - }, - computed: { - name(): string { - return Vue.filter('userName')(this.$store.state.i); - }, - }, - methods: { - signout() { - this.$root.signout(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -main - - > .signed-in-as - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - font-weight bold - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > .signout - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > footer - margin 16px - text-align center - color var(--text) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue deleted file mode 100644 index 81d2741ae5..0000000000 --- a/src/client/app/mobile/views/pages/signup.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> -<div class="signup"> - <h1>{{ $t('lets-start') }}</h1> - <mk-signup/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/signup.vue') -}); -</script> - -<style lang="stylus" scoped> -.signup - padding 32px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px 0 0 0 - font-size 1.5em - font-weight bold - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue deleted file mode 100644 index 19482ec382..0000000000 --- a/src/client/app/mobile/views/pages/tag.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/tag.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search-by-tag', - limit: 20, - params: { - tag: this.$route.params.tag - } - } - }; - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/ui.vue b/src/client/app/mobile/views/pages/ui.vue deleted file mode 100644 index 397ba5df07..0000000000 --- a/src/client/app/mobile/views/pages/ui.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template> - - <main> - <component :is="component" @init="init" v-bind="$attrs"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - component: { - required: true - } - }, - - data() { - return { - title: null, - icon: null, - }; - }, - - mounted() { - }, - - methods: { - init(v) { - this.title = v.title; - this.icon = v.icon; - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue deleted file mode 100644 index 9abe5b893c..0000000000 --- a/src/client/app/mobile/views/pages/user/home.notes.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div class="root notes"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div v-if="!fetching && notes.length > 0"> - <mk-note-card v-for="note in notes" :key="note.id" :note="note"/> - </div> - <p class="empty" v-if="!fetching && notes.length == 0">{{ $t('@.no-notes') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.notes.vue'), - props: ['user'], - data() { - return { - fetching: true, - notes: [] - }; - }, - mounted() { - this.$root.api('users/notes', { - userId: this.user.id, - }).then(notes => { - this.notes = notes; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.notes - - > div - overflow-x scroll - -webkit-overflow-scrolling touch - white-space nowrap - padding 8px - - > * - vertical-align top - - &:not(:last-child) - margin-right 8px - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue deleted file mode 100644 index b5547c916f..0000000000 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<div class="root photos"> - <p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <a v-for="(image, i) in images" :key="i" - class="img" - :style="`background-image: url(${thumbnail(image.file)})`" - :href="image.note | notePage" - ></a> - </div> - <p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.photos.vue'), - props: ['user'], - data() { - return { - fetching: true, - images: [] - }; - }, - mounted() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - if (this.images.length < 9) { - this.images.push({ - note, - file - }); - } - } - } - this.fetching = false; - }); - }, - methods: { - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.root.photos - - > .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 90px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> - diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue deleted file mode 100644 index 316b2a12fe..0000000000 --- a/src/client/app/mobile/views/pages/user/home.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="wojmldye"> - <x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - <mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> - <ui-container :body-togglable="true"> - <template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template> - <div> - <x-notes :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="image"/>{{ $t('images') }}</template> - <div> - <x-photos :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div style="padding:8px;"> - <x-activity :user="user"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XNotes from './home.notes.vue'; -import XPhotos from './home.photos.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.vue'), - components: { - XNotes, - XPhotos, - XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default), - XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default) - }, - props: ['user'], - data() { - return { - makeFrequentlyRepliedUsersPromise: () => this.$root.api('users/get_frequently_replied_users', { - userId: this.user.id - }).then(res => res.map(x => x.user)), - makeFollowersYouKnowPromise: () => this.$root.api('users/followers', { - userId: this.user.id, - iknow: true, - limit: 30 - }).then(res => res.users), - }; - } -}); -</script> - -<style lang="stylus" scoped> -.wojmldye - > .page - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - - > .note - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue deleted file mode 100644 index b8a79a6b34..0000000000 --- a/src/client/app/mobile/views/pages/user/index.vue +++ /dev/null @@ -1,349 +0,0 @@ -<template> -<mk-ui> - <template #header v-if="!fetching"> - <img :src="avator" alt=""><mk-user-name :user="user" :key="user.id"/> - </template> - <div class="wwtwuxyh" v-if="!fetching"> - <div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div> - <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> - <header> - <div class="banner" :style="style"></div> - <div class="body"> - <div class="top"> - <a class="avatar"> - <img :src="avator" alt="avatar"/> - </a> - <button class="menu" ref="menu" @click="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> - </div> - <div class="title"> - <h1><mk-user-name :user="user" :key="user.id" :nowrap="false"/></h1> - <span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span> - <span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> - </div> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/> - <x-integrations :user="user" style="margin:20px 0;"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="info"> - <p class="location" v-if="user.host === null && user.location"> - <fa icon="map-marker"/>{{ user.location }} - </p> - <p class="birthday" v-if="user.host === null && user.birthday"> - <fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) - </p> - </div> - <div class="status"> - <router-link :to="user | userPage()"> - <b>{{ user.notesCount | number }}</b> - <i>{{ $t('notes') }}</i> - </router-link> - <router-link :to="user | userPage('following')"> - <b>{{ user.followingCount | number }}</b> - <i>{{ $t('following') }}</i> - </router-link> - <router-link :to="user | userPage('followers')"> - <b>{{ user.followersCount | number }}</b> - <i>{{ $t('followers') }}</i> - </router-link> - </div> - </div> - </header> - <nav v-if="$route.name == 'user'" :class="{ shadow: $store.state.device.useShadow }"> - <div class="nav-container"> - <a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a> - <a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a> - <a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a> - </div> - </nav> - <main> - <template v-if="$route.name == 'user'"> - <x-home v-if="page == 'home'" :user="user" :key="user.id"/> - <mk-user-timeline v-if="page == 'notes'" :user="user" :key="`tl:${user.id}`"/> - <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" :key="`media:${user.id}`"/> - </template> - <router-view :user="user"></router-view> - </main> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as age from 's-age'; -import parseAcct from '../../../../../../misc/acct/parse'; -import Progress from '../../../../common/scripts/loading'; -import XUserMenu from '../../../../common/views/components/user-menu.vue'; -import XHome from './home.vue'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; -import XIntegrations from '../../../../common/views/components/integrations.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user.vue'), - components: { - XHome, - XIntegrations - }, - data() { - return { - fetching: true, - user: null, - page: this.$route.name == 'user' ? 'home' : null - }; - }, - computed: { - age(): number { - return age(this.user.birthday); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - } - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - Progress.done(); - document.title = `${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - }, - - menu() { - this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wwtwuxyh - $bg = var(--face) - - > .is-suspended - > .is-remote - &.is-suspended - color #570808 - background #ffdbdb - - &.is-remote - color #573c08 - background #fff0db - - > p - margin 0 auto - padding 14px - max-width 600px - font-size 14px - - > a - font-weight bold - - @media (max-width 500px) - padding 12px - font-size 12px - - > header - background $bg - - > .banner - padding-bottom 33.3% - background-color rgba(0, 0, 0, 0.1) - background-size cover - background-position center - - > .body - padding 12px - margin 0 auto - max-width 600px - - > .top - display flex - - > .avatar - display block - width 25% - height 40px - - > img - display block - position absolute - left -2px - bottom -2px - width 100% - background $bg - border 3px solid $bg - border-radius 6px - - @media (min-width 500px) - left -4px - bottom -4px - border 4px solid $bg - border-radius 12px - - > .menu - margin 0 0 0 auto - padding 8px - margin-right 8px - font-size 18px - color var(--text) - - > .title - margin 8px 0 - - > h1 - margin 0 - line-height 22px - font-size 20px - color var(--mobileUserPageName) - - > .username - display inline-block - line-height 20px - font-size 16px - font-weight bold - color var(--mobileUserPageAcct) - - > .followed - margin-left 8px - padding 2px 4px - font-size 12px - color var(--mobileUserPageFollowedFg) - background var(--mobileUserPageFollowedBg) - border-radius 4px - - > .description - margin 8px 0 - color var(--mobileUserPageDescription) - - @media (max-width 450px) - font-size 15px - - > .fields - margin 8px 0 - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - color var(--mobileUserPageStatusHighlight) - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - color var(--mobileUserPageStatusHighlight) - - > .info - margin 8px 0 - - @media (max-width 450px) - font-size 15px - - > p - display inline - margin 0 16px 0 0 - color var(--text) - - > i - margin-right 4px - - > .status - > a - color var(--text) - - &:not(:last-child) - margin-right 16px - - > b - margin-right 4px - font-size 16px - color var(--mobileUserPageStatusHighlight) - - > i - font-size 14px - - > button - color var(--text) - - > nav - position -webkit-sticky - position sticky - top 47px - background-color $bg - z-index 2 - - &.shadow - box-shadow 0 4px 4px var(--mobileUserPageHeaderShadow) - - > .nav-container - display flex - justify-content center - margin 0 auto - max-width 616px - - > a - display block - flex 1 1 - text-align center - line-height 48px - font-size 12px - text-decoration none - color var(--text) - border-bottom solid 2px transparent - - @media (min-width 400px) - line-height 52px - font-size 14px - - &[data-active] - font-weight bold - color var(--primary) - border-color var(--primary) - -</style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue deleted file mode 100644 index 6cf4a36f90..0000000000 --- a/src/client/app/mobile/views/pages/welcome.vue +++ /dev/null @@ -1,310 +0,0 @@ -<template> -<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> - <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> - - <div> - <img svg-inline src="../../../../assets/title.svg" alt="Misskey"> - <p class="host">{{ host }}</p> - <div class="about"> - <h2>{{ name || 'Misskey' }}</h2> - <p v-html="description || this.$t('@.about')"></p> - <router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> - </div> - <div class="signin"> - <a href="/signin" @click.prevent="signin()">{{ $t('@.signin') }}</a> - </div> - <div class="tl"> - <mk-welcome-timeline/> - </div> - <div class="hashtags"> - <mk-tag-cloud/> - </div> - <div class="photos"> - <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> - </div> - <div class="stats" v-if="stats"> - <span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span> - <span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span class="title" v-html="announcement.title"></span> - <mfm :text="announcement.text"/> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <article class="about-misskey"> - <h1>{{ $t('@.intro.title') }}</h1> - <p v-html="this.$t('@.intro.about')"></p> - <section> - <h2>{{ $t('@.intro.features') }}</h2> - <section> - <h3>{{ $t('@.intro.rich-contents') }}</h3> - <div class="image"><img src="/assets/about/post.png" alt=""></div> - <p v-html="this.$t('@.intro.rich-contents-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.reaction') }}</h3> - <div class="image"><img src="/assets/about/reaction.png" alt=""></div> - <p v-html="this.$t('@.intro.reaction-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.ui') }}</h3> - <div class="image"><img src="/assets/about/ui.png" alt=""></div> - <p v-html="this.$t('@.intro.ui-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.drive') }}</h3> - <div class="image"><img src="/assets/about/drive.png" alt=""></div> - <p v-html="this.$t('@.intro.drive-desc')"></p> - </section> - </section> - <p v-html="this.$t('@.intro.outro')"></p> - </article> - <div class="info" v-if="meta"> - <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - </div> - <footer> - <small>{{ copyright }}</small> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { copyright, host } from '../../../config'; -import { concat } from '../../../../../prelude/array'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/welcome.vue'), - data() { - return { - meta: null, - copyright, - stats: null, - banner: null, - host: toUnicode(host), - name: null, - description: '', - photos: [], - announcements: [] - }; - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.name = meta.name; - this.description = meta.description; - this.announcements = meta.announcements; - this.banner = meta.bannerUrl; - }); - - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('notes/local-timeline', { - fileType: image, - excludeNsfw: true, - limit: 6 - }).then((notes: any[]) => { - const files = concat(notes.map((n: any): any[] => n.files)); - this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); - }); - }, - methods: { - signin() { - this.$root.dialog({ - type: 'signin' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wgwfgvvimdjvhjfwxropcwksnzftjqes - text-align center - - > .banner - position absolute - top 0 - left 0 - width 100% - height 300px - background-position center - background-size cover - opacity 0.7 - - &:after - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 100px - background linear-gradient(transparent, var(--bg)) - - > div:not(.banner) - padding 32px - margin 0 auto - max-width 500px - - > svg - display block - width 200px - height 50px - margin 0 auto - - > .host - display block - text-align center - padding 6px 12px - line-height 32px - font-weight bold - color #333 - background rgba(#000, 0.035) - border-radius 6px - - > .about - margin-top 16px - padding 16px - color var(--text) - background var(--face) - border-radius 6px - - > h2 - margin 0 - - > p - margin 8px - - > .signup - font-weight bold - - > .signin - margin 16px 0 - - > .tl - margin 16px 0 - - > * - max-height 300px - border-radius 6px - overflow auto - -webkit-overflow-scrolling touch - - > .hashtags - padding 0 8px - height 200px - - > .photos - display grid - grid-template-rows 1fr 1fr 1fr - grid-template-columns 1fr 1fr - gap 8px - height 300px - margin-top 16px - - > div - border-radius 4px - background-position center center - background-size cover - - > .stats - margin 16px 0 - padding 8px - font-size 14px - color var(--text) - background rgba(#000, 0.1) - border-radius 6px - - > * - margin 0 8px - - > .announcements - margin 16px 0 - - > article - background var(--mobileAnnouncement) - border-radius 6px - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - > .about-misskey - margin 16px 0 - padding 32px - font-size 14px - background var(--face) - border-radius 6px - overflow hidden - color var(--text) - - > h1 - margin 0 - - & + p - margin-top 8px - - > p:last-child - margin-bottom 0 - - > section - > h2 - border-bottom 1px solid var(--faceDivider) - - > section - margin-bottom 16px - padding-bottom 16px - border-bottom 1px solid var(--faceDivider) - - > h3 - margin-bottom 8px - - > p - margin-bottom 0 - - > .image - > img - display block - width 100% - height 120px - object-fit cover - - > .info - padding 16px 0 - border solid 2px rgba(0, 0, 0, 0.1) - border-radius 8px - color var(--text) - - > * - margin 0 16px - - > footer - text-align center - color var(--text) - - > small - display block - margin 16px 0 0 0 - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue deleted file mode 100644 index 19df613b3a..0000000000 --- a/src/client/app/mobile/views/pages/widgets.vue +++ /dev/null @@ -1,192 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="home"/></span>{{ $t('dashboard') }}</template> - <template #func> - <button @click="customizing = !customizing"><fa icon="cog"/></button> - </template> - <main> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - <button @click="addWidget">{{ $t('add-widget') }}</button> - <p><a @click="hint">{{ $t('customization-tips') }}</a></p> - </header> - <x-draggable - :list="widgets" - handle=".handle" - animation="150" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle"><fa icon="bars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/> - </div> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/> - </template> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/widgets.vue'), - components: { - XDraggable - }, - - data() { - return { - showNav: false, - customizing: false, - widgetAdderSelected: null - }; - }, - - computed: { - widgets(): any[] { - return this.$store.getters.mobileHome || []; - } - }, - - created() { - if (this.widgets.length == 0) { - this.$store.commit('setMobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - } - }, - - mounted() { - document.title = this.$root.instanceName; - }, - - methods: { - hint() { - this.$root.dialog({ - type: 'info', - text: this.$t('widgets-hints') - }); - }, - - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveHome(); - }, - - addWidget() { - if(this.widgetAdderSelected == null) return; - - this.$store.commit('addMobileHomeWidget', { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }); - }, - - removeWidget(widget) { - this.$store.commit('removeMobileHomeWidget', widget); - }, - - saveHome() { - this.$store.commit('setMobileHome', this.widgets); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - margin 0 auto - padding 8px - max-width 500px - width 100% - - @media (min-width 500px) - padding 16px 8px - - @media (min-width 600px) - padding 32px 8px - - > header - padding 8px - background #fff - - .widget - margin-bottom 8px - - @media (min-width 600px) - margin-bottom 16px - - .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> diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue deleted file mode 100644 index 047784deac..0000000000 --- a/src/client/app/mobile/views/widgets/activity.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<div class="mkw-activity"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div :class="$style.body"> - <x-activity :user="$store.state.i"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'activity', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n(), - components: { - XActivity: () => import('../../../common/views/components/activity.vue').then(m => m.default) - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.body - padding 8px -</style> diff --git a/src/client/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts deleted file mode 100644 index 4de912b64c..0000000000 --- a/src/client/app/mobile/views/widgets/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Vue from 'vue'; - -import wActivity from './activity.vue'; -import wProfile from './profile.vue'; - -Vue.component('mkw-activity', wActivity); -Vue.component('mkw-profile', wProfile); diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue deleted file mode 100644 index d4ccc87e57..0000000000 --- a/src/client/app/mobile/views/widgets/profile.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div class="mkw-profile"> - <ui-container> - <div :class="$style.banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" - ></div> - <img :class="$style.avatar" - :src="$store.state.i.avatarUrl" - alt="avatar" - /> - <router-link :class="$style.name" :to="$store.state.i | userPage"> - <mk-user-name :user="$store.state.i"/> - </router-link> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; - -export default define({ - name: 'profile' -}); -</script> - -<style lang="stylus" module> -.banner - height 100px - background-color #f5f5f5 - background-size cover - background-position center - cursor pointer - -.banner:before - content "" - display block - width 100% - height 100% - background rgba(#000, 0.5) - -.avatar - display block - position absolute - width 58px - height 58px - margin 0 - vertical-align bottom - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(#000, 0.5) - -.name - display block - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - font-weight bold - text-shadow 0 0 8px rgba(#000, 0.5) - -</style> diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl deleted file mode 100644 index 614f29a835..0000000000 --- a/src/client/app/reset.styl +++ /dev/null @@ -1,37 +0,0 @@ -input - min-width 0 - -input:not([type]) -input[type='text'] -input[type='password'] -input[type='search'] -input[type='email'] -textarea -button -progress - -webkit-appearance none - -moz-appearance none - appearance none - box-shadow none - -textarea - font-family Roboto, HelveticaNeue, Arial, sans-serif - -button - margin 0 - background transparent - border none - cursor pointer - color inherit - touch-action manipulation - - * - pointer-events none - user-select none - - &[disabled] - cursor default - -pre - overflow auto - white-space pre diff --git a/src/client/app/safe.js b/src/client/app/safe.js deleted file mode 100644 index 88c603f6b9..0000000000 --- a/src/client/app/safe.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * ブラウザの検証 - */ - -// Detect an old browser -if (!('fetch' in window)) { - alert( - 'お使いのブラウザ(またはOS)のバージョンが旧式のため、Misskeyを動作させることができません。' + - 'バージョンを最新のものに更新するか、別のブラウザをお試しください。' + - '\n\n' + - 'Your browser (or your OS) seems outdated. ' + - 'To run Misskey, please update your browser to latest version or try other browsers.'); -} diff --git a/src/client/app/store.ts b/src/client/app/store.ts deleted file mode 100644 index fd3aceb728..0000000000 --- a/src/client/app/store.ts +++ /dev/null @@ -1,463 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import createPersistedState from 'vuex-persistedstate'; -import * as nestedProperty from 'nested-property'; - -import MiOS from './mios'; -import { erase } from '../../prelude/array'; -import getNoteSummary from '../../misc/get-note-summary'; - -const defaultSettings = { - keepCw: false, - tagTimelines: [], - fetchOnScroll: true, - remainDeletedNote: false, - showPostFormOnTopOfTl: false, - suggestRecentHashtags: true, - showClockOnHeader: true, - circleIcons: true, - contrastedAcct: true, - showFullAcct: false, - showVia: true, - showReplyTarget: true, - showMyRenotes: true, - showRenotedMyNotes: true, - showLocalRenotes: true, - loadRemoteMedia: true, - disableViaMobile: false, - memo: null, - iLikeSushi: false, - rememberNoteVisibility: false, - defaultNoteVisibility: 'public', - wallpaper: null, - webSearchEngine: 'https://www.google.com/?#q={{query}}', - mutedWords: [], - gamesReversiShowBoardLabels: false, - gamesReversiUseAvatarStones: true, - disableAnimatedMfm: false, - homeProfiles: {}, - mobileHomeProfiles: {}, - deckProfiles: {}, - uploadFolder: null, - pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]', - pasteDialog: false, - reactions: ['like', 'love', 'laugh', 'hmm', 'surprise', 'congrats', 'angry', 'confused', 'rip', 'pudding'] -}; - -const defaultDeviceSettings = { - homeProfile: 'Default', - mobileHomeProfile: 'Default', - deckProfile: 'Default', - deckMode: false, - deckColumnAlign: 'center', - deckColumnWidth: 'normal', - useShadow: false, - roundedCorners: true, - reduceMotion: false, - darkmode: true, - darkTheme: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2', - lightTheme: 'light', - lineWidth: 1, - fontSize: 0, - themes: [], - enableSounds: true, - soundVolume: 0.5, - mediaVolume: 0.5, - lang: null, - appTypeForce: 'auto', - debug: false, - lightmode: false, - loadRawImages: false, - alwaysShowNsfw: false, - postStyle: 'standard', - navbar: 'top', - mobileNotificationPosition: 'bottom', - useOsDefaultEmojis: false, - disableShowingAnimatedImages: false, - expandUsersPhotos: true, - expandUsersActivity: true, - enableMobileQuickNotificationView: false, - roomGraphicsQuality: 'medium', - roomUseOrthographicCamera: true, - activeEmojiCategoryName: undefined, - recentEmojis: [], -}; - -export default (os: MiOS) => new Vuex.Store({ - plugins: [createPersistedState({ - paths: ['i', 'device', 'settings'] - })], - - state: { - i: null, - indicate: false, - uiHeaderHeight: 0, - behindNotes: [] - }, - - getters: { - isSignedIn: state => state.i != null, - - home: state => state.settings.homeProfiles[state.device.homeProfile], - - mobileHome: state => state.settings.mobileHomeProfiles[state.device.mobileHomeProfile], - - deck: state => state.settings.deckProfiles[state.device.deckProfile], - }, - - mutations: { - updateI(state, x) { - state.i = x; - }, - - updateIKeyValue(state, x) { - state.i[x.key] = x.value; - }, - - indicate(state, x) { - state.indicate = x; - }, - - setUiHeaderHeight(state, height) { - state.uiHeaderHeight = height; - }, - - pushBehindNote(state, note) { - if (note.userId === state.i.id) return; - if (state.behindNotes.some(n => n.id === note.id)) return; - state.behindNotes.push(note); - document.title = `(${state.behindNotes.length}) ${getNoteSummary(note)}`; - }, - - clearBehindNotes(state) { - state.behindNotes = []; - document.title = os.instanceName; - }, - - setHome(state, data) { - Vue.set(state.settings.homeProfiles, state.device.homeProfile, data); - os.store.dispatch('settings/updateHomeProfile'); - }, - - setDeck(state, data) { - Vue.set(state.settings.deckProfiles, state.device.deckProfile, data); - os.store.dispatch('settings/updateDeckProfile'); - }, - - addHomeWidget(state, widget) { - state.settings.homeProfiles[state.device.homeProfile].unshift(widget); - os.store.dispatch('settings/updateHomeProfile'); - }, - - setMobileHome(state, data) { - Vue.set(state.settings.mobileHomeProfiles, state.device.mobileHomeProfile, data); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - updateWidget(state, x) { - let w; - - //#region Desktop home - const home = state.settings.homeProfiles[state.device.homeProfile]; - if (home) { - w = home.find(w => w.id == x.id); - if (w) { - w.data = x.data; - os.store.dispatch('settings/updateHomeProfile'); - } - } - //#endregion - - //#region Mobile home - const mobileHome = state.settings.mobileHomeProfiles[state.device.mobileHomeProfile]; - if (mobileHome) { - w = mobileHome.find(w => w.id == x.id); - if (w) { - w.data = x.data; - os.store.dispatch('settings/updateMobileHomeProfile'); - } - } - //#endregion - }, - - addMobileHomeWidget(state, widget) { - state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].unshift(widget); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - removeMobileHomeWidget(state, widget) { - Vue.set('state.settings.mobileHomeProfiles', state.device.mobileHomeProfile, state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].filter(w => w.id != widget.id)); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - addDeckColumn(state, column) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - if (column.name == undefined) column.name = null; - deck.columns.push(column); - deck.layout.push([column.id]); - os.store.dispatch('settings/updateDeckProfile'); - }, - - removeDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.columns = deck.columns.filter(c => c.id != id); - deck.layout = deck.layout.map(ids => erase(id, ids)); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const a = x.a; - const b = x.b; - const aX = deck.layout.findIndex(ids => ids.indexOf(a) != -1); - const aY = deck.layout[aX].findIndex(id => id == a); - const bX = deck.layout.findIndex(ids => ids.indexOf(b) != -1); - const bY = deck.layout[bX].findIndex(id => id == b); - deck.layout[aX][aY] = b; - deck.layout[bX][bY] = a; - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapLeftDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.layout.some((ids, i) => { - if (ids.indexOf(id) != -1) { - const left = deck.layout[i - 1]; - if (left) { - // https://vuejs.org/v2/guide/list.html#Caveats - //state.deck.layout[i - 1] = state.deck.layout[i]; - //state.deck.layout[i] = left; - deck.layout.splice(i - 1, 1, deck.layout[i]); - deck.layout.splice(i, 1, left); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapRightDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.layout.some((ids, i) => { - if (ids.indexOf(id) != -1) { - const right = deck.layout[i + 1]; - if (right) { - // https://vuejs.org/v2/guide/list.html#Caveats - //state.deck.layout[i + 1] = state.deck.layout[i]; - //state.deck.layout[i] = right; - deck.layout.splice(i + 1, 1, deck.layout[i]); - deck.layout.splice(i, 1, right); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapUpDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const ids = deck.layout.find(ids => ids.indexOf(id) != -1); - ids.some((x, i) => { - if (x == id) { - const up = ids[i - 1]; - if (up) { - // https://vuejs.org/v2/guide/list.html#Caveats - //ids[i - 1] = id; - //ids[i] = up; - ids.splice(i - 1, 1, id); - ids.splice(i, 1, up); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapDownDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const ids = deck.layout.find(ids => ids.indexOf(id) != -1); - ids.some((x, i) => { - if (x == id) { - const down = ids[i + 1]; - if (down) { - // https://vuejs.org/v2/guide/list.html#Caveats - //ids[i + 1] = id; - //ids[i] = down; - ids.splice(i + 1, 1, id); - ids.splice(i, 1, down); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - stackLeftDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1); - deck.layout = deck.layout.map(ids => erase(id, ids)); - const left = deck.layout[i - 1]; - if (left) deck.layout[i - 1].push(id); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - popRightDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1); - deck.layout = deck.layout.map(ids => erase(id, ids)); - deck.layout.splice(i + 1, 0, [id]); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - addDeckWidget(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.widgets.unshift(x.widget); - os.store.dispatch('settings/updateDeckProfile'); - }, - - removeDeckWidget(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.widgets = column.widgets.filter(w => w.id != x.widget.id); - os.store.dispatch('settings/updateDeckProfile'); - }, - - renameDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.name = x.name; - os.store.dispatch('settings/updateDeckProfile'); - }, - - updateDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - let column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column = x; - os.store.dispatch('settings/updateDeckProfile'); - } - }, - - actions: { - login(ctx, i) { - ctx.commit('updateI', i); - ctx.dispatch('settings/merge', i.clientData); - }, - - logout(ctx) { - ctx.commit('updateI', null); - document.cookie = `i=; max-age=0; domain=${document.location.hostname}`; - localStorage.removeItem('i'); - }, - - mergeMe(ctx, me) { - for (const [key, value] of Object.entries(me)) { - ctx.commit('updateIKeyValue', { key, value }); - } - - if (me.clientData) { - ctx.dispatch('settings/merge', me.clientData); - } - }, - }, - - modules: { - device: { - namespaced: true, - - state: defaultDeviceSettings, - - mutations: { - set(state, x: { key: string; value: any }) { - state[x.key] = x.value; - }, - - setTl(state, x) { - state.tl = { - src: x.src, - arg: x.arg - }; - }, - - setVisibility(state, visibility) { - state.visibility = visibility; - }, - } - }, - - settings: { - namespaced: true, - - state: defaultSettings, - - mutations: { - set(state, x: { key: string; value: any }) { - nestedProperty.set(state, x.key, x.value); - }, - }, - - actions: { - merge(ctx, settings) { - if (settings == null) return; - for (const [key, value] of Object.entries(settings)) { - ctx.commit('set', { key, value }); - } - }, - - set(ctx, x) { - ctx.commit('set', x); - - if (ctx.rootGetters.isSignedIn) { - os.api('i/update-client-setting', { - name: x.key, - value: x.value - }); - } - }, - - updateHomeProfile(ctx) { - const profiles = ctx.state.homeProfiles; - ctx.commit('set', { - key: 'homeProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'homeProfiles', - value: profiles - }); - }, - - updateMobileHomeProfile(ctx) { - const profiles = ctx.state.mobileHomeProfiles; - ctx.commit('set', { - key: 'mobileHomeProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'mobileHomeProfiles', - value: profiles - }); - }, - - updateDeckProfile(ctx) { - const profiles = ctx.state.deckProfiles; - ctx.commit('set', { - key: 'deckProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'deckProfiles', - value: profiles - }); - }, - } - } - } -}); diff --git a/src/client/app/sw.js b/src/client/app/sw.js deleted file mode 100644 index d080130e3d..0000000000 --- a/src/client/app/sw.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Service Worker - */ - -import composeNotification from './common/scripts/compose-notification'; - -// eslint-disable-next-line no-undef -const version = _VERSION_; -const cacheName = `mk-cache-${version}`; - -const apiUrl = `${location.origin}/api/`; - -// インストールされたとき -self.addEventListener('install', ev => { - console.info('installed'); - - ev.waitUntil( - caches.open(cacheName) - .then(cache => { - return cache.addAll([ - "/", - `/assets/desktop.${version}.js`, - `/assets/mobile.${version}.js`, - "/assets/error.jpg" - ]); - }) - .then(() => self.skipWaiting()) - ); -}); - -self.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== cacheName) - .map(name => caches.delete(name)) - )) - .then(() => self.clients.claim()) - ); -}); - -self.addEventListener('fetch', ev => { - if (ev.request.method !== 'GET' || ev.request.url.startsWith(apiUrl)) return; - ev.respondWith( - caches.match(ev.request) - .then(response => { - return response || fetch(ev.request); - }) - .catch(() => { - return caches.match("/"); - }) - ); -}); - -// プッシュ通知を受け取ったとき -self.addEventListener('push', ev => { - // クライアント取得 - ev.waitUntil(self.clients.matchAll({ - includeUncontrolled: true - }).then(clients => { - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length != 0) return; - - const { type, body } = ev.data.json(); - - const n = composeNotification(type, body); - return self.registration.showNotification(n.title, { - body: n.body, - icon: n.icon, - }); - })); -}); diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts deleted file mode 100644 index b16fcdff4b..0000000000 --- a/src/client/app/theme.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as tinycolor from 'tinycolor2'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - vars: { [key: string]: string }; - props: { [key: string]: string }; -}; - -export const lightTheme: Theme = require('../themes/light.json5'); -export const darkTheme: Theme = require('../themes/dark.json5'); -export const lavenderTheme: Theme = require('../themes/lavender.json5'); -export const futureTheme: Theme = require('../themes/future.json5'); -export const halloweenTheme: Theme = require('../themes/halloween.json5'); -export const cafeTheme: Theme = require('../themes/cafe.json5'); -export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5'); -export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5'); -export const monokaiTheme: Theme = require('../themes/monokai.json5'); -export const vividTheme: Theme = require('../themes/vivid.json5'); -export const rainyTheme: Theme = require('../themes/rainy.json5'); -export const mauveTheme: Theme = require('../themes/mauve.json5'); -export const grayTheme: Theme = require('../themes/gray.json5'); -export const tweetDeckTheme: Theme = require('../themes/tweet-deck.json5'); - -export const builtinThemes = [ - lightTheme, - darkTheme, - lavenderTheme, - futureTheme, - halloweenTheme, - cafeTheme, - japaneseSushiSetTheme, - gruvboxDarkTheme, - monokaiTheme, - vividTheme, - rainyTheme, - mauveTheme, - grayTheme, - tweetDeckTheme, -]; - -export function applyTheme(theme: Theme, persisted = true) { - document.documentElement.classList.add('changing-theme'); - - setTimeout(() => { - document.documentElement.classList.remove('changing-theme'); - }, 1000); - - // Deep copy - const _theme = JSON.parse(JSON.stringify(theme)); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id == _theme.base); - _theme.vars = Object.assign({}, base.vars, _theme.vars); - _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - - if (persisted) { - localStorage.setItem('theme', JSON.stringify(props)); - } -} - -function compile(theme: Theme): { [key: string]: string } { - function getColor(code: string): tinycolor.Instance { - // ref - if (code[0] == '@') { - return getColor(theme.props[code.substr(1)]); - } - if (code[0] == '$') { - return getColor(theme.vars[code.substr(1)]); - } - - // func - if (code[0] == ':') { - const parts = code.split('<'); - const func = parts.shift().substr(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - } - } - - return tinycolor(code); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - props[k] = genValue(getColor(v)); - } - - const primary = getColor(props['primary']); - - for (let i = 1; i < 10; i++) { - const color = primary.clone().setAlpha(i / 10); - props['primaryAlpha0' + i] = genValue(color); - } - - for (let i = 5; i < 100; i += 5) { - const color = primary.clone().lighten(i); - props['primaryLighten' + i] = genValue(color); - } - - for (let i = 5; i < 100; i += 5) { - const color = primary.clone().darken(i); - props['primaryDarken' + i] = genValue(color); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} diff --git a/src/client/app/tsconfig.json b/src/client/app/tsconfig.json deleted file mode 100644 index 3ec0271f63..0000000000 --- a/src/client/app/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": false, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "es2017", - "module": "esnext", - "moduleResolution": "node", - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": false, - "experimentalDecorators": true, - "resolveJsonModule": true, - "typeRoots": [ - "node_modules/@types", - "src/@types" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts" - ] -} diff --git a/src/client/app/v.d.ts b/src/client/app/v.d.ts deleted file mode 100644 index b3a21c6cdb..0000000000 --- a/src/client/app/v.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.vue' { - import Vue from 'vue'; - export default Vue; -} |