feat(WIP): 턴 조작기 고급 기능

- 잘라내기, 복사하기, 붙여넣기
- 반복하기
- 비우기
- 지우고 당기기, 뒤로 밀기
This commit is contained in:
2022-03-23 20:12:59 +09:00
parent bcf52e0460
commit 39ac64b837
3 changed files with 252 additions and 37 deletions
+248 -33
View File
@@ -32,6 +32,11 @@
<div class="commandSelectFormAnchor"> <div class="commandSelectFormAnchor">
<div v-if="isEditMode" class="row gx-0"> <div v-if="isEditMode" class="row gx-0">
<div class="col-4 d-grid">
<BDropdown left text="보관함">
<!-- 즐겨찾기 모드 -->
</BDropdown>
</div>
<div class="col-3 d-grid"> <div class="col-3 d-grid">
<BDropdown left text="범위"> <BDropdown left text="범위">
<BDropdownItem @click="selectTurn()">해제</BDropdownItem> <BDropdownItem @click="selectTurn()">해제</BDropdownItem>
@@ -54,37 +59,33 @@
</BDropdownText> </BDropdownText>
</BDropdown> </BDropdown>
</div> </div>
<div class="col-4 d-grid">
<BDropdown left text="보관함">
<!-- 즐겨찾기 모드 -->
</BDropdown>
</div>
<div class="col-5 d-grid"> <div class="col-5 d-grid">
<BDropdown right variant="info" text="선택한 턴을"> <BDropdown right variant="info" text="선택한 턴을">
<BDropdownItem> <BDropdownItem @click="clipboardCut">
<i class="bi bi-scissors"></i>&nbsp;잘라내기 <i class="bi bi-scissors"></i>&nbsp;잘라내기
</BDropdownItem> </BDropdownItem>
<BDropdownItem> <BDropdownItem @click="clipboardCopy">
<i class="bi bi-files"></i>&nbsp;복사하기 <i class="bi bi-files"></i>&nbsp;복사하기
</BDropdownItem> </BDropdownItem>
<BDropdownItem> <BDropdownItem @click="clipboardPaste">
<i class="bi bi-clipboard-fill"></i>&nbsp;붙여넣기 <i class="bi bi-clipboard-fill"></i>&nbsp;붙여넣기
</BDropdownItem> </BDropdownItem>
<BDropdownDivider /> <BDropdownDivider />
<BDropdownItem> <BDropdownItem>
<i class="bi bi-bookmark-plus-fill"></i>&nbsp;보관하기 <i class="bi bi-bookmark-plus-fill"></i>&nbsp;보관하기
</BDropdownItem> </BDropdownItem>
<BDropdownItem> <BDropdownItem @click="subRepeatCommand">
<i class="bi bi-arrow-repeat"></i>&nbsp;반복하기 <i class="bi bi-arrow-repeat"></i>&nbsp;반복하기
</BDropdownItem> </BDropdownItem>
<BDropdownDivider /> <BDropdownDivider />
<BDropdownItem> <BDropdownItem @click="eraseSelectedTurnList">
<i class="bi bi-eraser"></i>&nbsp;비우기 <i class="bi bi-eraser"></i>&nbsp;비우기
</BDropdownItem> </BDropdownItem>
<BDropdownItem> <BDropdownItem @click="eraseAndPullCommand">
<i class="bi bi-arrow-bar-up"></i>&nbsp;지우고 당기기 <i class="bi bi-arrow-bar-up"></i>&nbsp;지우고 당기기
</BDropdownItem> </BDropdownItem>
<BDropdownItem> <BDropdownItem @click="pushEmptyCommand">
<i class="bi bi-arrow-bar-down"></i>&nbsp;뒤로 밀기 <i class="bi bi-arrow-bar-down"></i>&nbsp;뒤로 밀기
</BDropdownItem> </BDropdownItem>
<!-- 최근에 실행한 10 --> <!-- 최근에 실행한 10 -->
@@ -263,11 +264,7 @@ selectTurn(...$event);
</BDropdown> </BDropdown>
</div> </div>
<div class="col d-grid"> <div class="col d-grid">
<BButton @click="toggleViewMaxTurn"> <BButton @click="toggleViewMaxTurn">{{ flippedMaxTurn == viewMaxTurn ? "펼치기" : "접기" }}</BButton>
{{
flippedMaxTurn == viewMaxTurn ? "펼치기" : "접기"
}}
</BButton>
</div> </div>
</div> </div>
</div> </div>
@@ -302,7 +299,7 @@ declare const staticValues: {
<script lang="ts" setup> <script lang="ts" setup>
import addMilliseconds from "date-fns/esm/addMilliseconds"; import addMilliseconds from "date-fns/esm/addMilliseconds";
import addMinutes from "date-fns/esm/addMinutes"; import addMinutes from "date-fns/esm/addMinutes";
import { isString, range } from "lodash"; import { clone, isString, min, range } from "lodash";
import { stringifyUrl } from "query-string"; import { stringifyUrl } from "query-string";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import { formatTime } from "@util/formatTime"; import { formatTime } from "@util/formatTime";
@@ -317,6 +314,7 @@ import CommandSelectForm from "@/components/CommandSelectForm.vue";
import { BButton, BButtonGroup, BDropdownItem, BDropdown, BDropdownText, BDropdownDivider } from "bootstrap-vue-3"; import { BButton, BButtonGroup, BDropdownItem, BDropdown, BDropdownText, BDropdownDivider } from "bootstrap-vue-3";
import { StoredActionsHelper } from "./util/StoredActionsHelper"; import { StoredActionsHelper } from "./util/StoredActionsHelper";
import type { TurnObj } from '@/defs'; import type { TurnObj } from '@/defs';
import { unwrap } from "./util/unwrap";
type TurnObjWithTime = TurnObj & { type TurnObjWithTime = TurnObj & {
time: string; time: string;
@@ -645,17 +643,28 @@ async function reserveCommandDirect(args: [number[], TurnObj][], reload = true):
return success; return success;
} }
async function reserveCommand() { function getSelectedTurnList(): number[] {
let reqTurnList: number[]; if (turnList.value.size) {
if (turnList.value.size == 0) { return Array.from(turnList.value);
reqTurnList = Array.from(prevTurnList.value.values());
} else {
reqTurnList = Array.from(turnList.value.values());
} }
if (prevTurnList.value.size) {
return Array.from(prevTurnList.value);
}
return [0];
}
if (reqTurnList.length == 0) { function releaseSelectedTurnList() {
reqTurnList.push(0); if (turnList.value.size > 0) {
prevTurnList.value.clear();
for (const v of turnList.value) {
prevTurnList.value.add(v);
}
turnList.value.clear();
} }
}
async function reserveCommand() {
let reqTurnList: number[] = getSelectedTurnList();
const commandName = selectedCommand.value.value; const commandName = selectedCommand.value.value;
@@ -682,13 +691,9 @@ async function reserveCommand() {
arg: {} arg: {}
}); });
if (turnList.value.size > 0) { releaseSelectedTurnList();
prevTurnList.value.clear();
for (const v of turnList.value) {
prevTurnList.value.add(v);
}
turnList.value.clear();
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert(`실패했습니다: ${e}`); alert(`실패했습니다: ${e}`);
@@ -738,11 +743,221 @@ watch(isEditMode, newEditMode => {
} }
}); });
const emptyTurnObj: TurnObj = { action: '휴식', brief: '휴식', arg: {} };
const storedActionsHelper = new StoredActionsHelper(staticValues.serverNick, 'general'); const storedActionsHelper = new StoredActionsHelper(staticValues.serverNick, 'general');
const recentActions = storedActionsHelper.recentActions; const recentActions = storedActionsHelper.recentActions;
const storedActions = storedActionsHelper.storedActions; const storedActions = storedActionsHelper.storedActions;
async function eraseSelectedTurnList(releaseSelect = true): Promise<boolean> {
const result = await reserveCommandDirect([[
getSelectedTurnList(),
emptyTurnObj
]]);
if (releaseSelect) {
releaseSelectedTurnList();
}
return result;
}
function refineQueryActions(): [number[], TurnObj][] {
const reqTurnList = getSelectedTurnList();
const selectedMinTurnIdx = unwrap(min<number>(reqTurnList));
const buffer: [number[], TurnObj][] = [];
for (const rawTurnIdx of reqTurnList) {
const turnIdx = rawTurnIdx - selectedMinTurnIdx;
const rawAction = reservedCommandList.value[rawTurnIdx]
buffer.push([[turnIdx], {
action: rawAction.action,
arg: clone(rawAction.arg),
brief: rawAction.brief
}]);
}
return buffer;
}
function amplifyQueryActions(rawActions: [number[], TurnObj][], reqTurnList: number[]): [number[], TurnObj][] {
if (reqTurnList.length < 1) {
return [];
}
let minQueryIdx = maxTurn;
let maxQueryIdx = 0;
for (const [turnList] of rawActions) {
for (const turnIdx of turnList) {
minQueryIdx = Math.min(minQueryIdx, turnIdx);
maxQueryIdx = Math.max(maxQueryIdx, turnIdx);
}
}
const queryLength = maxQueryIdx - minQueryIdx + 1;
const queryTurnList: number[] = [reqTurnList[0]];
for (const reqTurnIdx of reqTurnList) {
const last = queryTurnList[queryTurnList.length - 1];
if (reqTurnIdx < last + queryLength) {
continue;
}
queryTurnList.push(reqTurnIdx);
}
const actions: [number[], TurnObj][] = [];
for (const [baseTurnList, action] of rawActions) {
const subTurnList: number[] = [];
for (const baseTurnIdx of baseTurnList) {
for (const queryTurnIdx of queryTurnList) {
const targetTurn = baseTurnIdx + queryTurnIdx;
if (targetTurn >= maxTurn) {
continue;
}
subTurnList.push(baseTurnIdx + queryTurnIdx);
}
}
if (subTurnList.length == 0) {
continue;
}
actions.push([subTurnList, action]);
}
return actions;
}
const clipboard = ref<[number[], TurnObj][] | undefined>(undefined);
async function clipboardCut(releaseSelect = true) {
clipboardCopy(false);
return eraseSelectedTurnList(releaseSelect);
}
function clipboardCopy(releaseSelect = true) {
clipboard.value = refineQueryActions();
if (releaseSelect) {
releaseSelectedTurnList();
}
}
async function clipboardPaste(releaseSelect = true) {
const rawActions = clipboard.value;
if (rawActions === undefined) {
return;
}
const actions = amplifyQueryActions(rawActions, getSelectedTurnList());
if (actions.length === 0) {
return;
}
const result = await reserveCommandDirect(actions);
if (releaseSelect) {
releaseSelectedTurnList();
}
return result;
}
async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
const reqTurnList = getSelectedTurnList().sort();
const selectedMinTurnIdx = reqTurnList[0];
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
const rawActions = refineQueryActions();
const actions = amplifyQueryActions(rawActions, range(selectedMinTurnIdx, maxTurn, queryLength));
const result = await reserveCommandDirect(actions);
if (releaseSelect) {
releaseSelectedTurnList();
}
return result;
}
async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
const reqTurnList = getSelectedTurnList().sort();
const selectedMinTurnIdx = reqTurnList[0];
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
if (selectedMinTurnIdx === 0) {
await pushGeneralCommand(-queryLength);
return true;
}
if (selectedMinTurnIdx + queryLength == maxTurn) {
return eraseSelectedTurnList(releaseSelect);
}
const actions: [number[], TurnObj][] = [];
const emptyTurnList: number[] = [];
for (const srcTurnIdx of range(selectedMinTurnIdx + queryLength, maxTurn)) {
const rawAction = reservedCommandList.value[srcTurnIdx];
if (rawAction.action == emptyTurnObj.action) {
emptyTurnList.push(srcTurnIdx - queryLength);
continue;
}
actions.push([[srcTurnIdx - queryLength], {
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief
}]);
}
emptyTurnList.push(...range(maxTurn - queryLength, maxTurn));
actions.push([emptyTurnList, emptyTurnObj]);
const result = await reserveCommandDirect(actions);
if (releaseSelect) {
releaseSelectedTurnList();
}
return result;
}
async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
const reqTurnList = getSelectedTurnList().sort();
const selectedMinTurnIdx = reqTurnList[0];
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
if (selectedMinTurnIdx === 0) {
await pushGeneralCommand(queryLength);
return true;
}
if (selectedMaxTurnIdx == maxTurn) {
return eraseSelectedTurnList(releaseSelect);
}
const actions: [number[], TurnObj][] = [];
const emptyTurnList: number[] = [];
for (const srcTurnIdx of range(selectedMinTurnIdx, maxTurn - queryLength)) {
const rawAction = reservedCommandList.value[srcTurnIdx];
if (rawAction.action == emptyTurnObj.action) {
emptyTurnList.push(srcTurnIdx + queryLength);
continue;
}
actions.push([[srcTurnIdx + queryLength], {
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief
}]);
}
emptyTurnList.push(...range(selectedMinTurnIdx, selectedMinTurnIdx + queryLength));
actions.push([emptyTurnList, emptyTurnObj]);
const result = await reserveCommandDirect(actions);
if (releaseSelect) {
releaseSelectedTurnList();
}
return result;
}
onMounted(() => { onMounted(() => {
void reloadCommandList(); void reloadCommandList();
}); });
+1 -1
View File
@@ -425,7 +425,7 @@ export default defineComponent({
const emptyTurn: TurnObjWithTime[] = Array.from<TurnObjWithTime>({ const emptyTurn: TurnObjWithTime[] = Array.from<TurnObjWithTime>({
length: this.maxTurn, length: this.maxTurn,
}).fill({ }).fill({
arg: null, arg: {},
brief: "", brief: "",
action: "", action: "",
year: undefined, year: undefined,
+3 -3
View File
@@ -4,7 +4,7 @@ import { ref } from 'vue';
export class StoredActionsHelper { export class StoredActionsHelper {
public readonly recentActions = ref<TurnObj[]>([]); public readonly recentActions = ref<TurnObj[]>([]);
public readonly storedActions = ref(new Map<string, Record<number, TurnObj>>()); public readonly storedActions = ref(new Map<string, [number[], TurnObj][]>());
public readonly recentActionsKey: string; public readonly recentActionsKey: string;
public readonly storedActionsKey: string; public readonly storedActionsKey: string;
@@ -32,11 +32,11 @@ export class StoredActionsHelper {
} }
loadStoredActions() { loadStoredActions() {
const rawValue: [string, Record<number, TurnObj>][] = JSON.parse(localStorage.getItem(this.storedActionsKey) ?? '[]'); const rawValue: [string, [number[], TurnObj][]][] = JSON.parse(localStorage.getItem(this.storedActionsKey) ?? '[]');
this.storedActions.value = new Map(rawValue); this.storedActions.value = new Map(rawValue);
} }
setStoredActions(actionKey: string, actions: Record<number, TurnObj>) { setStoredActions(actionKey: string, actions: [number[], TurnObj][]) {
this.storedActions.value.set(actionKey, actions); this.storedActions.value.set(actionKey, actions);
this.saveStoredActions(); this.saveStoredActions();
} }