208 lines
4.6 KiB
Vue
208 lines
4.6 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
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']);
|
|
|
|
const { t } = useI18n();
|
|
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>
|
|
<div class="icon-label">{{ t(`apps.${app.name}`) }}</div>
|
|
</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>
|