nkbmusic/lib/core/models.dart
metatroncubeswdev 178d2d00ab Nanga Kathai: Flutter app, self-hosted backend and song tools
- 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>
2026-10-30 10:39:26 -04:00

118 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
/// Life events Badaga songs belong to. `id` matches the `occasion` column in Supabase.
enum Occasion {
huttu('huttu', 'Huttu', 'Birth & naming', Color(0xFF3B6E8F), Icons.spa_outlined),
habba('habba', 'Habba', 'Festival celebrations', Color(0xFFB5541E), Icons.celebration_outlined),
madhuvae('madhuvae', 'Madhuvae', 'Wedding songs', Color(0xFF9C2F4A), Icons.favorite_border),
devotional('devotional', 'Devotional', 'Hethai & temple songs', Color(0xFF6B4FA0), Icons.temple_hindu_outlined),
folk('folk', 'Folk songs', 'Traditional melodies', Color(0xFF2E7D5B), Icons.music_note_outlined),
saavu('saavu', 'Saavu', 'Remembrance', Color(0xFF4A4F57), Icons.local_florist_outlined),
instrumental('instrumental', 'Instrumental', 'Flute, drums and more', Color(0xFF8A5A2B), Icons.graphic_eq);
const Occasion(this.id, this.label, this.subtitle, this.color, this.icon);
final String id;
final String label;
final String subtitle;
final Color color;
final IconData icon;
static Occasion fromId(String? id) =>
Occasion.values.firstWhere((o) => o.id == id, orElse: () => Occasion.folk);
}
enum Tier { free, premium }
class Artist {
const Artist({required this.id, required this.name, this.hatti, this.photoUrl, this.bio});
final String id;
final String name;
final String? hatti;
/// Only set once the singer (or family) has given photo permission.
final String? photoUrl;
final String? bio;
String get initials {
final parts = name.trim().split(RegExp(r'\s+')).where((p) => p.isNotEmpty).toList();
if (parts.isEmpty) return '?';
if (parts.length == 1) return parts.first.substring(0, 1).toUpperCase();
return (parts.first[0] + parts.last[0]).toUpperCase();
}
factory Artist.fromJson(Map<String, dynamic> j) => Artist(
id: j['id'] as String,
name: j['name'] as String,
hatti: j['hatti'] as String?,
photoUrl: j['photoUrl'] as String?,
bio: j['bio'] as String?,
);
}
/// A cassette, CD or folder from the collection.
class Album {
const Album({required this.id, required this.title, this.year, this.coverUrl});
final String id;
final String title;
final int? year;
final String? coverUrl;
}
class Track {
const Track({
required this.id,
required this.titleLatin,
required this.artistIds,
required this.occasion,
required this.duration,
required this.audioUrl,
this.titleTamil,
this.meaning,
this.albumId,
this.coverAsset,
this.coverUrl,
this.tier = Tier.free,
});
final String id;
final String titleLatin;
final String? titleTamil;
/// English meaning of the title, also searchable.
final String? meaning;
final List<String> artistIds;
final String? albumId;
final Occasion occasion;
final Duration duration;
/// `asset:///...` for bundled audio, otherwise an https URL (signed R2 URL later).
final String audioUrl;
final String? coverAsset;
final String? coverUrl;
final Tier tier;
factory Track.fromJson(Map<String, dynamic> j) => Track(
id: j['id'] as String,
titleLatin: j['titleLatin'] as String,
titleTamil: j['titleTamil'] as String?,
meaning: j['meaning'] as String?,
artistIds: (j['artistIds'] as List).cast<String>(),
albumId: j['albumId'] as String?,
occasion: Occasion.fromId(j['occasion'] as String?),
duration: Duration(milliseconds: (j['durationMs'] as num?)?.toInt() ?? 0),
audioUrl: j['audioUrl'] as String,
coverAsset: j['coverAsset'] as String?,
coverUrl: j['coverUrl'] as String?,
tier: j['tier'] == 'premium' ? Tier.premium : Tier.free,
);
}
String formatDuration(Duration d) {
final m = d.inMinutes;
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$m:$s';
}