From 21bf41008309013f54ed81848276614cfe5f80a6 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Sat, 20 Jun 2026 17:10:19 -0600 Subject: [PATCH] Add custom public version names for Custom patcher mode --- src/app/discover/actions.ts | 36 +++++++++-- src/app/hack/[slug]/actions.ts | 31 +++++++++- src/app/hack/[slug]/page.tsx | 4 +- src/app/hack/[slug]/versions/page.tsx | 3 +- src/app/page.tsx | 59 ++++++++++++++----- src/components/Hack/PatcherVersionManager.tsx | 51 ++++++++++++++-- .../Hack/PatcherVersionSettings.tsx | 57 +++++++++++++++++- src/types/db.ts | 3 + src/utils/patches/hack-display-version.ts | 46 +++++++++++++++ .../20260620205300_custom_version_name.sql | 2 + 10 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 src/utils/patches/hack-display-version.ts create mode 100644 supabase/migrations/20260620205300_custom_version_name.sql diff --git a/src/app/discover/actions.ts b/src/app/discover/actions.ts index 5480f25..c6b2663 100644 --- a/src/app/discover/actions.ts +++ b/src/app/discover/actions.ts @@ -6,6 +6,7 @@ import { getCachedTagsWithUsage, buildTagFilterGroups } from "@/data/tags"; import { sortOrderedTags, OrderedTag, getCoverUrls } from "@/utils/format"; import { HackCardAttributes } from "@/components/HackCard"; import type { DiscoverSortOption } from "@/types/discover"; +import { resolveHackDisplayVersion } from "@/utils/patches/hack-display-version"; const TRENDING_WINDOW_DAYS = 3; const TIME_TO_LIVE = 600; // 10 minutes @@ -35,7 +36,7 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); + const customPatcherSlugs = new Set(); + if (slugs.length > 0) { + const { data: customPatchRows, error: customPatchRowsError } = await supabase + .from("hack_patcher_patches") + .select("hack_slug, sort_order, patches!inner(version)") + .in("hack_slug", slugs) + .order("sort_order", { ascending: true }); + if (customPatchRowsError) throw customPatchRowsError; + + (customPatchRows || []).forEach((row: any) => { + customPatcherSlugs.add(row.hack_slug); + if (customDefaultVersionsBySlug.has(row.hack_slug)) return; + const patch = Array.isArray(row.patches) ? row.patches[0] : row.patches; + if (patch?.version) customDefaultVersionsBySlug.set(row.hack_slug, patch.version); + }); + } + // Calculate trending scores if needed let trendingScores: Map | null = null; if (sort === "trending") { @@ -217,14 +236,21 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); const publishedAtBySlug = new Map(); (rows || []).forEach((r: any) => { - if (typeof r.current_patch === "number") { - const version = versionsByPatchId.get(r.current_patch) || "Pre-release"; - mappedVersions.set(r.slug, version); + const currentPatchVersion = typeof r.current_patch === "number" + ? versionsByPatchId.get(r.current_patch) || "Pre-release" + : ""; + mappedVersions.set(r.slug, resolveHackDisplayVersion({ + isArchive: r.is_archive, + isCustomPatcherActive: customPatcherSlugs.has(r.slug), + customVersionName: r.custom_version_name, + customDefaultPatchVersion: customDefaultVersionsBySlug.get(r.slug), + currentPatchVersion, + })); + if (typeof r.current_patch === "number") { const publishedAt = publishedAtByPatchId.get(r.current_patch) ?? null; publishedAtBySlug.set(r.slug, publishedAt); } else { - mappedVersions.set(r.slug, r.is_archive ? "Archive" : "Pre-release"); publishedAtBySlug.set(r.slug, null); } }); diff --git a/src/app/hack/[slug]/actions.ts b/src/app/hack/[slug]/actions.ts index ba4aaa4..1debddc 100644 --- a/src/app/hack/[slug]/actions.ts +++ b/src/app/hack/[slug]/actions.ts @@ -12,6 +12,7 @@ import { unstable_cache as cache } from "next/cache"; import { sortOrderedTags, getCoverUrls } from "@/utils/format"; import { Database, Constants } from "@/types/db"; import { getPatcherSelectablePatches } from "@/utils/patches/patcher-selectable-patches"; +import { CUSTOM_VERSION_NAME_MAX_LENGTH, resolveHackDisplayVersion } from "@/utils/patches/hack-display-version"; import type { SelectablePatch } from "@/types/patcher"; const PATCHES_DOWNLOAD_PERMISSION_VALUES = Constants.public.Enums[ @@ -39,6 +40,7 @@ export interface HackMetadata { completion_status: Database["public"]["Enums"]["Completion Status"] | null; verification_contact_info: string | null; }; + displayVersion: string; images: string[]; tags: string[]; profile: { @@ -72,7 +74,7 @@ export async function getHackMetadata(slug: string): Promise 0, + customVersionName: hack.custom_version_name, + customDefaultPatchVersion: selectablePatches[0]?.version, + currentPatchVersion: patch?.version, + }); return { hack, + displayVersion, images, tags, profile: profile ? { @@ -1005,6 +1015,7 @@ export async function confirmReuploadPatchVersion( export async function updatePatcherSelectablePatches( slug: string, patchIds: number[], + customVersionName?: string | null, ): Promise<{ ok: true } | { ok: false; error: string }> { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); @@ -1028,6 +1039,16 @@ export async function updatePatcherSelectablePatches( // Dedupe patch ids const uniquePatchIds = [...new Set(patchIds)]; + const trimmedCustomVersionName = customVersionName?.trim() || null; + + if (uniquePatchIds.length > 0) { + if (!trimmedCustomVersionName) { + return { ok: false, error: "Custom version name is required." }; + } + if (trimmedCustomVersionName.length > CUSTOM_VERSION_NAME_MAX_LENGTH) { + return { ok: false, error: "Custom version name must be 12 characters or fewer." }; + } + } if (uniquePatchIds.length > 0) { // Verify patches belong to this hack @@ -1060,6 +1081,12 @@ export async function updatePatcherSelectablePatches( }); if (replaceErr) return { ok: false, error: replaceErr.message }; + const { error: nameErr } = await supabase + .from("hacks") + .update({ custom_version_name: uniquePatchIds.length > 0 ? trimmedCustomVersionName : null }) + .eq("slug", slug); + if (nameErr) return { ok: false, error: nameErr.message }; + revalidateTag(`hack:${slug}:metadata`); revalidatePath(`/hack/${slug}`); revalidatePath(`/hack/${slug}/versions`); diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index 3fe757a..8286e7f 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -115,7 +115,7 @@ export default async function HackDetail({ params }: HackDetailProps) { if (!metadata) return notFound(); - const { hack, images, tags, profile, otherHacks, patch } = metadata; + const { hack, images, tags, profile, otherHacks, patch, displayVersion } = metadata; const baseRom = baseRoms.find((r) => r.id === hack.base_rom); const author = hack.original_author ? hack.original_author : (profile?.username ? `@${profile.username}` : "Unknown"); @@ -148,7 +148,7 @@ export default async function HackDetail({ params }: HackDetailProps) { // Extract patch info from cached metadata const patchFilename = patch?.filename || null; - const patchVersion = isArchive ? "Archive" : (patch?.version || ""); + const patchVersion = displayVersion; const patchId = patch?.id || null; const lastUpdated = patch ? new Date(patch.created_at).toLocaleDateString() : null; const patchCreatedAt = patch?.created_at || null; diff --git a/src/app/hack/[slug]/versions/page.tsx b/src/app/hack/[slug]/versions/page.tsx index f264fe8..5849fb7 100644 --- a/src/app/hack/[slug]/versions/page.tsx +++ b/src/app/hack/[slug]/versions/page.tsx @@ -21,7 +21,7 @@ export default async function VersionsPage({ params }: VersionsPageProps) { // Fetch hack const { data: hack } = await supabase .from("hacks") - .select("slug, title, created_by, current_patch, original_author, permission_from, base_rom, is_archive, patches_download_permission") + .select("slug, title, created_by, current_patch, custom_version_name, original_author, permission_from, base_rom, is_archive, patches_download_permission") .eq("slug", slug) .maybeSingle(); @@ -98,6 +98,7 @@ export default async function VersionsPage({ params }: VersionsPageProps) { hackSlug={slug} currentPatchId={hack.current_patch} initialSavedPatchIds={patcherSelection?.savedPatchIds ?? []} + initialCustomVersionName={hack.custom_version_name} patches={allPatches} baseRom={hack.base_rom} patchesDownloadPermission={hack.patches_download_permission} diff --git a/src/app/page.tsx b/src/app/page.tsx index a14b666..a454f14 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import HackCard from "@/components/HackCard"; import Button from "@/components/Button"; import { sortOrderedTags, getCoverUrls } from "@/utils/format"; import { HackCardAttributes } from "@/components/HackCard"; +import { resolveHackDisplayVersion } from "@/utils/patches/hack-display-version"; export const metadata: Metadata = { alternates: { @@ -22,7 +23,7 @@ export default async function Home() { // Fetch top 6 approved hacks ordered by downloads const { data: popularHacks } = await supabase .from("hacks") - .select("slug,title,summary,description,base_rom,downloads,created_by,current_patch,original_author,is_archive") + .select("slug,title,summary,description,base_rom,downloads,created_by,current_patch,custom_version_name,original_author,is_archive") .eq("approved", true) .not("current_patch", "is", null) .is("is_archive", false) @@ -75,21 +76,47 @@ export default async function Home() { }); // Fetch versions - let mappedVersions = new Map(); - await Promise.all( - popularHacks.map(async (r) => { - if (r.current_patch) { - const { data: currentPatch } = await supabase - .from("patches") - .select("version") - .eq("id", r.current_patch) - .maybeSingle(); - mappedVersions.set(r.slug, currentPatch?.version || "Pre-release"); - } else { - mappedVersions.set(r.slug, r.original_author ? "Archive" : "Pre-release"); - } - }) - ); + const patchIds = popularHacks + .map((hack) => hack.current_patch) + .filter((id): id is number => typeof id === "number"); + const versionsByPatchId = new Map(); + if (patchIds.length > 0) { + const { data: patchRows } = await supabase + .from("patches") + .select("id,version") + .in("id", patchIds); + (patchRows || []).forEach((patch) => { + versionsByPatchId.set(patch.id, patch.version || "Pre-release"); + }); + } + + const customDefaultVersionsBySlug = new Map(); + const customPatcherSlugs = new Set(); + const { data: customPatchRows } = await supabase + .from("hack_patcher_patches") + .select("hack_slug, sort_order, patches!inner(version)") + .in("hack_slug", slugs) + .order("sort_order", { ascending: true }); + (customPatchRows || []).forEach((row: any) => { + customPatcherSlugs.add(row.hack_slug); + if (customDefaultVersionsBySlug.has(row.hack_slug)) return; + const patch = Array.isArray(row.patches) ? row.patches[0] : row.patches; + if (patch?.version) customDefaultVersionsBySlug.set(row.hack_slug, patch.version); + }); + + const mappedVersions = new Map(); + popularHacks.forEach((hack) => { + const currentPatchVersion = typeof hack.current_patch === "number" + ? versionsByPatchId.get(hack.current_patch) || "Pre-release" + : ""; + mappedVersions.set(hack.slug, resolveHackDisplayVersion({ + isArchive: hack.is_archive, + isCustomPatcherActive: customPatcherSlugs.has(hack.slug), + customVersionName: hack.custom_version_name, + customDefaultPatchVersion: customDefaultVersionsBySlug.get(hack.slug), + currentPatchVersion, + })); + }); // Fetch profiles const userIds = [...new Set(popularHacks.map((h) => h.created_by).filter(Boolean))]; diff --git a/src/components/Hack/PatcherVersionManager.tsx b/src/components/Hack/PatcherVersionManager.tsx index 6eee212..cb1fba2 100644 --- a/src/components/Hack/PatcherVersionManager.tsx +++ b/src/components/Hack/PatcherVersionManager.tsx @@ -6,6 +6,7 @@ import { FiX } from "react-icons/fi"; import { updatePatcherSelectablePatches } from "@/app/hack/[slug]/actions"; import PatcherVersionSettings from "@/components/Hack/PatcherVersionSettings"; import VersionList from "@/components/Hack/VersionList"; +import { CUSTOM_VERSION_NAME_MAX_LENGTH, suggestCustomVersionName } from "@/utils/patches/hack-display-version"; import type { PatchesDownloadPermission } from "@/components/Hack/DownloadPermissionSettings"; type PatcherOption = "latest" | "custom"; @@ -24,6 +25,7 @@ interface PatcherVersionManagerProps { hackSlug: string; currentPatchId: number | null; initialSavedPatchIds: number[]; + initialCustomVersionName: string | null; patches: Patch[]; baseRom: string; patchesDownloadPermission: PatchesDownloadPermission; @@ -39,10 +41,19 @@ function optionFromSavedIds(savedPatchIds: number[]): PatcherOption { return savedPatchIds.length > 0 ? "custom" : "latest"; } +function initialCustomName(name: string | null, savedPatchIds: number[], patches: Patch[]) { + const trimmedName = name?.trim(); + if (trimmedName) return trimmedName.slice(0, CUSTOM_VERSION_NAME_MAX_LENGTH); + if (savedPatchIds.length === 0) return ""; + const firstSavedPatch = patches.find((patch) => patch.id === savedPatchIds[0]); + return (firstSavedPatch?.version || "").slice(0, CUSTOM_VERSION_NAME_MAX_LENGTH); +} + export default function PatcherVersionManager({ hackSlug, currentPatchId, initialSavedPatchIds, + initialCustomVersionName, patches, baseRom, patchesDownloadPermission, @@ -54,6 +65,8 @@ export default function PatcherVersionManager({ const [draftOption, setDraftOption] = useState(() => optionFromSavedIds(initialSavedPatchIds)); const [savedPatchIds, setSavedPatchIds] = useState(initialSavedPatchIds); const [draftPatchIds, setDraftPatchIds] = useState(initialSavedPatchIds); + const [savedCustomVersionName, setSavedCustomVersionName] = useState(() => initialCustomName(initialCustomVersionName, initialSavedPatchIds, patches)); + const [draftCustomVersionName, setDraftCustomVersionName] = useState(() => initialCustomName(initialCustomVersionName, initialSavedPatchIds, patches)); const [showPublishModal, setShowPublishModal] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -66,15 +79,19 @@ export default function PatcherVersionManager({ }>({}); const initialSavedKey = initialSavedPatchIds.join(","); + const initialCustomVersionNameKey = initialCustomVersionName ?? ""; useEffect(() => { const nextOption = optionFromSavedIds(initialSavedPatchIds); + const nextCustomVersionName = initialCustomName(initialCustomVersionName, initialSavedPatchIds, patches); setPublishedOption(nextOption); setDraftOption(nextOption); setSavedPatchIds(initialSavedPatchIds); setDraftPatchIds(initialSavedPatchIds); + setSavedCustomVersionName(nextCustomVersionName); + setDraftCustomVersionName(nextCustomVersionName); setSelectionMode(false); - }, [initialSavedKey, initialSavedPatchIds]); + }, [initialSavedKey, initialSavedPatchIds, initialCustomVersionNameKey, initialCustomVersionName, patches]); function clearSavedFeedbackTimers() { const t = savedTimersRef.current; @@ -139,10 +156,17 @@ export default function PatcherVersionManager({ .filter((patch) => !patch.published && !patch.archived) .map((patch) => patch.version) : []; + const suggestedCustomVersionName = useMemo( + () => suggestCustomVersionName(draftVersionLabels), + [draftVersionLabels], + ); const hasUnsavedChanges = draftOption !== publishedOption - || (draftOption === "custom" && !sameOrderedIds(draftPatchIds, savedPatchIds)); - const publishDisabled = saving || !hasUnsavedChanges || (draftOption === "custom" && draftPatchIds.length === 0); + || (draftOption === "custom" && !sameOrderedIds(draftPatchIds, savedPatchIds)) + || (draftOption === "custom" && draftCustomVersionName.trim() !== savedCustomVersionName); + const publishDisabled = saving + || !hasUnsavedChanges + || (draftOption === "custom" && (draftPatchIds.length === 0 || !draftCustomVersionName.trim())); function enterCustomSelectionMode() { setDraftOption("custom"); @@ -183,6 +207,7 @@ export default function PatcherVersionManager({ function cancelDraft() { setDraftOption(publishedOption); setDraftPatchIds(savedPatchIds); + setDraftCustomVersionName(savedCustomVersionName); setSelectionMode(false); setError(null); setPublishError(null); @@ -198,12 +223,18 @@ export default function PatcherVersionManager({ setShowPublishModal(true); } + function applySuggestedCustomVersionName() { + if (!suggestedCustomVersionName) return; + setDraftCustomVersionName(suggestedCustomVersionName); + } + async function confirmPublish() { const idsToSave = draftOption === "custom" ? draftPatchIds : []; + const customNameToSave = draftOption === "custom" ? draftCustomVersionName.trim() : null; setSaving(true); setPublishError(null); try { - const result = await updatePatcherSelectablePatches(hackSlug, idsToSave); + const result = await updatePatcherSelectablePatches(hackSlug, idsToSave, customNameToSave); if (!result.ok) { setPublishError(result.error || "Failed to publish changes"); return; @@ -211,6 +242,8 @@ export default function PatcherVersionManager({ setSavedPatchIds(idsToSave); setDraftPatchIds(idsToSave); + setSavedCustomVersionName(customNameToSave || ""); + setDraftCustomVersionName(customNameToSave || ""); setPublishedOption(draftOption); setSelectionMode(false); setShowPublishModal(false); @@ -242,6 +275,9 @@ export default function PatcherVersionManager({ latestVersionLabels={latestVersionLabels} liveVersionLabels={liveVersionLabels} draftVersionLabels={draftVersionLabels} + publishedCustomVersionName={savedCustomVersionName} + customVersionName={draftCustomVersionName} + suggestedCustomVersionName={suggestedCustomVersionName} customSelectionCount={draftPatchIds.length} selectionMode={selectionMode} hasUnsavedChanges={hasUnsavedChanges} @@ -253,6 +289,8 @@ export default function PatcherVersionManager({ onChooseOption={chooseOption} onEnterSelectionMode={enterCustomSelectionMode} onClearSelections={clearSelections} + onCustomVersionNameChange={setDraftCustomVersionName} + onApplySuggestedCustomVersionName={applySuggestedCustomVersionName} onCancel={cancelDraft} onPublish={openPublishModal} /> @@ -295,6 +333,11 @@ export default function PatcherVersionManager({ ) : (

