summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-07-11 10:13:11 +0900
committerGitHub <noreply@github.com>2020-07-11 10:13:11 +0900
commitcf3fc97202588e835ade5d6ab1a3c087e46958ad (patch)
treeb3fe472b455bf913a47df4d41b1363c7122bf1d9 /src/client/components
parentタイムライン上でTwitterウィジットを展開できるようにな... (diff)
downloadmisskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.gz
misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.bz2
misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.zip
Deck (#6504)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/deck/antenna-column.vue80
-rw-r--r--src/client/components/deck/column-core.vue50
-rw-r--r--src/client/components/deck/column.vue426
-rw-r--r--src/client/components/deck/direct-column.vue39
-rw-r--r--src/client/components/deck/list-column.vue87
-rw-r--r--src/client/components/deck/mentions-column.vue39
-rw-r--r--src/client/components/deck/notifications-column.vue69
-rw-r--r--src/client/components/deck/tl-column.vue141
-rw-r--r--src/client/components/deck/widgets-column.vue151
-rw-r--r--src/client/components/error.vue2
-rw-r--r--src/client/components/form-window.vue71
-rw-r--r--src/client/components/modal.vue9
-rw-r--r--src/client/components/note-header.vue1
-rw-r--r--src/client/components/note.vue118
-rw-r--r--src/client/components/sidebar.vue488
-rw-r--r--src/client/components/timeline.vue12
-rw-r--r--src/client/components/ui/container.vue38
-rw-r--r--src/client/components/ui/input.vue15
-rw-r--r--src/client/components/ui/select.vue6
-rw-r--r--src/client/components/ui/switch.vue2
-rw-r--r--src/client/components/ui/textarea.vue4
-rw-r--r--src/client/components/window.vue7
22 files changed, 1770 insertions, 85 deletions
diff --git a/src/client/components/deck/antenna-column.vue b/src/client/components/deck/antenna-column.vue
new file mode 100644
index 0000000000..83fe14f2cc
--- /dev/null
+++ b/src/client/components/deck/antenna-column.vue
@@ -0,0 +1,80 @@
+<template>
+<x-column :menu="menu" :column="column" :is-stacked="isStacked">
+ <template #header>
+ <fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
+ </template>
+
+ <x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import XTimeline from '../timeline.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XTimeline,
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ menu: null,
+ faSatellite
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ (this.$refs.timeline as any).reload();
+ }
+ },
+
+ created() {
+ this.menu = [{
+ icon: faCog,
+ text: this.$t('antenna'),
+ action: async () => {
+ const antennas = await this.$root.api('antennas/list');
+ this.$root.dialog({
+ title: this.$t('antenna'),
+ type: null,
+ select: {
+ items: antennas.map(x => ({
+ value: x, text: x.name
+ }))
+ },
+ showCancelButton: true
+ }).then(({ canceled, result: antenna }) => {
+ if (canceled) return;
+ this.column.antennaId = antenna.id;
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ });
+ }
+ }];
+ },
+
+ methods: {
+ focus() {
+ (this.$refs.timeline as any).focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/src/client/components/deck/column-core.vue b/src/client/components/deck/column-core.vue
new file mode 100644
index 0000000000..44f19e7eda
--- /dev/null
+++ b/src/client/components/deck/column-core.vue
@@ -0,0 +1,50 @@
+<template>
+<!-- TODO: リファクタの余地がありそう -->
+<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
+<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XTlColumn from './tl-column.vue';
+import XAntennaColumn from './antenna-column.vue';
+import XListColumn from './list-column.vue';
+import XNotificationsColumn from './notifications-column.vue';
+import XWidgetsColumn from './widgets-column.vue';
+import XMentionsColumn from './mentions-column.vue';
+import XDirectColumn from './direct-column.vue';
+
+export default Vue.extend({
+ components: {
+ XTlColumn,
+ XAntennaColumn,
+ XListColumn,
+ XNotificationsColumn,
+ XWidgetsColumn,
+ XMentionsColumn,
+ XDirectColumn
+ },
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+ methods: {
+ focus() {
+ this.$children[0].focus();
+ }
+ }
+});
+</script>
diff --git a/src/client/components/deck/column.vue b/src/client/components/deck/column.vue
new file mode 100644
index 0000000000..f7620e5749
--- /dev/null
+++ b/src/client/components/deck/column.vue
@@ -0,0 +1,426 @@
+<template>
+<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
+ @dragover.prevent.stop="onDragover"
+ @dragleave="onDragleave"
+ @drop.prevent.stop="onDrop"
+ v-hotkey="keymap"
+ :style="{ width: `${width}px` }"
+>
+ <header :class="{ indicated }"
+ draggable="true"
+ @click="goTop"
+ @dragstart="onDragstart"
+ @dragend="onDragend"
+ @contextmenu.prevent.stop="onContextmenu"
+ >
+ <button class="toggleActive _button" @click="toggleActive" v-if="isStacked">
+ <template v-if="active"><fa :icon="faAngleUp"/></template>
+ <template v-else><fa :icon="faAngleDown"/></template>
+ </button>
+ <div class="action">
+ <slot name="action"></slot>
+ </div>
+ <span class="header"><slot name="header"></slot></span>
+ <button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button>
+ <button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button>
+ </header>
+ <div ref="body" v-show="active">
+ <slot></slot>
+ </div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
+import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons';
+
+export default Vue.extend({
+ props: {
+ column: {
+ type: Object,
+ required: false,
+ default: null
+ },
+ isStacked: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ menu: {
+ type: Array,
+ required: false,
+ default: null
+ },
+ naked: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ indicated: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+
+ data() {
+ return {
+ active: true,
+ dragging: false,
+ draghover: false,
+ dropready: false,
+ faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes,
+ };
+ },
+
+ computed: {
+ isMainColumn(): boolean {
+ return this.column == null;
+ },
+
+ width(): number {
+ return this.isMainColumn ? 350 : this.column.width;
+ },
+
+ keymap(): any {
+ return {
+ 'shift+up': () => this.$parent.$emit('parentFocus', 'up'),
+ 'shift+down': () => this.$parent.$emit('parentFocus', 'down'),
+ 'shift+left': () => this.$parent.$emit('parentFocus', 'left'),
+ 'shift+right': () => this.$parent.$emit('parentFocus', 'right'),
+ };
+ }
+ },
+
+ watch: {
+ active(v) {
+ this.$emit('change-active-state', v);
+ },
+
+ dragging(v) {
+ this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
+ }
+ },
+
+ mounted() {
+ if (!this.isMainColumn) {
+ this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
+ this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
+ }
+ },
+
+ beforeDestroy() {
+ if (!this.isMainColumn) {
+ this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
+ this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
+ }
+ },
+
+ methods: {
+ onOtherDragStart() {
+ this.dropready = true;
+ },
+
+ onOtherDragEnd() {
+ this.dropready = false;
+ },
+
+ toggleActive() {
+ if (!this.isStacked) return;
+ this.active = !this.active;
+ },
+
+ getMenu() {
+ const items = [{
+ icon: faPencilAlt,
+ text: this.$t('rename'),
+ action: () => {
+ this.$root.dialog({
+ title: this.$t('rename'),
+ input: {
+ default: this.column.name,
+ allowEmpty: false
+ }
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name });
+ });
+ }
+ }, null, {
+ icon: faArrowLeft,
+ text: this.$t('swap-left'),
+ action: () => {
+ this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
+ }
+ }, {
+ icon: faArrowRight,
+ text: this.$t('swap-right'),
+ action: () => {
+ this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
+ }
+ }, this.isStacked ? {
+ icon: faArrowUp,
+ text: this.$t('swap-up'),
+ action: () => {
+ this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
+ }
+ } : undefined, this.isStacked ? {
+ icon: faArrowDown,
+ text: this.$t('swap-down'),
+ action: () => {
+ this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
+ }
+ } : undefined, null, {
+ icon: faWindowRestore,
+ text: this.$t('stack-left'),
+ action: () => {
+ this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
+ }
+ }, this.isStacked ? {
+ icon: faWindowMaximize,
+ text: this.$t('pop-right'),
+ action: () => {
+ this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
+ }
+ } : undefined, null, {
+ icon: faTrashAlt,
+ text: this.$t('remove'),
+ action: () => {
+ this.$store.commit('deviceUser/removeDeckColumn', this.column.id);
+ }
+ }];
+
+ if (this.menu) {
+ for (const i of this.menu.reverse()) {
+ items.unshift(i);
+ }
+ }
+
+ return items;
+ },
+
+ onContextmenu(e) {
+ if (this.isMainColumn) return;
+ this.showMenu();
+ },
+
+ showMenu() {
+ this.$root.menu({
+ items: this.getMenu(),
+ source: this.$refs.menu,
+ });
+ },
+
+ close() {
+ this.$router.push('/');
+ },
+
+ goTop() {
+ this.$refs.body.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ },
+
+ onDragstart(e) {
+ // メインカラムはドラッグさせない
+ if (this.isMainColumn) {
+ e.preventDefault();
+ return;
+ }
+
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('mk-deck-column', this.column.id);
+ this.dragging = true;
+ },
+
+ onDragend(e) {
+ this.dragging = false;
+ },
+
+ onDragover(e) {
+ // メインカラムにはドロップさせない
+ if (this.isMainColumn) {
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
+
+ // 自分自身がドラッグされている場合
+ if (this.dragging) {
+ // 自分自身にはドロップさせない
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
+
+ const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
+
+ e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
+
+ if (!this.dragging && isDeckColumn) this.draghover = true;
+ },
+
+ onDragleave() {
+ this.draghover = false;
+ },
+
+ onDrop(e) {
+ this.draghover = false;
+ this.$root.$emit('deck.column.dragEnd');
+
+ const id = e.dataTransfer.getData('mk-deck-column');
+ if (id != null && id != '') {
+ this.$store.commit('deviceUser/swapDeckColumn', {
+ a: this.column.id,
+ b: id
+ });
+ }
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.dnpfarvg {
+ $header-height: 42px;
+
+ height: 100%;
+ overflow: hidden;
+ box-shadow: 0 0 0 1px var(--deckColumnBorder);
+
+ &.draghover {
+ box-shadow: 0 0 0 2px var(--focus);
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ z-index: 1000;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--focus);
+ }
+ }
+
+ &.dragging {
+ box-shadow: 0 0 0 2px var(--focus);
+ }
+
+ &.dropready {
+ * {
+ pointer-events: none;
+ }
+ }
+
+ &:not(.active) {
+ flex-basis: $header-height;
+ min-height: $header-height;
+
+ > header.indicated {
+ box-shadow: 4px 0px var(--accent) inset;
+ }
+ }
+
+ &.naked {
+ //background: var(--deckAcrylicColumnBg);
+ background: transparent !important;
+
+ > header {
+ background: transparent;
+ box-shadow: none;
+
+ > button {
+ color: var(--fg);
+ }
+ }
+ }
+
+ &.paged {
+ > div {
+ background: var(--bg);
+ padding: var(--margin);
+ }
+ }
+
+ > header {
+ position: relative;
+ display: flex;
+ z-index: 2;
+ line-height: $header-height;
+ padding: 0 16px;
+ font-size: 0.9em;
+ color: var(--panelHeaderFg);
+ background: var(--panelHeaderBg);
+ box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
+ cursor: pointer;
+
+ &, * {
+ user-select: none;
+ }
+
+ &.indicated {
+ box-shadow: 0 3px 0 0 var(--accent);
+ }
+
+ > .header {
+ display: inline-block;
+ align-items: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ > span:only-of-type {
+ width: 100%;
+ }
+
+ > .toggleActive,
+ > .action > *,
+ > .menu,
+ > .close {
+ z-index: 1;
+ width: $header-height;
+ line-height: $header-height;
+ font-size: 16px;
+ color: var(--faceTextButton);
+
+ &:hover {
+ color: var(--faceTextButtonHover);
+ }
+
+ &:active {
+ color: var(--faceTextButtonActive);
+ }
+ }
+
+ > .toggleActive, > .action {
+ margin-left: -16px;
+ }
+
+ > .action {
+ z-index: 1;
+ }
+
+ > .action:empty {
+ display: none;
+ }
+
+ > .menu,
+ > .close {
+ margin-left: auto;
+ margin-right: -16px;
+ }
+ }
+
+ > div {
+ height: calc(100% - #{$header-height});
+ overflow: auto;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ box-sizing: border-box;
+ }
+}
+</style>
diff --git a/src/client/components/deck/direct-column.vue b/src/client/components/deck/direct-column.vue
new file mode 100644
index 0000000000..f340048d6a
--- /dev/null
+++ b/src/client/components/deck/direct-column.vue
@@ -0,0 +1,39 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
+ <template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
+
+ <x-direct/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import XDirect from '../../pages/messages.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XDirect
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ menu: null,
+ faEnvelope
+ }
+ },
+});
+</script>
diff --git a/src/client/components/deck/list-column.vue b/src/client/components/deck/list-column.vue
new file mode 100644
index 0000000000..a3576e8d67
--- /dev/null
+++ b/src/client/components/deck/list-column.vue
@@ -0,0 +1,87 @@
+<template>
+<x-column :menu="menu" :column="column" :is-stacked="isStacked">
+ <template #header>
+ <fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
+ </template>
+
+ <x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import XTimeline from '../timeline.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XTimeline,
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ faListUl
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ (this.$refs.timeline as any).reload();
+ }
+ },
+
+ created() {
+ this.menu = [{
+ icon: faCog,
+ text: this.$t('list'),
+ action: this.setList
+ }];
+ },
+
+ mounted() {
+ if (this.column.listId == null) {
+ this.setList();
+ }
+ },
+
+ methods: {
+ async setList() {
+ const lists = await this.$root.api('users/lists/list');
+ const { canceled, result: list } = await this.$root.dialog({
+ title: this.$t('list'),
+ type: null,
+ select: {
+ items: lists.map(x => ({
+ value: x, text: x.name
+ })),
+ default: this.column.listId
+ },
+ showCancelButton: true
+ });
+ if (canceled) return;
+ Vue.set(this.column, 'listId', list.id);
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ },
+
+ focus() {
+ (this.$refs.timeline as any).focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/src/client/components/deck/mentions-column.vue b/src/client/components/deck/mentions-column.vue
new file mode 100644
index 0000000000..19e49d2a89
--- /dev/null
+++ b/src/client/components/deck/mentions-column.vue
@@ -0,0 +1,39 @@
+<template>
+<x-column :column="column" :is-stacked="isStacked" :menu="menu">
+ <template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
+
+ <x-mentions/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAt } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import XMentions from '../../pages/mentions.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XMentions
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ menu: null,
+ faAt
+ }
+ },
+});
+</script>
diff --git a/src/client/components/deck/notifications-column.vue b/src/client/components/deck/notifications-column.vue
new file mode 100644
index 0000000000..58873aa130
--- /dev/null
+++ b/src/client/components/deck/notifications-column.vue
@@ -0,0 +1,69 @@
+<template>
+<x-column :column="column" :is-stacked="isStacked" :menu="menu">
+ <template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
+
+ <x-notifications/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCog } from '@fortawesome/free-solid-svg-icons';
+import { faBell } from '@fortawesome/free-regular-svg-icons';
+import XColumn from './column.vue';
+import XNotifications from '../notifications.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XNotifications
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ menu: null,
+ faBell
+ }
+ },
+
+ created() {
+ if (this.column.notificationType == null) {
+ this.column.notificationType = 'all';
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ }
+
+ this.menu = [{
+ icon: faCog,
+ text: this.$t('@.notification-type'),
+ action: () => {
+ this.$root.dialog({
+ title: this.$t('@.notification-type'),
+ type: null,
+ select: {
+ items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
+ value: x, text: this.$t('@.notification-types.' + x)
+ }))
+ default: this.column.notificationType,
+ },
+ showCancelButton: true
+ }).then(({ canceled, result: type }) => {
+ if (canceled) return;
+ this.column.notificationType = type;
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ });
+ }
+ }];
+ },
+});
+</script>
diff --git a/src/client/components/deck/tl-column.vue b/src/client/components/deck/tl-column.vue
new file mode 100644
index 0000000000..c3ee67af3a
--- /dev/null
+++ b/src/client/components/deck/tl-column.vue
@@ -0,0 +1,141 @@
+<template>
+<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
+ <template #header>
+ <fa v-if="column.tl === 'home'" :icon="faHome"/>
+ <fa v-else-if="column.tl === 'local'" :icon="faComments"/>
+ <fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
+ <fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
+ <span style="margin-left: 8px;">{{ column.name }}</span>
+ </template>
+
+ <div class="iwaalbte" v-if="disabled">
+ <p>
+ <fa :icon="faMinusCircle"/>
+ {{ $t('disabled-timeline.title') }}
+ </p>
+ <p class="desc">{{ $t('disabled-timeline.description') }}</p>
+ </div>
+ <x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import XTimeline from '../timeline.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XTimeline,
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ menu: null,
+ disabled: false,
+ indicated: false,
+ columnActive: true,
+ faMinusCircle, faHome, faComments, faShareAlt, faGlobe,
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ (this.$refs.timeline as any).reload();
+ }
+ },
+
+ created() {
+ this.menu = [{
+ icon: faCog,
+ text: this.$t('timeline'),
+ action: this.setType
+ }];
+ },
+
+ mounted() {
+ if (this.column.tl == null) {
+ this.setType();
+ } else {
+ this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
+ this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
+ this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl));
+ }
+ },
+
+ methods: {
+ async setType() {
+ const { canceled, result: src } = await this.$root.dialog({
+ title: this.$t('timeline'),
+ type: null,
+ select: {
+ items: [{
+ value: 'home', text: this.$t('_timelines.home')
+ }, {
+ value: 'local', text: this.$t('_timelines.local')
+ }, {
+ value: 'social', text: this.$t('_timelines.social')
+ }, {
+ value: 'global', text: this.$t('_timelines.global')
+ }]
+ },
+ showCancelButton: true
+ });
+ if (canceled) return;
+ Vue.set(this.column, 'tl', src);
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ },
+
+ queueUpdated(q) {
+ if (this.columnActive) {
+ this.indicated = q !== 0;
+ }
+ },
+
+ onNote() {
+ if (!this.columnActive) {
+ this.indicated = true;
+ }
+ },
+
+ onChangeActiveState(state) {
+ this.columnActive = state;
+
+ if (this.columnActive) {
+ this.indicated = false;
+ }
+ },
+
+ focus() {
+ (this.$refs.timeline as any).focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.iwaalbte {
+ text-align: center;
+
+ > p {
+ margin: 16px;
+
+ &.desc {
+ font-size: 14px;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/deck/widgets-column.vue b/src/client/components/deck/widgets-column.vue
new file mode 100644
index 0000000000..37b17451ec
--- /dev/null
+++ b/src/client/components/deck/widgets-column.vue
@@ -0,0 +1,151 @@
+<template>
+<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
+ <template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
+
+ <div class="wtdtxvec">
+ <template v-if="edit">
+ <header>
+ <select v-model="widgetAdderSelected" @change="addWidget">
+ <option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option>
+ </select>
+ </header>
+ <x-draggable
+ :list="column.widgets"
+ animation="150"
+ @sort="onWidgetSort"
+ >
+ <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
+ <button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
+ </div>
+ </x-draggable>
+ </template>
+ <component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/>
+ </div>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as XDraggable from 'vuedraggable';
+import { v4 as uuid } from 'uuid';
+import { faWindowMaximize, faTimes } from '@fortawesome/free-solid-svg-icons';
+import XColumn from './column.vue';
+import { widgets } from '../../widgets';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XDraggable,
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true,
+ },
+ isStacked: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ edit: false,
+ menu: null,
+ widgetAdderSelected: null,
+ widgets,
+ faWindowMaximize, faTimes
+ };
+ },
+
+ created() {
+ this.menu = [{
+ icon: 'cog',
+ text: this.$t('edit'),
+ action: () => {
+ this.edit = !this.edit;
+ }
+ }];
+ },
+
+ methods: {
+ widgetFunc(id) {
+ this.$refs[id][0].setting();
+ },
+
+ onWidgetSort() {
+ this.saveWidgets();
+ },
+
+ addWidget() {
+ this.$store.commit('deviceUser/addDeckWidget', {
+ id: this.column.id,
+ widget: {
+ name: this.widgetAdderSelected,
+ id: uuid(),
+ data: {}
+ }
+ });
+
+ this.widgetAdderSelected = null;
+ },
+
+ removeWidget(widget) {
+ this.$store.commit('deviceUser/removeDeckWidget', {
+ id: this.column.id,
+ widget
+ });
+ },
+
+ saveWidgets() {
+ this.$store.commit('deviceUser/updateDeckColumn', this.column);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.wtdtxvec {
+ padding-top: 1px; // ウィジェットのbox-shadowを利用した1px borderを隠さないようにするため
+
+ > header {
+ padding: 16px;
+
+ > * {
+ width: 100%;
+ padding: 4px;
+ }
+ }
+
+ > .widget, .customize-container {
+ margin: 8px;
+
+ &:first-of-type {
+ margin-top: 0;
+ }
+ }
+
+ .customize-container {
+ position: relative;
+ cursor: move;
+
+ > *:not(.remove) {
+ pointer-events: none;
+ }
+
+ > .remove {
+ position: absolute;
+ z-index: 2;
+ top: 8px;
+ right: 8px;
+ width: 32px;
+ height: 32px;
+ color: #fff;
+ background: rgba(#000, 0.7);
+ border-radius: 4px;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/error.vue b/src/client/components/error.vue
index b1d91fb3ef..90efa700b2 100644
--- a/src/client/components/error.vue
+++ b/src/client/components/error.vue
@@ -40,7 +40,7 @@ export default Vue.extend({
> img {
vertical-align: bottom;
- height: 150px;
+ height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
diff --git a/src/client/components/form-window.vue b/src/client/components/form-window.vue
new file mode 100644
index 0000000000..25eee91647
--- /dev/null
+++ b/src/client/components/form-window.vue
@@ -0,0 +1,71 @@
+<template>
+<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
+ <template #header>
+ {{ title }}
+ </template>
+ <div class="xkpnjxcv">
+ <label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
+ <mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input>
+ <mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input>
+ <mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea>
+ <mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch>
+ </label>
+ </div>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XWindow from './window.vue';
+import MkInput from './ui/input.vue';
+import MkTextarea from './ui/textarea.vue';
+import MkSwitch from './ui/switch.vue';
+
+export default Vue.extend({
+ components: {
+ XWindow,
+ MkInput,
+ MkTextarea,
+ MkSwitch,
+ },
+
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ form: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ values: {}
+ };
+ },
+
+ created() {
+ for (const item in this.form) {
+ Vue.set(this.values, item, this.form[item].default || null);
+ }
+ },
+
+ methods: {
+ ok() {
+ this.$emit('ok', this.values);
+ this.$refs.window.close();
+ },
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.xkpnjxcv {
+ > label {
+ display: block;
+ padding: 16px 24px;
+ }
+}
+</style>
diff --git a/src/client/components/modal.vue b/src/client/components/modal.vue
index 1a9d98a8cc..f941d4d503 100644
--- a/src/client/components/modal.vue
+++ b/src/client/components/modal.vue
@@ -1,10 +1,10 @@
<template>
<div class="mk-modal" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
- <div class="bg" ref="bg" v-if="show" @click="close()"></div>
+ <div class="bg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
</transition>
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
- <div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div>
+ <div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div>
</transition>
</div>
</template>
@@ -14,6 +14,11 @@ import Vue from 'vue';
export default Vue.extend({
props: {
+ canClose: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue
index 93cf2cdf39..039287818f 100644
--- a/src/client/components/note-header.vue
+++ b/src/client/components/note-header.vue
@@ -54,7 +54,6 @@ export default Vue.extend({
margin: 0 .5em 0 0;
padding: 0;
overflow: hidden;
- color: var(--noteHeaderName);
font-size: 1em;
font-weight: bold;
text-decoration: none;
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 118fef661c..badb9f12f3 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -724,61 +724,6 @@ export default Vue.extend({
transition: box-shadow 0.1s ease;
overflow: hidden;
- &.max-width_500px {
- font-size: 0.9em;
- }
-
- &.max-width_450px {
- > .renote {
- padding: 8px 16px 0 16px;
- }
-
- > .article {
- padding: 14px 16px 9px;
-
- > .avatar {
- margin: 0 10px 8px 0;
- width: 50px;
- height: 50px;
- }
- }
- }
-
- &.max-width_350px {
- > .article {
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 18px;
- }
- }
- }
- }
- }
- }
-
- &.max-width_300px {
- font-size: 0.825em;
-
- > .article {
- > .avatar {
- width: 44px;
- height: 44px;
- }
-
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 12px;
- }
- }
- }
- }
- }
- }
-
&:focus {
outline: none;
box-shadow: 0 0 0 3px var(--focus);
@@ -797,10 +742,6 @@ export default Vue.extend({
white-space: pre;
color: #d28a3f;
- @media (max-width: 450px) {
- padding: 8px 16px 0 16px;
- }
-
> [data-icon] {
margin-right: 4px;
}
@@ -985,5 +926,64 @@ export default Vue.extend({
> .reply {
border-top: solid 1px var(--divider);
}
+
+ &.max-width_500px {
+ font-size: 0.9em;
+ }
+
+ &.max-width_450px {
+ > .renote {
+ padding: 8px 16px 0 16px;
+ }
+
+ > .info {
+ padding: 8px 16px 0 16px;
+ }
+
+ > .article {
+ padding: 14px 16px 9px;
+
+ > .avatar {
+ margin: 0 10px 8px 0;
+ width: 50px;
+ height: 50px;
+ }
+ }
+ }
+
+ &.max-width_350px {
+ > .article {
+ > .main {
+ > .footer {
+ > .button {
+ &:not(:last-child) {
+ margin-right: 18px;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &.max-width_300px {
+ font-size: 0.825em;
+
+ > .article {
+ > .avatar {
+ width: 44px;
+ height: 44px;
+ }
+
+ > .main {
+ > .footer {
+ > .button {
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+ }
+ }
+ }
+ }
}
</style>
diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue
new file mode 100644
index 0000000000..3ddef7d127
--- /dev/null
+++ b/src/client/components/sidebar.vue
@@ -0,0 +1,488 @@
+<template>
+<div class="mvcprjjd">
+ <transition name="nav-back">
+ <div class="nav-back"
+ v-if="showing"
+ @click="showing = false"
+ @touchstart="showing = false"
+ ></div>
+ </transition>
+
+ <transition name="nav">
+ <nav class="nav" v-show="showing">
+ <div>
+ <button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
+ <mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
+ </button>
+ <button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
+ <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
+ </button>
+ <router-link class="item index" active-class="active" to="/" exact v-else>
+ <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
+ </router-link>
+ <template v-for="item in menu">
+ <div v-if="item === '-'" class="divider"></div>
+ <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to">
+ <fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span>
+ <i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i>
+ </component>
+ </template>
+ <div class="divider"></div>
+ <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
+ <fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
+ </button>
+ <button class="item _button" @click="more">
+ <fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
+ <i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i>
+ </button>
+ <router-link class="item" active-class="active" to="/preferences">
+ <fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
+ </router-link>
+ </div>
+ </nav>
+ </transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
+import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
+import { host, instanceName } from '../config';
+import { search } from '../scripts/search';
+
+export default Vue.extend({
+ data() {
+ return {
+ host: host,
+ showing: false,
+ searching: false,
+ accounts: [],
+ connection: null,
+ menuDef: this.$store.getters.nav({
+ search: this.search
+ }),
+ faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
+ };
+ },
+
+ computed: {
+ menu(): string[] {
+ return this.$store.state.deviceUser.menu;
+ },
+
+ otherNavItemIndicated(): boolean {
+ if (!this.$store.getters.isSignedIn) return false;
+ for (const def in this.menuDef) {
+ if (this.menu.includes(def)) continue;
+ if (this.menuDef[def].indicated) return true;
+ }
+ return false;
+ },
+ },
+
+ watch: {
+ $route(to, from) {
+ this.showing = false;
+ },
+ },
+
+ methods: {
+ show() {
+ this.showing = true;
+ },
+
+ search() {
+ if (this.searching) return;
+
+ this.$root.dialog({
+ title: this.$t('search'),
+ input: true
+ }).then(async ({ canceled, result: query }) => {
+ if (canceled || query == null || query === '') return;
+
+ this.searching = true;
+ search(this, query).finally(() => {
+ this.searching = false;
+ });
+ });
+ },
+
+ async openAccountMenu(ev) {
+ const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
+
+ const accountItems = accounts.map(account => ({
+ type: 'user',
+ user: account,
+ action: () => { this.switchAccount(account); }
+ }));
+
+ this.$root.menu({
+ items: [...[{
+ type: 'link',
+ text: this.$t('profile'),
+ to: `/@${ this.$store.state.i.username }`,
+ avatar: this.$store.state.i,
+ }, {
+ type: 'link',
+ text: this.$t('accountSettings'),
+ to: '/my/settings',
+ icon: faCog,
+ }, null, ...accountItems, {
+ icon: faPlus,
+ text: this.$t('addAcount'),
+ action: () => {
+ this.$root.menu({
+ items: [{
+ text: this.$t('existingAcount'),
+ action: () => { this.addAcount(); },
+ }, {
+ text: this.$t('createAccount'),
+ action: () => { this.createAccount(); },
+ }],
+ align: 'left',
+ fixed: true,
+ width: 240,
+ source: ev.currentTarget || ev.target,
+ });
+ },
+ }]],
+ align: 'left',
+ fixed: true,
+ width: 240,
+ source: ev.currentTarget || ev.target,
+ });
+ },
+
+ oepnInstanceMenu(ev) {
+ this.$root.menu({
+ items: [{
+ type: 'link',
+ text: this.$t('dashboard'),
+ to: '/instance',
+ icon: faTachometerAlt,
+ }, null, {
+ type: 'link',
+ text: this.$t('settings'),
+ to: '/instance/settings',
+ icon: faCog,
+ }, {
+ type: 'link',
+ text: this.$t('customEmojis'),
+ to: '/instance/emojis',
+ icon: faLaugh,
+ }, {
+ type: 'link',
+ text: this.$t('users'),
+ to: '/instance/users',
+ icon: faUsers,
+ }, {
+ type: 'link',
+ text: this.$t('files'),
+ to: '/instance/files',
+ icon: faCloud,
+ }, {
+ type: 'link',
+ text: this.$t('jobQueue'),
+ to: '/instance/queue',
+ icon: faExchangeAlt,
+ }, {
+ type: 'link',
+ text: this.$t('federation'),
+ to: '/instance/federation',
+ icon: faGlobe,
+ }, {
+ type: 'link',
+ text: this.$t('relays'),
+ to: '/instance/relays',
+ icon: faProjectDiagram,
+ }, {
+ type: 'link',
+ text: this.$t('announcements'),
+ to: '/instance/announcements',
+ icon: faBroadcastTower,
+ }],
+ align: 'left',
+ fixed: true,
+ width: 200,
+ source: ev.currentTarget || ev.target,
+ });
+ },
+
+ more(ev) {
+ const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
+ type: def.to ? 'link' : 'button',
+ text: this.$t(def.title),
+ icon: def.icon,
+ to: def.to,
+ action: def.action,
+ indicate: def.indicated,
+ }));
+ this.$root.menu({
+ items: [...items, null, {
+ type: 'link',
+ text: this.$t('help'),
+ to: '/docs',
+ icon: faQuestionCircle,
+ }, {
+ type: 'link',
+ text: this.$t('aboutX', { x: instanceName || host }),
+ to: '/about',
+ icon: faInfoCircle,
+ }, {
+ type: 'link',
+ text: this.$t('aboutMisskey'),
+ to: '/about-misskey',
+ icon: faInfoCircle,
+ }],
+ align: 'left',
+ fixed: true,
+ width: 200,
+ source: ev.currentTarget || ev.target,
+ });
+ },
+
+ async addAcount() {
+ this.$root.new(await import('./signin-dialog.vue').then(m => m.default)).$once('login', res => {
+ this.$store.dispatch('addAcount', res);
+ this.$root.dialog({
+ type: 'success',
+ iconOnly: true, autoClose: true
+ });
+ });
+ },
+
+ async createAccount() {
+ this.$root.new(await import('./signup-dialog.vue').then(m => m.default)).$once('signup', res => {
+ this.$store.dispatch('addAcount', res);
+ this.switchAccountWithToken(res.i);
+ });
+ },
+
+ async switchAccount(account: any) {
+ const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token;
+ this.switchAccountWithToken(token);
+ },
+
+ switchAccountWithToken(token: string) {
+ this.$root.dialog({
+ type: 'waiting',
+ iconOnly: true
+ });
+
+ this.$root.api('i', {}, token).then((i: any) => {
+ this.$store.dispatch('switchAccount', {
+ ...i,
+ token: token
+ }).then(() => {
+ this.$nextTick(() => {
+ location.reload();
+ });
+ });
+ });
+ },
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.nav-enter-active,
+.nav-leave-active {
+ opacity: 1;
+ transform: translateX(0);
+ transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.nav-enter,
+.nav-leave-active {
+ opacity: 0;
+ transform: translateX(-240px);
+}
+
+.nav-back-enter-active,
+.nav-back-leave-active {
+ opacity: 1;
+ transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.nav-back-enter,
+.nav-back-leave-active {
+ opacity: 0;
+}
+
+.mvcprjjd {
+ $ui-font-size: 1em; // TODO: どこかに集約したい
+ $nav-width: 250px; // TODO: どこかに集約したい
+ $nav-icon-only-width: 80px; // TODO: どこかに集約したい
+ $nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
+ $nav-hide-threshold: 650px; // TODO: どこかに集約したい
+
+ > .nav-back {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ width: 100%;
+ height: 100%;
+ background: var(--modalBg);
+ }
+
+ > .nav {
+ $avatar-size: 32px;
+ $avatar-margin: 8px;
+
+ flex: 0 0 $nav-width;
+ width: $nav-width;
+ box-sizing: border-box;
+
+ @media (max-width: $nav-icon-only-threshold) {
+ flex: 0 0 $nav-icon-only-width;
+ width: $nav-icon-only-width;
+ }
+
+ @media (max-width: $nav-hide-threshold) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ }
+
+ @media (min-width: $nav-hide-threshold + 1px) {
+ display: block !important;
+ }
+
+ > div {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ width: $nav-width;
+ height: 100vh;
+ box-sizing: border-box;
+ overflow: auto;
+ background: var(--navBg);
+ border-right: solid 1px var(--divider);
+
+ > .divider {
+ margin: 16px 0;
+ border-top: solid 1px var(--divider);
+ }
+
+ @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
+ width: $nav-icon-only-width;
+
+ > .divider {
+ margin: 8px auto;
+ width: calc(100% - 32px);
+ }
+
+ > .item {
+ &:first-child {
+ margin-bottom: 8px;
+ }
+
+ &:last-child {
+ margin-top: 8px;
+ }
+ }
+ }
+
+ > .item {
+ position: relative;
+ display: block;
+ padding-left: 32px;
+ font-size: $ui-font-size;
+ line-height: 3.2rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ text-align: left;
+ box-sizing: border-box;
+ color: var(--navFg);
+
+ > [data-icon] {
+ width: 32px;
+ }
+
+ > [data-icon],
+ > .avatar {
+ margin-right: $avatar-margin;
+ }
+
+ > .avatar {
+ width: $avatar-size;
+ height: $avatar-size;
+ vertical-align: middle;
+ }
+
+ > i {
+ position: absolute;
+ top: 0;
+ left: 20px;
+ color: var(--navIndicator);
+ font-size: 8px;
+ animation: blink 1s infinite;
+ }
+
+ &:hover {
+ text-decoration: none;
+ color: var(--navHoverFg);
+ }
+
+ &.active {
+ color: var(--navActive);
+ }
+
+ &:first-child, &:last-child {
+ position: sticky;
+ z-index: 1;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ background: var(--X14);
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ }
+
+ &:first-child {
+ top: 0;
+ margin-bottom: 16px;
+ border-bottom: solid 1px var(--divider);
+ }
+
+ &:last-child {
+ bottom: 0;
+ margin-top: 16px;
+ border-top: solid 1px var(--divider);
+ }
+
+ @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
+ padding-left: 0;
+ width: 100%;
+ text-align: center;
+ font-size: $ui-font-size * 1.2;
+ line-height: 3.7rem;
+
+ > [data-icon],
+ > .avatar {
+ margin-right: 0;
+ }
+
+ > i {
+ left: 10px;
+ }
+
+ > .text {
+ display: none;
+ }
+ }
+ }
+
+ @media (max-width: $nav-hide-threshold) {
+ > .index,
+ > .notifications {
+ display: none;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index bd1901a624..ce0fd95caf 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -17,9 +17,11 @@ export default Vue.extend({
required: true
},
list: {
+ type: String,
required: false
},
antenna: {
+ type: String,
required: false
},
sound: {
@@ -53,6 +55,8 @@ export default Vue.extend({
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
(this.$refs.tl as any).prepend(_note);
+ this.$emit('note');
+
if (this.sound) {
this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
}
@@ -77,10 +81,10 @@ export default Vue.extend({
if (this.src == 'antenna') {
endpoint = 'antennas/notes';
this.query = {
- antennaId: this.antenna.id
+ antennaId: this.antenna
};
this.connection = this.$root.stream.connectToChannel('antenna', {
- antennaId: this.antenna.id
+ antennaId: this.antenna
});
this.connection.on('note', prepend);
} else if (this.src == 'home') {
@@ -106,10 +110,10 @@ export default Vue.extend({
} else if (this.src == 'list') {
endpoint = 'notes/user-list-timeline';
this.query = {
- listId: this.list.id
+ listId: this.list
};
this.connection = this.$root.stream.connectToChannel('userList', {
- listId: this.list.id
+ listId: this.list
});
this.connection.on('note', prepend);
this.connection.on('userAdded', onUserAdded);
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
index 3fed1f65c7..6a718439aa 100644
--- a/src/client/components/ui/container.vue
+++ b/src/client/components/ui/container.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }">
+<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]">
<header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
@@ -47,6 +47,11 @@ export default Vue.extend({
required: false,
default: true
},
+ scrollable: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
},
data() {
return {
@@ -107,10 +112,19 @@ export default Vue.extend({
box-shadow: none !important;
}
+ &.scrollable {
+ display: flex;
+ flex-direction: column;
+
+ > div {
+ overflow: auto;
+ }
+ }
+
> header {
position: relative;
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
- z-index: 1;
+ z-index: 2;
background: var(--panelHeaderBg);
color: var(--panelHeaderFg);
@@ -118,10 +132,6 @@ export default Vue.extend({
margin: 0;
padding: 12px 16px;
- @media (max-width: 500px) {
- padding: 8px 10px;
- }
-
> [data-icon] {
margin-right: 6px;
}
@@ -141,5 +151,21 @@ export default Vue.extend({
height: 100%;
}
}
+
+ &.max-width_500px {
+ > header {
+ > .title {
+ padding: 8px 10px;
+ }
+ }
+ }
+}
+
+._forceContainerFull_ .ukygtjoj {
+ > header {
+ > .title {
+ padding: 12px 16px !important;
+ }
+ }
}
</style>
diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue
index c9f62e3cc0..d5317db7f9 100644
--- a/src/client/components/ui/input.vue
+++ b/src/client/components/ui/input.vue
@@ -20,6 +20,7 @@
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
+ :step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@@ -36,6 +37,7 @@
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
+ :step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@@ -114,6 +116,9 @@ export default Vue.extend({
spellcheck: {
required: false
},
+ step: {
+ required: false
+ },
debounce: {
required: false
},
@@ -164,7 +169,7 @@ export default Vue.extend({
},
v(v) {
if (this.type === 'number') {
- this.$emit('input', parseInt(v, 10));
+ this.$emit('input', parseFloat(v));
} else {
this.$emit('input', v);
}
@@ -297,7 +302,7 @@ export default Vue.extend({
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
@@ -312,7 +317,7 @@ export default Vue.extend({
top: -17px;
left: 0 !important;
pointer-events: none;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
@@ -343,7 +348,7 @@ export default Vue.extend({
padding: 0;
font: inherit;
font-weight: normal;
- font-size: 16px;
+ font-size: 1em;
line-height: $height;
color: var(--inputText);
background: transparent;
@@ -364,7 +369,7 @@ export default Vue.extend({
position: absolute;
z-index: 1;
top: 0;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue
index ce21949713..55f76553a7 100644
--- a/src/client/components/ui/select.vue
+++ b/src/client/components/ui/select.vue
@@ -135,7 +135,7 @@ export default Vue.extend({
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
pointer-events: none;
//will-change transform
@@ -150,7 +150,7 @@ export default Vue.extend({
padding: 0;
font: inherit;
font-weight: normal;
- font-size: 16px;
+ font-size: 1em;
height: 32px;
background: none;
border: none;
@@ -170,7 +170,7 @@ export default Vue.extend({
display: block;
align-self: center;
justify-self: center;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
color: rgba(#000, 0.54);
pointer-events: none;
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
index 18a2ec33f1..9652a01024 100644
--- a/src/client/components/ui/switch.vue
+++ b/src/client/components/ui/switch.vue
@@ -5,7 +5,7 @@
role="switch"
:aria-checked="checked"
:aria-disabled="disabled"
- @click="toggle"
+ @click.prevent="toggle"
>
<input
type="checkbox"
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
index fab307a202..a42813ee64 100644
--- a/src/client/components/ui/textarea.vue
+++ b/src/client/components/ui/textarea.vue
@@ -133,7 +133,7 @@ export default Vue.extend({
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
- font-size: 16px;
+ font-size: 1em;
line-height: 32px;
pointer-events: none;
//will-change transform
@@ -151,7 +151,7 @@ export default Vue.extend({
box-sizing: border-box;
font: inherit;
font-weight: normal;
- font-size: 16px;
+ font-size: 1em;
background: transparent;
border: none;
border-radius: 0;
diff --git a/src/client/components/window.vue b/src/client/components/window.vue
index db13985181..a0bff869b9 100644
--- a/src/client/components/window.vue
+++ b/src/client/components/window.vue
@@ -1,5 +1,5 @@
<template>
-<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
+<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }" :can-close="canClose">
<div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }">
<div class="header">
<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button>
@@ -57,6 +57,11 @@ export default Vue.extend({
required: false,
default: 400
},
+ canClose: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {