mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
feat(frontend): redesign autoload view layout, card styling, and instant sync
- Place Available Payloads on the left and Autoload Sequence on the right - Style payload cards in both columns to match dashboard (icons, badges, and title scroll on focus) - Wrap available payloads in matching glass-panel container with visible borders - Remove subtitle from Add Delay card - Reduce top and bottom margins on autoload view to fill viewport cleanly - Implement instant configuration saving with keepalive and pagehide/beforeunload sync
This commit is contained in:
1 parent
e23c017961
commit
265c827e7f
2 files changed
+428
-168
No files matched your search
@@ -564,7 +564,8 @@ function App() {
|
||||
const success = await api('/set_config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(merged)
|
||||
body: JSON.stringify(merged),
|
||||
keepalive: true,
|
||||
})
|
||||
if (success) {
|
||||
refreshConfig()
|
||||
@@ -817,7 +818,13 @@ function App() {
|
||||
)}>
|
||||
<main ref={mainRef} className={cn(
|
||||
"custom-scrollbar max-w-[1800px] mx-auto w-full flex flex-col",
|
||||
isPS5 ? "pt-16 px-16 pb-12 flex-1 overflow-y-auto" : "pt-6 px-6 pb-36 md:pt-16 md:px-16 md:pb-12 md:flex-1 md:overflow-y-auto"
|
||||
view === 'autoload'
|
||||
? (isPS5
|
||||
? "pt-8 px-12 pb-6 flex-1 min-h-0 overflow-hidden"
|
||||
: "pt-6 px-6 pb-24 md:pt-8 md:px-12 md:pb-6 md:flex-1 md:min-h-0 md:overflow-hidden")
|
||||
: (isPS5
|
||||
? "pt-16 px-16 pb-12 flex-1 overflow-y-auto"
|
||||
: "pt-6 px-6 pb-36 md:pt-16 md:px-16 md:pb-12 md:flex-1 md:overflow-y-auto")
|
||||
)}>
|
||||
<ErrorBoundary title={t("app.error.view_error", "View Render Error")} canDismiss>
|
||||
{view === 'dashboard' && (() => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
||||
import { RefreshCw, ArrowLeft, ArrowRight, Activity, Zap, ChevronUp, ChevronDown, Trash2, CheckCircle2 } from 'lucide-react'
|
||||
import { cn, isPS5, isSystemPayload } from '../../utils/helpers'
|
||||
import { RefreshCw, ArrowLeft, ArrowRight, Activity, Zap, ChevronUp, ChevronDown, Trash2, Globe } from 'lucide-react'
|
||||
import { cn, isPS5, isSystemPayload, parsePayloadName } from '../../utils/helpers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PayloadName from '../ui/PayloadName'
|
||||
import PayloadIcon from '../ui/PayloadIcon'
|
||||
import Modal from '../ui/Modal'
|
||||
import { useRepository } from '../../contexts/RepositoryContext'
|
||||
|
||||
const AutoloadView = ({
|
||||
items = [],
|
||||
@@ -16,6 +18,9 @@ const AutoloadView = ({
|
||||
setDelayModal: setDelayModalProp,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const repoContext = useRepository()
|
||||
const getRepoEntry = repoContext?.getRepoEntry
|
||||
const findRepoEntryById = repoContext?.findRepoEntryById
|
||||
const [subView, setSubView] = useState('list')
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const [autoloadList, setAutoloadList] = useState([])
|
||||
@@ -23,51 +28,103 @@ const AutoloadView = ({
|
||||
const showDelayModal = delayModalProp !== undefined ? delayModalProp : localDelayModal
|
||||
const setShowDelayModal = setDelayModalProp || setLocalDelayModal
|
||||
const [customDelay, setCustomDelay] = useState('')
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const stateRef = useRef({ enabled: false, list: [] })
|
||||
stateRef.current = { enabled, list: autoloadList }
|
||||
|
||||
const isSavingRef = useRef(false)
|
||||
const pendingSaveRef = useRef(false)
|
||||
const lastSyncedRef = useRef('')
|
||||
|
||||
// Load initial config
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
if (config && !isInitialized) {
|
||||
const en = config.AUTOLOAD_ENABLED === true || config.AUTOLOAD_ENABLED === "true"
|
||||
const listStr = config.AUTOLOAD_LIST || ''
|
||||
setEnabled(en)
|
||||
setAutoloadList(listStr.split(',').filter(Boolean))
|
||||
const parsedList = listStr.split(',').filter(Boolean)
|
||||
setAutoloadList(parsedList)
|
||||
stateRef.current = { enabled: en, list: parsedList }
|
||||
lastSyncedRef.current = `${en}:${listStr}`
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}, [config])
|
||||
}, [config, isInitialized])
|
||||
|
||||
// Debounced Auto-Save
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return
|
||||
// Immediate save coordinator with in-flight queueing
|
||||
const saveState = useCallback(async (targetList, targetEnabled) => {
|
||||
const listToSave = targetList !== undefined ? targetList : stateRef.current.list
|
||||
const enabledToSave = targetEnabled !== undefined ? targetEnabled : stateRef.current.enabled
|
||||
|
||||
const currentState = `${enabled}:${autoloadList.join(',')}`
|
||||
if (currentState === lastSyncedRef.current) return
|
||||
const finalList = listToSave.map((p) => (p === 'DELAY' ? '!1000' : p))
|
||||
const finalStr = finalList.join(',')
|
||||
const syncKey = `${enabledToSave}:${finalStr}`
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const shouldEnable = enabled
|
||||
const finalList = autoloadList.map((p) => (p === 'DELAY' ? '!1000' : p))
|
||||
const finalStr = finalList.join(',')
|
||||
if (syncKey === lastSyncedRef.current) return
|
||||
|
||||
setSaving(true)
|
||||
if (isSavingRef.current) {
|
||||
pendingSaveRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
isSavingRef.current = true
|
||||
pendingSaveRef.current = false
|
||||
|
||||
try {
|
||||
const success = await onSaveConfig({
|
||||
AUTOLOAD_ENABLED: shouldEnable,
|
||||
AUTOLOAD_ENABLED: enabledToSave,
|
||||
AUTOLOAD_LIST: finalStr,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
lastSyncedRef.current = `${shouldEnable}:${finalStr}`
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
lastSyncedRef.current = syncKey
|
||||
}
|
||||
setSaving(false)
|
||||
}, 1500)
|
||||
} finally {
|
||||
isSavingRef.current = false
|
||||
if (pendingSaveRef.current) {
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
}, [onSaveConfig])
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [autoloadList, enabled, isInitialized, onSaveConfig])
|
||||
// Safety flush on component unmount and window unload/pagehide
|
||||
useEffect(() => {
|
||||
const flushSave = () => {
|
||||
const { list, enabled: en } = stateRef.current
|
||||
const finalList = list.map((p) => (p === 'DELAY' ? '!1000' : p))
|
||||
const finalStr = finalList.join(',')
|
||||
const syncKey = `${en}:${finalStr}`
|
||||
if (syncKey !== lastSyncedRef.current) {
|
||||
lastSyncedRef.current = syncKey
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
...config,
|
||||
AUTOLOAD_ENABLED: en,
|
||||
AUTOLOAD_LIST: finalStr,
|
||||
})
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon('/set_config', body)
|
||||
} else {
|
||||
fetch('/set_config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
keepalive: true,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
/* ignore flush errors */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', flushSave)
|
||||
window.addEventListener('pagehide', flushSave)
|
||||
return () => {
|
||||
flushSave()
|
||||
window.removeEventListener('beforeunload', flushSave)
|
||||
window.removeEventListener('pagehide', flushSave)
|
||||
}
|
||||
}, [config])
|
||||
|
||||
const availableSlots = useMemo(() => {
|
||||
if (items && items.length > 0) {
|
||||
@@ -99,8 +156,87 @@ const AutoloadView = ({
|
||||
[items]
|
||||
)
|
||||
|
||||
const idCounts = useMemo(() => {
|
||||
const counts = {}
|
||||
if (items) {
|
||||
for (const it of items) {
|
||||
if (it?.kind === 'repo' && it?.id) {
|
||||
counts[it.id] = (counts[it.id] || 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}, [items])
|
||||
|
||||
const resolveCardMeta = useCallback(
|
||||
(keyOrItem) => {
|
||||
if (!keyOrItem) return null
|
||||
const item = typeof keyOrItem === 'object' ? keyOrItem : getItemInfo(keyOrItem)
|
||||
const rawKey = typeof keyOrItem === 'string' ? keyOrItem : (keyOrItem.slot || keyOrItem.filename || '')
|
||||
|
||||
const parsed = parsePayloadName(item?.filename || rawKey)
|
||||
if (parsed.isDelay) {
|
||||
return {
|
||||
isDelay: true,
|
||||
displayName: t("autoload.delay_item", "Delay ({{seconds}}s)", { seconds: parsed.delaySeconds || 1 }),
|
||||
version: null,
|
||||
author: null,
|
||||
icon: null,
|
||||
sourceName: null,
|
||||
iconName: 'Delay',
|
||||
}
|
||||
}
|
||||
|
||||
const itemObj = item || {
|
||||
slot: rawKey,
|
||||
filename: rawKey.split('/').pop(),
|
||||
name: rawKey.split('/').pop(),
|
||||
kind: 'local',
|
||||
}
|
||||
|
||||
let displayName = itemObj.name || itemObj.filename || ''
|
||||
let version = itemObj.version || null
|
||||
let author = null
|
||||
let icon = null
|
||||
let sourceName = null
|
||||
|
||||
if (itemObj.kind === 'local') {
|
||||
displayName = itemObj.name || parsed.displayName
|
||||
version = itemObj.version || parsed.version
|
||||
sourceName = t("app.dashboard.other_source_badge", "Other Source")
|
||||
} else {
|
||||
const repoMeta = (getRepoEntry && itemObj.source_url && itemObj.id)
|
||||
? getRepoEntry(itemObj.source_url, itemObj.id)
|
||||
: (findRepoEntryById && itemObj.id ? findRepoEntryById(itemObj.id) : null)
|
||||
displayName = repoMeta?.name || itemObj.name || itemObj.id || itemObj.filename
|
||||
version = itemObj.version || repoMeta?.version || null
|
||||
author = repoMeta?.author || itemObj.author || null
|
||||
icon = repoMeta?.icon || null
|
||||
const hasDup = Boolean(itemObj.id && idCounts[itemObj.id] > 1)
|
||||
if (hasDup || config?.MULTI_SOURCES_ENABLED) {
|
||||
sourceName = itemObj.source_name || repoMeta?.source_name || null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDelay: false,
|
||||
slot: itemObj.slot || rawKey,
|
||||
filename: itemObj.filename || rawKey,
|
||||
displayName,
|
||||
version,
|
||||
author,
|
||||
icon,
|
||||
sourceName,
|
||||
iconName: itemObj.filename || displayName || 'Payload',
|
||||
}
|
||||
},
|
||||
[getItemInfo, getRepoEntry, findRepoEntryById, idCounts, config, t]
|
||||
)
|
||||
|
||||
const handleToggle = (val) => {
|
||||
setEnabled(val)
|
||||
stateRef.current = { enabled: val, list: autoloadList }
|
||||
saveState(autoloadList, val)
|
||||
}
|
||||
|
||||
const addPayload = (slotKey) => {
|
||||
@@ -112,93 +248,145 @@ const AutoloadView = ({
|
||||
return
|
||||
}
|
||||
}
|
||||
setAutoloadList([...autoloadList, slotKey])
|
||||
const nextList = [...autoloadList, slotKey]
|
||||
setAutoloadList(nextList)
|
||||
stateRef.current = { enabled, list: nextList }
|
||||
saveState(nextList, enabled)
|
||||
setSubView('list')
|
||||
}
|
||||
|
||||
const addDelay = (ms) => {
|
||||
setAutoloadList([...autoloadList, `!${ms}`])
|
||||
const nextList = [...autoloadList, `!${ms}`]
|
||||
setAutoloadList(nextList)
|
||||
stateRef.current = { enabled, list: nextList }
|
||||
saveState(nextList, enabled)
|
||||
setShowDelayModal(false)
|
||||
setSubView('list')
|
||||
}
|
||||
|
||||
const removePayload = (index) => {
|
||||
const nextList = autoloadList.filter((_, idx) => idx !== index)
|
||||
setAutoloadList(nextList)
|
||||
stateRef.current = { enabled, list: nextList }
|
||||
saveState(nextList, enabled)
|
||||
}
|
||||
|
||||
const moveUp = (index) => {
|
||||
if (index === 0) return
|
||||
const newList = [...autoloadList]
|
||||
;[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]]
|
||||
setAutoloadList(newList)
|
||||
const nextList = [...autoloadList]
|
||||
;[nextList[index - 1], nextList[index]] = [nextList[index], nextList[index - 1]]
|
||||
setAutoloadList(nextList)
|
||||
stateRef.current = { enabled, list: nextList }
|
||||
saveState(nextList, enabled)
|
||||
}
|
||||
|
||||
const moveDown = (index) => {
|
||||
if (index === autoloadList.length - 1) return
|
||||
const newList = [...autoloadList]
|
||||
;[newList[index + 1], newList[index]] = [newList[index], newList[index + 1]]
|
||||
setAutoloadList(newList)
|
||||
const nextList = [...autoloadList]
|
||||
;[nextList[index + 1], nextList[index]] = [nextList[index], nextList[index + 1]]
|
||||
setAutoloadList(nextList)
|
||||
stateRef.current = { enabled, list: nextList }
|
||||
saveState(nextList, enabled)
|
||||
}
|
||||
|
||||
const renderAvailable = () => (
|
||||
<div className="space-y-8 flex flex-col h-full min-h-0">
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="flex items-center justify-between shrink-0 h-11 mb-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{subView === 'add' && (
|
||||
<button onClick={() => setSubView('list')} className="p-2 bg-white/5 rounded-xl border border-white/10 lg:hidden">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<h3 className="label-caps !text-white !opacity-100 text-xl tracking-widest">{t("autoload.available_title", "Available Payloads")}</h3>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">
|
||||
{t("autoload.available_title", "Available Payloads")}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar min-h-0 pb-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{availableSlots.map((item) => {
|
||||
const isKstuff = (item.id || item.filename || item.slot || '').toLowerCase().includes('kstuff')
|
||||
const hasKstuff = autoloadList.some((x) => x.toLowerCase().includes('kstuff'))
|
||||
const isBlocked = isKstuff && hasKstuff
|
||||
<div className="glass-panel p-4 md:p-5 rounded-ps-3xl border-white/10 flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar min-h-0 pb-2">
|
||||
<div className="grid grid-cols-1 gap-2.5">
|
||||
{availableSlots.map((item) => {
|
||||
const meta = resolveCardMeta(item)
|
||||
const isKstuff = (item.id || item.filename || item.slot || '').toLowerCase().includes('kstuff')
|
||||
const hasKstuff = autoloadList.some((x) => x.toLowerCase().includes('kstuff'))
|
||||
const isBlocked = isKstuff && hasKstuff
|
||||
|
||||
return (
|
||||
return (
|
||||
<button
|
||||
key={item.slot}
|
||||
onClick={() => !isBlocked && addPayload(item.slot)}
|
||||
disabled={isBlocked}
|
||||
className={cn(
|
||||
"group glass-card pl-4 pr-3 py-2.5 rounded-ps-xl flex flex-col justify-center border transition-all text-left relative min-h-[72px] w-full",
|
||||
isBlocked
|
||||
? "opacity-40 grayscale cursor-not-allowed border-white/5"
|
||||
: "border-white/5 hover:border-ps-blue hover:bg-ps-blue/5 focus:border-ps-blue focus:outline-none"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full z-10 my-auto">
|
||||
<div className="flex items-center gap-3.5 min-w-0 pr-2 flex-1">
|
||||
<PayloadIcon icon={meta?.icon} name={meta?.iconName} className="w-11 h-11 text-xl shrink-0" />
|
||||
<div className="min-w-0 flex-1 flex flex-col justify-center">
|
||||
<PayloadName
|
||||
path={meta?.filename || meta?.slot}
|
||||
version={meta?.version}
|
||||
hideVersion={true}
|
||||
displayName={meta?.displayName}
|
||||
className="text-white text-lg md:text-xl leading-snug"
|
||||
stacked={false}
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{meta?.version && (
|
||||
<span className="text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0 leading-none">
|
||||
{meta.version}
|
||||
</span>
|
||||
)}
|
||||
{meta?.author && (
|
||||
<div className="text-[10px] text-zinc-500 font-medium truncate leading-none">
|
||||
{meta.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{meta?.sourceName && (
|
||||
<div className="flex items-center mt-0.5">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0 max-w-full select-none leading-none">
|
||||
<Globe className="w-3 h-3 text-zinc-400 shrink-0" />
|
||||
<span className="truncate">{meta.sourceName}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<ArrowRight className={cn(
|
||||
"w-5 h-5 transition-all shrink-0",
|
||||
isBlocked
|
||||
? "text-zinc-800"
|
||||
: "text-zinc-500 group-hover:text-ps-blue group-hover:translate-x-1"
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 rounded-ps-xl bg-ps-blue/0 group-hover:bg-ps-blue/5 transition-colors z-0 pointer-events-none" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<div className="pt-2 border-t border-white/10 mt-1">
|
||||
<button
|
||||
key={item.slot}
|
||||
onClick={() => !isBlocked && addPayload(item.slot)}
|
||||
disabled={isBlocked}
|
||||
className={cn(
|
||||
"flex items-start justify-between p-6 glass-card rounded-2xl border-white/20 transition-all text-left",
|
||||
isBlocked ? "opacity-40 cursor-not-allowed" : "bg-white/[0.03] hover:border-ps-blue group"
|
||||
)}
|
||||
onClick={() => setShowDelayModal(true)}
|
||||
className="group glass-card pl-4 pr-3 py-2.5 rounded-ps-xl flex items-center justify-between border border-dashed border-white/20 hover:border-ps-blue hover:bg-ps-blue/5 focus:border-ps-blue focus:outline-none transition-all text-left relative min-h-[58px] w-full"
|
||||
>
|
||||
<PayloadName
|
||||
path={item.filename || item.slot}
|
||||
displayName={item.name || item.filename}
|
||||
version={item.version}
|
||||
className={cn("text-xl", isBlocked ? "text-zinc-500" : "text-white")}
|
||||
stacked
|
||||
/>
|
||||
<ArrowRight className={cn("w-6 h-6 transition-all shrink-0 mt-1", isBlocked ? "text-zinc-800" : "text-zinc-500 group-hover:text-ps-blue group-hover:translate-x-2")} />
|
||||
<div className="flex items-center gap-3.5 min-w-0 pr-2">
|
||||
<div className="w-10 h-10 text-lg shrink-0 rounded-xl flex items-center justify-center border border-ps-blue/30 bg-ps-blue/10 text-ps-blue">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="font-bold text-white uppercase tracking-tight text-base leading-none">
|
||||
{t("autoload.add_delay_btn", "Add Delay")}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-zinc-500 group-hover:text-ps-blue group-hover:translate-x-1 transition-all shrink-0" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<div className="pt-4 border-t border-white/10 mt-4">
|
||||
<button
|
||||
onClick={() => setShowDelayModal(true)}
|
||||
className="w-full flex items-center justify-between p-6 bg-white/[0.03] rounded-2xl border border-dashed border-white/20 hover:border-ps-blue group transition-all"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Zap className="w-6 h-6 text-ps-blue" />
|
||||
<span className="font-bold text-white uppercase tracking-tight text-xl">{t("autoload.add_delay_btn", "Add Delay")}</span>
|
||||
</div>
|
||||
<ArrowRight className="w-6 h-6 text-zinc-500 group-hover:text-ps-blue group-hover:translate-x-2 transition-all" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-8 border-t border-white/5 mt-8 text-center space-y-4">
|
||||
<p className="text-zinc-500 text-sm font-bold uppercase tracking-widest opacity-60">{t("autoload.missing_payload", "Missing a payload?")}</p>
|
||||
<button
|
||||
onClick={() => onRedirect('storage', 'usb-storage')}
|
||||
className="group flex flex-col items-center mx-auto space-y-3"
|
||||
>
|
||||
<div className="flex items-center space-x-3 text-ps-blue group-hover:text-white transition-colors">
|
||||
<span className="font-black italic text-lg uppercase tracking-tight">{t("autoload.move_usb_btn", "Move from USB to Internal")}</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-600 max-w-[200px] leading-relaxed">{t("autoload.move_usb_desc", "Required for payloads you want to use in the Autoload sequence.")}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,66 +394,115 @@ const AutoloadView = ({
|
||||
)
|
||||
|
||||
const renderSequence = () => (
|
||||
<div className="space-y-8 flex flex-col h-full min-h-0">
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-4xl font-extrabold text-white tracking-tight">
|
||||
{t("autoload.sequence_title_1", "Autoload")} <span className="text-ps-blue">{t("autoload.sequence_title_2", "Sequence")}</span>
|
||||
</h2>
|
||||
<div className="h-6 mt-1 overflow-hidden">
|
||||
{saving ? (
|
||||
<div className="flex items-center space-x-2 text-ps-blue/60 text-xs font-bold uppercase tracking-widest">
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
<span>{t("autoload.saving", "Saving Changes...")}</span>
|
||||
</div>
|
||||
) : saved ? (
|
||||
<div className="flex items-center space-x-2 text-emerald-500 text-xs font-bold uppercase tracking-widest animate-in slide-in-from-bottom-2">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
<span>{t("autoload.saved", "All Changes Saved")}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="flex items-center justify-between shrink-0 h-11 mb-4">
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">
|
||||
{t("autoload.sequence_title_1", "Autoload")} <span className="text-ps-blue">{t("autoload.sequence_title_2", "Sequence")}</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => handleToggle(false)}
|
||||
className="px-4 py-2 rounded-xl font-black uppercase italic tracking-tighter bg-red-600/10 text-red-500 border border-red-500/30 hover:bg-red-600 hover:text-white transition-all shadow-lg text-xs"
|
||||
className="px-3.5 py-1.5 rounded-xl font-black uppercase italic tracking-wider bg-red-600/10 text-red-500 border border-red-500/30 hover:bg-red-600 hover:text-white transition-all shadow-lg text-xs shrink-0"
|
||||
>
|
||||
{t("autoload.disable_btn", "Disable Autoload")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-ps-3xl border-white/10 flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar space-y-4 pr-2 mb-2 pb-6">
|
||||
{autoloadList.map((p, i) => {
|
||||
const item = getItemInfo(p)
|
||||
return (
|
||||
<div key={`${p}-${i}`} className="relative flex items-center justify-between p-4 bg-white/5 rounded-2xl border border-white/10 animate-in slide-in-from-left duration-200">
|
||||
<div className="absolute top-0 left-0 w-6 h-6 rounded-full bg-dark flex items-center justify-center z-20">
|
||||
<span className="text-gray-500 text-[12px] font-black">{i + 1}</span>
|
||||
<div className="glass-panel p-4 md:p-5 rounded-ps-3xl border-white/10 flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar pr-2 min-h-0 pb-2">
|
||||
<div className="grid grid-cols-1 gap-2.5">
|
||||
{autoloadList.map((p, i) => {
|
||||
const meta = resolveCardMeta(p)
|
||||
return (
|
||||
<div
|
||||
key={`${p}-${i}`}
|
||||
tabIndex={0}
|
||||
className="group glass-card pl-4 pr-3 py-2.5 rounded-ps-xl flex flex-col justify-center border border-white/5 hover:border-ps-blue/40 focus:border-ps-blue focus:outline-none transition-all text-left relative min-h-[72px]"
|
||||
>
|
||||
<div className="flex items-center justify-between w-full z-10 my-auto">
|
||||
<div className="flex items-center gap-3.5 min-w-0 pr-2 flex-1">
|
||||
<div className="relative shrink-0">
|
||||
{meta?.isDelay ? (
|
||||
<div className="w-11 h-11 text-xl rounded-2xl flex items-center justify-center border border-ps-blue/30 bg-ps-blue/10 text-ps-blue">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
) : (
|
||||
<PayloadIcon icon={meta?.icon} name={meta?.iconName} className="w-11 h-11 text-xl shrink-0" />
|
||||
)}
|
||||
<div className="absolute -top-1.5 -left-1.5 z-20 w-5 h-5 rounded-full bg-ps-blue border border-black/60 flex items-center justify-center text-[10px] font-black text-white shadow select-none">
|
||||
{i + 1}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 flex flex-col justify-center">
|
||||
<PayloadName
|
||||
path={meta?.filename || p}
|
||||
version={meta?.version}
|
||||
hideVersion={true}
|
||||
displayName={meta?.displayName}
|
||||
className="text-white text-lg md:text-xl leading-snug"
|
||||
stacked={false}
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{meta?.version && (
|
||||
<span className="text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0 leading-none">
|
||||
{meta.version}
|
||||
</span>
|
||||
)}
|
||||
{meta?.author && (
|
||||
<div className="text-[10px] text-zinc-500 font-medium truncate leading-none">
|
||||
{meta.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{meta?.sourceName && (
|
||||
<div className="flex items-center mt-0.5">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0 max-w-full select-none leading-none">
|
||||
<Globe className="w-3 h-3 text-zinc-400 shrink-0" />
|
||||
<span className="truncate">{meta.sourceName}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 z-20 ml-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
moveUp(i)
|
||||
}}
|
||||
disabled={i === 0}
|
||||
title={t("autoload.move_up", "Move Up")}
|
||||
className="p-1.5 bg-white/5 hover:bg-ps-blue hover:text-white text-zinc-400 rounded-lg border border-white/5 hover:border-ps-blue transition-all disabled:opacity-20 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
moveDown(i)
|
||||
}}
|
||||
disabled={i === autoloadList.length - 1}
|
||||
title={t("autoload.move_down", "Move Down")}
|
||||
className="p-1.5 bg-white/5 hover:bg-ps-blue hover:text-white text-zinc-400 rounded-lg border border-white/5 hover:border-ps-blue transition-all disabled:opacity-20 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removePayload(i)
|
||||
}}
|
||||
title={t("autoload.remove", "Remove")}
|
||||
className="p-1.5 bg-white/5 hover:bg-red-600 hover:text-white text-zinc-400 hover:border-red-500 rounded-lg border border-white/5 transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 rounded-ps-xl bg-ps-blue/0 group-hover:bg-ps-blue/5 transition-colors z-0 pointer-events-none" />
|
||||
</div>
|
||||
<div className="flex items-center min-w-0 pl-2">
|
||||
<PayloadName
|
||||
path={item?.filename || p}
|
||||
displayName={item?.name || item?.filename}
|
||||
version={item?.version}
|
||||
className="text-white"
|
||||
stacked
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button onClick={() => moveUp(i)} disabled={i === 0} className="p-2 bg-white/10 text-zinc-400 hover:bg-ps-blue hover:text-white rounded-xl disabled:opacity-20">
|
||||
<ChevronUp className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={() => moveDown(i)} disabled={i === autoloadList.length - 1} className="p-2 bg-white/10 text-zinc-400 hover:bg-ps-blue hover:text-white rounded-xl disabled:opacity-20">
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={() => setAutoloadList(autoloadList.filter((_, idx) => idx !== i))} className="p-2 bg-white/10 text-zinc-400 hover:bg-red-600 hover:text-white rounded-xl">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{autoloadList.length === 0 && (
|
||||
<div className="flex-1 flex flex-col items-center justify-center opacity-10 italic py-20">
|
||||
<RefreshCw className="w-16 h-16 mb-4" />
|
||||
@@ -315,50 +552,66 @@ const AutoloadView = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full min-h-0 animate-fade-in">
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-12 gap-8 min-h-0 pb-12">
|
||||
<div className={cn("h-full min-h-0", isPS5 ? "col-span-12 lg:col-span-8" : subView === 'list' ? "col-span-12 lg:col-span-8" : "hidden lg:block lg:col-span-8")}>
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className={cn(
|
||||
"gap-8 h-full min-h-0",
|
||||
isPS5 ? "grid grid-cols-2" : "hidden lg:grid lg:grid-cols-2"
|
||||
)}>
|
||||
{renderAvailable()}
|
||||
{renderSequence()}
|
||||
</div>
|
||||
<div className={cn("h-full min-h-0", isPS5 ? "col-span-12 lg:col-span-4" : subView === 'add' ? "col-span-12 lg:col-span-4" : "hidden lg:block lg:col-span-4")}>
|
||||
{renderAvailable()}
|
||||
<div className={cn(
|
||||
"h-full flex flex-col min-h-0",
|
||||
isPS5 ? "hidden" : "lg:hidden"
|
||||
)}>
|
||||
{subView === 'list' ? renderSequence() : renderAvailable()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal show={showDelayModal} title={t("autoload.add_delay_modal_title", "Add Delay")} onClose={() => setShowDelayModal(false)}>
|
||||
<div className="space-y-6">
|
||||
<p className="text-zinc-400 font-medium text-sm">
|
||||
{t("autoload.add_delay_modal_desc", "Add a pause between payloads to allow background services to initialize properly.")}
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[1, 2, 3, 5, 10, 15].map((s) => (
|
||||
<Modal
|
||||
show={showDelayModal}
|
||||
title={t("autoload.delay_modal.title", "Configure Delay")}
|
||||
onClose={() => setShowDelayModal(false)}
|
||||
footer={
|
||||
<button
|
||||
onClick={() => setShowDelayModal(false)}
|
||||
className="w-full py-4 bg-white/5 hover:bg-white/10 text-white rounded-2xl font-bold uppercase tracking-tight transition-all"
|
||||
>
|
||||
{t("autoload.delay_modal.cancel", "Cancel")}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<div className="grid grid-cols-3 gap-3 md:gap-4">
|
||||
{[1, 3, 5].map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => addDelay(s * 1000)}
|
||||
className="p-4 bg-white/5 hover:bg-ps-blue text-white rounded-xl font-bold text-center border border-white/10 hover:border-ps-blue transition-all"
|
||||
className="py-4 md:py-6 bg-ps-blue/20 hover:bg-ps-blue border border-ps-blue/30 text-white rounded-2xl font-black text-xl md:text-2xl transition-all shadow-lg"
|
||||
>
|
||||
{s}s
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex space-x-2 pt-2">
|
||||
<input
|
||||
type="number"
|
||||
placeholder={t("autoload.custom_seconds", "Custom seconds...")}
|
||||
value={customDelay}
|
||||
onChange={(e) => setCustomDelay(e.target.value)}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-none focus:border-ps-blue"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const val = parseFloat(customDelay)
|
||||
if (val > 0) addDelay(val * 1000)
|
||||
}}
|
||||
disabled={!customDelay || parseFloat(customDelay) <= 0}
|
||||
className="px-6 bg-ps-blue text-white rounded-xl font-bold disabled:opacity-50"
|
||||
>
|
||||
{t("autoload.add_btn", "Add")}
|
||||
</button>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<p className="label-caps !text-zinc-500 text-sm md:text-base">{t("autoload.delay_modal.custom_delay_label", "Custom Delay (ms)")}</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<input
|
||||
type="number"
|
||||
placeholder={t("autoload.delay_modal.placeholder", "e.g. 2500")}
|
||||
value={customDelay}
|
||||
onChange={(e) => setCustomDelay(e.target.value)}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-2xl p-4 md:p-5 text-white font-mono text-xl md:text-2xl focus:border-ps-blue outline-none transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={() => customDelay && addDelay(parseInt(customDelay))}
|
||||
className="py-4 md:py-0 px-8 md:px-10 bg-ps-blue text-white rounded-2xl font-black uppercase italic tracking-tighter text-lg md:text-xl shadow-2xl hover:bg-ps-blue/80 transition-all shrink-0"
|
||||
>
|
||||
{t("autoload.delay_modal.add_btn", "Add")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in new issue
Block a user