Play rejected version code 54 for still targeting 34. From API 35 Android forces edge-to-edge: content is drawn behind the system bars and it is up to each screen to keep clear of them. Seven screens had no SafeArea and their bottom action button would have ended up under the gesture bar, so FondoFarolero gains a respetarBarras flag that insets the content while the background still paints full bleed. The seventeen screens that already handle it are untouched. The navigation bar colour is ignored under edge-to-edge, so its icons are now forced light to stay visible over the dark background. Verified on the merged manifest: targetSdkVersion 35, and the release bundle builds. The visual result has not been checked on a device.
278 lines
11 KiB
Dart
278 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../servicios/servicio_idioma.dart';
|
|
import '../servicios/servicio_perfil_usuario.dart';
|
|
import '../tema/componentes_farolero.dart';
|
|
import '../tema/tema_app.dart';
|
|
|
|
class PantallaAjustes extends StatefulWidget {
|
|
const PantallaAjustes({super.key});
|
|
|
|
@override
|
|
State<PantallaAjustes> createState() => _PantallaAjustesState();
|
|
}
|
|
|
|
class _PantallaAjustesState extends State<PantallaAjustes> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final servicioIdioma = context.watch<ServicioIdioma>();
|
|
final perfil = context.watch<ServicioPerfilUsuario>().perfil;
|
|
final nombrePerfil = perfil.nombre.trim().isEmpty
|
|
? l10n.defaultPlayerName
|
|
: perfil.nombre.trim();
|
|
final inicialPerfil = nombrePerfil.substring(0, 1).toUpperCase();
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(l10n.settingsTitle)),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
respetarBarras: true,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const ArteGameplayFarolero.ajustes(height: 142),
|
|
const SizedBox(height: 8),
|
|
Card(
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: () => _editarPerfil(context),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
AvatarFarolero(
|
|
texto: inicialPerfil,
|
|
assetPath: perfil.avatarAsset,
|
|
size: 96,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
nombrePerfil,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
Text(
|
|
'@${perfil.nick}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Image.asset(
|
|
'assets/ui/generated/actions/action_settings_gear.webp',
|
|
width: 38,
|
|
height: 38,
|
|
fit: BoxFit.contain,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.language,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_opcionIdioma(
|
|
context,
|
|
bandera: '\u{1F310}',
|
|
nombre: '${l10n.automaticLanguage} (${_nombreIdiomaDelSistema()})',
|
|
codigo: 'sistema',
|
|
seleccionado: servicioIdioma.codigoActual == 'sistema',
|
|
onTap: () => servicioIdioma.cambiarIdioma('sistema'),
|
|
),
|
|
const Divider(height: 1),
|
|
...ServicioIdioma.idiomasSoportados.entries.map((entrada) {
|
|
return _opcionIdioma(
|
|
context,
|
|
bandera: entrada.value.bandera,
|
|
nombre: entrada.value.nombre,
|
|
codigo: entrada.key,
|
|
seleccionado: servicioIdioma.codigoActual == entrada.key,
|
|
onTap: () => servicioIdioma.cambiarIdioma(entrada.key),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _opcionIdioma(
|
|
BuildContext context, {
|
|
required String bandera,
|
|
required String nombre,
|
|
required String codigo,
|
|
required bool seleccionado,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return ListTile(
|
|
leading: Text(bandera, style: const TextStyle(fontSize: 24)),
|
|
title: Text(nombre),
|
|
trailing: seleccionado
|
|
? IconoFarolero(Icons.check_circle, color: TemaApp.colorAcento)
|
|
: null,
|
|
onTap: onTap,
|
|
dense: true,
|
|
selected: seleccionado,
|
|
selectedTileColor: TemaApp.colorAcento.withValues(alpha: 0.1),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
);
|
|
}
|
|
|
|
Future<void> _editarPerfil(BuildContext context) async {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
|
final actual = servicioPerfil.perfil;
|
|
final nombreController = TextEditingController(text: actual.nombre);
|
|
final nickController = TextEditingController(text: actual.nick);
|
|
var avatarSeleccionado = actual.avatarAsset;
|
|
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
title: Text(l10n.deviceProfile),
|
|
content: SizedBox(
|
|
width: 520,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const ArteGameplayFarolero.ajustes(height: 118),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: nombreController,
|
|
textCapitalization: TextCapitalization.words,
|
|
onChanged: (_) => setDialogState(() {}),
|
|
decoration: InputDecoration(
|
|
labelText: l10n.profileName,
|
|
prefixIcon: IconoFarolero(Icons.person),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: nickController,
|
|
decoration: InputDecoration(
|
|
labelText: l10n.profileNick,
|
|
prefixIcon: IconoFarolero(Icons.alternate_email),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
AvatarFarolero(
|
|
texto: nombreController.text.isEmpty
|
|
? '?'
|
|
: nombreController.text.substring(0, 1).toUpperCase(),
|
|
assetPath: avatarSeleccionado,
|
|
size: 112,
|
|
),
|
|
const SizedBox(height: 18),
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final columnas = constraints.maxWidth >= 420 ? 4 : 3;
|
|
return GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: columnas,
|
|
mainAxisSpacing: 14,
|
|
crossAxisSpacing: 14,
|
|
),
|
|
itemCount: ServicioPerfilUsuario.avatares.length,
|
|
itemBuilder: (context, index) {
|
|
final avatar = ServicioPerfilUsuario.avatares[index];
|
|
final seleccionado = avatar == avatarSeleccionado;
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(999),
|
|
onTap: () => setDialogState(
|
|
() => avatarSeleccionado = avatar,
|
|
),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: seleccionado
|
|
? TemaApp.colorNaranja
|
|
: Colors.transparent,
|
|
width: 3,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: AvatarFarolero(
|
|
texto: '',
|
|
assetPath: avatar,
|
|
size: 86,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: Text(l10n.cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
await servicioPerfil.guardar(
|
|
nombre: nombreController.text,
|
|
nick: nickController.text,
|
|
avatarAsset: avatarSeleccionado,
|
|
);
|
|
if (ctx.mounted) Navigator.pop(ctx);
|
|
},
|
|
child: Text(l10n.save),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
nombreController.dispose();
|
|
nickController.dispose();
|
|
}
|
|
|
|
String _nombreIdiomaDelSistema() {
|
|
final locale = WidgetsBinding.instance.platformDispatcher.locale;
|
|
final codigo = locale.countryCode != null && locale.countryCode!.isNotEmpty
|
|
? '${locale.languageCode}_${locale.countryCode}'
|
|
: locale.languageCode;
|
|
final info = ServicioIdioma.idiomasSoportados[codigo] ??
|
|
ServicioIdioma.idiomasSoportados[locale.languageCode];
|
|
return info?.nombre ?? locale.languageCode;
|
|
}
|
|
}
|