From 3e2f2f72b36cf467c89ccffc3d4d3d83a668810d Mon Sep 17 00:00:00 2001 From: Eichenholz Date: Fri, 7 Aug 2026 09:15:20 +0200 Subject: [PATCH] FIX - allow apps to move across the dock --- frontend/src/stores/app-store.test.ts | 3 +- frontend/src/stores/app-store.ts | 4 +- frontend/src/utils/homeLayout.test.ts | 53 ++++++++++++++++++++------ frontend/src/utils/homeLayout.ts | 24 ++++++------ frontend/src/views/SpringboardView.vue | 49 ++++++++++++++++-------- sky_phone/source/html/index.html | 2 +- 6 files changed, 91 insertions(+), 44 deletions(-) diff --git a/frontend/src/stores/app-store.test.ts b/frontend/src/stores/app-store.test.ts index da80147..680d4a1 100644 --- a/frontend/src/stores/app-store.test.ts +++ b/frontend/src/stores/app-store.test.ts @@ -76,7 +76,8 @@ describe('app store', () => { const apps = useAppStoreStore() apps.hydrate(null) - apps.moveHomeApp('mail', 'grid', 'grid', 0) + const mailIndex = apps.homeLayout.grid.indexOf('mail') + apps.moveHomeApp('grid', mailIndex, 'grid', 0) expect(apps.homeLayout.grid[0]).toBe('mail') apps.removeHomeApp('mail') diff --git a/frontend/src/stores/app-store.ts b/frontend/src/stores/app-store.ts index a6835e0..b0e83ec 100644 --- a/frontend/src/stores/app-store.ts +++ b/frontend/src/stores/app-store.ts @@ -94,15 +94,15 @@ export const useAppStoreStore = defineStore('app-store', { this.persist() }, moveHomeApp( - appId: LaunchablePhoneAppId, from: HomeArea, + sourceIndex: number, to: HomeArea, targetIndex: number, ): void { this.homeLayout = moveHomeApp( this.homeLayout, - appId, from, + sourceIndex, to, targetIndex, ) diff --git a/frontend/src/utils/homeLayout.test.ts b/frontend/src/utils/homeLayout.test.ts index c2e9cbe..6fa813a 100644 --- a/frontend/src/utils/homeLayout.test.ts +++ b/frontend/src/utils/homeLayout.test.ts @@ -76,8 +76,32 @@ describe('home layout', () => { expect(layout.dock).toEqual(['messages', null, 'clock', null]) }) + it('preserves independently positioned shortcuts for the same app', () => { + const grid: HomeLayout['grid'] = Array.from( + { length: HOME_GRID_PAGE_SIZE }, + () => null, + ) + grid[0] = 'phone' + grid[5] = 'phone' + + const layout = parseHomeLayout( + { + dock: ['phone', null, null, null], + grid, + hidden: [], + version: 2, + }, + defaults, + [...installed], + ) + + expect(layout.grid[0]).toBe('phone') + expect(layout.grid[5]).toBe('phone') + expect(layout.dock[0]).toBe('phone') + }) + it('moves to an exact empty slot without compacting other apps', () => { - const moved = moveHomeApp(defaults, 'mail', 'grid', 'grid', 12) + const moved = moveHomeApp(defaults, 'grid', 2, 'grid', 12) expect(moved.grid[2]).toBeNull() expect(moved.grid[12]).toBe('mail') @@ -86,7 +110,7 @@ describe('home layout', () => { }) it('shifts occupied grid slots instead of replacing their apps', () => { - const reordered = moveHomeApp(defaults, 'mail', 'grid', 'grid', 0) + const reordered = moveHomeApp(defaults, 'grid', 2, 'grid', 0) expect(reordered.grid.slice(0, 5)).toEqual([ 'mail', 'phone', @@ -97,7 +121,7 @@ describe('home layout', () => { }) it('shifts an occupied dock slot toward its gap', () => { - const docked = moveHomeApp(defaults, 'mail', 'grid', 'dock', 2) + const docked = moveHomeApp(defaults, 'grid', 2, 'dock', 2) expect(docked.dock).toEqual(['phone', 'messages', 'mail', 'clock']) expect(docked.grid[2]).toBeNull() @@ -108,20 +132,27 @@ describe('home layout', () => { ...defaults, dock: ['phone', 'messages', 'clock', 'notes'], } - const docked = moveHomeApp(layout, 'mail', 'grid', 'dock', 1) + const docked = moveHomeApp(layout, 'grid', 2, 'dock', 1) expect(docked.dock).toEqual(['phone', 'mail', 'messages', 'clock']) expect(docked.grid[2]).toBe('notes') }) + it('moves shortcuts between the dock and grid independently', () => { + const movedToGrid = moveHomeApp(defaults, 'dock', 0, 'grid', 5) + + expect(movedToGrid.dock[0]).toBeNull() + expect(movedToGrid.grid[0]).toBe('phone') + expect(movedToGrid.grid[5]).toBe('phone') + + const movedToDock = moveHomeApp(movedToGrid, 'grid', 1, 'dock', 3) + expect(movedToDock.grid[1]).toBeNull() + expect(movedToDock.dock[1]).toBe('messages') + expect(movedToDock.dock[3]).toBe('messages') + }) + it('removes shortcuts without closing gaps and restores the first gap', () => { - const layout: HomeLayout = moveHomeApp( - defaults, - 'phone', - 'grid', - 'grid', - 10, - ) + const layout: HomeLayout = moveHomeApp(defaults, 'grid', 0, 'grid', 10) const removed = removeHomeApp(layout, 'phone') expect(removed.grid[0]).toBeNull() expect(removed.grid[10]).toBeNull() diff --git a/frontend/src/utils/homeLayout.ts b/frontend/src/utils/homeLayout.ts index 06ae0e0..adf62e1 100644 --- a/frontend/src/utils/homeLayout.ts +++ b/frontend/src/utils/homeLayout.ts @@ -52,16 +52,13 @@ function readSlots( const slots = createSlots(length) if (!Array.isArray(value)) return slots - const usedIds = new Set() for (let index = 0; index < Math.min(value.length, length); index += 1) { const valueId = value[index] if ( typeof valueId === 'string' && - availableIds.has(valueId as LaunchablePhoneAppId) && - !usedIds.has(valueId as LaunchablePhoneAppId) + availableIds.has(valueId as LaunchablePhoneAppId) ) { slots[index] = valueId as LaunchablePhoneAppId - usedIds.add(valueId as LaunchablePhoneAppId) } } return slots @@ -226,21 +223,27 @@ export function restoreHomeApp( export function moveHomeApp( layout: HomeLayout, - appId: LaunchablePhoneAppId, from: HomeArea, + sourceIndex: number, to: HomeArea, targetIndex: number, ): HomeLayout { const next: HomeLayout = { dock: [...layout.dock], grid: [...layout.grid], - hidden: layout.hidden.filter((id) => id !== appId), + hidden: [...layout.hidden], version: 2, } const source = next[from] - const sourceIndex = source.indexOf(appId) const target = next[to] - if (sourceIndex === -1 || targetIndex < 0 || targetIndex >= target.length) { + const appId = source[sourceIndex] + if ( + !appId || + sourceIndex < 0 || + sourceIndex >= source.length || + targetIndex < 0 || + targetIndex >= target.length + ) { return layout } @@ -251,11 +254,6 @@ export function moveHomeApp( return next } - const duplicateIndex = target.indexOf(appId) - if (duplicateIndex !== -1) { - target[duplicateIndex] = null - } - source[sourceIndex] = insertIntoSlot(target, targetIndex, appId) return next } diff --git a/frontend/src/views/SpringboardView.vue b/frontend/src/views/SpringboardView.vue index 9773b80..bb0ef1a 100644 --- a/frontend/src/views/SpringboardView.vue +++ b/frontend/src/views/SpringboardView.vue @@ -29,8 +29,8 @@ const editMode = ref(false) const dragOffset = ref(0) const dragging = ref(false) const draggingHomeApp = ref<{ - appId: LaunchablePhoneAppId area: HomeArea + index: number } | null>(null) let pointerStart = 0 let pointerStartedAt = 0 @@ -150,8 +150,8 @@ function enterEditMode(): void { dragOffset.value = 0 } -function startHomeDrag(appId: LaunchablePhoneAppId, area: HomeArea): void { - draggingHomeApp.value = { appId, area } +function startHomeDrag(area: HomeArea, index: number): void { + draggingHomeApp.value = { area, index } } function finishHomeDrag(event: PointerEvent): void { @@ -160,20 +160,32 @@ function finishHomeDrag(event: PointerEvent): void { const target = document .elementsFromPoint(event.clientX, event.clientY) .find((element) => !element.closest('.app-icon-item--dragging')) - const targetItem = target?.closest('[data-home-index]') const targetArea = target?.closest('[data-home-area]') + let targetItem = target?.closest('[data-home-index]') + if (!targetItem && targetArea) { + const slotItems = Array.from( + targetArea.querySelectorAll('[data-home-index]'), + ) + targetItem = slotItems.reduce((closest, slot) => { + if (!closest) return slot + const slotBounds = slot.getBoundingClientRect() + const closestBounds = closest.getBoundingClientRect() + const slotDistance = Math.hypot( + event.clientX - (slotBounds.left + slotBounds.width / 2), + event.clientY - (slotBounds.top + slotBounds.height / 2), + ) + const closestDistance = Math.hypot( + event.clientX - (closestBounds.left + closestBounds.width / 2), + event.clientY - (closestBounds.top + closestBounds.height / 2), + ) + return slotDistance < closestDistance ? slot : closest + }, undefined) + } const area = (targetItem?.dataset.homeArea ?? targetArea?.dataset.homeArea) as HomeArea | undefined - if (area === 'grid' || area === 'dock') { - const fallbackIndex = - area === 'dock' - ? appStore.homeLayout.dock.length - : appStore.homeLayout.grid.length - const targetIndex = Number.parseInt( - targetItem?.dataset.homeIndex ?? `${fallbackIndex}`, - 10, - ) - appStore.moveHomeApp(dragged.appId, dragged.area, area, targetIndex) + if ((area === 'grid' || area === 'dock') && targetItem) { + const targetIndex = Number.parseInt(targetItem.dataset.homeIndex ?? '', 10) + appStore.moveHomeApp(dragged.area, dragged.index, area, targetIndex) } draggingHomeApp.value = null } @@ -252,7 +264,12 @@ watch(isAppPage, (visible) => { :edit-mode="editMode" @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" - @dragstart="startHomeDrag(app.id, 'grid')" + @dragstart=" + startHomeDrag( + 'grid', + pageIndex * HOME_GRID_PAGE_SIZE + appIndex, + ) + " @edit="enterEditMode" @remove="removeHomeApp(app.id)" /> @@ -392,7 +409,7 @@ watch(isAppPage, (visible) => { :show-label="false" @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" - @dragstart="startHomeDrag(app.id, 'dock')" + @dragstart="startHomeDrag('dock', appIndex)" @edit="enterEditMode" @remove="removeHomeApp(app.id)" /> diff --git a/sky_phone/source/html/index.html b/sky_phone/source/html/index.html index b10f17b..5607f8e 100644 --- a/sky_phone/source/html/index.html +++ b/sky_phone/source/html/index.html @@ -4,7 +4,7 @@ Sky Phone - +