diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-01-30 04:37:25 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-01-30 04:37:25 +0900 |
| commit | f6154dc0af1a0d65819e87240f4385f9573095cb (patch) | |
| tree | 699a5ca07d6727b7f8497d4769f25d6d62f94b5a /src/client/app/desktop/views/components | |
| parent | Add Event activity-type support (#5785) (diff) | |
| download | misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.gz misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.bz2 misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.zip | |
v12 (#5712)
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Diffstat (limited to 'src/client/app/desktop/views/components')
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> |