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: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ" columnAlign: "カラムの寄せ"
columnMargin: "カラム間のマージン"
columnHeaderHeight: "カラムのヘッダー幅"
addColumn: "カラムを追加" addColumn: "カラムを追加"
swapLeft: "左に移動" swapLeft: "左に移動"
swapRight: "右に移動" swapRight: "右に移動"

View file

@ -10,18 +10,6 @@
<option value="center">{{ i18n.ts.center }}</option> <option value="center">{{ i18n.ts.center }}</option>
</FormRadios> </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> <FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div> </div>
</template> </template>
@ -41,8 +29,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow')); const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
const profile = computed(deckStore.makeGetterSetter('profile')); const profile = computed(deckStore.makeGetterSetter('profile'));
watch(navWindow, async () => { watch(navWindow, async () => {

View file

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

View file

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

View file

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

View file

@ -4,7 +4,8 @@
verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall', verySmall: defaultStore.reactiveState.statusbarSize.value === 'verySmall',
small: defaultStore.reactiveState.statusbarSize.value === 'small', small: defaultStore.reactiveState.statusbarSize.value === 'small',
medium: defaultStore.reactiveState.statusbarSize.value === 'medium', 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 }"> <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; font-size: 0.875em;
} }
&.veryLarge {
--height: 30px;
font-size: 0.9em;
}
> .item { > .item {
display: inline-flex; display: inline-flex;
vertical-align: bottom; vertical-align: bottom;

View file

@ -1,32 +1,37 @@
<template> <template>
<div <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"/> <XSidebar v-if="!isMobile"/>
<div class="main"> <div class="main">
<XStatusBars class="statusbars"/> <XStatusBars class="statusbars"/>
<div ref="columnsEl" class="columns" @contextmenu.self.prevent="onContextmenu"> <div class="columnsWrapper">
<template v-for="ids in layout"> <div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため --> <template v-for="ids in layout">
<section <!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
v-if="ids.length > 1" <section
class="folder column" v-if="ids.length > 1"
: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' }" 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-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
<DeckColumnCore </section>
v-else <DeckColumnCore
:ref="ids[0]" v-else
:key="ids[0]" :ref="ids[0]"
class="column" :key="ids[0]"
:column="columns.find(c => c.id === ids[0])" class="column"
:is-stacked="false" :column="columns.find(c => c.id === ids[0])"
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }" :is-stacked="false"
@parent-focus="moveFocus(ids[0], $event)" :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> />
</template>
</div>
<div class="sideMenu">
<button class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
</div>
</div> </div>
</div> </div>
@ -183,22 +188,14 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
// TODO: // TODO:
--margin: var(--marginHalf); --margin: var(--marginHalf);
--deckDividerThickness: 5px;
display: flex; display: flex;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ // 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100); height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box; box-sizing: border-box;
flex: 1; flex: 1;
&.center {
> .column:first-of-type {
margin-left: auto;
}
> .column:last-of-type {
margin-right: auto;
}
}
&.isMobile { &.isMobile {
padding-bottom: 100px; padding-bottom: 100px;
} }
@ -209,24 +206,55 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> .columns { > .columnsWrapper {
display: flex;
flex: 1; flex: 1;
padding: var(--deckMargin); display: flex;
overflow-x: auto; flex-direction: row;
overflow-y: clip;
> .column { > .columns {
flex-shrink: 0; flex: 1;
margin-right: var(--deckMargin); display: flex;
overflow-x: auto;
overflow-y: clip;
&.folder { &.center {
display: flex; > .column:first-of-type {
flex-direction: column; margin-left: auto;
> *:not(:last-child) {
margin-bottom: var(--deckMargin);
} }
> .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> <template>
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため --> <!-- 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 }" :class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
@dragover.prevent.stop="onDragover" @dragover.prevent.stop="onDragover"
@dragleave="onDragleave" @dragleave="onDragleave"
@drop.prevent.stop="onDrop" @drop.prevent.stop="onDrop"
> >
<header :class="{ indicated }" <header
:class="{ indicated }"
draggable="true" draggable="true"
@click="goTop" @click="goTop"
@dragstart="onDragstart" @dragstart="onDragstart"
@ -22,7 +23,7 @@
<slot name="action"></slot> <slot name="action"></slot>
</div> </div>
<span class="header"><slot name="header"></slot></span> <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> </header>
<div v-show="active" ref="body"> <div v-show="active" ref="body">
<slot></slot> <slot></slot>
@ -39,9 +40,8 @@ export type DeckFunc = {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide, watch } from 'vue'; 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 * 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'; import { i18n } from '@/i18n';
provide('shouldHeaderThin', true); provide('shouldHeaderThin', true);
@ -105,7 +105,7 @@ function onOtherDragEnd() {
function toggleActive() { function toggleActive() {
if (!props.isStacked) return; if (!props.isStacked) return;
updateColumn(props.column.id, { updateColumn(props.column.id, {
active: !props.column.active active: !props.column.active,
}); });
} }
@ -118,69 +118,83 @@ function getMenu() {
name: { name: {
type: 'string', type: 'string',
label: i18n.ts.name, label: i18n.ts.name,
default: props.column.name default: props.column.name,
}, },
width: { width: {
type: 'number', type: 'number',
label: i18n.ts.width, label: i18n.ts.width,
default: props.column.width default: props.column.width,
}, },
flexible: { flexible: {
type: 'boolean', type: 'boolean',
label: i18n.ts.flexible, label: i18n.ts.flexible,
default: props.column.flexible default: props.column.flexible,
} },
}); });
if (canceled) return; if (canceled) return;
updateColumn(props.column.id, result); updateColumn(props.column.id, result);
} },
}, null, { }, null, {
icon: 'fas fa-arrow-left', icon: 'fas fa-arrow-left',
text: i18n.ts._deck.swapLeft, text: i18n.ts._deck.swapLeft,
action: () => { action: () => {
swapLeftColumn(props.column.id); swapLeftColumn(props.column.id);
} },
}, { }, {
icon: 'fas fa-arrow-right', icon: 'fas fa-arrow-right',
text: i18n.ts._deck.swapRight, text: i18n.ts._deck.swapRight,
action: () => { action: () => {
swapRightColumn(props.column.id); swapRightColumn(props.column.id);
} },
}, props.isStacked ? { }, props.isStacked ? {
icon: 'fas fa-arrow-up', icon: 'fas fa-arrow-up',
text: i18n.ts._deck.swapUp, text: i18n.ts._deck.swapUp,
action: () => { action: () => {
swapUpColumn(props.column.id); swapUpColumn(props.column.id);
} },
} : undefined, props.isStacked ? { } : undefined, props.isStacked ? {
icon: 'fas fa-arrow-down', icon: 'fas fa-arrow-down',
text: i18n.ts._deck.swapDown, text: i18n.ts._deck.swapDown,
action: () => { action: () => {
swapDownColumn(props.column.id); swapDownColumn(props.column.id);
} },
} : undefined, null, { } : undefined, null, {
icon: 'fas fa-window-restore', icon: 'fas fa-window-restore',
text: i18n.ts._deck.stackLeft, text: i18n.ts._deck.stackLeft,
action: () => { action: () => {
stackLeftColumn(props.column.id); stackLeftColumn(props.column.id);
} },
}, props.isStacked ? { }, props.isStacked ? {
icon: 'fas fa-window-maximize', icon: 'fas fa-window-maximize',
text: i18n.ts._deck.popRight, text: i18n.ts._deck.popRight,
action: () => { action: () => {
popRightColumn(props.column.id); popRightColumn(props.column.id);
} },
} : undefined, null, { } : undefined, null, {
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
text: i18n.ts.remove, text: i18n.ts.remove,
danger: true, danger: true,
action: () => { action: () => {
removeColumn(props.column.id); 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; return items;
} }
function showSettingsMenu(ev: MouseEvent) {
os.popupMenu(getMenu(), ev.currentTarget ?? ev.target);
}
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev); os.contextMenu(getMenu(), ev);
} }
@ -188,7 +202,7 @@ function onContextmenu(ev: MouseEvent) {
function goTop() { function goTop() {
body.scrollTo({ body.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: 'smooth',
}); });
} }
@ -239,15 +253,13 @@ function onDrop(ev) {
<style lang="scss" scoped> <style lang="scss" scoped>
.dnpfarvg { .dnpfarvg {
--root-margin: 10px; --root-margin: 10px;
--deckColumnHeaderHeight: 42px;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
contain: content; contain: strict;
box-shadow: 0 0 8px 0 var(--shadow);
&.draghover { &.draghover {
box-shadow: 0 0 0 2px var(--focus);
&:after { &:after {
content: ""; content: "";
display: block; display: block;
@ -262,7 +274,18 @@ function onDrop(ev) {
} }
&.dragging { &.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 { &.dropready {

View file

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