Organize global settings and move controller testing out of game preparation
This commit is contained in:
1 parent
d8511da8a9
commit
b163562247
15 files changed
+127
-55
No files matched your search
+2
-2
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
*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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
*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
|
||||
|
||||
|
||||
@@ -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()}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.14.0",
|
||||
"build": 22,
|
||||
"version": "1.14.1",
|
||||
"build": 23,
|
||||
"minimumMacOSMajor": 26
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Screenshots use the actual SwiftUI views and synthetic library records; no real
|
||||
account, game launch or physical controller result is represented.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 927 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 942 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
@@ -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.
|
||||
Reference in new issue
Block a user