No current patch is set.

)} + {draftOption === "custom" && ( +

+ Public version name: {draftCustomVersionName.trim()} +

+ )} {selectedUnpublishedVersionLabels.length > 0 && (

Selected unpublished versions will be published when these changes are saved. diff --git a/src/components/Hack/PatcherVersionSettings.tsx b/src/components/Hack/PatcherVersionSettings.tsx index a03ca55..12caacc 100644 --- a/src/components/Hack/PatcherVersionSettings.tsx +++ b/src/components/Hack/PatcherVersionSettings.tsx @@ -3,6 +3,7 @@ import React from "react"; import { FaCode } from "react-icons/fa"; import CollapsibleCard from "@/components/Primitives/CollapsibleCard"; +import { CUSTOM_VERSION_NAME_MAX_LENGTH } from "@/utils/patches/hack-display-version"; type PatcherOption = "latest" | "custom"; @@ -12,6 +13,9 @@ interface PatcherVersionSettingsProps { latestVersionLabels: string[]; liveVersionLabels: string[]; draftVersionLabels: string[]; + publishedCustomVersionName: string; + customVersionName: string; + suggestedCustomVersionName: string | null; customSelectionCount: number; selectionMode: boolean; hasUnsavedChanges: boolean; @@ -23,6 +27,8 @@ interface PatcherVersionSettingsProps { onChooseOption: (option: PatcherOption) => void; onEnterSelectionMode: () => void; onClearSelections: () => void; + onCustomVersionNameChange: (name: string) => void; + onApplySuggestedCustomVersionName: () => void; onCancel: () => void; onPublish: () => void; } @@ -42,6 +48,9 @@ export default function PatcherVersionSettings({ latestVersionLabels, liveVersionLabels, draftVersionLabels, + publishedCustomVersionName, + customVersionName, + suggestedCustomVersionName, customSelectionCount, selectionMode, hasUnsavedChanges, @@ -53,10 +62,25 @@ export default function PatcherVersionSettings({ onChooseOption, onEnterSelectionMode, onClearSelections, + onCustomVersionNameChange, + onApplySuggestedCustomVersionName, onCancel, onPublish, }: PatcherVersionSettingsProps) { const isSwitchingFromLatestToCustom = publishedOption === "latest" && draftOption === "custom"; + const liveCustomVersionName = publishedCustomVersionName.trim(); + const showCustomVersionNameHint = draftOption === "custom" && selectionMode && publishDisabled && customVersionName.trim().length === 0; + const showSuggestedNameButton = suggestedCustomVersionName !== null && customVersionName.trim() !== suggestedCustomVersionName; + const customOptionDetail = publishedOption === "custom" && liveCustomVersionName + ? ( + <> + {liveCustomVersionName}: {versionSummary(liveVersionLabels, "No custom versions are published.")} + + ) + : versionSummary(liveVersionLabels, "No custom versions are published."); + const liveOptionLabel = publishedOption === "custom" && liveCustomVersionName + ? `${optionLabel(publishedOption)} (${liveCustomVersionName})` + : optionLabel(publishedOption); const summaryStatus = (() => { if (hasUnsavedChanges && draftOption !== publishedOption) { return { prefix: " ยท Draft: ", label: optionLabel(draftOption) }; @@ -72,7 +96,7 @@ export default function PatcherVersionSettings({ const summary = ( <> Live: - {optionLabel(publishedOption)} + {liveOptionLabel} {summaryStatus && ( <> {summaryStatus.prefix} @@ -110,7 +134,7 @@ export default function PatcherVersionSettings({ saved={publishedOption === "custom"} label="Custom" description="Choose specific non-archived versions for the downloader. Great for multiple variants of the same version, like builds with different features or optional changes." - detail={versionSummary(liveVersionLabels, "No custom versions are published.")} + detail={customOptionDetail} onSelect={onChooseOption} /> @@ -122,6 +146,33 @@ export default function PatcherVersionSettings({ ? versionSummary(draftVersionLabels, "No versions selected.") : "No versions selected. Choose at least one version to publish Custom."} + + onCustomVersionNameChange(event.target.value)} + maxLength={CUSTOM_VERSION_NAME_MAX_LENGTH} + className="mt-1 block h-9 w-full rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-sm text-foreground outline-none transition-colors placeholder:text-foreground/35 focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent)]/20" + placeholder="e.g. 2.1.2" + /> +

+ Shown on the hack page and discover cards. Max {CUSTOM_VERSION_NAME_MAX_LENGTH} characters. +

+ {showCustomVersionNameHint && ( +

Custom version name is required.

+ )} + {showSuggestedNameButton && ( + + )} )}
@@ -226,7 +277,7 @@ function OptionCard({ saved: boolean; label: string; description: string; - detail: string; + detail: React.ReactNode; onSelect: (option: PatcherOption) => void; }) { return ( diff --git a/src/types/db.ts b/src/types/db.ts index 2764a7c..c2d2e19 100644 --- a/src/types/db.ts +++ b/src/types/db.ts @@ -177,6 +177,7 @@ export type Database = { created_at: string created_by: string current_patch: number | null + custom_version_name: string | null description: string downloads: number estimated_release: string | null @@ -212,6 +213,7 @@ export type Database = { created_at?: string created_by: string current_patch?: number | null + custom_version_name?: string | null description: string downloads?: number estimated_release?: string | null @@ -247,6 +249,7 @@ export type Database = { created_at?: string created_by?: string current_patch?: number | null + custom_version_name?: string | null description?: string downloads?: number estimated_release?: string | null diff --git a/src/utils/patches/hack-display-version.ts b/src/utils/patches/hack-display-version.ts new file mode 100644 index 0000000..63aa5cd --- /dev/null +++ b/src/utils/patches/hack-display-version.ts @@ -0,0 +1,46 @@ +export const CUSTOM_VERSION_NAME_MAX_LENGTH = 12; + +interface ResolveHackDisplayVersionArgs { + isArchive: boolean; + isCustomPatcherActive: boolean; + customVersionName?: string | null; + customDefaultPatchVersion?: string | null; + currentPatchVersion?: string | null; +} + +export function resolveHackDisplayVersion({ + isArchive, + isCustomPatcherActive, + customVersionName, + customDefaultPatchVersion, + currentPatchVersion, +}: ResolveHackDisplayVersionArgs) { + if (isArchive) return "Archive"; + if (isCustomPatcherActive) { + return customVersionName?.trim() + || customDefaultPatchVersion + || currentPatchVersion + || ""; + } + return currentPatchVersion || ""; +} + +export function suggestCustomVersionName(versionLabels: string[]) { + if (versionLabels.length === 0) return null; + + let prefix = versionLabels[0]; + for (const label of versionLabels.slice(1)) { + let index = 0; + while (index < prefix.length && index < label.length && prefix[index] === label[index]) { + index += 1; + } + prefix = prefix.slice(0, index); + if (!prefix) return null; + } + + const suggestion = prefix + .replace(/[-_.+ ]+$/g, "") + .trim() + .slice(0, CUSTOM_VERSION_NAME_MAX_LENGTH); + return suggestion || null; +} diff --git a/supabase/migrations/20260620205300_custom_version_name.sql b/supabase/migrations/20260620205300_custom_version_name.sql new file mode 100644 index 0000000..57fc14b --- /dev/null +++ b/supabase/migrations/20260620205300_custom_version_name.sql @@ -0,0 +1,2 @@ +alter table if exists public.hacks + add column if not exists custom_version_name text;