enhance(client): refine deck

Fix #7720
This commit is contained in:
syuilo 2022-07-03 20:30:58 +09:00
parent af6dd4194f
commit 1163c85db6
9 changed files with 132 additions and 96 deletions

View file

@ -1721,8 +1721,6 @@ _notification:
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
columnMargin: "カラム間のマージン"
columnHeaderHeight: "カラムのヘッダー幅"
addColumn: "カラムを追加"
swapLeft: "左に移動"
swapRight: "右に移動"

View file

@ -10,18 +10,6 @@
<option value="center">{{ i18n.ts.center }}</option>
</FormRadios>
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
<option :value="42">{{ i18n.ts.narrow }}</option>
<option :value="45">{{ i18n.ts.medium }}</option>
<option :value="48">{{ i18n.ts.wide }}</option>
</FormRadios>
<FormInput v-model="columnMargin" type="number" class="_formBlock">
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
<template #suffix>px</template>
</FormInput>
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div>
</template>
@ -41,8 +29,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
const profile = computed(deckStore.makeGetterSetter('profile'));
watch(navWindow, async () => {

View file

@ -12,6 +12,7 @@
<option value="small">{{ i18n.ts.small }}</option>
<option value="medium">{{ i18n.ts.medium }}</option>
<option value="large">{{ i18n.ts.large }}</option>
<option value="veryLarge">{{ i18n.ts.large }}+</option>
</FormRadios>
</div>
</template>

View file

@ -77,6 +77,7 @@
codeString: '#ffb675',
codeNumber: '#cfff9e',
codeBoolean: '#c59eff',
deckDivider: '#000',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View file

@ -77,6 +77,7 @@
codeString: '#b98710',
codeNumber: '#0fbbbb',
codeBoolean: '#62b70c',
deckDivider: ':darken<3<@bg',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',

View file

@ -4,7 +4,8 @@
verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall',
small: defaultStore.reactiveState.statusbarSize.value === 'small',
medium: defaultStore.reactiveState.statusbarSize.value === 'medium',
large: defaultStore.reactiveState.statusbarSize.value === 'large'
large: defaultStore.reactiveState.statusbarSize.value === 'large',
veryLarge: defaultStore.reactiveState.statusbarSize.value === 'veryLarge',
}"
>
<div v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="{ black: x.black }">
@ -46,6 +47,11 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
font-size: 0.875em;
}
&.veryLarge {
--height: 30px;
font-size: 0.9em;
}
> .item {
display: inline-flex;
vertical-align: bottom;

View file

@ -1,32 +1,37 @@
<template>
<div
class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
class="mk-deck" :class="[{ isMobile }]"
>
<XSidebar v-if="!isMobile"/>
<div class="main">
<XStatusBars class="statusbars"/>
<div ref="columnsEl" class="columns" @contextmenu.self.prevent="onContextmenu">
<template v-for="ids in layout">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-if="ids.length > 1"
class="folder column"
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
>
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
</section>
<DeckColumnCore
v-else
:ref="ids[0]"
:key="ids[0]"
class="column"
:column="columns.find(c => c.id === ids[0])"
:is-stacked="false"
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
@parent-focus="moveFocus(ids[0], $event)"
/>
</template>
<div class="columnsWrapper">
<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
<template v-for="ids in layout">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-if="ids.length > 1"
class="folder column"
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
>
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
</section>
<DeckColumnCore
v-else
:ref="ids[0]"
:key="ids[0]"
class="column"
:column="columns.find(c => c.id === ids[0])"
:is-stacked="false"
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
@parent-focus="moveFocus(ids[0], $event)"
/>
</template>
</div>
<div class="sideMenu">
<button class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
</div>
</div>
</div>
@ -183,22 +188,14 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
// TODO:
--margin: var(--marginHalf);
--deckDividerThickness: 5px;
display: flex;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
flex: 1;
&.center {
> .column:first-of-type {
margin-left: auto;
}
> .column:last-of-type {
margin-right: auto;
}
}
&.isMobile {
padding-bottom: 100px;
}
@ -209,24 +206,55 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
display: flex;
flex-direction: column;
> .columns {
display: flex;
> .columnsWrapper {
flex: 1;
padding: var(--deckMargin);
overflow-x: auto;
overflow-y: clip;
display: flex;
flex-direction: row;
> .column {
flex-shrink: 0;
margin-right: var(--deckMargin);
> .columns {
flex: 1;
display: flex;
overflow-x: auto;
overflow-y: clip;
&.folder {
display: flex;
flex-direction: column;
> *:not(:last-child) {
margin-bottom: var(--deckMargin);
&.center {
> .column:first-of-type {
margin-left: auto;
}
> .column:last-of-type {
margin-right: auto;
}
}
> .column {
flex-shrink: 0;
border-right: solid var(--deckDividerThickness) var(--deckDivider);
&:first-child {
border-left: solid var(--deckDividerThickness) var(--deckDivider);
}
&.folder {
display: flex;
flex-direction: column;
> *:not(:last-child) {
border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
}
}
}
}
> .sideMenu {
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
> .button {
width: 100%;
aspect-ratio: 1;
}
}
}

View file

@ -1,13 +1,14 @@
<template>
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section v-hotkey="keymap" class="dnpfarvg _panel _narrow_"
<section
v-hotkey="keymap" class="dnpfarvg _narrow_"
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
@dragover.prevent.stop="onDragover"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
>
<header :class="{ indicated }"
<header
:class="{ indicated }"
draggable="true"
@click="goTop"
@dragstart="onDragstart"
@ -22,7 +23,7 @@
<slot name="action"></slot>
</div>
<span class="header"><slot name="header"></slot></span>
<button v-if="func" v-tooltip="func.title" class="menu _button" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-cog"></i></button>
</header>
<div v-show="active" ref="body">
<slot></slot>
@ -39,9 +40,8 @@ export type DeckFunc = {
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
import * as os from '@/os';
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store';
import { deckStore } from './deck-store';
import { i18n } from '@/i18n';
provide('shouldHeaderThin', true);
@ -105,7 +105,7 @@ function onOtherDragEnd() {
function toggleActive() {
if (!props.isStacked) return;
updateColumn(props.column.id, {
active: !props.column.active
active: !props.column.active,
});
}
@ -118,69 +118,83 @@ function getMenu() {
name: {
type: 'string',
label: i18n.ts.name,
default: props.column.name
default: props.column.name,
},
width: {
type: 'number',
label: i18n.ts.width,
default: props.column.width
default: props.column.width,
},
flexible: {
type: 'boolean',
label: i18n.ts.flexible,
default: props.column.flexible
}
default: props.column.flexible,
},
});
if (canceled) return;
updateColumn(props.column.id, result);
}
},
}, null, {
icon: 'fas fa-arrow-left',
text: i18n.ts._deck.swapLeft,
action: () => {
swapLeftColumn(props.column.id);
}
},
}, {
icon: 'fas fa-arrow-right',
text: i18n.ts._deck.swapRight,
action: () => {
swapRightColumn(props.column.id);
}
},
}, props.isStacked ? {
icon: 'fas fa-arrow-up',
text: i18n.ts._deck.swapUp,
action: () => {
swapUpColumn(props.column.id);
}
},
} : undefined, props.isStacked ? {
icon: 'fas fa-arrow-down',
text: i18n.ts._deck.swapDown,
action: () => {
swapDownColumn(props.column.id);
}
},
} : undefined, null, {
icon: 'fas fa-window-restore',
text: i18n.ts._deck.stackLeft,
action: () => {
stackLeftColumn(props.column.id);
}
},
}, props.isStacked ? {
icon: 'fas fa-window-maximize',
text: i18n.ts._deck.popRight,
action: () => {
popRightColumn(props.column.id);
}
},
} : undefined, null, {
icon: 'fas fa-trash-alt',
text: i18n.ts.remove,
danger: true,
action: () => {
removeColumn(props.column.id);
}
},
}];
if (props.func) {
items.unshift(null);
items.unshift({
icon: props.func.icon,
text: props.func.title,
action: props.func.handler,
});
}
return items;
}
function showSettingsMenu(ev: MouseEvent) {
os.popupMenu(getMenu(), ev.currentTarget ?? ev.target);
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev);
}
@ -188,7 +202,7 @@ function onContextmenu(ev: MouseEvent) {
function goTop() {
body.scrollTo({
top: 0,
behavior: 'smooth'
behavior: 'smooth',
});
}
@ -239,15 +253,13 @@ function onDrop(ev) {
<style lang="scss" scoped>
.dnpfarvg {
--root-margin: 10px;
--deckColumnHeaderHeight: 42px;
height: 100%;
overflow: hidden;
contain: content;
box-shadow: 0 0 8px 0 var(--shadow);
contain: strict;
&.draghover {
box-shadow: 0 0 0 2px var(--focus);
&:after {
content: "";
display: block;
@ -262,7 +274,18 @@ function onDrop(ev) {
}
&.dragging {
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);
opacity: 0.5;
}
}
&.dropready {

View file

@ -54,14 +54,6 @@ export const deckStore = markRaw(new Storage('deck', {
where: 'deviceAccount',
default: true,
},
columnMargin: {
where: 'deviceAccount',
default: 16,
},
columnHeaderHeight: {
where: 'deviceAccount',
default: 42,
},
}));
export const loadDeck = async () => {