summaryrefslogtreecommitdiff
path: root/src/client/app/desktop/views/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-01-30 04:37:25 +0900
committerGitHub <noreply@github.com>2020-01-30 04:37:25 +0900
commitf6154dc0af1a0d65819e87240f4385f9573095cb (patch)
tree699a5ca07d6727b7f8497d4769f25d6d62f94b5a /src/client/app/desktop/views/components
parentAdd Event activity-type support (#5785) (diff)
downloadmisskey-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/desktop/views/components')
-rw-r--r--src/client/app/desktop/views/components/activity.calendar.vue80
-rw-r--r--src/client/app/desktop/views/components/activity.chart.vue108
-rw-r--r--src/client/app/desktop/views/components/activity.vue86
-rw-r--r--src/client/app/desktop/views/components/calendar.vue252
-rw-r--r--src/client/app/desktop/views/components/choose-file-from-drive-window.vue136
-rw-r--r--src/client/app/desktop/views/components/choose-folder-from-drive-window.vue56
-rw-r--r--src/client/app/desktop/views/components/context-menu.menu.vue121
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue90
-rw-r--r--src/client/app/desktop/views/components/crop-window.vue189
-rw-r--r--src/client/app/desktop/views/components/detail-notes.vue56
-rw-r--r--src/client/app/desktop/views/components/drive-window.vue61
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue339
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue313
-rw-r--r--src/client/app/desktop/views/components/drive.nav-folder.vue118
-rw-r--r--src/client/app/desktop/views/components/drive.vue760
-rw-r--r--src/client/app/desktop/views/components/emoji-picker-dialog.vue84
-rw-r--r--src/client/app/desktop/views/components/game-window.vue38
-rw-r--r--src/client/app/desktop/views/components/index.ts35
-rw-r--r--src/client/app/desktop/views/components/media-video-dialog.vue47
-rw-r--r--src/client/app/desktop/views/components/media-video.vue102
-rw-r--r--src/client/app/desktop/views/components/messaging-room-window.vue37
-rw-r--r--src/client/app/desktop/views/components/messaging-window.vue42
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue356
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue88
-rw-r--r--src/client/app/desktop/views/components/note.sub.vue106
-rw-r--r--src/client/app/desktop/views/components/note.vue323
-rw-r--r--src/client/app/desktop/views/components/notes.vue182
-rw-r--r--src/client/app/desktop/views/components/notifications.vue379
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue140
-rw-r--r--src/client/app/desktop/views/components/post-form.vue331
-rw-r--r--src/client/app/desktop/views/components/progress-dialog.vue98
-rw-r--r--src/client/app/desktop/views/components/renote-form-window.vue66
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue111
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue38
-rw-r--r--src/client/app/desktop/views/components/settings.vue86
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue48
-rw-r--r--src/client/app/desktop/views/components/ui-container.vue138
-rw-r--r--src/client/app/desktop/views/components/ui-notification.vue62
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue343
-rw-r--r--src/client/app/desktop/views/components/ui.header.clock.vue109
-rw-r--r--src/client/app/desktop/views/components/ui.header.messaging.vue69
-rw-r--r--src/client/app/desktop/views/components/ui.header.nav.vue141
-rw-r--r--src/client/app/desktop/views/components/ui.header.notifications.vue136
-rw-r--r--src/client/app/desktop/views/components/ui.header.post.vue54
-rw-r--r--src/client/app/desktop/views/components/ui.header.search.vue82
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue161
-rw-r--r--src/client/app/desktop/views/components/ui.sidebar.vue363
-rw-r--r--src/client/app/desktop/views/components/ui.vue108
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue71
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue164
-rw-r--r--src/client/app/desktop/views/components/window.vue620
51 files changed, 0 insertions, 8123 deletions
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>