mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
ADD - integrate widgets into springboard
This commit is contained in:
@@ -1,17 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { Search, X } from 'lucide-vue-next'
|
||||
import { kGlass } from 'konsta/vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Pencil, Plus, Search, Trash2, X } from 'lucide-vue-next'
|
||||
import { kButton, kGlass, kList, kListItem, kSheet } from 'konsta/vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
import SpringboardWidgets from '@/components/SpringboardWidgets.vue'
|
||||
import SpringboardWidgetGrid from '@/components/SpringboardWidgetGrid.vue'
|
||||
import WidgetConfigSheet from '@/components/WidgetConfigSheet.vue'
|
||||
import WidgetPickerSheet from '@/components/WidgetPickerSheet.vue'
|
||||
import { PHONE_APPS } from '@/config/apps'
|
||||
import { useAppStoreStore } from '@/stores/app-store'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { useWidgetsStore } from '@/stores/widgets'
|
||||
import type { PhoneAppCategory, PhoneAppDefinition } from '@/types/apps'
|
||||
import type { LaunchablePhoneAppId } from '@/types/apps'
|
||||
import { HOME_GRID_PAGE_SIZE, type HomeArea } from '@/utils/homeLayout'
|
||||
import { paginateItems } from '@/utils/pages'
|
||||
import type { WidgetKind, WidgetSettings, WidgetSize } from '@/types/widgets'
|
||||
import {
|
||||
deleteHomePage as previewHomePageDelete,
|
||||
HOME_GRID_PAGE_SIZE,
|
||||
MAX_HOME_GRID_PAGES,
|
||||
type HomeArea,
|
||||
} from '@/utils/homeLayout'
|
||||
import {
|
||||
deleteWidgetPage as previewWidgetPageDelete,
|
||||
moveWidget as previewWidgetMove,
|
||||
WIDGET_GRID_COLUMNS,
|
||||
widgetOccupiedCells,
|
||||
} from '@/utils/widgetLayout'
|
||||
|
||||
const APP_LIBRARY_CATEGORIES: PhoneAppCategory[] = [
|
||||
'games',
|
||||
@@ -22,18 +36,42 @@ const APP_LIBRARY_CATEGORIES: PhoneAppCategory[] = [
|
||||
]
|
||||
const phone = usePhoneStore()
|
||||
const appStore = useAppStoreStore()
|
||||
const widgets = useWidgetsStore()
|
||||
const searchQuery = ref('')
|
||||
const searchFocused = ref(false)
|
||||
const showAllApps = ref(false)
|
||||
const editMode = ref(false)
|
||||
const widgetPickerOpened = ref(false)
|
||||
const widgetActionId = ref<string | null>(null)
|
||||
const widgetConfigId = ref<string | null>(null)
|
||||
const draggingWidgetId = ref<string | null>(null)
|
||||
const widgetDragGrip = ref<{ x: number; y: number } | null>(null)
|
||||
const widgetDragSize = ref<{ height: number; width: number } | null>(null)
|
||||
const widgetDragPreview = ref<{
|
||||
column: number
|
||||
id: string
|
||||
page: number
|
||||
row: number
|
||||
} | null>(null)
|
||||
const dragOffset = ref(0)
|
||||
const dragging = ref(false)
|
||||
const pageTransitioning = ref(false)
|
||||
const draggingHomeApp = ref<{
|
||||
area: HomeArea
|
||||
index: number
|
||||
} | null>(null)
|
||||
let pointerStart = 0
|
||||
let pointerStartY = 0
|
||||
let pointerStartedAt = 0
|
||||
let pointerPageStart = 0
|
||||
let backgroundHoldTimer: number | undefined
|
||||
let blankTapCandidate = false
|
||||
let widgetPreviewTimer: number | undefined
|
||||
let pendingWidgetPointer: { clientX: number; clientY: number } | null = null
|
||||
let lastWidgetPointer: { clientX: number; clientY: number } | null = null
|
||||
let edgePageTimer: number | undefined
|
||||
let edgePageDirection = 0
|
||||
let edgePageLocked = false
|
||||
|
||||
const installedApps = computed(() =>
|
||||
PHONE_APPS.filter(
|
||||
@@ -43,19 +81,106 @@ const installedApps = computed(() =>
|
||||
const installedAppsById = computed(
|
||||
() => new Map(installedApps.value.map((app) => [app.id, app])),
|
||||
)
|
||||
const gridSlots = computed(() =>
|
||||
appStore.homeLayout.grid.map((id) =>
|
||||
id ? (installedAppsById.value.get(id) ?? null) : null,
|
||||
const hiddenApps = computed(() =>
|
||||
installedApps.value.filter((app) =>
|
||||
appStore.homeLayout.hidden.includes(app.id),
|
||||
),
|
||||
)
|
||||
const appPages = computed(() =>
|
||||
paginateItems(gridSlots.value, HOME_GRID_PAGE_SIZE),
|
||||
const gridEntries = computed(() =>
|
||||
appStore.homeLayout.grid
|
||||
.map((id, sourceIndex) => ({
|
||||
app: id ? (installedAppsById.value.get(id) ?? null) : null,
|
||||
sourceIndex,
|
||||
}))
|
||||
.filter(
|
||||
(entry): entry is { app: PhoneAppDefinition; sourceIndex: number } =>
|
||||
entry.app !== null,
|
||||
),
|
||||
)
|
||||
const previewWidgetLayout = computed(() => {
|
||||
const preview = widgetDragPreview.value
|
||||
return preview
|
||||
? previewWidgetMove(
|
||||
widgets.layout,
|
||||
preview.id,
|
||||
preview.page,
|
||||
preview.column,
|
||||
preview.row,
|
||||
)
|
||||
: widgets.layout
|
||||
})
|
||||
const appPages = computed(() => {
|
||||
const entries = [...gridEntries.value]
|
||||
const pages: Array<{
|
||||
cells: Array<{ app: PhoneAppDefinition; sourceIndex: number } | null>
|
||||
page: number
|
||||
}> = []
|
||||
const maxWidgetPage = Math.max(
|
||||
1,
|
||||
...previewWidgetLayout.value.instances.map((instance) => instance.page),
|
||||
)
|
||||
const persistedHomePages = Math.max(
|
||||
1,
|
||||
Math.ceil(appStore.homeLayout.grid.length / HOME_GRID_PAGE_SIZE),
|
||||
)
|
||||
const lastHomePage = Math.max(maxWidgetPage, persistedHomePages)
|
||||
let page = 1
|
||||
let entryIndex = 0
|
||||
while (entryIndex < entries.length || page <= lastHomePage) {
|
||||
const occupied = widgetOccupiedCells(
|
||||
previewWidgetLayout.value.instances,
|
||||
page,
|
||||
)
|
||||
if (widgetDragPreview.value) {
|
||||
for (const cell of widgetOccupiedCells(widgets.layout.instances, page)) {
|
||||
occupied.add(cell)
|
||||
}
|
||||
}
|
||||
const cells = Array.from<{
|
||||
app: PhoneAppDefinition
|
||||
sourceIndex: number
|
||||
} | null>({ length: HOME_GRID_PAGE_SIZE }).fill(null)
|
||||
for (let cell = 0; cell < HOME_GRID_PAGE_SIZE; cell += 1) {
|
||||
if (occupied.has(cell) || entryIndex >= entries.length) continue
|
||||
cells[cell] = entries[entryIndex]
|
||||
entryIndex += 1
|
||||
}
|
||||
pages.push({ cells, page })
|
||||
page += 1
|
||||
}
|
||||
return pages.length
|
||||
? pages
|
||||
: [{ cells: Array(HOME_GRID_PAGE_SIZE).fill(null), page: 1 }]
|
||||
})
|
||||
const pageCount = computed(() => appPages.value.length + 2)
|
||||
const libraryPage = computed(() => pageCount.value - 1)
|
||||
const isAppPage = computed(
|
||||
() => phone.currentPage > 0 && phone.currentPage < libraryPage.value,
|
||||
)
|
||||
const isEditablePage = computed(
|
||||
() => phone.currentPage >= 0 && phone.currentPage < libraryPage.value,
|
||||
)
|
||||
const canAddHomePage = computed(
|
||||
() =>
|
||||
appStore.homeLayout.grid.length < HOME_GRID_PAGE_SIZE * MAX_HOME_GRID_PAGES,
|
||||
)
|
||||
const addingHomePage = ref(false)
|
||||
const persistedHomePageCount = computed(() =>
|
||||
Math.max(1, Math.ceil(appStore.homeLayout.grid.length / HOME_GRID_PAGE_SIZE)),
|
||||
)
|
||||
const canDeleteCurrentPage = computed(() => {
|
||||
if (phone.currentPage < 1 || persistedHomePageCount.value <= 1) return false
|
||||
const remainingPages = persistedHomePageCount.value - 1
|
||||
return (
|
||||
previewHomePageDelete(appStore.homeLayout, phone.currentPage) !==
|
||||
appStore.homeLayout &&
|
||||
previewWidgetPageDelete(
|
||||
widgets.layout,
|
||||
phone.currentPage,
|
||||
remainingPages,
|
||||
) !== widgets.layout
|
||||
)
|
||||
})
|
||||
const dockSlots = computed(() =>
|
||||
appStore.homeLayout.dock.map((id) =>
|
||||
id ? (installedAppsById.value.get(id) ?? null) : null,
|
||||
@@ -113,53 +238,348 @@ const trackStyle = computed(() => ({
|
||||
width: `${pageCount.value * 100}%`,
|
||||
}))
|
||||
const pageStyle = computed(() => ({ width: `${100 / pageCount.value}%` }))
|
||||
const activeWidget = computed(() =>
|
||||
widgets.layout.instances.find(
|
||||
(instance) => instance.id === widgetActionId.value,
|
||||
),
|
||||
)
|
||||
const configuredWidget = computed(
|
||||
() =>
|
||||
widgets.layout.instances.find(
|
||||
(instance) => instance.id === widgetConfigId.value,
|
||||
) ?? null,
|
||||
)
|
||||
|
||||
function clearBackgroundHold(): void {
|
||||
if (backgroundHoldTimer !== undefined)
|
||||
window.clearTimeout(backgroundHoldTimer)
|
||||
backgroundHoldTimer = undefined
|
||||
}
|
||||
|
||||
function changePage(page: number): void {
|
||||
const targetPage = Math.max(0, Math.min(page, pageCount.value - 1))
|
||||
if (targetPage === phone.currentPage) {
|
||||
pageTransitioning.value = false
|
||||
return
|
||||
}
|
||||
pageTransitioning.value = true
|
||||
phone.setCurrentPage(targetPage, pageCount.value)
|
||||
}
|
||||
|
||||
function finishPageTransition(event: TransitionEvent): void {
|
||||
if (event.propertyName !== 'transform') return
|
||||
pageTransitioning.value = false
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent): void {
|
||||
if (editMode.value) return
|
||||
const target = event.target as HTMLElement
|
||||
if (target.closest('button, input')) return
|
||||
if (target.closest('button, input, .home-widget-shell')) {
|
||||
clearBackgroundHold()
|
||||
dragging.value = false
|
||||
dragOffset.value = 0
|
||||
blankTapCandidate = false
|
||||
return
|
||||
}
|
||||
pointerStart = event.clientX
|
||||
pointerStartY = event.clientY
|
||||
pointerStartedAt = Date.now()
|
||||
pointerPageStart = phone.currentPage
|
||||
blankTapCandidate = editMode.value
|
||||
if (editMode.value) return
|
||||
dragging.value = true
|
||||
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
||||
clearBackgroundHold()
|
||||
backgroundHoldTimer = window.setTimeout(() => {
|
||||
enterEditMode()
|
||||
backgroundHoldTimer = undefined
|
||||
}, 520)
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent): void {
|
||||
const distanceX = event.clientX - pointerStart
|
||||
const distanceY = event.clientY - pointerStartY
|
||||
if (Math.hypot(distanceX, distanceY) > 8) {
|
||||
clearBackgroundHold()
|
||||
blankTapCandidate = false
|
||||
}
|
||||
if (!dragging.value) return
|
||||
dragOffset.value = event.clientX - pointerStart
|
||||
if (Math.abs(distanceY) > Math.abs(distanceX)) {
|
||||
dragging.value = false
|
||||
dragOffset.value = 0
|
||||
return
|
||||
}
|
||||
const atFirstPage = pointerPageStart === 0 && distanceX > 0
|
||||
const atLastPage = pointerPageStart === pageCount.value - 1 && distanceX < 0
|
||||
dragOffset.value = atFirstPage || atLastPage ? distanceX * 0.28 : distanceX
|
||||
}
|
||||
|
||||
function finishPointer(event: PointerEvent): void {
|
||||
clearBackgroundHold()
|
||||
if (editMode.value && blankTapCandidate) {
|
||||
blankTapCandidate = false
|
||||
editMode.value = false
|
||||
return
|
||||
}
|
||||
if (!dragging.value) return
|
||||
const distance = event.clientX - pointerStart
|
||||
const elapsed = Math.max(1, Date.now() - pointerStartedAt)
|
||||
const velocity = Math.abs(distance) / elapsed
|
||||
if (Math.abs(distance) > 48 || velocity > 0.45) {
|
||||
phone.setCurrentPage(
|
||||
phone.currentPage + (distance < 0 ? 1 : -1),
|
||||
pageCount.value,
|
||||
)
|
||||
changePage(pointerPageStart + (distance < 0 ? 1 : -1))
|
||||
}
|
||||
dragging.value = false
|
||||
dragOffset.value = 0
|
||||
}
|
||||
|
||||
function cancelPointer(): void {
|
||||
clearBackgroundHold()
|
||||
blankTapCandidate = false
|
||||
dragging.value = false
|
||||
dragOffset.value = 0
|
||||
}
|
||||
|
||||
function enterEditMode(): void {
|
||||
editMode.value = true
|
||||
dragging.value = false
|
||||
dragOffset.value = 0
|
||||
}
|
||||
|
||||
function openWidgetMenu(id: string): void {
|
||||
enterEditMode()
|
||||
widgetActionId.value = id
|
||||
}
|
||||
|
||||
function startWidgetDrag(id: string, event: PointerEvent): void {
|
||||
const widget = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[data-widget-id]'),
|
||||
).find((candidate) => candidate.dataset.widgetId === id)
|
||||
if (!widget) return
|
||||
pageTransitioning.value = false
|
||||
const bounds = widget.getBoundingClientRect()
|
||||
draggingWidgetId.value = id
|
||||
widgetDragGrip.value = {
|
||||
x: event.clientX - bounds.left,
|
||||
y: event.clientY - bounds.top,
|
||||
}
|
||||
widgetDragSize.value = { height: bounds.height, width: bounds.width }
|
||||
}
|
||||
|
||||
function updateWidgetDragPreview(event: {
|
||||
clientX: number
|
||||
clientY: number
|
||||
}): void {
|
||||
const id = draggingWidgetId.value
|
||||
const grip = widgetDragGrip.value
|
||||
if (!id || !grip) return
|
||||
const page = phone.currentPage
|
||||
const grid = document.querySelector<HTMLElement>(
|
||||
`[data-widget-page="${page}"]`,
|
||||
)
|
||||
const pageElement = grid?.closest<HTMLElement>('.springboard-page')
|
||||
const springboard = grid?.closest<HTMLElement>('.springboard')
|
||||
if (!grid || !pageElement || !springboard) return
|
||||
const renderedGridBounds = grid.getBoundingClientRect()
|
||||
const pageBounds = pageElement.getBoundingClientRect()
|
||||
const springboardBounds = springboard.getBoundingClientRect()
|
||||
const gridLeft =
|
||||
springboardBounds.left + (renderedGridBounds.left - pageBounds.left)
|
||||
const gridStyle = getComputedStyle(grid)
|
||||
const columnGap = Number.parseFloat(gridStyle.columnGap) || 0
|
||||
const rowGap = Number.parseFloat(gridStyle.rowGap) || 0
|
||||
const columnWidth =
|
||||
(renderedGridBounds.width - columnGap * (WIDGET_GRID_COLUMNS - 1)) /
|
||||
WIDGET_GRID_COLUMNS
|
||||
const rowHeight = Number.parseFloat(gridStyle.gridAutoRows)
|
||||
const column = Math.round(
|
||||
(event.clientX - grip.x - gridLeft) / (columnWidth + columnGap),
|
||||
)
|
||||
const row = Math.round(
|
||||
(event.clientY - grip.y - renderedGridBounds.top) / (rowHeight + rowGap),
|
||||
)
|
||||
if (Number.isFinite(rowHeight)) {
|
||||
const current = widgetDragPreview.value
|
||||
if (
|
||||
!current ||
|
||||
current.id !== id ||
|
||||
current.page !== page ||
|
||||
current.column !== column ||
|
||||
current.row !== row
|
||||
) {
|
||||
widgetDragPreview.value = { column, id, page, row }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearEdgePageTurn(): void {
|
||||
if (edgePageTimer !== undefined) window.clearTimeout(edgePageTimer)
|
||||
edgePageTimer = undefined
|
||||
edgePageDirection = 0
|
||||
edgePageLocked = false
|
||||
}
|
||||
|
||||
function queueEdgePageTurn(
|
||||
event: PointerEvent,
|
||||
dragType: 'app' | 'widget',
|
||||
): void {
|
||||
const springboard = document.querySelector<HTMLElement>('.springboard')
|
||||
if (!springboard) return
|
||||
const bounds = springboard.getBoundingClientRect()
|
||||
const edgeSize = Math.min(42, bounds.width * 0.12)
|
||||
let direction = 0
|
||||
if (dragType === 'widget' && widgetDragGrip.value && widgetDragSize.value) {
|
||||
const widgetLeft = event.clientX - widgetDragGrip.value.x
|
||||
const widgetRight = widgetLeft + widgetDragSize.value.width
|
||||
const overhang = Math.min(22, widgetDragSize.value.width * 0.14)
|
||||
if (widgetLeft <= bounds.left - overhang) direction = -1
|
||||
else if (widgetRight >= bounds.right + overhang) direction = 1
|
||||
} else if (event.clientX <= bounds.left + edgeSize) direction = -1
|
||||
else if (event.clientX >= bounds.right - edgeSize) direction = 1
|
||||
const minimumPage = dragType === 'widget' ? 0 : 1
|
||||
const maximumPage = libraryPage.value - 1
|
||||
const destination = phone.currentPage + direction
|
||||
|
||||
if (
|
||||
direction === 0 ||
|
||||
destination < minimumPage ||
|
||||
destination > maximumPage
|
||||
) {
|
||||
if (edgePageTimer !== undefined) window.clearTimeout(edgePageTimer)
|
||||
edgePageTimer = undefined
|
||||
edgePageDirection = 0
|
||||
edgePageLocked = false
|
||||
return
|
||||
}
|
||||
if (edgePageLocked && edgePageDirection === direction) return
|
||||
if (edgePageLocked) edgePageLocked = false
|
||||
if (edgePageTimer !== undefined && edgePageDirection === direction) return
|
||||
if (edgePageTimer !== undefined) window.clearTimeout(edgePageTimer)
|
||||
edgePageDirection = direction
|
||||
edgePageTimer = window.setTimeout(() => {
|
||||
const targetPage = phone.currentPage + direction
|
||||
changePage(targetPage)
|
||||
if (dragType === 'widget' && lastWidgetPointer) {
|
||||
updateWidgetDragPreview(lastWidgetPointer)
|
||||
}
|
||||
edgePageTimer = undefined
|
||||
edgePageDirection = direction
|
||||
edgePageLocked = true
|
||||
}, 520)
|
||||
}
|
||||
|
||||
function queueWidgetDragPreview(event: PointerEvent): void {
|
||||
queueEdgePageTurn(event, 'widget')
|
||||
lastWidgetPointer = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
}
|
||||
pendingWidgetPointer = lastWidgetPointer
|
||||
if (widgetPreviewTimer !== undefined) return
|
||||
widgetPreviewTimer = window.setTimeout(() => {
|
||||
if (pendingWidgetPointer) updateWidgetDragPreview(pendingWidgetPointer)
|
||||
pendingWidgetPointer = null
|
||||
widgetPreviewTimer = undefined
|
||||
}, 110)
|
||||
}
|
||||
|
||||
function clearWidgetDragPreview(): void {
|
||||
clearEdgePageTurn()
|
||||
if (widgetPreviewTimer !== undefined) window.clearTimeout(widgetPreviewTimer)
|
||||
widgetPreviewTimer = undefined
|
||||
pendingWidgetPointer = null
|
||||
lastWidgetPointer = null
|
||||
widgetDragGrip.value = null
|
||||
widgetDragSize.value = null
|
||||
widgetDragPreview.value = null
|
||||
}
|
||||
|
||||
function finishWidgetDrag(event: PointerEvent): void {
|
||||
const id = draggingWidgetId.value
|
||||
if (!id) return
|
||||
lastWidgetPointer = { clientX: event.clientX, clientY: event.clientY }
|
||||
updateWidgetDragPreview(lastWidgetPointer)
|
||||
const preview = widgetDragPreview.value
|
||||
if (preview?.id === id) {
|
||||
widgets.move(id, preview.page, preview.column, preview.row)
|
||||
}
|
||||
draggingWidgetId.value = null
|
||||
clearWidgetDragPreview()
|
||||
}
|
||||
|
||||
function stopWidgetDrag(): void {
|
||||
draggingWidgetId.value = null
|
||||
clearWidgetDragPreview()
|
||||
}
|
||||
|
||||
function removeWidget(id: string): void {
|
||||
widgets.remove(id)
|
||||
if (widgetActionId.value === id) widgetActionId.value = null
|
||||
if (widgetConfigId.value === id) widgetConfigId.value = null
|
||||
}
|
||||
|
||||
async function addWidget(kind: WidgetKind, size: WidgetSize): Promise<void> {
|
||||
const targetPage = isEditablePage.value ? phone.currentPage : 1
|
||||
const addedId = widgets.add(kind, size, targetPage)
|
||||
if (!addedId) return
|
||||
widgetPickerOpened.value = false
|
||||
editMode.value = true
|
||||
await nextTick()
|
||||
const added = widgets.layout.instances.find(
|
||||
(instance) => instance.id === addedId,
|
||||
)
|
||||
if (added) changePage(added.page)
|
||||
}
|
||||
|
||||
function openWidgetConfig(): void {
|
||||
widgetConfigId.value = widgetActionId.value
|
||||
widgetActionId.value = null
|
||||
}
|
||||
|
||||
async function saveWidgetConfig(
|
||||
size: WidgetSize,
|
||||
settings: WidgetSettings,
|
||||
): Promise<void> {
|
||||
if (!widgetConfigId.value) return
|
||||
const id = widgetConfigId.value
|
||||
widgets.resize(id, size)
|
||||
widgets.updateSettings(id, settings)
|
||||
widgetConfigId.value = null
|
||||
editMode.value = true
|
||||
await nextTick()
|
||||
const configured = widgets.layout.instances.find(
|
||||
(instance) => instance.id === id,
|
||||
)
|
||||
if (configured) changePage(configured.page)
|
||||
}
|
||||
|
||||
function targetHomeIndex(page: number, cell: number): number {
|
||||
return Math.min(
|
||||
Math.max(0, appStore.homeLayout.grid.length - 1),
|
||||
(page - 1) * HOME_GRID_PAGE_SIZE + cell,
|
||||
)
|
||||
}
|
||||
|
||||
function startHomeDrag(area: HomeArea, index: number): void {
|
||||
draggingHomeApp.value = { area, index }
|
||||
}
|
||||
|
||||
function moveHomeDrag(event: PointerEvent): void {
|
||||
if (draggingHomeApp.value?.area === 'grid') {
|
||||
queueEdgePageTurn(event, 'app')
|
||||
}
|
||||
}
|
||||
|
||||
function finishHomeDrag(event: PointerEvent): void {
|
||||
clearEdgePageTurn()
|
||||
const dragged = draggingHomeApp.value
|
||||
if (!dragged) return
|
||||
const target = document
|
||||
.elementsFromPoint(event.clientX, event.clientY)
|
||||
.find((element) => !element.closest('.app-icon-item--dragging'))
|
||||
.find(
|
||||
(element) =>
|
||||
!element.closest('.app-icon-item--dragging') &&
|
||||
(element.closest('[data-home-index]') ||
|
||||
element.closest('[data-home-area]')),
|
||||
)
|
||||
const targetArea = target?.closest<HTMLElement>('[data-home-area]')
|
||||
let targetItem = target?.closest<HTMLElement>('[data-home-index]')
|
||||
if (!targetItem && targetArea) {
|
||||
@@ -191,13 +611,40 @@ function finishHomeDrag(event: PointerEvent): void {
|
||||
}
|
||||
|
||||
function stopHomeDrag(): void {
|
||||
clearEdgePageTurn()
|
||||
draggingHomeApp.value = null
|
||||
}
|
||||
|
||||
async function addHomePage(): Promise<void> {
|
||||
if (addingHomePage.value) return
|
||||
addingHomePage.value = true
|
||||
try {
|
||||
if (!appStore.addHomePage()) return
|
||||
await nextTick()
|
||||
changePage(appPages.value.length)
|
||||
} finally {
|
||||
addingHomePage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentPage(): Promise<void> {
|
||||
if (!canDeleteCurrentPage.value) return
|
||||
const deletedPage = phone.currentPage
|
||||
const remainingPages = persistedHomePageCount.value - 1
|
||||
widgets.deletePage(deletedPage, remainingPages)
|
||||
appStore.deleteHomePage(deletedPage)
|
||||
await nextTick()
|
||||
changePage(Math.min(deletedPage, appPages.value.length))
|
||||
}
|
||||
|
||||
function removeHomeApp(appId: LaunchablePhoneAppId): void {
|
||||
appStore.removeHomeApp(appId)
|
||||
}
|
||||
|
||||
function restoreHomeApp(appId: LaunchablePhoneAppId): void {
|
||||
appStore.restoreHomeApp(appId)
|
||||
}
|
||||
|
||||
function clearSearch(): void {
|
||||
searchQuery.value = ''
|
||||
searchFocused.value = false
|
||||
@@ -209,7 +656,7 @@ function openAllApps(): void {
|
||||
showAllApps.value = true
|
||||
}
|
||||
|
||||
watch(isAppPage, (visible) => {
|
||||
watch(isEditablePage, (visible) => {
|
||||
if (!visible) editMode.value = false
|
||||
})
|
||||
</script>
|
||||
@@ -222,6 +669,7 @@ watch(isAppPage, (visible) => {
|
||||
{
|
||||
'springboard--dragging': dragging,
|
||||
'springboard--editing': editMode,
|
||||
'springboard--widget-dragging': draggingWidgetId !== null,
|
||||
},
|
||||
]"
|
||||
>
|
||||
@@ -231,57 +679,82 @@ watch(isAppPage, (visible) => {
|
||||
@pointerdown="onPointerDown"
|
||||
@pointermove="onPointerMove"
|
||||
@pointerup="finishPointer"
|
||||
@pointercancel="finishPointer"
|
||||
@pointercancel="cancelPointer"
|
||||
@transitionend.self="finishPageTransition"
|
||||
@transitioncancel.self="finishPageTransition"
|
||||
>
|
||||
<section
|
||||
class="springboard-page springboard-page--widgets"
|
||||
:style="pageStyle"
|
||||
:aria-label="phone.t('Home.widgets.label')"
|
||||
>
|
||||
<SpringboardWidgets />
|
||||
<div class="springboard-widget-page-scroll">
|
||||
<SpringboardWidgetGrid
|
||||
:page="0"
|
||||
:dragging-widget-id="draggingWidgetId"
|
||||
:edit-mode="editMode"
|
||||
@dragcancel="stopWidgetDrag"
|
||||
@dragend="finishWidgetDrag"
|
||||
@dragmove="queueWidgetDragPreview"
|
||||
@dragstart="startWidgetDrag"
|
||||
@menu="openWidgetMenu"
|
||||
@remove="removeWidget"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-for="(apps, pageIndex) in appPages"
|
||||
:key="`apps-${pageIndex}`"
|
||||
v-for="page in appPages"
|
||||
:key="`apps-${page.page}`"
|
||||
class="springboard-page springboard-page--apps"
|
||||
:style="pageStyle"
|
||||
:aria-label="phone.t('Home.apps')"
|
||||
>
|
||||
<div class="app-grid" data-home-area="grid">
|
||||
<SpringboardWidgetGrid
|
||||
:page="page.page"
|
||||
:dragging-widget-id="draggingWidgetId"
|
||||
:edit-mode="editMode"
|
||||
@dragcancel="stopWidgetDrag"
|
||||
@dragend="finishWidgetDrag"
|
||||
@dragmove="queueWidgetDragPreview"
|
||||
@dragstart="startWidgetDrag"
|
||||
@menu="openWidgetMenu"
|
||||
@remove="removeWidget"
|
||||
/>
|
||||
<TransitionGroup
|
||||
:name="
|
||||
draggingWidgetId && !pageTransitioning ? 'app-reflow' : undefined
|
||||
"
|
||||
tag="div"
|
||||
class="app-grid"
|
||||
data-home-area="grid"
|
||||
>
|
||||
<template
|
||||
v-for="(app, appIndex) in apps"
|
||||
:key="
|
||||
app?.id ??
|
||||
`grid-empty-${pageIndex * HOME_GRID_PAGE_SIZE + appIndex}`
|
||||
"
|
||||
v-for="(cell, cellIndex) in page.cells"
|
||||
:key="cell?.app.id ?? `grid-empty-${page.page}-${cellIndex}`"
|
||||
>
|
||||
<AppIcon
|
||||
v-if="app"
|
||||
:app="app"
|
||||
v-if="cell"
|
||||
:app="cell.app"
|
||||
data-home-area="grid"
|
||||
:data-home-index="pageIndex * HOME_GRID_PAGE_SIZE + appIndex"
|
||||
:data-home-index="cell.sourceIndex"
|
||||
:edit-mode="editMode"
|
||||
@dragcancel="stopHomeDrag"
|
||||
@dragend="finishHomeDrag"
|
||||
@dragstart="
|
||||
startHomeDrag(
|
||||
'grid',
|
||||
pageIndex * HOME_GRID_PAGE_SIZE + appIndex,
|
||||
)
|
||||
"
|
||||
@dragmove="moveHomeDrag"
|
||||
@dragstart="startHomeDrag('grid', cell.sourceIndex)"
|
||||
@edit="enterEditMode"
|
||||
@remove="removeHomeApp(app.id)"
|
||||
@remove="removeHomeApp(cell.app.id)"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="app-grid-slot"
|
||||
data-home-area="grid"
|
||||
:data-home-index="pageIndex * HOME_GRID_PAGE_SIZE + appIndex"
|
||||
:data-home-index="targetHomeIndex(page.page, cellIndex)"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -314,6 +787,32 @@ watch(isAppPage, (visible) => {
|
||||
class="app-library-groups"
|
||||
:class="{ 'app-library-groups--behind': showAllApps }"
|
||||
>
|
||||
<article
|
||||
v-if="hiddenApps.length"
|
||||
class="app-library-group app-library-group--removed"
|
||||
>
|
||||
<div class="app-library-group__icons">
|
||||
<div
|
||||
v-for="app in hiddenApps.slice(0, 4)"
|
||||
:key="app.id"
|
||||
class="app-library-restore-item"
|
||||
>
|
||||
<AppIcon :app="app" compact :show-label="false" />
|
||||
<k-button
|
||||
small
|
||||
rounded
|
||||
class="app-library-restore-button"
|
||||
:aria-label="
|
||||
phone.t('Home.addToHome', { app: phone.t(app.labelKey) })
|
||||
"
|
||||
@click.stop="restoreHomeApp(app.id)"
|
||||
>
|
||||
<Plus :size="13" :stroke-width="3" />
|
||||
</k-button>
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ phone.t('Home.removedFromHome') }}</span>
|
||||
</article>
|
||||
<article
|
||||
v-for="group in appGroups"
|
||||
:key="group.key"
|
||||
@@ -366,6 +865,18 @@ watch(isAppPage, (visible) => {
|
||||
>
|
||||
<AppIcon :app="app" compact :show-label="false" />
|
||||
<span>{{ phone.t(app.labelKey) }}</span>
|
||||
<k-button
|
||||
v-if="appStore.homeLayout.hidden.includes(app.id)"
|
||||
small
|
||||
rounded
|
||||
class="app-library-row-restore"
|
||||
:aria-label="
|
||||
phone.t('Home.addToHome', { app: phone.t(app.labelKey) })
|
||||
"
|
||||
@click.stop="restoreHomeApp(app.id)"
|
||||
>
|
||||
<Plus :size="14" :stroke-width="3" />
|
||||
</k-button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="filteredApps.length === 0" class="app-library-empty">
|
||||
@@ -378,7 +889,20 @@ watch(isAppPage, (visible) => {
|
||||
|
||||
<Transition name="edit-done">
|
||||
<k-glass
|
||||
v-if="editMode && isAppPage"
|
||||
v-if="editMode && isEditablePage"
|
||||
component="button"
|
||||
class="springboard-edit-add"
|
||||
type="button"
|
||||
:aria-label="phone.t('Home.widgetSystem.add')"
|
||||
@click="widgetPickerOpened = true"
|
||||
>
|
||||
<Plus :size="20" :stroke-width="2.5" />
|
||||
</k-glass>
|
||||
</Transition>
|
||||
|
||||
<Transition name="edit-done">
|
||||
<k-glass
|
||||
v-if="editMode && isEditablePage"
|
||||
component="button"
|
||||
class="springboard-edit-done"
|
||||
type="button"
|
||||
@@ -409,6 +933,7 @@ watch(isAppPage, (visible) => {
|
||||
:show-label="false"
|
||||
@dragcancel="stopHomeDrag"
|
||||
@dragend="finishHomeDrag"
|
||||
@dragmove="moveHomeDrag"
|
||||
@dragstart="startHomeDrag('dock', appIndex)"
|
||||
@edit="enterEditMode"
|
||||
@remove="removeHomeApp(app.id)"
|
||||
@@ -425,18 +950,134 @@ watch(isAppPage, (visible) => {
|
||||
</Transition>
|
||||
|
||||
<nav
|
||||
v-if="isAppPage && appPages.length > 1"
|
||||
v-if="phone.currentPage < libraryPage"
|
||||
class="page-indicator"
|
||||
:class="{ 'page-indicator--without-dock': phone.currentPage === 0 }"
|
||||
:aria-label="phone.t('Home.pages')"
|
||||
>
|
||||
<button
|
||||
v-for="(_, pageIndex) in appPages"
|
||||
:key="pageIndex"
|
||||
v-for="pageIndex in appPages.length + 1"
|
||||
:key="pageIndex - 1"
|
||||
type="button"
|
||||
:class="{ active: phone.currentPage === pageIndex + 1 }"
|
||||
:aria-label="`${phone.t('Home.page')} ${pageIndex + 1}`"
|
||||
@click="phone.setCurrentPage(pageIndex + 1, pageCount)"
|
||||
:class="{ active: phone.currentPage === pageIndex - 1 }"
|
||||
:aria-label="`${phone.t('Home.page')} ${pageIndex}`"
|
||||
@click="changePage(pageIndex - 1)"
|
||||
></button>
|
||||
<k-button
|
||||
v-if="editMode && canAddHomePage"
|
||||
small
|
||||
rounded
|
||||
class="springboard-page-add"
|
||||
type="button"
|
||||
:aria-label="phone.t('Home.addPage')"
|
||||
@click="addHomePage"
|
||||
>
|
||||
<Plus :size="12" :stroke-width="2.8" />
|
||||
</k-button>
|
||||
<k-button
|
||||
v-if="editMode && canDeleteCurrentPage"
|
||||
small
|
||||
rounded
|
||||
class="springboard-page-delete"
|
||||
type="button"
|
||||
:aria-label="phone.t('Home.deletePage')"
|
||||
@click="deleteCurrentPage"
|
||||
>
|
||||
<Trash2 :size="12" :stroke-width="2.4" />
|
||||
</k-button>
|
||||
</nav>
|
||||
|
||||
<k-sheet
|
||||
:opened="widgetActionId !== null"
|
||||
class="widget-action-sheet"
|
||||
@backdropclick="widgetActionId = null"
|
||||
>
|
||||
<div class="widget-sheet-handle" />
|
||||
<h3>
|
||||
{{
|
||||
activeWidget
|
||||
? phone.t(`Home.widgetSystem.${activeWidget.kind}.name`)
|
||||
: phone.t('Home.widgets.label')
|
||||
}}
|
||||
</h3>
|
||||
<k-list inset strong class="widget-action-list">
|
||||
<k-list-item
|
||||
link
|
||||
link-component="button"
|
||||
:title="phone.t('Home.widgetSystem.editWidget')"
|
||||
@click="openWidgetConfig"
|
||||
>
|
||||
<template #media><Pencil :size="20" /></template>
|
||||
</k-list-item>
|
||||
<k-list-item
|
||||
link
|
||||
link-component="button"
|
||||
class="widget-action-remove"
|
||||
:title="phone.t('Home.widgetSystem.removeWidget')"
|
||||
@click="activeWidget && removeWidget(activeWidget.id)"
|
||||
>
|
||||
<template #media><Trash2 :size="20" /></template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
<k-button
|
||||
large
|
||||
rounded
|
||||
class="widget-action-cancel"
|
||||
@click="widgetActionId = null"
|
||||
>
|
||||
{{ phone.t('Common.cancel') }}
|
||||
</k-button>
|
||||
</k-sheet>
|
||||
|
||||
<WidgetPickerSheet
|
||||
:opened="widgetPickerOpened"
|
||||
@add="addWidget"
|
||||
@close="widgetPickerOpened = false"
|
||||
/>
|
||||
<WidgetConfigSheet
|
||||
:instance="configuredWidget"
|
||||
:opened="widgetConfigId !== null"
|
||||
@close="widgetConfigId = null"
|
||||
@save="saveWidgetConfig"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.widget-action-sheet) {
|
||||
z-index: 105;
|
||||
padding: 9px 0 30px;
|
||||
border-radius: 25px 25px 0 0;
|
||||
}
|
||||
|
||||
.widget-sheet-handle {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
margin: 0 auto 9px;
|
||||
border-radius: 999px;
|
||||
background: rgb(142 142 147 / 45%);
|
||||
}
|
||||
|
||||
.widget-action-sheet h3 {
|
||||
margin: 3px 18px 9px;
|
||||
color: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.widget-action-list {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.widget-action-remove :deep([class*='title']),
|
||||
.widget-action-remove :deep(svg) {
|
||||
color: #ff3b30 !important;
|
||||
}
|
||||
|
||||
.widget-action-cancel {
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user