windows/components/DesktopIcon.vue

208 lines
4.6 KiB
Vue
Raw Permalink Normal View History

2025-09-25 05:38:59 +00:00
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
2025-09-25 09:21:54 +00:00
import { useI18n } from 'vue-i18n';
2025-09-25 05:38:59 +00:00
import type { AppInfo } from '../stores/apps';
import { useAppsStore } from '../stores/apps';
import { useDraggable } from '../composables/useDraggable';
interface DesktopIconProps {
app: AppInfo;
x: number;
y: number;
onPositionChange: (x: number, y: number) => void;
}
const props = defineProps<DesktopIconProps>();
const emit = defineEmits(['launch']);
2025-09-25 09:21:54 +00:00
const { t } = useI18n();
2025-09-25 05:38:59 +00:00
const appsStore = useAppsStore();
const iconRef = ref<HTMLElement | null>(null);
const isDragging = ref(false);
const isSelected = ref(false);
const position = ref({ x: props.x, y: props.y });
// Desktop boundaries (excluding taskbar)
const desktopBounds = computed(() => {
if (typeof window === 'undefined') {
return { top: 48, left: 0, right: 800, bottom: 600 }; // Default values for SSR
}
return {
top: 48, // Below taskbar
left: 0,
right: window.innerWidth - 80, // Icon width
bottom: window.innerHeight - 80 // Icon height
};
});
const updateBounds = () => {
// This will be called when window resizes
};
onMounted(() => {
if (typeof window !== 'undefined') {
window.addEventListener('resize', updateBounds);
updateBounds();
}
});
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', updateBounds);
}
});
// Use draggable composable
useDraggable(iconRef, {
position: position,
onDrag: (x, y) => {
// Constrain to desktop bounds
const bounds = desktopBounds.value;
const constrainedX = Math.max(bounds.left, Math.min(x, bounds.right));
const constrainedY = Math.max(bounds.top, Math.min(y, bounds.bottom));
position.value = { x: constrainedX, y: constrainedY };
},
onDragStart: () => {
isDragging.value = true;
isSelected.value = true;
},
onDragEnd: () => {
isDragging.value = false;
// Update parent with new position
props.onPositionChange(position.value.x, position.value.y);
},
constraints: computed(() => desktopBounds.value),
targetSize: computed(() => ({ width: 80, height: 80 })),
enabled: ref(true),
});
const iconStyle = computed(() => ({
left: `${position.value.x}px`,
top: `${position.value.y}px`,
}));
const iconClasses = computed(() => ({
'desktop-icon': true,
'is-dragging': isDragging.value,
'is-selected': isSelected.value,
}));
function handleClick() {
if (!isDragging.value) {
isSelected.value = true;
emit('launch', props.app.id);
}
}
function handleDoubleClick() {
emit('launch', props.app.id);
}
// Click outside to deselect
function handleDocumentClick(event: MouseEvent) {
if (iconRef.value && !iconRef.value.contains(event.target as Node)) {
isSelected.value = false;
}
}
onMounted(() => {
if (typeof document !== 'undefined') {
document.addEventListener('click', handleDocumentClick);
}
});
onUnmounted(() => {
if (typeof document !== 'undefined') {
document.removeEventListener('click', handleDocumentClick);
}
});
</script>
<template>
<div
ref="iconRef"
class="desktop-icon"
:class="iconClasses"
:style="iconStyle"
@click="handleClick"
@dblclick="handleDoubleClick"
>
<div class="icon-container">
<div class="icon-image">{{ app.icon }}</div>
2025-09-25 09:21:54 +00:00
<div class="icon-label">{{ t(`apps.${app.name}`) }}</div>
2025-09-25 05:38:59 +00:00
</div>
</div>
</template>
<style scoped>
.desktop-icon {
position: absolute;
width: 80px;
height: 80px;
cursor: pointer;
user-select: none;
transition: transform 0.1s ease;
}
.desktop-icon:hover {
transform: scale(1.05);
}
.desktop-icon.is-dragging {
transform: scale(1.1);
z-index: 1000;
transition: none;
}
.desktop-icon.is-selected .icon-container {
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
.icon-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.icon-image {
font-size: 32px;
margin-bottom: 4px;
text-align: center;
line-height: 1;
}
.icon-label {
font-size: 11px;
color: white;
text-align: center;
line-height: 1.2;
word-break: break-word;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
max-width: 100%;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Light theme adjustments */
.theme-light .icon-label {
color: #333;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
}
.theme-light .desktop-icon.is-selected .icon-container {
background: rgba(0, 0, 0, 0.1);
}
</style>