- 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>
159 lines
5.6 KiB
Dart
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()}';
|
|
}
|