Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30786f9706 |
+2
-2
@@ -18,9 +18,9 @@ class GameManagerApp extends StatelessWidget {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'GameManager',
|
title: 'GameManager',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.dark(),
|
||||||
darkTheme: AppTheme.dark(),
|
darkTheme: AppTheme.dark(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.dark,
|
||||||
home: AuthGate(auth: auth, api: api),
|
home: AuthGate(auth: auth, api: api),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class GameServer {
|
|||||||
final String? motd;
|
final String? motd;
|
||||||
final bool hasIcon;
|
final bool hasIcon;
|
||||||
final bool hasImage;
|
final bool hasImage;
|
||||||
|
final bool custom;
|
||||||
|
|
||||||
const GameServer({
|
const GameServer({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -34,6 +35,7 @@ class GameServer {
|
|||||||
this.motd,
|
this.motd,
|
||||||
this.hasIcon = false,
|
this.hasIcon = false,
|
||||||
this.hasImage = false,
|
this.hasImage = false,
|
||||||
|
this.custom = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory GameServer.fromJson(Map<String, dynamic> json) => GameServer(
|
factory GameServer.fromJson(Map<String, dynamic> json) => GameServer(
|
||||||
@@ -52,6 +54,7 @@ class GameServer {
|
|||||||
motd: json['motd'] as String?,
|
motd: json['motd'] as String?,
|
||||||
hasIcon: json['has_icon'] as bool? ?? false,
|
hasIcon: json['has_icon'] as bool? ?? false,
|
||||||
hasImage: json['has_image'] as bool? ?? false,
|
hasImage: json['has_image'] as bool? ?? false,
|
||||||
|
custom: json['custom'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool get isUp => status == 'online' || status == 'running';
|
bool get isUp => status == 'online' || status == 'running';
|
||||||
@@ -84,3 +87,27 @@ class GameServer {
|
|||||||
_ => (label: 'Éteint', color: const Color(0xFF8E8E93)),
|
_ => (label: 'Éteint', color: const Color(0xFF8E8E93)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Conteneur Docker (pour lier un serveur depuis l'app).
|
||||||
|
class DockerContainer {
|
||||||
|
DockerContainer({required this.name, required this.image, required this.state});
|
||||||
|
final String name;
|
||||||
|
final String image;
|
||||||
|
final String state;
|
||||||
|
|
||||||
|
factory DockerContainer.fromJson(Map<String, dynamic> j) => DockerContainer(
|
||||||
|
name: j['name'] as String,
|
||||||
|
image: (j['image'] as String?) ?? '',
|
||||||
|
state: (j['state'] as String?) ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get running => state == 'running';
|
||||||
|
|
||||||
|
/// Image sans le registre ni le digest, pour la lisibilité.
|
||||||
|
/// ex: git.nfteam.ovh/neckfire/the-cycle:preprod → the-cycle:preprod
|
||||||
|
String get prettyImage {
|
||||||
|
var s = image.split('/').last;
|
||||||
|
if (s.length > 34) s = '${s.substring(0, 33)}…';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models.dart';
|
||||||
|
import '../services/api_service.dart';
|
||||||
|
|
||||||
|
/// Formulaire d'ajout d'un serveur : infos + sélection du/des conteneur(s)
|
||||||
|
/// Docker liés (liste lisible : nom + image courte + état).
|
||||||
|
class AddServerScreen extends StatefulWidget {
|
||||||
|
const AddServerScreen({super.key, required this.api});
|
||||||
|
final ApiService api;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddServerScreen> createState() => _AddServerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddServerScreenState extends State<AddServerScreen> {
|
||||||
|
final _name = TextEditingController();
|
||||||
|
final _game = TextEditingController();
|
||||||
|
final _subtitle = TextEditingController();
|
||||||
|
final _connect = TextEditingController();
|
||||||
|
final _openUrl = TextEditingController();
|
||||||
|
final _minecraft = TextEditingController();
|
||||||
|
final _search = TextEditingController();
|
||||||
|
|
||||||
|
String _env = 'prod';
|
||||||
|
bool _web = false;
|
||||||
|
bool _onDemand = false;
|
||||||
|
final _selected = <String>{};
|
||||||
|
|
||||||
|
List<DockerContainer>? _containers;
|
||||||
|
String? _error;
|
||||||
|
bool _saving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadContainers();
|
||||||
|
_search.addListener(() => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (final c in [_name, _game, _subtitle, _connect, _openUrl, _minecraft, _search]) {
|
||||||
|
c.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContainers() async {
|
||||||
|
try {
|
||||||
|
final list = await widget.api.containers();
|
||||||
|
if (mounted) setState(() => _containers = list);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) setState(() => _error = '$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (_name.text.trim().isEmpty || _selected.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Nom et au moins un conteneur requis.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _saving = true);
|
||||||
|
try {
|
||||||
|
await widget.api.createServer({
|
||||||
|
'name': _name.text.trim(),
|
||||||
|
'game': _game.text.trim(),
|
||||||
|
'subtitle': _subtitle.text.trim(),
|
||||||
|
'env': _env,
|
||||||
|
'containers': _selected.toList(),
|
||||||
|
'connect': _web ? _openUrl.text.trim() : _connect.text.trim(),
|
||||||
|
'web': _web,
|
||||||
|
'open_url': _web ? _openUrl.text.trim() : null,
|
||||||
|
'minecraft': _minecraft.text.trim(),
|
||||||
|
'on_demand': _onDemand,
|
||||||
|
});
|
||||||
|
if (mounted) Navigator.pop(context, true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
|
||||||
|
setState(() => _saving = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Nouveau serveur')),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _name,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: const InputDecoration(labelText: 'Nom *', hintText: 'ex: Valheim'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _game,
|
||||||
|
decoration: const InputDecoration(labelText: 'Jeu', hintText: 'ex: Valheim'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _subtitle,
|
||||||
|
decoration: const InputDecoration(labelText: 'Sous-titre', hintText: 'ex: serveur privé'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_envSelector(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Jeu web (bouton « Ouvrir »)'),
|
||||||
|
value: _web,
|
||||||
|
onChanged: (v) => setState(() => _web = v),
|
||||||
|
),
|
||||||
|
if (_web)
|
||||||
|
TextField(
|
||||||
|
controller: _openUrl,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
decoration: const InputDecoration(labelText: 'URL du jeu', hintText: 'https://…'),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
TextField(
|
||||||
|
controller: _connect,
|
||||||
|
decoration: const InputDecoration(labelText: 'Adresse de connexion', hintText: 'ex: jeu.nfteam.ovh'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _minecraft,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Minecraft host:port (MOTD, optionnel)',
|
||||||
|
hintText: 'ex: 192.168.1.136:25565',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Serveur à la demande'),
|
||||||
|
value: _onDemand,
|
||||||
|
onChanged: (v) => setState(() => _onDemand = v),
|
||||||
|
),
|
||||||
|
const Divider(height: 28),
|
||||||
|
Text('Conteneur(s) lié(s) *', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_containerPicker(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: _saving ? null : _save,
|
||||||
|
icon: _saving
|
||||||
|
? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: const Icon(Icons.check),
|
||||||
|
label: const Text('Créer'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _envSelector() {
|
||||||
|
return SegmentedButton<String>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(value: 'prod', label: Text('Production'), icon: Icon(Icons.public)),
|
||||||
|
ButtonSegment(value: 'preprod', label: Text('Preprod'), icon: Icon(Icons.science_outlined)),
|
||||||
|
],
|
||||||
|
selected: {_env},
|
||||||
|
onSelectionChanged: (s) => setState(() => _env = s.first),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _containerPicker() {
|
||||||
|
if (_error != null) {
|
||||||
|
return Text('Impossible de charger les conteneurs.\n$_error');
|
||||||
|
}
|
||||||
|
if (_containers == null) {
|
||||||
|
return const Padding(padding: EdgeInsets.all(20), child: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
final q = _search.text.toLowerCase();
|
||||||
|
final list = _containers!
|
||||||
|
.where((c) => q.isEmpty || c.name.toLowerCase().contains(q) || c.image.toLowerCase().contains(q))
|
||||||
|
.toList();
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _search,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
hintText: 'Filtrer les conteneurs…',
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_selected.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('${_selected.length} sélectionné(s)',
|
||||||
|
style: Theme.of(context).textTheme.labelMedium),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final c in list)
|
||||||
|
CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
value: _selected.contains(c.name),
|
||||||
|
onChanged: (v) => setState(() {
|
||||||
|
v == true ? _selected.add(c.name) : _selected.remove(c.name);
|
||||||
|
}),
|
||||||
|
title: Text(c.name, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
subtitle: Text(c.prettyImage,
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
|
||||||
|
secondary: Container(
|
||||||
|
width: 10, height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: c.running ? const Color(0xFF34C759) : const Color(0xFF8E8E93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (list.isEmpty)
|
||||||
|
const Padding(padding: EdgeInsets.all(16), child: Text('Aucun conteneur.')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,27 @@ class _ServerDetailScreenState extends State<ServerDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmDelete() async {
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Supprimer ce serveur ?'),
|
||||||
|
content: Text('« ${s.name} » sera retiré de l\'app (le conteneur Docker n\'est pas touché).'),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Annuler')),
|
||||||
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Supprimer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
try {
|
||||||
|
await widget.api.deleteServer(s.id);
|
||||||
|
if (mounted) Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
_snack('$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _snack(String m) {
|
void _snack(String m) {
|
||||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m)));
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m)));
|
||||||
}
|
}
|
||||||
@@ -96,7 +117,17 @@ class _ServerDetailScreenState extends State<ServerDetailScreen> {
|
|||||||
final t = s.theme;
|
final t = s.theme;
|
||||||
final badge = s.statusBadge;
|
final badge = s.statusBadge;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(s.name)),
|
appBar: AppBar(
|
||||||
|
title: Text(s.name),
|
||||||
|
actions: [
|
||||||
|
if (s.custom)
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Supprimer',
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: _confirmDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: _load,
|
onRefresh: _load,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../services/api_service.dart';
|
|||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../services/update_service.dart';
|
import '../services/update_service.dart';
|
||||||
import '../widgets/server_card.dart';
|
import '../widgets/server_card.dart';
|
||||||
|
import 'add_server_screen.dart';
|
||||||
import 'server_detail_screen.dart';
|
import 'server_detail_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
@@ -159,10 +160,21 @@ class _ServersScreenState extends State<ServersScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: _openAdd,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Ajouter'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _openAdd() async {
|
||||||
|
final added = await Navigator.push<bool>(context,
|
||||||
|
MaterialPageRoute(builder: (_) => AddServerScreen(api: widget.api)));
|
||||||
|
if (added == true) _refresh();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (_error != null && _servers == null) {
|
if (_error != null && _servers == null) {
|
||||||
return ListView(children: [
|
return ListView(children: [
|
||||||
|
|||||||
@@ -45,6 +45,47 @@ class ApiService {
|
|||||||
return GameServer.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
return GameServer.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<DockerContainer>> containers() async {
|
||||||
|
final base = await baseUrl;
|
||||||
|
final res = await http.get(Uri.parse('$base/api/containers'), headers: await _headers());
|
||||||
|
if (res.statusCode == 401) throw UnauthorizedException();
|
||||||
|
if (res.statusCode != 200) throw ApiException('Erreur ${res.statusCode}');
|
||||||
|
return (jsonDecode(res.body) as List)
|
||||||
|
.map((e) => DockerContainer.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GameServer> createServer(Map<String, dynamic> payload) async {
|
||||||
|
final base = await baseUrl;
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('$base/api/servers'),
|
||||||
|
headers: {...await _headers(), 'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(payload),
|
||||||
|
);
|
||||||
|
if (res.statusCode == 401) throw UnauthorizedException();
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
throw ApiException(_msg(res.body) ?? 'Création refusée (${res.statusCode})');
|
||||||
|
}
|
||||||
|
return GameServer.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteServer(String id) async {
|
||||||
|
final base = await baseUrl;
|
||||||
|
final res = await http.delete(Uri.parse('$base/api/servers/$id'), headers: await _headers());
|
||||||
|
if (res.statusCode == 401) throw UnauthorizedException();
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
throw ApiException(_msg(res.body) ?? 'Suppression refusée (${res.statusCode})');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _msg(String body) {
|
||||||
|
try {
|
||||||
|
return (jsonDecode(body) as Map<String, dynamic>)['detail'] as String?;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> start(String id) => _action(id, 'start');
|
Future<void> start(String id) => _action(id, 'start');
|
||||||
Future<void> stop(String id) => _action(id, 'stop');
|
Future<void> stop(String id) => _action(id, 'stop');
|
||||||
|
|
||||||
|
|||||||
+97
-11
@@ -1,42 +1,128 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Thèmes Material 3 (clair + sombre) avec un accent unifié.
|
/// Thème « flamme / chaud sombre » — anthracite + accents orange→rouge.
|
||||||
|
/// Remplace la palette Material par défaut (violet/teal).
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
static const _seed = Color(0xFF5E5CE6);
|
// Palette
|
||||||
|
static const bg = Color(0xFF14110F); // fond anthracite
|
||||||
|
static const surface1 = Color(0xFF1F1B18); // cartes / champs
|
||||||
|
static const surface2 = Color(0xFF262119); // éléments surélevés
|
||||||
|
static const flame = Color(0xFFFF7A18); // orange accent
|
||||||
|
static const ember = Color(0xFFFF3D00); // rouge braise
|
||||||
|
static const ink = Color(0xFFF5EFE9); // texte principal
|
||||||
|
static const inkMuted = Color(0xFFA89F97); // texte secondaire
|
||||||
|
|
||||||
static ThemeData light() => _base(Brightness.light);
|
static ThemeData light() => dark(); // app pensée sombre → même thème
|
||||||
static ThemeData dark() => _base(Brightness.dark);
|
static ThemeData dark() {
|
||||||
|
const scheme = ColorScheme(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primary: flame,
|
||||||
|
onPrimary: Color(0xFF230F00),
|
||||||
|
primaryContainer: Color(0xFF3A2410),
|
||||||
|
onPrimaryContainer: Color(0xFFFFD9B8),
|
||||||
|
secondary: ember,
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
secondaryContainer: Color(0xFF4A1A0A),
|
||||||
|
onSecondaryContainer: Color(0xFFFFD5C8),
|
||||||
|
tertiary: Color(0xFFFFB74D),
|
||||||
|
onTertiary: Color(0xFF241400),
|
||||||
|
error: Color(0xFFFF6B5E),
|
||||||
|
onError: Color(0xFF3A0A05),
|
||||||
|
surface: bg,
|
||||||
|
onSurface: ink,
|
||||||
|
surfaceContainerLowest: Color(0xFF0F0D0B),
|
||||||
|
surfaceContainerLow: surface1,
|
||||||
|
surfaceContainer: surface1,
|
||||||
|
surfaceContainerHigh: surface2,
|
||||||
|
surfaceContainerHighest: Color(0xFF2E2820),
|
||||||
|
onSurfaceVariant: inkMuted,
|
||||||
|
outline: Color(0xFF4A433D),
|
||||||
|
outlineVariant: Color(0xFF332E29),
|
||||||
|
inverseSurface: ink,
|
||||||
|
onInverseSurface: bg,
|
||||||
|
shadow: Colors.black,
|
||||||
|
scrim: Colors.black,
|
||||||
|
);
|
||||||
|
|
||||||
static ThemeData _base(Brightness brightness) {
|
|
||||||
final scheme = ColorScheme.fromSeed(seedColor: _seed, brightness: brightness);
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
colorScheme: scheme,
|
||||||
scaffoldBackgroundColor: scheme.surface,
|
scaffoldBackgroundColor: bg,
|
||||||
appBarTheme: AppBarTheme(
|
canvasColor: bg,
|
||||||
backgroundColor: scheme.surface,
|
appBarTheme: const AppBarTheme(
|
||||||
foregroundColor: scheme.onSurface,
|
backgroundColor: bg,
|
||||||
|
foregroundColor: ink,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
color: scheme.onSurface,
|
color: ink,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardTheme(
|
||||||
|
color: surface1,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
|
tabBarTheme: const TabBarTheme(
|
||||||
|
labelColor: flame,
|
||||||
|
unselectedLabelColor: inkMuted,
|
||||||
|
indicatorColor: flame,
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
filledButtonTheme: FilledButtonThemeData(
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: flame,
|
||||||
|
foregroundColor: const Color(0xFF230F00),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
|
||||||
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: ink,
|
||||||
|
side: const BorderSide(color: Color(0xFF4A433D)),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: flame,
|
||||||
|
foregroundColor: Color(0xFF230F00),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: surface1,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF332E29)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: const BorderSide(color: flame, width: 2),
|
||||||
|
),
|
||||||
|
labelStyle: const TextStyle(color: inkMuted),
|
||||||
|
),
|
||||||
|
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||||
|
color: flame,
|
||||||
|
linearTrackColor: Color(0xFF332E29),
|
||||||
|
),
|
||||||
|
dividerTheme: const DividerThemeData(color: Color(0xFF332E29)),
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
backgroundColor: surface2,
|
||||||
|
contentTextStyle: TextStyle(color: ink),
|
||||||
|
),
|
||||||
|
dialogTheme: const DialogTheme(backgroundColor: surface1),
|
||||||
|
chipTheme: const ChipThemeData(backgroundColor: surface2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user