nkbmusic/lib/features/player/sound_sheet.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

159 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import '../../app/theme.dart';
import '../../audio/player_providers.dart';
import '../../audio/sound_settings.dart';
void showSoundSettings(BuildContext context) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: NkColors.surface,
builder: (_) => const _SoundSheet(),
);
}
class _SoundSheet extends ConsumerWidget {
const _SoundSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(soundSettingsProvider);
final settings = ref.read(soundSettingsProvider.notifier);
final text = Theme.of(context).textTheme;
if (!soundEffectsSupported) {
return const Padding(
padding: EdgeInsets.fromLTRB(24, 0, 24, 40),
child: Text(
'On iPhone, sound is adjusted in Settings > Apps > Music > EQ, '
'which applies to all apps.',
style: TextStyle(color: NkColors.muted),
),
);
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: Text('Sound', style: text.titleLarge)),
TextButton(onPressed: settings.reset, child: const Text('Reset')),
],
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Equalizer', style: TextStyle(fontWeight: FontWeight.w700)),
subtitle: const Text('Shape the sound to your taste', style: TextStyle(color: NkColors.muted)),
value: s.eqEnabled,
onChanged: settings.setEnabled,
),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final p in EqPreset.values.where((p) => p != EqPreset.custom || s.preset == EqPreset.custom))
ChoiceChip(
label: Text(p.label),
selected: s.eqEnabled && s.preset == p,
onSelected: (_) => settings.setPreset(p),
),
],
),
const SizedBox(height: 16),
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: s.eqEnabled ? 1 : 0.4,
child: const _Bands(),
),
const Divider(height: 32, color: NkColors.outline),
Row(
children: [
const Icon(Icons.volume_up_rounded, color: NkColors.sun),
const SizedBox(width: 10),
const Expanded(child: Text('Volume boost', style: TextStyle(fontWeight: FontWeight.w700))),
Text(s.boostDb == 0 ? 'Off' : '+${s.boostDb.round()} dB', style: const TextStyle(color: NkColors.muted)),
],
),
Slider(
value: s.boostDb,
max: 10,
divisions: 10,
onChanged: settings.setBoost,
),
const Text(
'Makes quiet old recordings louder. Keep it low with headphones.',
style: TextStyle(color: NkColors.muted, fontSize: 13),
),
],
),
),
);
}
}
/// One vertical slider per equalizer band. The bands are only known once a song has loaded.
class _Bands extends ConsumerWidget {
const _Bands();
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(soundSettingsProvider); // rebuild when a preset changes the gains
final eq = ref.read(audioHandlerProvider).equalizer;
return FutureBuilder<AndroidEqualizerParameters>(
future: eq.parameters,
builder: (context, snap) {
final p = snap.data;
if (p == null) {
return const SizedBox(
height: 60,
child: Center(
child: Text('Play a song to adjust the bands', style: TextStyle(color: NkColors.muted)),
),
);
}
return SizedBox(
height: 210,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
for (final band in p.bands)
StreamBuilder<double>(
stream: band.gainStream,
builder: (context, g) => Column(
children: [
Text('${(g.data ?? 0) >= 0 ? '+' : ''}${(g.data ?? 0).toStringAsFixed(0)}',
style: const TextStyle(color: NkColors.muted, fontSize: 12)),
Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Slider(
min: p.minDecibels,
max: p.maxDecibels,
value: (g.data ?? 0).clamp(p.minDecibels, p.maxDecibels),
onChanged: (v) => ref.read(soundSettingsProvider.notifier).setBandGain(band.index, v),
),
),
),
Text(_hz(band.centerFrequency), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
],
),
),
],
),
);
},
);
}
static String _hz(double hz) => hz >= 1000 ? '${(hz / 1000).toStringAsFixed(hz >= 10000 ? 0 : 1)}k' : '${hz.round()}';
}