diff --git a/README-es.md b/README-es.md index c720fd0..5935cc6 100644 --- a/README-es.md +++ b/README-es.md @@ -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.13.png) +![Biblioteca personal de juegos](docs/images/nxnative-library-es-v1.14.1.png) *Ejemplos de interfaz con registros de prueba. Cada usuario aporta sus juegos.* @@ -60,7 +60,7 @@ El motor actual funciona **sin conexión**. No admite los servicios en línea de ## Idioma -La app inicia en **English**. Cambia a **Español** con el selector de la esquina superior; la elección se guarda en este Mac. Cambia el idioma de la interfaz, no el del juego. Los detalles técnicos se muestran en su idioma original. +La app inicia en **English**. Cambia a **Español** en **Configuración → General**; la elección se guarda en este Mac. Cambia el idioma de la interfaz, no el del juego. Los detalles técnicos se muestran en su idioma original. ## Actualizar la app diff --git a/README.md b/README.md index b2806d5..f25bb9e 100644 --- a/README.md +++ b/README.md @@ -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.13.png) +![The personal game library](docs/images/nxnative-library-en-v1.14.1.png) *Interface examples use synthetic records. Games are supplied by each user.* @@ -60,7 +60,7 @@ The current runtime plays **offline**. Nintendo online services, LAN/netplay and ## Language -The app starts in **English**. Choose **Español** using the sidebar selector; your selection is remembered on this Mac. This changes the interface language, not the game's language. Build diagnostics keep their original language. +The app starts in **English**. Choose **Español** under **Settings → General**; your selection is remembered on this Mac. This changes the interface language, not the game's language. Build diagnostics keep their original language. ## Updating the app diff --git a/desktop/NxNativeEasy/ControllerTest.swift b/desktop/NxNativeEasy/ControllerTest.swift index f0773b5..5a0cdaa 100644 --- a/desktop/NxNativeEasy/ControllerTest.swift +++ b/desktop/NxNativeEasy/ControllerTest.swift @@ -54,6 +54,7 @@ struct ControllerInputState: Equatable { struct ControllerTestView: View { let text: (String) -> String + var embedded=false @Environment(\.dismiss) private var dismiss @StateObject private var tester=ControllerTester() private let lime=Color(red:0.79,green:0.95,blue:0.37) @@ -97,7 +98,7 @@ struct ControllerTestView: View { .font(.system(size:13)).foregroundStyle(.secondary) } Spacer() - Button(text("Cerrar")){dismiss()}.keyboardShortcut(.cancelAction) + if !embedded {Button(text("Cerrar")){dismiss()}.keyboardShortcut(.cancelAction)} } if tester.devices.isEmpty { Label(text("Conecta un control a este Mac por Bluetooth."),systemImage:"gamecontroller") @@ -154,7 +155,7 @@ struct ControllerTestView: View { }.padding(26).background(Color.white.opacity(0.045),in:RoundedRectangle(cornerRadius:24)) Text(text("Esta prueba comprueba la entrada del control en este Mac, no la compatibilidad de un juego.")) .font(.system(size:11)).foregroundStyle(.secondary) - }.padding(26).frame(width:690).preferredColorScheme(.dark) + }.padding(26).frame(maxWidth:embedded ? .infinity : nil).frame(width:embedded ? nil : 690).preferredColorScheme(.dark) .onReceive(tick) {_ in if NSApplication.shared.isActive {tester.sample()} else {tester.clear()}} .onDisappear {tester.clear()} } diff --git a/desktop/NxNativeEasy/LibraryWorkspace.swift b/desktop/NxNativeEasy/LibraryWorkspace.swift index d2fa708..2c08db5 100644 --- a/desktop/NxNativeEasy/LibraryWorkspace.swift +++ b/desktop/NxNativeEasy/LibraryWorkspace.swift @@ -6,15 +6,13 @@ struct LibraryWorkspace:View { @ObservedObject var library:GameLibrary @ObservedObject var updater:AppUpdater @ObservedObject var sideload:SideloadModel - private enum Page {case library,prepare,devices,account} + private enum Page {case library,prepare,devices,settings} @State private var page=Page.library private var preparing:Bool {page == .prepare} @State private var query="" @State private var selectedGameID:String? @State private var rebuildAdvice:[String:String]=[:] @State private var adviceGeneration=UUID() - @State private var copiedDiagnostics=false - @AppStorage("buildNotifications") private var buildNotifications=true @State private var removing:OwnedGameOutput? @Environment(\.scenePhase) private var scenePhase private let accent=Color(red:0.79,green:0.95,blue:0.37) @@ -73,14 +71,7 @@ struct LibraryWorkspace:View { 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) + nav("Configuración",icon:"gearshape",selected:page == .settings){page = .settings} }.padding(20).frame(width:190).frame(maxHeight:.infinity).background(.white.opacity(0.025)) Divider() ZStack { @@ -88,16 +79,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 { - VStack(spacing:0) { - HStack { - Toggle(model.text("Notificar cuando termine una preparación"),isOn:$buildNotifications).toggleStyle(.switch) - Spacer() - Button(model.text(copiedDiagnostics ? "Diagnóstico copiado" : "Copiar diagnóstico")){model.copyDiagnostics();copiedDiagnostics=true} - }.padding(24) - SideloadView(model:sideload,text:model.text,accountOnly:true,embedded:true) - } - } + if page == .settings {WorkspaceSettingsView(model:model,sideload:sideload)} if page == .devices { SideloadLibraryView(model:sideload,text:model.text,canInstall:!model.running && !updater.busy,prepareGame:{ page = .prepare;model.folder=sideload.hasPendingUpdate ? sideload.gameFolder : nil;model.failed=false;model.preparationStep=0;model.target="ios" @@ -176,6 +158,7 @@ struct LibraryWorkspace:View { if page != .prepare {model.target="";model.folder=nil;model.preparationStep=0} model.acceptDrop(items);if !model.failed {remember()};page = .prepare;return true } + .onReceive(NotificationCenter.default.publisher(for:Notification.Name("nxnative.openSettings"))){_ in page = .settings} .onReceive(NotificationCenter.default.publisher(for:Notification.Name("nxnative.showLibrary"))){_ in page = .library;if !sideload.busy {model.showSideload=false};library.refresh()} .onReceive(NotificationCenter.default.publisher(for:Notification.Name("nxnative.showBuildActivity"))){_ in page = .prepare;model.preparationStep=4;model.showDetails=true} .onChange(of:model.showDeviceLibrary){_,show in if show {page = .devices;model.showDeviceLibrary=false}} @@ -198,6 +181,62 @@ struct LibraryWorkspace:View { } } +struct WorkspaceSettingsView:View { + @ObservedObject var model:BuildModel + @ObservedObject var sideload:SideloadModel + enum Section:String,CaseIterable {case general,apple,controllers} + @State var section=Section.general + @State private var copiedDiagnostics=false + @AppStorage("buildNotifications") private var buildNotifications=true + private var version:String {Bundle.main.object(forInfoDictionaryKey:"CFBundleShortVersionString") as? String ?? "development"} + private func title(_ section:Section)->String { + switch section {case .general:return "General";case .apple:return "Cuenta Apple";case .controllers:return "Control"} + } + var body:some View { + VStack(alignment:.leading,spacing:0) { + VStack(alignment:.leading,spacing:20) { + Text(model.text("Configuración")).font(.system(size:30,weight:.bold,design:.rounded)) + Picker(model.text("Sección de configuración"),selection:$section) { + ForEach(Section.allCases,id:\.self) {item in Text(model.text(title(item))).tag(item)} + }.pickerStyle(.segmented).labelsHidden() + }.padding(28) + Divider() + switch section { + case .general: + ScrollView { + VStack(alignment:.leading,spacing:24) { + VStack(alignment:.leading,spacing:20) { + Label(model.text("Personaliza la app"),systemImage:"slider.horizontal.3").font(.headline) + HStack { + VStack(alignment:.leading,spacing:5) {Text(model.text("Idioma de la interfaz")).font(.callout.bold());Text(model.text("El idioma del juego se configura por separado.")).font(.caption).foregroundStyle(.secondary)} + Spacer() + Picker(model.text("Idioma de la interfaz"),selection:$model.language){Text("English").tag(EasyLanguage.english);Text("Español").tag(EasyLanguage.spanish)}.labelsHidden().frame(width:180) + } + Divider() + Toggle(model.text("Notificar cuando termine una preparación"),isOn:$buildNotifications).toggleStyle(.switch) + }.padding(22).frame(maxWidth:.infinity,alignment:.leading).background(.white.opacity(0.035),in:RoundedRectangle(cornerRadius:16)) + VStack(alignment:.leading,spacing:16) { + Label(model.text("Ayuda y diagnóstico"),systemImage:"lifepreserver").font(.headline) + Text(model.text("Comparte un resumen de la app y del último proceso, con datos sensibles ocultos.")).font(.callout).foregroundStyle(.secondary) + Button(model.text(copiedDiagnostics ? "Diagnóstico copiado" : "Copiar diagnóstico")){model.copyDiagnostics();copiedDiagnostics=true} + }.padding(22).frame(maxWidth:.infinity,alignment:.leading).background(.white.opacity(0.035),in:RoundedRectangle(cornerRadius:16)) + Text("nxnative Easy · "+version).font(.caption).foregroundStyle(.secondary) + }.padding(28) + } + case .apple: + SideloadView(model:sideload,text:model.text,accountOnly:true,embedded:true) + case .controllers: + ScrollView { + VStack(alignment:.leading,spacing:12) { + Text(model.text("Prueba el control conectado a este Mac sin abrir ni preparar un juego.")).font(.callout).foregroundStyle(.secondary).padding(.horizontal,26).padding(.top,24) + ControllerTestView(text:model.text,embedded:true) + } + } + } + }.frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.topLeading) + } +} + struct GameLibraryDetail:View { let game:OwnedGameOutput let title:String diff --git a/desktop/NxNativeEasy/Localization.swift b/desktop/NxNativeEasy/Localization.swift index 037fe44..d3347a9 100644 --- a/desktop/NxNativeEasy/Localization.swift +++ b/desktop/NxNativeEasy/Localization.swift @@ -12,6 +12,15 @@ enum EasyLanguage: String, CaseIterable { struct EasyStrings { static let english: [String:String] = [ + "Configuración":"Settings", + "General":"General", + "Sección de configuración":"Settings section", + "Personaliza la app":"Make it yours", + "El idioma del juego se configura por separado.":"The game language is configured separately.", + "Ayuda y diagnóstico":"Help and diagnostics", + "Comparte un resumen de la app y del último proceso, con datos sensibles ocultos.":"Share a summary of the app and its last process with sensitive details removed.", + "Prueba el control conectado a este Mac sin abrir ni preparar un juego.":"Test the controller connected to this Mac without opening or preparing a game.", + "Cuenta gratuita":"Free account", "Cupos disponibles estimados":"Estimated available slots", "Cuenta gratuita confirmada por su membresía. Estimación basada en los registros devueltos; Apple confirma la disponibilidad al registrar.":"Free team confirmed by membership. Estimate based on returned registrations; Apple confirms availability when registering.", diff --git a/desktop/NxNativeEasy/main.swift b/desktop/NxNativeEasy/main.swift index 6c14931..209ff3c 100644 --- a/desktop/NxNativeEasy/main.swift +++ b/desktop/NxNativeEasy/main.swift @@ -343,8 +343,6 @@ struct ContentView: View { @ObservedObject var updater: AppUpdater @ObservedObject var sideload:SideloadModel @ObservedObject var model:BuildModel - @StateObject private var controllerMonitor=ControllerMonitor() - @State private var showControllerTest=false private let ink=Color(red:0.93,green:0.94,blue:0.90) private let lime=Color(red:0.79,green:0.95,blue:0.37) var body: some View { @@ -409,26 +407,6 @@ struct ContentView: View { }.buttonStyle(.plain).disabled(model.running || updater.busy || sideload.busy).accessibilityIdentifier("target-"+target) } } - if model.isMac { - HStack(alignment:.top,spacing:10) { - Image(systemName:controllerMonitor.controllers.contains(where:{$0.supported}) ? "checkmark.circle.fill" : "gamecontroller") - .foregroundStyle(controllerMonitor.controllers.contains(where:{$0.supported}) ? lime : .gray) - VStack(alignment:.leading,spacing:4) { - if controllerMonitor.controllers.isEmpty { - Text(model.text("Sin control conectado en este Mac")).font(.system(size:13,weight:.medium)) - Text(model.text("Conéctalo por Bluetooth y aparecerá aquí.")).font(.system(size:12)).foregroundStyle(.secondary) - } else { - ForEach(controllerMonitor.controllers) { controller in - Text((controller.name.isEmpty ? model.text("Control") : controller.name)+" · "+model.text(controller.supported ? "Listo en este Mac" : "Sin perfil compatible")) - .font(.system(size:13,weight:.medium)) - } - } - } - Spacer() - Button(model.text("Probar control")){showControllerTest=true} - .disabled(model.running || updater.busy || sideload.busy).accessibilityIdentifier("test-controller") - }.accessibilityIdentifier("controller-status") - } Button(model.text("Continuar")){model.preparationStep=1}.buttonStyle(LibraryPrimaryButtonStyle()).disabled(model.target.isEmpty || model.running || sideload.busy || updater.busy) } if model.preparationStep==1 { @@ -586,7 +564,6 @@ struct ContentView: View { .sheet(isPresented:$model.showSideload) {SideloadView(model:sideload,text:model.text)} .sheet(isPresented:$model.showAnalysis) {if let game=model.folder {GameAnalysisView(repo:model.repo,game:game,text:model.text)} else {Text(model.text("Primero selecciona la carpeta de un juego preparado.")).padding(24)}} .sheet(isPresented:$model.showCatalog) {GameCatalogView(model:model)} - .sheet(isPresented:$showControllerTest) {ControllerTestView(text:model.text)} .task {sideload.canRefresh={ !model.running && !updater.busy };await updater.check();sideload.checkRefresh()} .onReceive(Timer.publish(every:21600,on:.main,in:.common).autoconnect()) {_ in Task {await updater.check()}} .alert(model.text("Ya tienes la versión más reciente."),isPresented:$updater.showNotice) {Button(model.text("Aceptar"),role:.cancel) {}} @@ -692,6 +669,11 @@ struct GameCatalogView: View { } .defaultSize(width:1060,height:700) .commands { + CommandGroup(replacing:.appSettings) { + Button(EasyStrings.text("Configuración",language:EasyLanguage(rawValue:appLanguage) ?? .english)) { + NotificationCenter.default.post(name:Notification.Name("nxnative.openSettings"),object:nil) + }.keyboardShortcut(",",modifiers:.command) + } CommandMenu(EasyStrings.text("Herramientas de análisis",language:EasyLanguage(rawValue:appLanguage) ?? .english)) { Button(EasyStrings.text("Analizar con REA",language:EasyLanguage(rawValue:appLanguage) ?? .english)) { NotificationCenter.default.post(name:Notification.Name("nxnative.openAnalysis"),object:nil) diff --git a/desktop/release.json b/desktop/release.json index 7d283ea..4c1c281 100644 --- a/desktop/release.json +++ b/desktop/release.json @@ -1,5 +1,5 @@ { - "version": "1.14.0", - "build": 22, + "version": "1.14.1", + "build": 23, "minimumMacOSMajor": 26 } diff --git a/docs/agents/07-upstream-maintenance.md b/docs/agents/07-upstream-maintenance.md index 2877a2f..650c0d4 100644 --- a/docs/agents/07-upstream-maintenance.md +++ b/docs/agents/07-upstream-maintenance.md @@ -28,8 +28,10 @@ appear everywhere without being forgotten. No recurring schedule is implied. using the reviewed Apple policy and distinct returned registrations. Individual is not proof of free membership. Keep new-device consent bound to team/device, fresh helper-side checks and automatic-refresh refusal to register a new device. - Keep Apple Account in the application sidebar, independent of Devices; - show the signed-in email and retain account-free save export/content copy. + Keep the global Settings page in the sidebar, with General, Apple Account + and Controller sections. Show account email inside its page, not a narrow + sidebar card. Keep controller testing independent of game preparation; 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 diff --git a/docs/frontend-library.md b/docs/frontend-library.md index e70aaa7..42ed377 100644 --- a/docs/frontend-library.md +++ b/docs/frontend-library.md @@ -160,7 +160,7 @@ a build is current. Advice is informational; it never rebuilds a game by itself, changes installed apps or migrates a save. It compares source history, not the installed binary's contents, and a dirty build is not a reproducible snapshot. -**Copy diagnostics** is available globally under Apple Account and in build logs. +**Copy diagnostics** is available under Settings → General and in build logs. It copies app/OS/source/target/stage and up to 50 build-log lines. Home prefixes, key-value material, email and authentication/device-detail lines are redacted. Raw signing/provider logs, credentials and environment dumps are not included. @@ -178,3 +178,26 @@ elapsed time, live logs and source-provided ETA when available. No overall ETA i invented. A stage or installation operation without a percentage/estimate says so and retains elapsed-time/log visibility. The preparation step rail represents workflow milestones, not the compiler's overall percentage. + +## Global settings and controller testing (1.14.1) + +The sidebar footer now contains one Settings destination. Account email, language +and version no longer compete in its narrow column. Settings has three sections: +General (language, completion notices, redacted diagnostics and version), Apple +Account (the same shared session/team/registration UI) and Controller (the live +input tester). The standard Command-comma shortcut opens this in-window page. + +The tester is embedded and scrollable, without a Close button or navigation +modal. It detects attached controllers and displays buttons, sticks and triggers +without choosing a platform, game or source folder. It samples only while visible +and clears transient input on leaving; no pairing, input-handler installation or +game/file/preference modification was added. The preparation screen no longer +contains controller status or a test action. Controller gameplay mappings and the +four in-game touch profiles remain unchanged. + +![Settings, Spanish interface example](images/nxnative-settings-es-v1.14.1.png) + +![Controller test embedded in Settings](images/nxnative-controller-settings-es-v1.14.1.png) + +Screenshots use the actual SwiftUI views and synthetic library records; no real +account, game launch or physical controller result is represented. diff --git a/docs/images/nxnative-controller-settings-es-v1.14.1.png b/docs/images/nxnative-controller-settings-es-v1.14.1.png new file mode 100644 index 0000000..98e2410 Binary files /dev/null and b/docs/images/nxnative-controller-settings-es-v1.14.1.png differ diff --git a/docs/images/nxnative-library-en-v1.14.1.png b/docs/images/nxnative-library-en-v1.14.1.png new file mode 100644 index 0000000..6ab1a27 Binary files /dev/null and b/docs/images/nxnative-library-en-v1.14.1.png differ diff --git a/docs/images/nxnative-library-es-v1.14.1.png b/docs/images/nxnative-library-es-v1.14.1.png new file mode 100644 index 0000000..d02661d Binary files /dev/null and b/docs/images/nxnative-library-es-v1.14.1.png differ diff --git a/docs/images/nxnative-settings-en-v1.14.1.png b/docs/images/nxnative-settings-en-v1.14.1.png new file mode 100644 index 0000000..59c0a46 Binary files /dev/null and b/docs/images/nxnative-settings-en-v1.14.1.png differ diff --git a/docs/images/nxnative-settings-es-v1.14.1.png b/docs/images/nxnative-settings-es-v1.14.1.png new file mode 100644 index 0000000..c138b90 Binary files /dev/null and b/docs/images/nxnative-settings-es-v1.14.1.png differ diff --git a/docs/personal-ios.md b/docs/personal-ios.md index c93d7d8..240c94d 100644 --- a/docs/personal-ios.md +++ b/docs/personal-ios.md @@ -647,3 +647,19 @@ SwiftUI compilation and synthetic new/existing registration renders passed. Documentation and the 78-profile catalog passed. The production native helper was rebuilt with the pinned toolchain; real-account registration/signing remains unexecuted. No games or save data were accessed or modified by these tests. + +## Settings organization and independent controller test (1.14.1) + +Replaced the cramped sidebar account/language/version stack with one Settings +entry. General hosts language, notifications, diagnostics and version; Apple +Account retains the shared model and full account/registration interface; +Controller embeds the existing live tester inside the page. Command-comma opens +Settings. Controller UI was removed from preparation rather than duplicated. +No account/token storage, mapping, game pipeline, native helper, save or game +file changed. Existing launcher instances were not restarted. + +Validation: compiled controller lifecycle/input fixtures and localization passed, +including live buttons, D-pad, sticks, triggers, release and disconnection. Whole +SwiftUI compilation and offscreen Settings/library/controller renders validate +the layout; they do not claim new physical hardware/gameplay tests. The native +helper is reused byte-for-byte for this UI-only patch.