mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ENH - rework homescreen editing and widgets
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
HOME_GRID_COLUMNS,
|
||||
HOME_GRID_PAGE_SIZE,
|
||||
HOME_GRID_ROWS,
|
||||
HOME_LAYOUT_VERSION,
|
||||
homeKeyboardTarget,
|
||||
isHomeFolder,
|
||||
MAX_HOME_GRID_PAGES,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
moveHomeAppToGridPage,
|
||||
moveHomeFolderApp,
|
||||
parseHomeLayout,
|
||||
reflowHomeGridForWidgetChange,
|
||||
removeHomeApp,
|
||||
renameHomeFolder,
|
||||
restoreHomeApp,
|
||||
@@ -52,7 +54,8 @@ function pageLayout(pageCounts: readonly number[]): HomeLayout {
|
||||
dock: [generatedApp(999), null, null, null],
|
||||
grid,
|
||||
hidden: [],
|
||||
version: 5,
|
||||
pageCount: pageCounts.length,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +75,7 @@ describe('home layout', () => {
|
||||
null,
|
||||
])
|
||||
expect(layout.dock).toEqual(['phone', 'messages', 'clock', null])
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('migrates compact persisted arrays and appends newly installed apps', () => {
|
||||
@@ -89,7 +92,7 @@ describe('home layout', () => {
|
||||
expect(layout.dock).toEqual(['messages', null, null, null])
|
||||
expect(layout.grid.slice(0, 4)).toEqual(['mail', 'clock', 'notes', null])
|
||||
expect(layout.hidden).toEqual(['phone'])
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('closes gaps within a versioned page without moving apps between pages', () => {
|
||||
@@ -127,7 +130,7 @@ describe('home layout', () => {
|
||||
.slice(0, HOME_GRID_PAGE_SIZE)
|
||||
.filter((item) => item !== null).length
|
||||
expect(layout.grid.slice(0, pageItemCount)).not.toContain(null)
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('preserves page membership while expanding version 3 pages', () => {
|
||||
@@ -147,7 +150,7 @@ describe('home layout', () => {
|
||||
expect(layout.grid.slice(0, 3)).toEqual(['phone', 'messages', 'notes'])
|
||||
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('mail')
|
||||
expect(layout.grid[HOME_GRID_PAGE_SIZE + 1]).toBe('clock')
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('reads version 4 pages directly before migrating the schema', () => {
|
||||
@@ -167,7 +170,7 @@ describe('home layout', () => {
|
||||
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
||||
expect(layout.grid[0]).toBe('phone')
|
||||
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('messages')
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('preserves folders and page membership in version 5 layouts', () => {
|
||||
@@ -194,7 +197,7 @@ describe('home layout', () => {
|
||||
'notes',
|
||||
])
|
||||
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('clock')
|
||||
expect(layout.version).toBe(5)
|
||||
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
||||
})
|
||||
|
||||
it('preserves independently positioned shortcuts for the same app', () => {
|
||||
@@ -323,6 +326,61 @@ describe('home layout', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('materializes the visible pages before moving a widget forward', () => {
|
||||
const layout = pageLayout([0, HOME_GRID_PAGE_SIZE, 7])
|
||||
const pageTwoApps = layout.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
||||
.filter((item) => item !== null)
|
||||
const pageThreeApps = layout.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
||||
.filter((item) => item !== null)
|
||||
const reflowed = reflowHomeGridForWidgetChange(
|
||||
layout,
|
||||
[HOME_GRID_PAGE_SIZE, 20, HOME_GRID_PAGE_SIZE],
|
||||
[HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE, 20],
|
||||
)
|
||||
|
||||
expect(reflowed).not.toBeNull()
|
||||
expect(
|
||||
reflowed?.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
||||
.filter(Boolean),
|
||||
).toEqual(pageTwoApps.slice(0, 20))
|
||||
expect(
|
||||
reflowed?.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
||||
.filter(Boolean),
|
||||
).toEqual([...pageTwoApps.slice(20), ...pageThreeApps])
|
||||
})
|
||||
|
||||
it('moves widget overflow only to later pages', () => {
|
||||
const layout = pageLayout([0, HOME_GRID_PAGE_SIZE, 20])
|
||||
const allApps = layout.grid.filter(Boolean).sort()
|
||||
const reflowed = reflowHomeGridForWidgetChange(
|
||||
layout,
|
||||
[HOME_GRID_PAGE_SIZE, 20, HOME_GRID_PAGE_SIZE],
|
||||
[HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE, 20],
|
||||
)
|
||||
|
||||
expect(reflowed).not.toBeNull()
|
||||
expect(
|
||||
reflowed?.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
||||
.filter(Boolean),
|
||||
).toHaveLength(20)
|
||||
expect(
|
||||
reflowed?.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
||||
.filter(Boolean),
|
||||
).toHaveLength(20)
|
||||
expect(
|
||||
reflowed?.grid
|
||||
.slice(HOME_GRID_PAGE_SIZE * 3, HOME_GRID_PAGE_SIZE * 4)
|
||||
.filter(Boolean),
|
||||
).toHaveLength(4)
|
||||
expect(reflowed?.grid.filter(Boolean).sort()).toEqual(allApps)
|
||||
})
|
||||
|
||||
it('provides bounded keyboard reorder targets without wrapping rows', () => {
|
||||
expect(homeKeyboardTarget(defaults, 'grid', 1, 'right')).toBe(2)
|
||||
expect(homeKeyboardTarget(defaults, 'grid', 3, 'right')).toBeNull()
|
||||
@@ -423,15 +481,37 @@ describe('home layout', () => {
|
||||
}
|
||||
|
||||
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * MAX_HOME_GRID_PAGES)
|
||||
expect(layout.pageCount).toBe(MAX_HOME_GRID_PAGES)
|
||||
expect(addHomePage(layout)).toBe(layout)
|
||||
})
|
||||
|
||||
it('restores an explicitly persisted empty trailing page', () => {
|
||||
const layout = parseHomeLayout(
|
||||
{
|
||||
dock: defaults.dock,
|
||||
grid: defaults.grid.slice(0, 5),
|
||||
hidden: [],
|
||||
pageCount: 2,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
},
|
||||
defaults,
|
||||
[...installed],
|
||||
)
|
||||
|
||||
expect(layout.pageCount).toBe(2)
|
||||
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
||||
expect(
|
||||
layout.grid.slice(HOME_GRID_PAGE_SIZE).every((item) => item === null),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('deletes a page and moves its items into remaining empty slots', () => {
|
||||
let layout = addHomePage(defaults)
|
||||
layout = moveHomeApp(layout, 'grid', 0, 'grid', HOME_GRID_PAGE_SIZE)
|
||||
const deleted = deleteHomePage(layout, 2)
|
||||
|
||||
expect(deleted.grid).toHaveLength(HOME_GRID_PAGE_SIZE)
|
||||
expect(deleted.pageCount).toBe(1)
|
||||
expect(deleted.grid).toContain('phone')
|
||||
expect(deleteHomePage(deleted, 1)).toBe(deleted)
|
||||
})
|
||||
@@ -462,7 +542,7 @@ describe('home layout', () => {
|
||||
' Dienstprogramme ',
|
||||
)
|
||||
const parsed = parseHomeLayout(renamed, defaults, [...installed])
|
||||
expect(parsed.version).toBe(5)
|
||||
expect(parsed.version).toBe(HOME_LAYOUT_VERSION)
|
||||
expect(getHomeFolder(parsed, 'folder-work-123456')).toEqual({
|
||||
apps: ['notes', 'mail'],
|
||||
id: 'folder-work-123456',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const HOME_GRID_PAGE_SIZE = HOME_GRID_COLUMNS * HOME_GRID_ROWS
|
||||
export const HOME_FOLDER_PAGE_SIZE = 9
|
||||
export const HOME_FOLDER_NAME_MAX_LENGTH = 32
|
||||
export const MAX_HOME_GRID_PAGES = 5
|
||||
export const HOME_LAYOUT_VERSION = 5
|
||||
export const HOME_LAYOUT_VERSION = 6
|
||||
const LEGACY_HOME_GRID_PAGE_SIZE = 20
|
||||
|
||||
export type HomeArea = 'dock' | 'grid'
|
||||
@@ -27,6 +27,7 @@ export type HomeLayout = {
|
||||
dock: HomeSlot[]
|
||||
grid: HomeSlot[]
|
||||
hidden: LaunchablePhoneAppId[]
|
||||
pageCount: number
|
||||
version: typeof HOME_LAYOUT_VERSION
|
||||
}
|
||||
|
||||
@@ -90,10 +91,19 @@ function cloneLayout(layout: HomeLayout): HomeLayout {
|
||||
dock: layout.dock.map(cloneItem),
|
||||
grid: layout.grid.map(cloneItem),
|
||||
hidden: [...layout.hidden],
|
||||
pageCount: layout.pageCount,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
function clampPageCount(value: number): number {
|
||||
return Math.max(1, Math.min(MAX_HOME_GRID_PAGES, Math.trunc(value)))
|
||||
}
|
||||
|
||||
function inferredPageCount(slots: readonly HomeSlot[]): number {
|
||||
return clampPageCount(Math.ceil(slots.length / HOME_GRID_PAGE_SIZE))
|
||||
}
|
||||
|
||||
function normalizeFolder(folder: HomeFolder): HomeSlot {
|
||||
if (folder.apps.length === 0) return null
|
||||
if (folder.apps.length === 1) return folder.apps[0]
|
||||
@@ -354,6 +364,43 @@ function serializeGridPages(pages: readonly IndexedHomeItem[][]): HomeSlot[] {
|
||||
return slots
|
||||
}
|
||||
|
||||
export function reflowHomeGridForWidgetChange(
|
||||
layout: HomeLayout,
|
||||
previousCapacities: HomeGridPageCapacities,
|
||||
nextCapacities: HomeGridPageCapacities,
|
||||
): HomeLayout | null {
|
||||
const currentPages = distributeGridPages(
|
||||
layout.grid,
|
||||
previousCapacities,
|
||||
layout.pageCount,
|
||||
)
|
||||
if (!currentPages) return null
|
||||
|
||||
const materializedGrid = serializeGridPages(currentPages)
|
||||
const nextPages = distributeGridPages(
|
||||
materializedGrid,
|
||||
nextCapacities,
|
||||
Math.max(layout.pageCount, currentPages.length),
|
||||
)
|
||||
if (!nextPages) return null
|
||||
|
||||
const nextGrid = serializeGridPages(nextPages)
|
||||
const nextPageCount = clampPageCount(
|
||||
Math.max(layout.pageCount, nextPages.length),
|
||||
)
|
||||
if (
|
||||
nextPageCount === layout.pageCount &&
|
||||
JSON.stringify(nextGrid) === JSON.stringify(layout.grid)
|
||||
) {
|
||||
return layout
|
||||
}
|
||||
|
||||
const next = cloneLayout(layout)
|
||||
next.grid = nextGrid
|
||||
next.pageCount = nextPageCount
|
||||
return next
|
||||
}
|
||||
|
||||
export function moveHomeAppToGridPage(
|
||||
layout: HomeLayout,
|
||||
from: HomeArea,
|
||||
@@ -416,6 +463,9 @@ export function moveHomeAppToGridPage(
|
||||
|
||||
const next = cloneLayout(layout)
|
||||
next.grid = serializeGridPages(pages)
|
||||
next.pageCount = clampPageCount(
|
||||
Math.max(layout.pageCount, targetPage, pages.length),
|
||||
)
|
||||
if (from === 'dock') next.dock[sourceIndex] = null
|
||||
if (
|
||||
JSON.stringify(next.grid) === JSON.stringify(layout.grid) &&
|
||||
@@ -446,7 +496,13 @@ export function createDefaultHomeLayout(
|
||||
dock[index] = id
|
||||
}
|
||||
|
||||
return { dock, grid, hidden: [], version: HOME_LAYOUT_VERSION }
|
||||
return {
|
||||
dock,
|
||||
grid,
|
||||
hidden: [],
|
||||
pageCount: inferredPageCount(grid),
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseHomeLayout(
|
||||
@@ -461,13 +517,19 @@ export function parseHomeLayout(
|
||||
const availableIds = new Set(installedIds)
|
||||
if (
|
||||
preservePersistedIds &&
|
||||
(source.version === 3 || source.version === 4 || source.version === 5)
|
||||
(source.version === 3 ||
|
||||
source.version === 4 ||
|
||||
source.version === 5 ||
|
||||
source.version === 6)
|
||||
) {
|
||||
for (const collection of [source.dock, source.grid, source.hidden]) {
|
||||
if (!Array.isArray(collection)) continue
|
||||
for (const item of collection) {
|
||||
if (isPersistableAppId(item)) availableIds.add(item)
|
||||
if (source.version === 5 && isHomeFolder(item)) {
|
||||
if (
|
||||
(source.version === 5 || source.version === 6) &&
|
||||
isHomeFolder(item)
|
||||
) {
|
||||
for (const appId of item.apps) {
|
||||
if (isPersistableAppId(appId)) availableIds.add(appId)
|
||||
}
|
||||
@@ -488,8 +550,15 @@ export function parseHomeLayout(
|
||||
) * HOME_GRID_PAGE_SIZE
|
||||
: Math.min(source.grid.length, HOME_GRID_PAGE_SIZE * MAX_HOME_GRID_PAGES)
|
||||
: 0
|
||||
const persistedPageCount =
|
||||
source.version === 6 &&
|
||||
typeof source.pageCount === 'number' &&
|
||||
Number.isInteger(source.pageCount)
|
||||
? clampPageCount(source.pageCount)
|
||||
: clampPageCount(Math.ceil(persistedGridLength / HOME_GRID_PAGE_SIZE))
|
||||
const gridLength = Math.max(
|
||||
defaults.grid.length,
|
||||
persistedPageCount * HOME_GRID_PAGE_SIZE,
|
||||
getGridCapacity(persistedGridLength),
|
||||
)
|
||||
let grid: HomeSlot[]
|
||||
@@ -507,9 +576,13 @@ export function parseHomeLayout(
|
||||
new Set(),
|
||||
false,
|
||||
)
|
||||
} else if (source.version === 4 || source.version === 5) {
|
||||
} else if (
|
||||
source.version === 4 ||
|
||||
source.version === 5 ||
|
||||
source.version === 6
|
||||
) {
|
||||
const folderIds = new Set<string>()
|
||||
const allowFolders = source.version === 5
|
||||
const allowFolders = source.version === 5 || source.version === 6
|
||||
grid = readSlots(
|
||||
source.grid,
|
||||
availableIds,
|
||||
@@ -567,6 +640,7 @@ export function parseHomeLayout(
|
||||
dock: dock.map(cloneItem),
|
||||
grid: compactGridPages(grid),
|
||||
hidden,
|
||||
pageCount: inferredPageCount(grid),
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
@@ -589,6 +663,7 @@ export function removeHomeApp(
|
||||
hidden: layout.hidden.includes(appId)
|
||||
? [...layout.hidden]
|
||||
: [...layout.hidden, appId],
|
||||
pageCount: layout.pageCount,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
@@ -613,25 +688,32 @@ export function restoreHomeApp(
|
||||
dock: layout.dock.map(cloneItem),
|
||||
grid: compactGridPages(grid),
|
||||
hidden: layout.hidden.filter((id) => id !== appId),
|
||||
pageCount: inferredPageCount(grid),
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
export function addHomePage(layout: HomeLayout): HomeLayout {
|
||||
if (layout.grid.length >= HOME_GRID_PAGE_SIZE * MAX_HOME_GRID_PAGES) {
|
||||
if (layout.pageCount >= MAX_HOME_GRID_PAGES) {
|
||||
return layout
|
||||
}
|
||||
|
||||
const pageCount = layout.pageCount + 1
|
||||
const requiredGridLength = pageCount * HOME_GRID_PAGE_SIZE
|
||||
return {
|
||||
dock: layout.dock.map(cloneItem),
|
||||
grid: [...layout.grid.map(cloneItem), ...createSlots(HOME_GRID_PAGE_SIZE)],
|
||||
grid: [
|
||||
...layout.grid.map(cloneItem),
|
||||
...createSlots(Math.max(0, requiredGridLength - layout.grid.length)),
|
||||
],
|
||||
hidden: [...layout.hidden],
|
||||
pageCount,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteHomePage(layout: HomeLayout, page: number): HomeLayout {
|
||||
const pageCount = Math.ceil(layout.grid.length / HOME_GRID_PAGE_SIZE)
|
||||
const pageCount = layout.pageCount
|
||||
if (pageCount <= 1 || page < 1 || page > pageCount) return layout
|
||||
|
||||
const pageStart = (page - 1) * HOME_GRID_PAGE_SIZE
|
||||
@@ -648,6 +730,7 @@ export function deleteHomePage(layout: HomeLayout, page: number): HomeLayout {
|
||||
dock: layout.dock.map(cloneItem),
|
||||
grid: compactGridPages(grid),
|
||||
hidden: [...layout.hidden],
|
||||
pageCount: pageCount - 1,
|
||||
version: HOME_LAYOUT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
maximumRenderedWidgetPage,
|
||||
nearestSpringboardRectIndex,
|
||||
resolveSpringboardEdgeTurn,
|
||||
resolveSpringboardHomeEdgeTurn,
|
||||
springboardEdgeDirection,
|
||||
@@ -18,6 +19,17 @@ describe('springboard widget drag', () => {
|
||||
expect(springboardEdgeDirection(284, 100, 468)).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps both phone edges reachable at normal and reduced FiveM scales', () => {
|
||||
expect(springboardEdgeDirection(125, 100, 468)).toBe(-1)
|
||||
expect(springboardEdgeDirection(150, 100, 468)).toBe(0)
|
||||
expect(springboardEdgeDirection(420, 100, 468)).toBe(0)
|
||||
expect(springboardEdgeDirection(443, 100, 468)).toBe(1)
|
||||
expect(springboardEdgeDirection(117, 100, 247)).toBe(-1)
|
||||
expect(springboardEdgeDirection(200, 100, 247)).toBe(0)
|
||||
expect(springboardEdgeDirection(230, 100, 247)).toBe(1)
|
||||
expect(springboardEdgeDirection(100, 100, 100)).toBe(0)
|
||||
})
|
||||
|
||||
it('resolves reachable page destinations without crossing page bounds', () => {
|
||||
expect(resolveSpringboardEdgeTurn(455, 100, 468, 1, 0, 3)).toEqual({
|
||||
destination: 2,
|
||||
@@ -31,6 +43,34 @@ describe('springboard widget drag', () => {
|
||||
expect(resolveSpringboardEdgeTurn(455, 100, 468, 3, 0, 3)).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves consecutive edge turns while an app remains at the same edge', () => {
|
||||
const firstTurn = resolveSpringboardHomeEdgeTurn(
|
||||
113,
|
||||
100,
|
||||
468,
|
||||
4,
|
||||
4,
|
||||
5,
|
||||
false,
|
||||
)
|
||||
expect(firstTurn).toEqual({
|
||||
destination: 3,
|
||||
direction: -1,
|
||||
previewsPage: false,
|
||||
})
|
||||
expect(
|
||||
resolveSpringboardHomeEdgeTurn(
|
||||
113,
|
||||
100,
|
||||
468,
|
||||
firstTurn?.destination ?? 4,
|
||||
4,
|
||||
5,
|
||||
false,
|
||||
),
|
||||
).toEqual({ destination: 2, direction: -1, previewsPage: false })
|
||||
})
|
||||
|
||||
it('previews one new trailing page without crossing the home-page limit', () => {
|
||||
expect(
|
||||
resolveSpringboardHomeEdgeTurn(455, 100, 468, 2, 2, 5, false),
|
||||
@@ -60,15 +100,44 @@ describe('springboard widget drag', () => {
|
||||
expect(springboardPageDragCompensation(2, 2, 368)).toBe(0)
|
||||
})
|
||||
|
||||
it('selects drop slots directly in viewport order', () => {
|
||||
const slots = [
|
||||
{ height: 80, left: 100, top: 150, width: 70 },
|
||||
{ height: 80, left: 180, top: 150, width: 70 },
|
||||
{ height: 80, left: 260, top: 150, width: 70 },
|
||||
]
|
||||
|
||||
expect(nearestSpringboardRectIndex(115, 190, slots)).toBe(0)
|
||||
expect(nearestSpringboardRectIndex(300, 190, slots)).toBe(2)
|
||||
expect(nearestSpringboardRectIndex(220, 190, slots)).toBe(1)
|
||||
expect(nearestSpringboardRectIndex(220, 190, [])).toBeNull()
|
||||
})
|
||||
|
||||
it('converts viewport coordinates into local phone coordinates under zoom', () => {
|
||||
expect(
|
||||
springboardViewportToLocal(169, 238, 100, 100, 253.92, 582.36, 368, 844),
|
||||
).toEqual({ x: 100, y: 200 })
|
||||
const point = springboardViewportToLocal(
|
||||
169,
|
||||
238,
|
||||
100,
|
||||
100,
|
||||
253.92,
|
||||
582.36,
|
||||
368,
|
||||
844,
|
||||
)
|
||||
expect(point.x).toBeCloseTo(100)
|
||||
expect(point.y).toBeCloseTo(200)
|
||||
expect(
|
||||
springboardViewportToLocal(200, 250, 100, 50, 0, 0, 368, 844),
|
||||
).toEqual({ x: 100, y: 200 })
|
||||
expect(
|
||||
springboardViewportDeltaToLocal(69, 138, 253.92, 582.36, 368, 844),
|
||||
).toEqual({ x: 100, y: 200 })
|
||||
const delta = springboardViewportDeltaToLocal(
|
||||
69,
|
||||
138,
|
||||
253.92,
|
||||
582.36,
|
||||
368,
|
||||
844,
|
||||
)
|
||||
expect(delta.x).toBeCloseTo(100)
|
||||
expect(delta.y).toBeCloseTo(200)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,40 @@ export type SpringboardLocalPoint = {
|
||||
y: number
|
||||
}
|
||||
|
||||
export type SpringboardViewportRect = {
|
||||
height: number
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
}
|
||||
|
||||
export type SpringboardDragMetrics = {
|
||||
layoutHeight: number
|
||||
layoutWidth: number
|
||||
viewportHeight: number
|
||||
viewportLeft: number
|
||||
viewportTop: number
|
||||
viewportWidth: number
|
||||
}
|
||||
|
||||
export function readSpringboardDragMetrics(
|
||||
element: Element | null,
|
||||
): SpringboardDragMetrics | null {
|
||||
const surface = element?.closest<HTMLElement>(
|
||||
'.springboard-page, .home-folder-panel',
|
||||
)
|
||||
if (!surface) return null
|
||||
const bounds = surface.getBoundingClientRect()
|
||||
return {
|
||||
layoutHeight: surface.offsetHeight,
|
||||
layoutWidth: surface.offsetWidth,
|
||||
viewportHeight: bounds.height,
|
||||
viewportLeft: bounds.left,
|
||||
viewportTop: bounds.top,
|
||||
viewportWidth: bounds.width,
|
||||
}
|
||||
}
|
||||
|
||||
export function springboardViewportToLocal(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
@@ -55,6 +89,27 @@ export function springboardPageDragCompensation(
|
||||
return (currentPage - startPage) * Math.max(0, pageWidth)
|
||||
}
|
||||
|
||||
export function nearestSpringboardRectIndex(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
rects: readonly SpringboardViewportRect[],
|
||||
): number | null {
|
||||
if (!rects.length) return null
|
||||
|
||||
let nearestIndex = 0
|
||||
let nearestDistance = Number.POSITIVE_INFINITY
|
||||
for (const [index, rect] of rects.entries()) {
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
const distance = Math.hypot(clientX - centerX, clientY - centerY)
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance
|
||||
nearestIndex = index
|
||||
}
|
||||
}
|
||||
return nearestIndex
|
||||
}
|
||||
|
||||
export function springboardSwipeIntent(
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
@@ -70,7 +125,8 @@ export function springboardEdgeDirection(
|
||||
right: number,
|
||||
): PageTurnDirection {
|
||||
const width = Math.max(0, right - left)
|
||||
const edgeSize = Math.min(42, width * 0.12)
|
||||
if (width === 0) return 0
|
||||
const edgeSize = Math.min(48, Math.max(28, width * 0.12))
|
||||
if (clientX <= left + edgeSize) return -1
|
||||
if (clientX >= right - edgeSize) return 1
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user