From 44a4faebc00c67becba925bd4bb928b941cde91a Mon Sep 17 00:00:00 2001
From: remi <768.jac@gmail.com>
Date: Sat, 8 Apr 2023 05:55:05 +0200
Subject: [PATCH] feat: add minimize/Fold button for windows (#10508)

* add window minimizing!

* Fix window being able to go offscreen

* Revert en-US.yml changes

---------

Co-authored-by: mothmoon <remilia@remilia.se>
---
 CHANGELOG.md                                  |  1 +
 locales/ja-JP.yml                             |  1 +
 packages/frontend/src/components/MkWindow.vue | 64 +++++++++++++------
 3 files changed, 48 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e701d84bbf..3fce3195ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
 		  - 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
 			- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
 	- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
+- Add Minimizing ("folding") of windows
 
 ### Server
 - イベント用Redisを別サーバーに分離できるように
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 66b591760c..04e4d52b28 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -920,6 +920,7 @@ pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知
 sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
 sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
 windowMaximize: "最大化"
+windowMinimize: "最小化"
 windowRestore: "元に戻す"
 caption: "キャプション"
 loggedInAsBot: "Botアカウントでログイン中"
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 30b5391e9a..1afcb104e9 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -11,15 +11,17 @@
 		<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
 			<div :class="[$style.header, { [$style.mini]: mini }]" @contextmenu.prevent.stop="onContextmenu">
 				<span :class="$style.headerLeft">
-					<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
+					<button v-for="button in buttonsLeft" v-if="!minimized" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
 				</span>
 				<span :class="$style.headerTitle" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
 					<slot name="header"></slot>
 				</span>
 				<span :class="$style.headerRight">
-					<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
+					<button v-for="button in buttonsRight" v-if="!minimized" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
+					<button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ti ti-maximize"></i></button>
+					<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ti ti-minimize"></i></button>
 					<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
-					<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
+					<button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
 					<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
 				</span>
 			</div>
@@ -27,7 +29,7 @@
 				<slot></slot>
 			</div>
 		</div>
-		<template v-if="canResize">
+		<template v-if="canResize && !minimized">
 			<div :class="$style.handleTop" @mousedown.prevent="onTopHandleMousedown"></div>
 			<div :class="$style.handleRight" @mousedown.prevent="onRightHandleMousedown"></div>
 			<div :class="$style.handleBottom" @mousedown.prevent="onBottomHandleMousedown"></div>
@@ -100,10 +102,11 @@ let rootEl = $shallowRef<HTMLElement | null>();
 let showing = $ref(true);
 let beforeClickedAt = 0;
 let maximized = $ref(false);
-let unMaximizedTop = '';
-let unMaximizedLeft = '';
-let unMaximizedWidth = '';
-let unMaximizedHeight = '';
+let minimized = $ref(false);
+let unResizedTop = '';
+let unResizedLeft = '';
+let unResizedWidth = '';
+let unResizedHeight = '';
 
 function close() {
 	showing = false;
@@ -132,10 +135,10 @@ function top() {
 
 function maximize() {
 	maximized = true;
-	unMaximizedTop = rootEl.style.top;
-	unMaximizedLeft = rootEl.style.left;
-	unMaximizedWidth = rootEl.style.width;
-	unMaximizedHeight = rootEl.style.height;
+	unResizedTop = rootEl.style.top;
+	unResizedLeft = rootEl.style.left;
+	unResizedWidth = rootEl.style.width;
+	unResizedHeight = rootEl.style.height;
 	rootEl.style.top = '0';
 	rootEl.style.left = '0';
 	rootEl.style.width = '100%';
@@ -144,10 +147,35 @@ function maximize() {
 
 function unMaximize() {
 	maximized = false;
-	rootEl.style.top = unMaximizedTop;
-	rootEl.style.left = unMaximizedLeft;
-	rootEl.style.width = unMaximizedWidth;
-	rootEl.style.height = unMaximizedHeight;
+	rootEl.style.top = unResizedTop;
+	rootEl.style.left = unResizedLeft;
+	rootEl.style.width = unResizedWidth;
+	rootEl.style.height = unResizedHeight;
+}
+
+function minimize() {
+	minimized = true;
+	unResizedWidth = rootEl.style.width;
+	unResizedHeight = rootEl.style.height;
+	rootEl.style.width = minWidth + 'px';
+	rootEl.style.height = props.mini ? '32px' : '39px';
+}
+
+function unMinimize() {
+	const main = rootEl;
+	if (main == null) return;
+
+	minimized = false;
+	rootEl.style.width = unResizedWidth;
+	rootEl.style.height = unResizedHeight;
+	const browserWidth = window.innerWidth;
+	const browserHeight = window.innerHeight;
+	const windowWidth = main.offsetWidth;
+	const windowHeight = main.offsetHeight;
+
+	const position = main.getBoundingClientRect();
+	if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';
+	if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';
 }
 
 function onBodyMousedown() {
@@ -155,7 +183,7 @@ function onBodyMousedown() {
 }
 
 function onDblClick() {
-	maximize();
+	if (minimized) {unMinimize();} else {maximize();}
 }
 
 function onHeaderMousedown(evt: MouseEvent) {
@@ -187,7 +215,7 @@ function onHeaderMousedown(evt: MouseEvent) {
 
 	const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
 	const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
-	const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
+	const moveBaseX = beforeMaximized ? parseInt(unResizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
 	const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
 	const browserWidth = window.innerWidth;
 	const browserHeight = window.innerHeight;