windows/components/Window.vue

253 lines
7.7 KiB
Vue

<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue';
import type { WindowState } from '../stores/windows';
import { useWindowsStore } from '../stores/windows';
import { useDraggable } from '../composables/useDraggable';
import type { SnapType } from '../composables/useDraggable';
import { useResizable } from '../composables/useResizable';
import { useBreakpoint } from '../composables/useBreakpoint';
const props = defineProps<{ window: WindowState }>();
const emit = defineEmits(['snap-preview', 'snap-execute']);
const store = useWindowsStore();
const { focusWindow, closeWindow, minimizeWindow, toggleMaximize, updateWindowPosition, updateWindowSize } = store;
const { isMobile } = useBreakpoint();
const windowRef = ref<HTMLElement | null>(null);
const titleBarRef = ref<HTMLElement | null>(null);
const isDragging = ref(false);
const isResizing = ref(false);
const isDraggableAndResizable = computed(() => !isMobile.value);
const position = computed(() => ({ x: props.window.x, y: props.window.y }));
const windowDimensions = computed(() => ({ width: props.window.width, height: props.window.height }));
const draggableConstraints = ref({ top: 48, right: window.innerWidth, bottom: window.innerHeight, left: 0 });
const updateConstraints = () => {
draggableConstraints.value = { top: 48, right: window.innerWidth, bottom: window.innerHeight, left: 0 };
};
onMounted(() => {
window.addEventListener('resize', updateConstraints);
updateConstraints();
});
onUnmounted(() => {
window.removeEventListener('resize', updateConstraints);
});
useDraggable(titleBarRef, {
position: position,
onDrag: (x, y) => {
if (!props.window.isMaximized) {
updateWindowPosition({ id: props.window.id, x, y });
}
},
onDragStart: () => { isDragging.value = true; },
onDragEnd: (snapType) => {
isDragging.value = false;
emit('snap-execute', { windowId: props.window.id, snapType });
},
onSnap: (snapType: SnapType) => {
emit('snap-preview', snapType);
},
constraints: draggableConstraints,
targetSize: windowDimensions,
enabled: isDraggableAndResizable,
});
useResizable(windowRef, {
initialSize: { width: props.window.width, height: props.window.height },
initialPosition: { x: props.window.x, y: props.window.y },
onResize: ({ x, y, width, height }) => {
if (!props.window.isMaximized) {
updateWindowPosition({ id: props.window.id, x, y });
updateWindowSize({ id: props.window.id, width, height });
}
},
onResizeStart: () => { isResizing.value = true; },
onResizeEnd: () => { isResizing.value = false; },
constraints: draggableConstraints,
enabled: isDraggableAndResizable,
});
const windowStyle = computed(() => ({
left: `${props.window.x}px`,
top: `${props.window.y}px`,
width: `${props.window.width}px`,
height: `${props.window.height}px`,
zIndex: props.window.zIndex,
}));
const windowClasses = computed(() => ({
'is-maximized': props.window.isMaximized,
'is-minimized': props.window.isMinimized,
'is-focused': props.window.isFocused,
'is-dragging': isDragging.value,
'is-resizing': isResizing.value,
'is-mobile': isMobile.value,
}));
function onMouseDown() {
focusWindow(props.window.id);
}
</script>
<template>
<div
ref="windowRef"
class="window-container"
:style="windowStyle"
:class="windowClasses"
@mousedown="onMouseDown"
>
<div class="title-bar" ref="titleBarRef">
<div class="title">{{ window.title }}</div>
<div class="controls">
<button @click.stop="minimizeWindow(window.id)" class="control-btn minimize" :title="$t('common.minimize')">_</button>
<button @click.stop="toggleMaximize(window.id)" class="control-btn maximize" :title="$t('common.maximize')">[]</button>
<button @click.stop="closeWindow(window.id)" class="control-btn close" :title="$t('common.close')">X</button>
</div>
</div>
<div class="content">
<p>ID: {{ window.id }}</p>
<p>X: {{ window.x.toFixed(0) }}, Y: {{ window.y.toFixed(0) }}</p>
<p>W: {{ window.width.toFixed(0) }}, H: {{ window.height.toFixed(0) }}</p>
<p>Z: {{ window.zIndex }}</p>
<p>Focused: {{ window.isFocused }}</p>
</div>
<div v-if="isDraggableAndResizable">
<div class="resizer" data-direction="n"></div>
<div class="resizer" data-direction="ne"></div>
<div class="resizer" data-direction="e"></div>
<div class="resizer" data-direction="se"></div>
<div class="resizer" data-direction="s"></div>
<div class="resizer" data-direction="sw"></div>
<div class="resizer" data-direction="w"></div>
<div class="resizer" data-direction="nw"></div>
</div>
</div>
</template>
<style scoped>
.window-container {
position: absolute;
background: var(--window-background);
border-radius: var(--rounded-window);
box-shadow: var(--shadow-window);
display: flex;
flex-direction: column;
transition: opacity 0.2s ease, transform 0.2s ease, width 0.2s ease, height 0.2s ease, left 0.2s ease, top 0.2s ease;
border: 1px solid var(--window-border-color);
}
.window-container.is-mobile {
left: 0 !important;
top: 48px !important; /* Adjusted for top taskbar */
width: 100vw !important;
height: calc(100vh - 48px) !important;
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
}
.window-container.is-dragging,
.window-container.is-resizing {
transition: none !important;
}
.window-container.is-focused {
border-color: var(--window-border-color-focused);
}
.window-container.is-maximized {
left: 0 !important;
top: 48px !important; /* Adjusted for top taskbar */
width: 100vw !important;
height: calc(100vh - 48px) !important;
border-radius: 0;
transition: width 0.2s ease, height 0.2s ease;
}
.window-container.is-minimized {
opacity: 0;
transform: translateY(200px);
pointer-events: none;
}
.title-bar {
height: 36px;
background: var(--title-bar-background);
color: var(--title-bar-text-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
flex-shrink: 0;
border-top-left-radius: var(--rounded-window);
border-top-right-radius: var(--rounded-window);
}
.window-container.is-maximized .title-bar,
.window-container.is-mobile .title-bar {
border-radius: 0;
}
.title {
font-weight: bold;
user-select: none;
}
.controls {
display: flex;
gap: 8px;
}
.control-btn {
width: 20px;
height: 20px;
border: none;
border-radius: var(--rounded-control-btn);
display: flex;
justify-content: center;
align-items: center;
color: var(--title-bar-text-color);
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.minimize { background: var(--control-btn-minimize-bg); }
.maximize { background: var(--control-btn-maximize-bg); }
.close { background: var(--control-btn-close-bg); }
.content {
flex-grow: 1;
padding: 16px;
color: var(--content-text-color);
overflow: auto;
}
.resizer {
position: absolute;
z-index: 10;
}
.resizer[data-direction="n"] { top: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize; }
.resizer[data-direction="s"] { bottom: -4px; left: 4px; right: 4px; height: 8px; cursor: ns-resize; }
.resizer[data-direction="e"] { top: 4px; right: -4px; bottom: 4px; width: 8px; cursor: ew-resize; }
.resizer[data-direction="w"] { top: 4px; left: -4px; bottom: 4px; width: 8px; cursor: ew-resize; }
.resizer[data-direction="ne"] { top: -4px; right: -4px; width: 12px; height: 12px; cursor: nesw-resize; }
.resizer[data-direction="sw"] { bottom: -4px; left: -4px; width: 12px; height: 12px; cursor: nesw-resize; }
.resizer[data-direction="nw"] { top: -4px; left: -4px; width: 12px; height: 12px; cursor: nwse-resize; }
.resizer[data-direction="se"] { bottom: -4px; right: -4px; width: 12px; height: 12px; cursor: nwse-resize; }
</style>