Move Apple account to app navigation and add selected-game library detail

This commit is contained in:
Erick Sanchez committed 2026-10-27 14:27:29 -06:00
1 parent b09028c0c9
commit 0741a07def
12 files changed
+225 -53

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ Cuando abra la app:
**Necesitas:** Mac con Apple silicon (M1 o posterior), macOS 26+ y Xcode 26+. El comando instala las otras herramientas. Los juegos y las llaves los aportas tú. La primera compilación puede tardar.
![Biblioteca personal de juegos](docs/images/nxnative-library-es-v1.10.png)
![Biblioteca personal de juegos](docs/images/nxnative-library-es-v1.12.png)
*Ejemplo de interfaz con registros de prueba. Cada usuario aporta sus juegos.*
+1 -1
View File
@@ -32,7 +32,7 @@ When the app opens:
**Required:** an Apple silicon Mac (M1 or newer), macOS 26+ and Xcode 26+. The command installs the other tools. You must provide your own games and keys. Compilation can take time, especially for the first game. The window shows the current stage, real counters when available, elapsed time and a live log. **Show saved logs** keeps the session output accessible; **Show compiler logs** opens the original tool logs.
![The personal game library](docs/images/nxnative-library-en-v1.10.png)
![The personal game library](docs/images/nxnative-library-en-v1.12.png)
*Interface example with synthetic library records. Games are supplied by each user.*
+57 -29
View File
@@ -6,16 +6,18 @@ struct LibraryWorkspace:View {
@ObservedObject var library:GameLibrary
@ObservedObject var updater:AppUpdater
@ObservedObject var sideload:SideloadModel
private enum Page {case library,prepare,devices}
private enum Page {case library,prepare,devices,account}
@State private var page=Page.library
private var preparing:Bool {page == .prepare}
@State private var query=""
@State private var selectedGameID:String?
@State private var removing:OwnedGameOutput?
@Environment(\.scenePhase) private var scenePhase
private let accent=Color(red:0.79,green:0.95,blue:0.37)
private var occupied:Bool {model.running || updater.busy || sideload.busy}
private func name(_ game:OwnedGameOutput)->String {model.catalog.first(where:{$0.id==game.titleID})?.name ?? game.folder.lastPathComponent}
private var results:[OwnedGameOutput] {library.games.filter {query.isEmpty || (name($0)+" "+($0.titleID ?? "")+" "+$0.item.folder).localizedCaseInsensitiveContains(query)}}
private var selectedResult:OwnedGameOutput? {results.first(where:{$0.id==selectedGameID}) ?? results.first}
private func configure(_ game:OwnedGameOutput) {
guard !occupied else {return}
model.folder=game.folder;model.defaults.set(game.item.folder,forKey:"lastGameFolder")
@@ -46,6 +48,13 @@ struct LibraryWorkspace:View {
nav("Dispositivos",icon:"ipad.and.iphone",selected:page == .devices){page = .devices}
nav("Compatibilidad",icon:"checklist",selected:false){model.openCatalog()}
Spacer()
Divider()
Button {page = .account} label:{
VStack(alignment:.leading,spacing:5) {
Label(model.text("Cuenta Apple"),systemImage:"person.crop.circle").font(.system(size:12,weight:.semibold))
Text(sideload.authenticated ? sideload.email : model.text("Sin sesión iniciada")).font(.caption).foregroundStyle(.secondary).lineLimit(2).textSelection(.enabled)
}.frame(maxWidth:.infinity,alignment:.leading).padding(9).background(page == .account ? accent.opacity(0.1) : .clear,in:RoundedRectangle(cornerRadius:9))
}.buttonStyle(.plain).accessibilityLabel(model.text("Cuenta Apple"))
Picker(model.text("Idioma de la interfaz"),selection:$model.language){Text("English").tag(EasyLanguage.english);Text("Español").tag(EasyLanguage.spanish)}.labelsHidden()
Text(Bundle.main.object(forInfoDictionaryKey:"CFBundleShortVersionString") as? String ?? "nxnative Easy").font(.caption).foregroundStyle(.secondary)
}.padding(20).frame(width:190).frame(maxHeight:.infinity).background(.white.opacity(0.025))
@@ -55,6 +64,7 @@ struct LibraryWorkspace:View {
// update checks and sheets continue when browsing the library.
ContentView(updater:updater,sideload:sideload,model:model)
.opacity(preparing ? 1 : 0).allowsHitTesting(preparing).accessibilityHidden(!preparing)
if page == .account {SideloadView(model:sideload,text:model.text,accountOnly:true,embedded:true)}
if page == .devices {
SideloadLibraryView(model:sideload,text:model.text,canInstall:!model.running && !updater.busy,prepareGame:{
page = .prepare;model.target="ios";if !model.chooseFolder() {sideload.cancelPendingUpdate()}
@@ -89,17 +99,29 @@ struct LibraryWorkspace:View {
}.frame(maxWidth:.infinity,maxHeight:.infinity)
} else if results.isEmpty {Text(model.text("No hay juegos que coincidan con tu búsqueda.")).foregroundStyle(.secondary).frame(maxWidth:.infinity,maxHeight:.infinity)}
else {
ScrollView {
LazyVGrid(columns:[GridItem(.adaptive(minimum:230,maximum:340),spacing:18)],alignment:.leading,spacing:18) {
ForEach(results) {game in
GameLibraryCard(game:game,title:name(game),catalog:model.catalog.first(where:{$0.id==game.titleID}),text:model.text,building:model.running && model.folder?.resolvingSymlinksInPath().path==game.folder.resolvingSymlinksInPath().path,disabled:occupied,open:{
if let mac=game.macApp {NSWorkspace.shared.open(mac)}
else if game.iosIPA != nil {install(game)}
else {configure(game)}
},prepare:{configure(game)},install:{install(game)},remove:{removing=game})
}
}.padding(.bottom,8)
HStack(alignment:.top,spacing:0) {
ScrollView {
LazyVStack(spacing:5) {
ForEach(results) {game in
Button {selectedGameID=game.id} label:{
HStack(spacing:9) {
if let image=GameArtworkCache.image(model.catalog.first(where:{$0.id==game.titleID})?.artworkURL) {Image(nsImage:image).resizable().scaledToFill().frame(width:42,height:42).clipped().clipShape(RoundedRectangle(cornerRadius:6))}
else {Image(systemName:"gamecontroller").frame(width:42,height:42)}
VStack(alignment:.leading,spacing:3) {Text(name(game)).font(.system(size:12,weight:.semibold)).lineLimit(2);Text(model.text(game.ready ? "Preparado" : "Sin preparar")).font(.caption2).foregroundStyle(.secondary)}
Spacer(minLength:0)
}.padding(8).background(selectedResult?.id==game.id ? accent.opacity(0.12) : .clear,in:RoundedRectangle(cornerRadius:8))
}.buttonStyle(.plain)
}
}.padding(.trailing,12)
}.frame(width:210)
Divider()
if let game=selectedResult {
ScrollView {
GameLibraryDetail(game:game,title:name(game),catalog:model.catalog.first(where:{$0.id==game.titleID}),text:model.text,building:model.running && model.folder?.resolvingSymlinksInPath().path==game.folder.resolvingSymlinksInPath().path,disabled:occupied,open:{if let mac=game.macApp {NSWorkspace.shared.open(mac)}},prepare:{configure(game)},install:{install(game)},remove:{removing=game})
}.padding(.leading,20)
}
}
}
Text(model.text("Esta biblioteca sólo contiene las carpetas que tú añades. No incluye ni descarga juegos.")).font(.caption).foregroundStyle(.secondary)
}.padding(28).frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.topLeading)
@@ -127,7 +149,7 @@ struct LibraryWorkspace:View {
}
}
struct GameLibraryCard:View {
struct GameLibraryDetail:View {
let game:OwnedGameOutput
let title:String
let catalog:CatalogGame?
@@ -138,31 +160,37 @@ struct GameLibraryCard:View {
let prepare:()->Void
let install:()->Void
let remove:()->Void
private let accent=Color(red:0.79,green:0.95,blue:0.37)
var body:some View {
VStack(alignment:.leading,spacing:12) {
VStack(alignment:.leading,spacing:18) {
ZStack(alignment:.bottomLeading) {
if let catalog {GameArtworkView(game:catalog)}else{Rectangle().fill(accent.opacity(0.08)).overlay(Image(systemName:"gamecontroller").font(.system(size:38,weight:.light)).foregroundStyle(accent))}
LinearGradient(colors:[.clear,.black.opacity(0.75)],startPoint:.center,endPoint:.bottom)
if building {Label(text("Compilando"),systemImage:"hammer.fill").font(.caption.bold()).padding(10).foregroundStyle(.white)}
}.frame(height:132).clipped().clipShape(RoundedRectangle(cornerRadius:10))
Text(title).font(.headline).lineLimit(2).frame(height:40,alignment:.topLeading)
HStack(spacing:8) {
if game.macApp != nil {Label("Mac",systemImage:"desktopcomputer").foregroundStyle(accent)}
if game.iosIPA != nil {Label("iPhone / iPad",systemImage:"iphone").foregroundStyle(accent)}
if !game.ready {Label(text(game.missing ? "Carpeta no disponible" : "Sin preparar"),systemImage:game.missing ? "externaldrive.badge.exclamationmark" : "circle.dashed").foregroundStyle(.secondary)}
}.font(.caption)
HStack {
Button(text(game.macApp != nil ? "Abrir juego" : game.iosIPA != nil ? "Instalar" : "Preparar"),action:open).buttonStyle(LibraryPrimaryButtonStyle()).disabled(disabled || game.missing)
Spacer()
if let catalog {GameArtworkView(game:catalog)}else {Rectangle().fill(.white.opacity(0.05)).overlay(Image(systemName:"gamecontroller").font(.system(size:50)).foregroundStyle(.secondary))}
LinearGradient(colors:[.clear,.black.opacity(0.85)],startPoint:.center,endPoint:.bottom)
VStack(alignment:.leading,spacing:6) {
if building {Label(text("Compilando"),systemImage:"hammer.fill").font(.caption.bold())}
Text(title).font(.system(size:28,weight:.bold,design:.rounded)).lineLimit(2)
}.padding(20).foregroundStyle(.white)
}.frame(height:240).clipped().clipShape(RoundedRectangle(cornerRadius:14))
HStack(spacing:12) {
if game.macApp != nil {Button(action:open){Label(text("Jugar en Mac"),systemImage:"play.fill")}.buttonStyle(LibraryPrimaryButtonStyle()).disabled(disabled || game.missing)}
if game.iosIPA != nil {Button(action:install){Label(text("Instalar en iPhone / iPad"),systemImage:"iphone")}.buttonStyle(LibraryPrimaryButtonStyle()).disabled(disabled || game.missing)}
if !game.ready {Button(text("Preparar juego"),action:prepare).buttonStyle(LibraryPrimaryButtonStyle()).disabled(disabled || game.missing)}
Spacer(minLength:0)
Menu {
if game.macApp != nil && game.iosIPA != nil {Button(text("Instalar en iPhone / iPad"),action:install).disabled(disabled)}
Button(text("Preparar o actualizar"),action:prepare).disabled(disabled || game.missing)
Button(text("Mostrar carpeta")){NSWorkspace.shared.activateFileViewerSelecting([game.folder])}
Button(text("Quitar de esta lista"),role:.destructive,action:remove).disabled(disabled)
} label:{Image(systemName:"ellipsis")}.menuStyle(.borderlessButton).fixedSize().accessibilityLabel(text("Acciones del juego"))
}
}.padding(13).background(.white.opacity(0.035),in:RoundedRectangle(cornerRadius:15)).overlay(RoundedRectangle(cornerRadius:15).strokeBorder(.white.opacity(0.055)))
HStack(spacing:18) {
Label(text(game.macApp != nil ? "Mac listo" : "Mac sin preparar"),systemImage:"desktopcomputer")
Label(text(game.iosIPA != nil ? "iPhone / iPad listo" : "iPhone / iPad sin preparar"),systemImage:"iphone")
}.font(.caption).foregroundStyle(.secondary)
if let version=game.version {Text(text("Versión del juego")+" · "+version).font(.callout).foregroundStyle(.secondary)}
if game.missing {Label(text("Carpeta no disponible"),systemImage:"externaldrive.badge.exclamationmark").foregroundStyle(.orange)}
Divider()
Button(text("Preparar o actualizar"),action:prepare).disabled(disabled || game.missing)
Text(text("Usa tus archivos para preparar otra plataforma o aplicar un update. Tus partidas y archivos originales se conservan.")).font(.caption).foregroundStyle(.secondary)
}.frame(maxWidth:.infinity,alignment:.leading).padding(.bottom,12)
}
}
+10
View File
@@ -12,6 +12,16 @@ enum EasyLanguage: String, CaseIterable {
struct EasyStrings {
static let english: [String:String] = [
"Cuenta Apple":"Apple Account",
"Preparado":"Prepared",
"Exportar partidas y copiar romfs no requieren iniciar sesión con Apple. Sólo conecta y autoriza tu dispositivo.":"Exporting saves and copying romfs do not require Apple sign-in. Just connect and trust your device.",
"Jugar en Mac":"Play on Mac",
"Mac listo":"Mac ready",
"Mac sin preparar":"Mac not prepared",
"iPhone / iPad listo":"iPhone / iPad ready",
"iPhone / iPad sin preparar":"iPhone / iPad not prepared",
"Versión del juego":"Game version",
"Usa tus archivos para preparar otra plataforma o aplicar un update. Tus partidas y archivos originales se conservan.":"Use your files to prepare another platform or apply an update. Your saves and original files are preserved.",
"Cuenta Apple · Todos tus dispositivos":"Apple Account · All your devices",
"Sin sesión iniciada":"Not signed in",
"Sólo para firmar, instalar y renovar apps. Exportar partidas y copiar romfs no requieren iniciar sesión.":"Only needed to sign, install and renew apps. Exporting saves and copying romfs do not require signing in.",
+4 -15
View File
@@ -418,6 +418,7 @@ struct SideloadView:View {
@ObservedObject var model:SideloadModel
let text:(String)->String
var accountOnly=false
var embedded=false
@Environment(\.dismiss) private var dismiss
@State private var password=""
@State private var code=""
@@ -427,7 +428,7 @@ struct SideloadView:View {
var body:some View {
ScrollView {
VStack(alignment:.leading,spacing:16) {
HStack {Text(text(accountOnly || model.ipa==nil ? "Tu cuenta Apple" : "Instalar en iPhone / iPad")).font(.title2.bold());Spacer();Button(text("Cerrar")){dismiss()}.disabled(model.busy)}
HStack {Text(text(accountOnly || model.ipa==nil ? "Tu cuenta Apple" : "Instalar en iPhone / iPad")).font(.title2.bold());Spacer();if !embedded {Button(text("Cerrar")){dismiss()}.disabled(model.busy)}}
if !accountOnly {
HStack(spacing:18) {
Label(text("1 · Dispositivo"),systemImage:model.device.isEmpty ? "circle" : "checkmark.circle.fill")
@@ -488,7 +489,7 @@ struct SideloadView:View {
InstallActivityView(model:model,text:text)
if !model.busy {Text(text(model.phase)).font(.callout)}
}.padding(24)
}.frame(width:760,height:720).tint(Color(red:0.79,green:0.95,blue:0.37)).preferredColorScheme(.dark).interactiveDismissDisabled(model.busy)
}.frame(maxWidth:.infinity,maxHeight:.infinity).frame(width:embedded ? nil : 760,height:embedded ? nil : 720).tint(Color(red:0.79,green:0.95,blue:0.37)).preferredColorScheme(.dark).interactiveDismissDisabled(model.busy)
}
}
@@ -529,22 +530,10 @@ struct SideloadLibraryView:View {
}
Spacer();if !embedded {Button(text("Cerrar")){dismiss()}.disabled(model.busy)}
}.padding(24)
VStack(alignment:.leading,spacing:8) {
HStack(spacing:12) {
Image(systemName:model.authenticated ? "person.crop.circle.badge.checkmark" : "person.crop.circle").font(.title2).foregroundStyle(accent)
VStack(alignment:.leading,spacing:4) {
Text(text("Cuenta Apple · Todos tus dispositivos")).font(.headline)
if model.authenticated {Text(model.email).textSelection(.enabled)}
else {Text(text("Sin sesión iniciada")).foregroundStyle(.secondary)}
}
Spacer()
Button(text(model.authenticated ? "Ver cuenta y equipos" : "Iniciar sesión")){accountOnly=true;showInstaller=true}.disabled(model.busy || !canInstall)
}
Text(text("Sólo para firmar, instalar y renovar apps. Exportar partidas y copiar romfs no requieren iniciar sesión.")).font(.callout).foregroundStyle(.secondary)
}.padding(.horizontal,24).padding(.bottom,20)
Divider()
ScrollView {
VStack(alignment:.leading,spacing:20) {
Text(text("Exportar partidas y copiar romfs no requieren iniciar sesión con Apple. Sólo conecta y autoriza tu dispositivo.")).font(.callout).foregroundStyle(.secondary)
HStack {
Label(text("Dispositivos conectados"),systemImage:"cable.connector").font(.headline)
Spacer();Button(text("Buscar dispositivos")){model.listDevices()}.disabled(model.busy || !canInstall)
+2 -2
View File
@@ -1,5 +1,5 @@
{
"version": "1.11.1",
"build": 18,
"version": "1.12.0",
"build": 19,
"minimumMacOSMajor": 26
}
+3 -1
View File
@@ -20,7 +20,9 @@ appear everywhere without being forgotten. No recurring schedule is implied.
ARM64 signing/install helper and explicit personal-team selection, explicit
external key selection, unsigned/lightweight iOS output and complete Mac apps.
Keep personal commit attribution, original upstream authors and licensing.
Preserve the personal folder library, both-platform output discovery and
Keep Apple Account in the application sidebar, independent of Devices;
show the signed-in email and retain account-free save export/content copy.
Preserve the collection list/detail, personal folder library, both-platform output discovery and
metadata-only removal. Keep one shared preparation model alive while browsing.
Use structured build events for stages/counts/ETA; never guess overall progress
or runtime state from diagnostic log wording. Retain history on log-tail replay.
+25 -4
View File
@@ -27,13 +27,14 @@ than an API for live game/controller state.
2. The personal library remembers folders across launches, including the previous
game folder from an older launcher. It only links paths and small metadata;
it never moves game inputs, build caches, keys or saves.
3. Each card resolves its title/art from the local accepted profile catalog when
3. Each entry resolves its title/art from the local accepted profile catalog when
possible and shows the outputs actually found: complete Mac app, iOS IPA or
unprepared/unavailable folder. A prepared IPA means ready for installation,
not proof that the game is installed or plays on a phone.
4. **Open game** opens an existing Mac game. **Install** opens our native installer
for an existing iOS output. If both platforms exist, the menu also offers iOS
installation. **Prepare or update** returns to the common form and preserves
4. Selecting a game shows its artwork, actual platform outputs and actions.
**Play on Mac** opens its existing Mac app. **Install on iPhone / iPad** opens
our native installer for an existing iOS output. When both outputs exist, both
actions remain visible. **Prepare or update** returns to the common form and preserves
existing preparation identities. Native authentication/install validation
limits in [sideloading](sideloading.md) still apply.
5. Browsing and searching remain available while a build runs; switching the
@@ -95,3 +96,23 @@ actual ETA, resets, retained history, invalid values and failed/cancelled exit.
Offscreen visual checks use synthetic folder/output metadata and existing artwork;
they do not build, install or play a game. Production compilation/signature checks
validate the launcher bundle, not Apple login or game/device compatibility.
## Collection detail and application account (1.12.0)
The library uses a searchable game list and selected-game detail: large existing
artwork, platform readiness and play/install/update actions. Search selection
falls back to the first matching result; unavailable folders remain listed.
An iOS output is described as prepared, not as installed or tested on a phone.
No input paths or save locations migrate in this presentation change.
Apple Account is an application-level destination in the persistent sidebar,
reachable from Library, Prepare and Devices. It shows the authenticated email
or an explicit signed-out state. The account page is inline and contains no
device picker or installation action. Device installation still uses the shared
session and retains each app's account/team/device identity. Devices contains
no account card and explicitly explains that save export and RomFS copy require
connection/trust, not Apple sign-in. Account changes are disabled while the
helper is busy. This does not change login persistence or sign-in validation.
See the [expanded NX2Apple review](nx2apple-review.md) for the pinned source
inventory, useful features, integration decisions and validation limits.
Binary file not shown.

After

Width:  |  Height:  |  Size: 914 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 KiB

+102
View File
@@ -0,0 +1,102 @@
# NX2Apple integration review
Reviewed on 2026-10-27 against
[LNPANDY/NX2Apple at 7ae7ba65cb077f51ed8a153907bc4b2af0712621](https://github.com/LNPANDY/NX2Apple/tree/7ae7ba65cb077f51ed8a153907bc4b2af0712621).
This is a pinned reference review, not an engine merge. It supersedes the earlier
three-file frontend survey for feature planning. Reference files remain outside
our checkout. No third-party source, fonts or new art assets were imported.
## Scope and evidence
The review inventoried the complete checkout and examined the native launcher's
state, collection, build process, settings, device routes, save transfer and
self-update boundaries, their test cases, the Qt/Windows architecture, and the
fork-specific archive integration points. The native frontend has 24 Swift source
files and 15 Swift test files with 129 named test methods at this revision.
This is a static architecture/integration review; it is not a line-by-line
correctness audit of every engine file and does not establish that their tests,
Windows cross-build, physical installation or save restoration work here.
No foreign setup/build/install scripts or device operations were executed.
The existing upstream frontend contract remains the integration boundary:
[frontends.md](https://github.com/seedds/nxnative/blob/ceaf6d6ee21bdfb986e9b6f97cbb1a7ce539991d/docs/frontends.md).
Follow structured build events and metadata provided by the built game. Missing
runtime APIs must not be replaced with guessed state from textual diagnostics.
## Native modules and decisions
Paths below are under `frontend/Sources/NSPortBuilder/` in the pinned reference.
| Source area | What is useful | Decision for this fork |
|---|---|---|
| `LauncherView.swift`, `LauncherStyles.swift` | A persistent collection, selected game, build activity across navigation | Implemented an original list/detail collection with existing artwork; keep our sidebar and shared preparation model. No copied layout, palette or fonts. |
| `Library.swift` | Records source inputs, both-platform outputs, tool revision, launch choices and installed device; validates Mac bundles; bounded discovery | Retain our private schema and explicit folder ownership. Both-platform actions are now visible in detail. A future explicit import can discover completed Mac apps without scanning unrelated drives. |
| `ContentView.swift` | Build orchestration, dependency setup, process groups, structured progress, stdout/stderr, separate source/app updates | Keep our existing JSON timeline, retained logs, process-group cancellation and session-leader fix. Do not import its orchestration wholesale. |
| `LaunchOptionsView.swift`, settings portion of `Library.swift` | Reads `dm/launcher.json`, validates saved values, groups basic/advanced settings, render-size context | Good future feature after implementing the upstream metadata/environment contract. Keep the built game's own launcher authoritative for now; do not duplicate unsupported options. |
| Mod portion of `Library.swift` | RomFS overlays, ordered precedence, marks code-only mods unusable | Candidate for a separate feature with host-support detection, exact TitleID and launch-environment tests. It is not arbitrary source/DLC editing and is not enabled by this release. |
| `OlderTools.swift` | Records build commit; compares relevant shared/profile files; ignores notes/tests | Highest-priority follow-up: explain when rebuilding can help. Record provenance per successful platform build; unknown/shallow history means unknown, not current. Do not force a rebuild for every frontend/docs commit. |
| `KeyCheck.swift` | Checks selected NSP/NSZ NCA headers before a long build; distinguishes old/missing keys from unchecked inputs | Useful follow-up with bounded file reads and synthetic crypto vectors. XCI is explicitly unchecked there. Never bundle, display or log key values. Prefer reusing extraction knowledge over maintaining conflicting parsers. |
| `DropFiles.swift` | Drag/drop folders, base game, update, DLC and keys into preparation | Useful follow-up. Our folder pipeline must validate title relationships before accepting ambiguous filenames; keep explicit external key selection. |
| `BuildNotifications.swift` | Background success/failure notice; click routes to game or build log; suppress cancellation noise | Useful follow-up with opt-in permission at first build and no notification while foregrounded. Not implemented in 1.12.0. |
| `Diagnostics.swift` | App/chip/version/tool/dependency report; last command/log tail; home/key-value redaction | Useful follow-up. Our signing logs require stricter credential/token/account redaction too; never include environment dumps, key bytes or raw provider replies. |
| `FreeSpace.swift` | Surveys disposable build parts off-thread; protects outputs/inputs; refuses active build | Useful follow-up, not an automatic cleanup port. Must protect our source/tools, external RomFS and saves as well as IPA/app. Re-resolve containment and symlinks at deletion time; calculate recoverable bytes separately from protected content. |
| `Devices.swift` | Reads paired devices with devicectl; derives signing teams from certificate OU | A different route: it depends on existing Xcode development identities. Keep our deduplicated device helper, explicit account/team choice and restricted own-game discovery. It does not provide a drop-in Sideloadly login/renewal replacement. |
| `SaveTransfer.swift` | Reads committed save/save_device, compares dates, stages incoming Mac data and backs up replacements | Do not port its write/restore flow yet. It uses `Documents/games/<TitleID>/save`; our working app uses `Documents/save`. Retain our read-only ZIP, stability/hashes and alias refusal. A newer timestamp alone does not resolve a save conflict. |
| `Gamepad.swift`, `PadFocus.swift` | Launcher navigation with controller focus; only acts while app active | Good optional living-room mode later. Our controller test already owns GameController handlers; integrate one input dispatcher first so navigation cannot steal test/game events. |
| `Controls.swift`, `ControlPresets.swift` | Per-launch input environment, standard/by-label/custom/automatic layouts | Review mapping semantics against our current runtime before adding presets. Keep our four touch profiles and controller test. Avoid silently changing a working Xbox/PS mapping. |
| `AppUpdate.swift`, `UpdateSheet.swift` | Release digest/version/identity validation, staging, deferred swap, changelog | Keep our checked updater and source preservation. Binary-release staging is a worthwhile future distribution improvement, independent of engine merges. Do not auto-pull foreign main into our fork. |
| `Localization.swift`, `Typography.swift`, `EmbeddedFonts.swift`, `NSPortBuilderApp.swift`, package/icon resources | Presentation, language and app bootstrap | Keep English/Spanish, original icon and system fonts. No additional font/visual dependency needed. |
## Core, transfer and cross-platform boundaries
- The reference has `dm/host/romfs_arc.*` and `dm/tools/romfs_arc.py` for an index
plus shards, alongside one-image and directory routes. Its iOS shell recognizes
per-game `Documents/games/<TitleID>/` data and derives saves from that layout.
These require coherent host, packer and app-shell changes; they are not a UI
feature or a safe path substitution. A future proposal needs byte-for-byte
archive verification, interrupted-transfer tests, preserved save paths and a
deliberately reversible migration. Our current directory RomFS stays intact.
- `scripts/push_romfs.sh` uses devicectl and a single image. Our existing helper
uses a staged directory transfer with a recovery journal. A shorter script is
not grounds to drop recovery/version checks or touch the working phone game.
- The Swift `makeVenv` implementation drains stdout and then stderr separately.
A child producing enough stderr before stdout EOF can block both processes.
This is a static risk, not a reproduced failure. Keep our concurrent/combined
output handling rather than copying that helper as written.
- Qt's `model.py`, `ui.py`, platform utilities, localization and tests cover a
separate PySide interface. `win/driver.py` loads the pipeline with replacements
for Apple-only tools; toolchain, SDK fetch, doctor, shims, app builder, IPA modes
and compatibility C/Objective-C are an independent cross-toolchain project.
Its own Qt README leaves genuine Windows compilation/link/Metal/IPA validation
open. Do not advertise Windows build support from a source inspection.
- Runtime, shader and profile changes must continue through our pinned-upstream
maintenance process. Do not merge this fork's complete `dm/` or profile list
merely to obtain its launcher. A frontend feature does not prove new game,
device, network or engine compatibility.
## Licensing
The reference root is MIT and retains the original jhaizhou attribution plus
LNPANDY's archive-subsystem notice. If source is adopted later, preserve the
applicable notice and attribution with the imported code. This release uses an
original SwiftUI implementation and existing fork assets; linking a reference
is not a substitute for license compliance on any future copied code.
## Delivered now and next candidates
1. **Delivered in 1.12.0:** Apple Account is an application page in the persistent
sidebar, independent of devices; collection list/detail, artwork, both platform
actions, search and non-destructive library management; current bilingual
screenshots. Existing build activity and save/content boundaries remain.
2. **Next useful independent changes:** build provenance/rebuild advice,
diagnostic export, background completion notices and validated drag/drop.
3. **Needs deeper contract/device work:** launch settings/mods, transactional save
restoration, archive migration and binary release distribution. These are
evaluated candidates, not features advertised as implemented.
Validation for our delivered change: compiled Foundation library/localization/
sideload-client fixtures, full SwiftUI compilation and ad-hoc bundle verification,
and synthetic English/Spanish visual checks. The screenshots contain example
metadata, no real accounts or game/save payloads. No login, signing, install,
restore or game-content copy was performed. Existing native-signing validation
limits remain unchanged.
+20
View File
@@ -561,3 +561,23 @@ Validation: compiled Swift client/localization fixtures and whole-app Swift
type checking passed. This change is UI-only; the existing native helper is
reused byte-for-byte in the local bundle. No real Apple login, signing, device
installation or game/save mutation was performed.
## Application account and collection detail (1.12.0)
Apple Account now lives in the persistent application sidebar and opens an
inline account-only page from every workspace section. Devices no longer owns
an account card. The sidebar displays the authenticated email or signed-out
state; session storage, account/team/device bindings and credential handling
are unchanged. Device copy/export explicitly remain available without Apple
login. The collection now selects a game from a searchable list and shows its
existing artwork, prepared platforms and separate play/install/update actions.
No library schema, original path, installed game or save data was migrated.
The expanded pinned NX2Apple review is in docs/nx2apple-review.md. It records
native/Qt/core integration boundaries and candidates without claiming a whole
engine audit or importing unsupported save/archive/runtime changes. English and
Spanish README screenshots were regenerated from our actual SwiftUI views with
synthetic metadata. Compiled library/localization/sideload fixtures, SwiftUI
compilation, documentation and catalog checks passed. The unchanged native
helper is reused byte-for-byte in the local application build. No real account
login, signing, installation, game launch or save modification was performed.