- Flutter app (Android/iOS): home, explore, albums, search, player with equalizer, mini player, sign up/sign in with email codes, profile, encrypted offline downloads, About page - server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx (or Caddy), migrations, backups, email templates - supabase/migrations: schema, row-level security, profiles - tools/: song scanner and importer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
4.9 KiB
Dart
132 lines
4.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
/// Nanga Kathai palette, taken from the heritage emblem.
|
|
abstract final class NkColors {
|
|
static const ground = Color(0xFF0B1A14); // night tea
|
|
static const surface = Color(0xFF14281F); // estate
|
|
static const raised = Color(0xFF1F3A2B);
|
|
static const miniPlayer = Color(0xFF1A3326);
|
|
static const outline = Color(0xFF2A4537);
|
|
static const emblemGreen = Color(0xFF1F4A2E);
|
|
static const teaLeaf = Color(0xFF7FA857);
|
|
static const sun = Color(0xFFF5B335); // accent
|
|
static const roofRed = Color(0xFFC9502F);
|
|
static const cream = Color(0xFFFBF7EE); // primary text
|
|
static const muted = Color(0xFF9FB3A6); // secondary text
|
|
static const mutedOnPhoto = Color(0xFFDCE5DF);
|
|
static const like = Color(0xFFE0503F);
|
|
}
|
|
|
|
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,
|
|
outline: NkColors.outline,
|
|
error: NkColors.roofRed,
|
|
),
|
|
);
|
|
|
|
// google_fonts 9 builds its TextTheme helpers on package:material_ui, so only its
|
|
// TextStyle API is used here.
|
|
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.1);
|
|
|
|
return base.copyWith(
|
|
textTheme: body.copyWith(
|
|
headlineLarge: display(32),
|
|
headlineMedium: display(28),
|
|
headlineSmall: display(24),
|
|
titleLarge: display(20, FontWeight.w700),
|
|
titleMedium: body.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
|
bodySmall: body.bodySmall?.copyWith(color: NkColors.muted),
|
|
),
|
|
splashFactory: InkSparkle.splashFactory,
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: NkColors.sun,
|
|
foregroundColor: NkColors.ground,
|
|
minimumSize: const Size(64, 52),
|
|
shape: const StadiumBorder(),
|
|
textStyle: GoogleFonts.manrope(fontSize: 16, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: NkColors.cream,
|
|
minimumSize: const Size(64, 50),
|
|
side: const BorderSide(color: NkColors.outline, width: 1.5),
|
|
shape: const StadiumBorder(),
|
|
textStyle: GoogleFonts.manrope(fontSize: 15, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: NkColors.surface,
|
|
hintStyle: const TextStyle(color: NkColors.muted),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
borderSide: const BorderSide(color: NkColors.outline, width: 1.5),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
borderSide: const BorderSide(color: NkColors.outline, width: 1.5),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
borderSide: const BorderSide(color: NkColors.sun, width: 1.5),
|
|
),
|
|
),
|
|
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,
|
|
),
|
|
sliderTheme: const SliderThemeData(
|
|
activeTrackColor: NkColors.sun,
|
|
inactiveTrackColor: Color(0x38FBF7EE),
|
|
thumbColor: NkColors.sun,
|
|
trackHeight: 4,
|
|
overlayShape: RoundSliderOverlayShape(overlayRadius: 18),
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: NkColors.ground,
|
|
indicatorColor: Colors.transparent,
|
|
height: 72,
|
|
labelTextStyle: WidgetStateProperty.resolveWith(
|
|
(s) => GoogleFonts.manrope(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: s.contains(WidgetState.selected) ? NkColors.sun : NkColors.muted,
|
|
),
|
|
),
|
|
iconTheme: WidgetStateProperty.resolveWith(
|
|
(s) => IconThemeData(color: s.contains(WidgetState.selected) ? NkColors.sun : NkColors.muted),
|
|
),
|
|
),
|
|
snackBarTheme: const SnackBarThemeData(
|
|
backgroundColor: NkColors.raised,
|
|
contentTextStyle: TextStyle(color: NkColors.cream),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|