Email-code sign-in for admins only; Overview with a needs-attention list; Songs with search, playback, editing, approve/hide and bulk edit; Albums and Singers with merge; creator applications queue. Adds the admin views, functions and creator_applications table, a deploy-admin script and a how-to guide. The admin nginx config no longer caches unhashed JS and sends its security headers again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
5.0 KiB
Dart
133 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
/// Nanga Kathai palette (same as the app, lib/app/theme.dart).
|
|
abstract final class NkColors {
|
|
static const ground = Color(0xFF0B1A14);
|
|
static const surface = Color(0xFF14281F);
|
|
static const raised = Color(0xFF1F3A2B);
|
|
static const outline = Color(0xFF2A4537);
|
|
static const teaLeaf = Color(0xFF7FA857);
|
|
static const sun = Color(0xFFF5B335);
|
|
static const roofRed = Color(0xFFC9502F);
|
|
static const cream = Color(0xFFFBF7EE);
|
|
static const muted = Color(0xFF9FB3A6);
|
|
static const info = Color(0xFF6FA8C9);
|
|
}
|
|
|
|
ThemeData buildNkTheme() {
|
|
final base = ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.dark,
|
|
scaffoldBackgroundColor: NkColors.ground,
|
|
colorScheme: const ColorScheme.dark(
|
|
primary: NkColors.sun,
|
|
onPrimary: NkColors.ground,
|
|
secondary: NkColors.teaLeaf,
|
|
surface: NkColors.ground,
|
|
onSurface: NkColors.cream,
|
|
surfaceContainer: NkColors.surface,
|
|
surfaceContainerHigh: NkColors.raised,
|
|
surfaceContainerHighest: NkColors.raised,
|
|
outline: NkColors.outline,
|
|
outlineVariant: NkColors.outline,
|
|
error: NkColors.roofRed,
|
|
),
|
|
);
|
|
|
|
final body = base.textTheme.apply(
|
|
fontFamily: GoogleFonts.manrope().fontFamily,
|
|
bodyColor: NkColors.cream,
|
|
displayColor: NkColors.cream,
|
|
);
|
|
TextStyle display(double size, [FontWeight w = FontWeight.w800]) =>
|
|
GoogleFonts.sora(fontSize: size, fontWeight: w, color: NkColors.cream, height: 1.15);
|
|
OutlineInputBorder border(Color c) =>
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: c, width: 1.5));
|
|
|
|
return base.copyWith(
|
|
textTheme: body.copyWith(
|
|
headlineMedium: display(28),
|
|
headlineSmall: display(22),
|
|
titleLarge: display(18, FontWeight.w700),
|
|
titleMedium: body.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
|
bodySmall: body.bodySmall?.copyWith(color: NkColors.muted),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: NkColors.sun,
|
|
foregroundColor: NkColors.ground,
|
|
minimumSize: const Size(64, 44),
|
|
shape: const StadiumBorder(),
|
|
textStyle: GoogleFonts.manrope(fontSize: 15, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: NkColors.cream,
|
|
minimumSize: const Size(64, 42),
|
|
side: const BorderSide(color: NkColors.outline, width: 1.5),
|
|
shape: const StadiumBorder(),
|
|
textStyle: GoogleFonts.manrope(fontSize: 14, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: NkColors.sun,
|
|
textStyle: GoogleFonts.manrope(fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: NkColors.surface,
|
|
isDense: true,
|
|
hintStyle: const TextStyle(color: NkColors.muted),
|
|
labelStyle: const TextStyle(color: NkColors.muted),
|
|
border: border(NkColors.outline),
|
|
enabledBorder: border(NkColors.outline),
|
|
focusedBorder: border(NkColors.sun),
|
|
),
|
|
chipTheme: base.chipTheme.copyWith(
|
|
backgroundColor: NkColors.raised,
|
|
selectedColor: NkColors.sun,
|
|
side: BorderSide.none,
|
|
shape: const StadiumBorder(),
|
|
labelStyle: GoogleFonts.manrope(fontWeight: FontWeight.w600, color: NkColors.cream),
|
|
secondaryLabelStyle: GoogleFonts.manrope(fontWeight: FontWeight.w700, color: NkColors.ground),
|
|
showCheckmark: false,
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: NkColors.surface,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
side: const BorderSide(color: NkColors.outline),
|
|
),
|
|
),
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: NkColors.surface,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
),
|
|
dividerTheme: const DividerThemeData(color: NkColors.outline, space: 1),
|
|
navigationRailTheme: NavigationRailThemeData(
|
|
backgroundColor: NkColors.surface,
|
|
indicatorColor: NkColors.raised,
|
|
selectedIconTheme: const IconThemeData(color: NkColors.sun),
|
|
unselectedIconTheme: const IconThemeData(color: NkColors.muted),
|
|
selectedLabelTextStyle: GoogleFonts.manrope(color: NkColors.sun, fontWeight: FontWeight.w700),
|
|
unselectedLabelTextStyle: GoogleFonts.manrope(color: NkColors.muted, fontWeight: FontWeight.w600),
|
|
),
|
|
checkboxTheme: CheckboxThemeData(
|
|
fillColor: WidgetStateProperty.resolveWith((s) => s.contains(WidgetState.selected) ? NkColors.sun : null),
|
|
checkColor: const WidgetStatePropertyAll(NkColors.ground),
|
|
),
|
|
snackBarTheme: const SnackBarThemeData(
|
|
backgroundColor: NkColors.raised,
|
|
contentTextStyle: TextStyle(color: NkColors.cream),
|
|
behavior: SnackBarBehavior.floating,
|
|
width: 420,
|
|
),
|
|
);
|
|
}
|