updates
This commit is contained in:
parent
fc22d11df1
commit
27a8a62475
32 changed files with 354 additions and 335 deletions
|
|
@ -13,7 +13,7 @@ Future<void> showAddToPlaylistSheet(
|
|||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: RatuneColors.background,
|
||||
backgroundColor: TimbreColors.background,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => _AddToPlaylistSheet(songs: songs),
|
||||
);
|
||||
|
|
@ -32,24 +32,24 @@ class _AddToPlaylistSheet extends ConsumerWidget {
|
|||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.lg),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: RatuneSpacing.xl),
|
||||
const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl),
|
||||
child: Text('Add to playlist',
|
||||
style:
|
||||
TextStyle(color: accent, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
if (!connected)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(RatuneSpacing.xl),
|
||||
padding: EdgeInsets.all(TimbreSpacing.xl),
|
||||
child: Text('Connect to a server to manage playlists.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
else ...[
|
||||
_Tile(
|
||||
|
|
@ -123,21 +123,21 @@ class _Tile extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: RatuneSpacing.xl),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: accent ?? RatuneColors.dimmed),
|
||||
const SizedBox(width: RatuneSpacing.lg),
|
||||
Icon(icon, size: 20, color: accent ?? TimbreColors.dimmed),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: accent ?? RatuneColors.foreground)),
|
||||
style: TextStyle(color: accent ?? TimbreColors.foreground)),
|
||||
),
|
||||
if (trailing != null)
|
||||
Text(trailing!,
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -158,15 +158,15 @@ Future<String?> promptPlaylistName(
|
|||
builder: (ctx) {
|
||||
final accent = Theme.of(ctx).colorScheme.primary;
|
||||
return AlertDialog(
|
||||
backgroundColor: RatuneColors.surface,
|
||||
backgroundColor: TimbreColors.surface,
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: const TextStyle(color: RatuneColors.foreground),
|
||||
style: const TextStyle(color: TimbreColors.foreground),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Playlist name',
|
||||
hintStyle: TextStyle(color: RatuneColors.dimmed),
|
||||
hintStyle: TextStyle(color: TimbreColors.dimmed),
|
||||
),
|
||||
onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -25,17 +25,17 @@ class BrowserScreen extends ConsumerWidget {
|
|||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.xl,
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.xl,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: RatuneSpacing.xl,
|
||||
runSpacing: RatuneSpacing.xs,
|
||||
spacing: TimbreSpacing.xl,
|
||||
runSpacing: TimbreSpacing.xs,
|
||||
children: [
|
||||
_Action(
|
||||
icon: Icons.search,
|
||||
|
|
@ -67,9 +67,9 @@ class BrowserScreen extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
_ModeSelector(mode: mode),
|
||||
const SizedBox(height: RatuneSpacing.lg),
|
||||
const SizedBox(height: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: client == null
|
||||
? const HairlinePanel(
|
||||
|
|
@ -105,13 +105,13 @@ class _ModeSelector extends ConsumerWidget {
|
|||
onTap: () => ref.read(browseModeProvider.notifier).state = m,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: RatuneSpacing.md),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.md),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: active ? RatuneColors.foreground : RatuneColors.dimmed,
|
||||
color: active ? TimbreColors.foreground : TimbreColors.dimmed,
|
||||
fontWeight: active ? FontWeight.w700 : FontWeight.w400,
|
||||
decoration:
|
||||
active ? TextDecoration.underline : TextDecoration.none,
|
||||
|
|
@ -127,9 +127,9 @@ class _ModeSelector extends ConsumerWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
label('Artists', BrowseMode.artists),
|
||||
const Text('|', style: TextStyle(color: RatuneColors.border)),
|
||||
const Text('|', style: TextStyle(color: TimbreColors.border)),
|
||||
label('Albums', BrowseMode.albums),
|
||||
const Text('|', style: TextStyle(color: RatuneColors.border)),
|
||||
const Text('|', style: TextStyle(color: TimbreColors.border)),
|
||||
label('Tracks', BrowseMode.tracks),
|
||||
],
|
||||
);
|
||||
|
|
@ -148,7 +148,7 @@ class _ArtistsPanel extends ConsumerWidget {
|
|||
trailing: artists.hasValue && artists.value!.isNotEmpty
|
||||
? '(${artists.value!.length})'
|
||||
: null,
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.md),
|
||||
child: artists.when(
|
||||
loading: () => const _Centered(child: _Loading()),
|
||||
error: (e, _) => _Centered(child: _ErrorText('$e')),
|
||||
|
|
@ -186,7 +186,7 @@ class _AlbumsPanel extends ConsumerWidget {
|
|||
trailing: albums.hasValue && albums.value!.isNotEmpty
|
||||
? '(${albums.value!.length})'
|
||||
: null,
|
||||
padding: const EdgeInsets.all(RatuneSpacing.md),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.md),
|
||||
child: albums.when(
|
||||
loading: () => const _Centered(child: _Loading()),
|
||||
error: (e, _) => _Centered(child: _ErrorText('$e')),
|
||||
|
|
@ -200,8 +200,8 @@ class _AlbumsPanel extends ConsumerWidget {
|
|||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: cols,
|
||||
mainAxisSpacing: RatuneSpacing.md,
|
||||
crossAxisSpacing: RatuneSpacing.md,
|
||||
mainAxisSpacing: TimbreSpacing.md,
|
||||
crossAxisSpacing: TimbreSpacing.md,
|
||||
// Square art + two caption lines; extra vertical slack so
|
||||
// the tile never sub-pixel-overflows.
|
||||
childAspectRatio: 0.68,
|
||||
|
|
@ -241,7 +241,7 @@ class _AlbumTile extends StatelessWidget {
|
|||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ColoredBox(
|
||||
color: RatuneColors.surface,
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(
|
||||
artUri!,
|
||||
|
|
@ -253,12 +253,12 @@ class _AlbumTile extends StatelessWidget {
|
|||
: const _AlbumArtFallback(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.xs),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
Text(
|
||||
album.name ?? 'Unknown album',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground),
|
||||
style: const TextStyle(color: TimbreColors.foreground),
|
||||
),
|
||||
if (album.artist != null)
|
||||
Text(
|
||||
|
|
@ -266,7 +266,7 @@ class _AlbumTile extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.dimmed, fontSize: 12),
|
||||
color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -279,7 +279,7 @@ class _AlbumArtFallback extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) => const Center(
|
||||
child: Icon(Icons.album_outlined,
|
||||
color: RatuneColors.dimmed, size: 32),
|
||||
color: TimbreColors.dimmed, size: 32),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -317,8 +317,8 @@ class _TracksPanelState extends ConsumerState<_TracksPanel> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const _Loading(),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
Text(label, style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
Text(label, style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -357,7 +357,7 @@ class _TracksPanelState extends ConsumerState<_TracksPanel> {
|
|||
title: 'Tracks',
|
||||
active: true,
|
||||
trailing: index.songs.isNotEmpty ? '(${index.songs.length})' : null,
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.md),
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
|
|
@ -368,11 +368,11 @@ class _TracksPanelState extends ConsumerState<_TracksPanel> {
|
|||
: () => ref.read(libraryIndexProvider.notifier).refresh(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: RatuneSpacing.lg,
|
||||
vertical: RatuneSpacing.sm,
|
||||
horizontal: TimbreSpacing.lg,
|
||||
vertical: TimbreSpacing.sm,
|
||||
),
|
||||
child: Text('↻ refresh',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -395,13 +395,13 @@ class _Action extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.sm),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.sm),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: RatuneColors.dimmed),
|
||||
const SizedBox(width: RatuneSpacing.sm),
|
||||
Text(label, style: const TextStyle(color: RatuneColors.foreground)),
|
||||
Icon(icon, size: 16, color: TimbreColors.dimmed),
|
||||
const SizedBox(width: TimbreSpacing.sm),
|
||||
Text(label, style: const TextStyle(color: TimbreColors.foreground)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -561,33 +561,33 @@ class BrowseRow extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.lg),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: TimbreSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null)
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(leading!,
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground),
|
||||
style: const TextStyle(color: TimbreColors.foreground),
|
||||
),
|
||||
),
|
||||
if (isDone)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.sm),
|
||||
padding: const EdgeInsets.only(left: TimbreSpacing.sm),
|
||||
child:
|
||||
Icon(Icons.download_done, size: 14, color: accent),
|
||||
)
|
||||
else if (isActive)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: RatuneSpacing.sm),
|
||||
padding: EdgeInsets.only(left: TimbreSpacing.sm),
|
||||
child: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
|
|
@ -595,9 +595,9 @@ class BrowseRow extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Text(trailing!,
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
],
|
||||
if (onPlayNext != null)
|
||||
_RowIcon(icon: Icons.playlist_play, onTap: onPlayNext!),
|
||||
|
|
@ -612,7 +612,7 @@ class BrowseRow extends StatelessWidget {
|
|||
onRemoveDownload: onRemoveDownload,
|
||||
),
|
||||
if (onPlayNext == null && onAddToQueue == null && !_hasMenu)
|
||||
const SizedBox(width: RatuneSpacing.lg),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -639,11 +639,11 @@ class _RowMenu extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, size: 20, color: RatuneColors.dimmed),
|
||||
color: RatuneColors.surface,
|
||||
icon: const Icon(Icons.more_vert, size: 20, color: TimbreColors.dimmed),
|
||||
color: TimbreColors.surface,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: RatuneSpacing.minTouchTarget),
|
||||
const BoxConstraints(minWidth: TimbreSpacing.minTouchTarget),
|
||||
onSelected: (v) {
|
||||
switch (v) {
|
||||
case 'playlist':
|
||||
|
|
@ -684,9 +684,9 @@ class _RowIcon extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: RatuneSpacing.minTouchTarget,
|
||||
height: RatuneSpacing.minTouchTarget,
|
||||
child: Icon(icon, size: 20, color: RatuneColors.dimmed),
|
||||
width: TimbreSpacing.minTouchTarget,
|
||||
height: TimbreSpacing.minTouchTarget,
|
||||
child: Icon(icon, size: 20, color: TimbreColors.dimmed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -728,11 +728,11 @@ class _NotConnected extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Not connected.',
|
||||
style: TextStyle(color: RatuneColors.foreground)),
|
||||
SizedBox(height: RatuneSpacing.sm),
|
||||
style: TextStyle(color: TimbreColors.foreground)),
|
||||
SizedBox(height: TimbreSpacing.sm),
|
||||
Text('Tap the status bar to add a Subsonic server.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -744,7 +744,7 @@ class _Centered extends StatelessWidget {
|
|||
final Widget child;
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.xl),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.xl),
|
||||
child: Center(child: child),
|
||||
);
|
||||
}
|
||||
|
|
@ -766,7 +766,7 @@ class _ErrorText extends StatelessWidget {
|
|||
Widget build(BuildContext context) => Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: RatuneColors.dimmed),
|
||||
style: const TextStyle(color: TimbreColors.dimmed),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import '../theme/tokens.dart';
|
|||
Future<void> showConnectSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: RatuneColors.background,
|
||||
backgroundColor: TimbreColors.background,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => const _ConnectSheet(),
|
||||
);
|
||||
|
|
@ -66,10 +66,10 @@ class _ConnectSheetState extends ConsumerState<_ConnectSheet> {
|
|||
final accent = Theme.of(context).colorScheme.primary;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: RatuneSpacing.xl,
|
||||
right: RatuneSpacing.xl,
|
||||
top: RatuneSpacing.xl,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + RatuneSpacing.xl,
|
||||
left: TimbreSpacing.xl,
|
||||
right: TimbreSpacing.xl,
|
||||
top: TimbreSpacing.xl,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + TimbreSpacing.xl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -77,22 +77,22 @@ class _ConnectSheetState extends ConsumerState<_ConnectSheet> {
|
|||
children: [
|
||||
Text('Connect to server',
|
||||
style: TextStyle(color: accent, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: RatuneSpacing.lg),
|
||||
const SizedBox(height: TimbreSpacing.lg),
|
||||
_field(_url, 'Server URL', 'https://navidrome.example.com',
|
||||
keyboard: TextInputType.url),
|
||||
_field(_user, 'Username', 'you'),
|
||||
_field(_pass, 'Password', '••••••••', obscure: true),
|
||||
if (conn.status == ConnStatus.error && conn.error != null) ...[
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
Text(conn.error!,
|
||||
style: const TextStyle(color: Color(0xFFE06C75))),
|
||||
],
|
||||
const SizedBox(height: RatuneSpacing.lg),
|
||||
const SizedBox(height: TimbreSpacing.lg),
|
||||
FilledButton(
|
||||
onPressed: _busy ? null : _connect,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: accent,
|
||||
foregroundColor: RatuneColors.background,
|
||||
foregroundColor: TimbreColors.background,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
),
|
||||
child: _busy
|
||||
|
|
@ -116,22 +116,22 @@ class _ConnectSheetState extends ConsumerState<_ConnectSheet> {
|
|||
TextInputType? keyboard,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: RatuneSpacing.md),
|
||||
padding: const EdgeInsets.only(bottom: TimbreSpacing.md),
|
||||
child: TextField(
|
||||
controller: c,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboard,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: const TextStyle(color: RatuneColors.foreground),
|
||||
style: const TextStyle(color: TimbreColors.foreground),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
labelStyle: const TextStyle(color: RatuneColors.dimmed),
|
||||
hintStyle: const TextStyle(color: RatuneColors.dimmed),
|
||||
labelStyle: const TextStyle(color: TimbreColors.dimmed),
|
||||
hintStyle: const TextStyle(color: TimbreColors.dimmed),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(color: RatuneColors.border),
|
||||
borderSide: BorderSide(color: TimbreColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class DownloadsScreen extends ConsumerWidget {
|
|||
TextButton(
|
||||
onPressed: () => _confirmClear(context, controller),
|
||||
child: const Text('Clear all',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -37,24 +37,24 @@ class DownloadsScreen extends ConsumerWidget {
|
|||
child: (active.isEmpty && completed.isEmpty)
|
||||
? const Center(
|
||||
child: Text('No downloads yet.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.lg),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.lg),
|
||||
children: [
|
||||
if (active.isNotEmpty) ...[
|
||||
HairlinePanel(
|
||||
title: 'Downloading',
|
||||
trailing: '(${active.length})',
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: RatuneSpacing.md),
|
||||
vertical: TimbreSpacing.md),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final d in active) _ActiveRow(info: d),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.xl),
|
||||
const SizedBox(height: TimbreSpacing.xl),
|
||||
],
|
||||
HairlinePanel(
|
||||
title: 'Saved',
|
||||
|
|
@ -63,12 +63,12 @@ class DownloadsScreen extends ConsumerWidget {
|
|||
? null
|
||||
: '${completed.length} · ${_fmtBytes(downloads.totalBytes)}',
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
|
||||
const EdgeInsets.symmetric(vertical: TimbreSpacing.md),
|
||||
child: completed.isEmpty
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(RatuneSpacing.lg),
|
||||
padding: EdgeInsets.all(TimbreSpacing.lg),
|
||||
child: Text('Nothing saved for offline yet.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
|
|
@ -93,7 +93,7 @@ class DownloadsScreen extends ConsumerWidget {
|
|||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: RatuneColors.surface,
|
||||
backgroundColor: TimbreColors.surface,
|
||||
title: const Text('Remove all downloads?'),
|
||||
content: const Text(
|
||||
'This deletes every saved file for this server. It cannot be undone.'),
|
||||
|
|
@ -121,7 +121,7 @@ class _ActiveRow extends StatelessWidget {
|
|||
final failed = info.status == DownloadStatus.failed;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: RatuneSpacing.lg, vertical: RatuneSpacing.xs),
|
||||
horizontal: TimbreSpacing.lg, vertical: TimbreSpacing.xs),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -131,8 +131,8 @@ class _ActiveRow extends StatelessWidget {
|
|||
Text(info.song.title ?? 'Untitled',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
const SizedBox(height: RatuneSpacing.xs),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
if (failed)
|
||||
const Text('Failed',
|
||||
style: TextStyle(color: Color(0xFFE06C75), fontSize: 12))
|
||||
|
|
@ -140,18 +140,18 @@ class _ActiveRow extends StatelessWidget {
|
|||
LinearProgressIndicator(
|
||||
value: info.progress > 0 ? info.progress : null,
|
||||
minHeight: 3,
|
||||
backgroundColor: RatuneColors.border,
|
||||
backgroundColor: TimbreColors.border,
|
||||
color: accent,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Text(
|
||||
failed
|
||||
? '—'
|
||||
: (info.status == DownloadStatus.queued ? 'Queued' : ''),
|
||||
style: const TextStyle(color: RatuneColors.dimmed, fontSize: 12),
|
||||
style: const TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -178,8 +178,8 @@ class _SavedRow extends StatelessWidget {
|
|||
onTap: onPlay,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.lg),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: TimbreSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -190,7 +190,7 @@ class _SavedRow extends StatelessWidget {
|
|||
Text(info.song.title ?? 'Untitled',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
Text(
|
||||
[
|
||||
info.song.artist,
|
||||
|
|
@ -199,7 +199,7 @@ class _SavedRow extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: RatuneColors.dimmed, fontSize: 12),
|
||||
const TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -208,10 +208,10 @@ class _SavedRow extends StatelessWidget {
|
|||
onTap: onRemove,
|
||||
customBorder: const CircleBorder(),
|
||||
child: const SizedBox(
|
||||
width: RatuneSpacing.minTouchTarget,
|
||||
height: RatuneSpacing.minTouchTarget,
|
||||
width: TimbreSpacing.minTouchTarget,
|
||||
height: TimbreSpacing.minTouchTarget,
|
||||
child: Icon(Icons.delete_outline,
|
||||
size: 20, color: RatuneColors.dimmed),
|
||||
size: 20, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ class FavoritesScreen extends ConsumerWidget {
|
|||
),
|
||||
error: (e, _) => Center(
|
||||
child: Text('$e',
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
data: (s) {
|
||||
if (s.songs.isEmpty && s.albums.isEmpty && s.artists.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No favorites yet.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
|
|
@ -96,10 +96,10 @@ class _SectionLabel extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.sm,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.sm,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ class HomeScreen extends ConsumerWidget {
|
|||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.xl,
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.xl,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
),
|
||||
children: [
|
||||
const _HeroCard(),
|
||||
const SizedBox(height: RatuneSpacing.xl),
|
||||
const SizedBox(height: TimbreSpacing.xl),
|
||||
|
||||
// Recently Played — from local history.
|
||||
if (recentAlbums.isNotEmpty)
|
||||
|
|
@ -93,10 +93,10 @@ class HomeScreen extends ConsumerWidget {
|
|||
|
||||
if (recentAlbums.isEmpty && client == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: RatuneSpacing.xl),
|
||||
padding: EdgeInsets.only(top: TimbreSpacing.xl),
|
||||
child: Text(
|
||||
'Connect to a server and start listening — your home fills in as you play.',
|
||||
style: TextStyle(color: RatuneColors.dimmed),
|
||||
style: TextStyle(color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -143,11 +143,11 @@ class _HeroCard extends ConsumerWidget {
|
|||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.library_music_outlined,
|
||||
color: RatuneColors.dimmed, size: 40),
|
||||
SizedBox(width: RatuneSpacing.lg),
|
||||
color: TimbreColors.dimmed, size: 40),
|
||||
SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: Text('Browse your library to start listening',
|
||||
style: TextStyle(color: RatuneColors.foreground)),
|
||||
style: TextStyle(color: TimbreColors.foreground)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -170,19 +170,19 @@ class _HeroCard extends ConsumerWidget {
|
|||
width: 64,
|
||||
height: 64,
|
||||
child: ColoredBox(
|
||||
color: RatuneColors.surface,
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(artUri,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => const Icon(
|
||||
Icons.album_outlined, color: RatuneColors.dimmed))
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: const Icon(Icons.album_outlined,
|
||||
color: RatuneColors.dimmed),
|
||||
color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: RatuneSpacing.lg),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -192,7 +192,7 @@ class _HeroCard extends ConsumerWidget {
|
|||
children: [
|
||||
Icon(hasCurrent ? Icons.play_arrow : Icons.history,
|
||||
size: 14, color: accent),
|
||||
const SizedBox(width: RatuneSpacing.xs),
|
||||
const SizedBox(width: TimbreSpacing.xs),
|
||||
Text(hasCurrent ? 'NOW PLAYING' : 'RESUME',
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
|
|
@ -201,20 +201,20 @@ class _HeroCard extends ConsumerWidget {
|
|||
fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.xs),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.foreground,
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
if (hasCurrent) ...[
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
BlockProgressBar(progress: progress, cells: 32, height: 6),
|
||||
],
|
||||
],
|
||||
|
|
@ -239,10 +239,10 @@ class _HeroShell extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.lg),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: RatuneColors.surface,
|
||||
border: Border.all(color: RatuneColors.borderActive),
|
||||
color: TimbreColors.surface,
|
||||
border: Border.all(color: TimbreColors.borderActive),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
|
|
@ -263,19 +263,19 @@ class _Shelf extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
if (cards.isEmpty) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: RatuneSpacing.xl),
|
||||
padding: const EdgeInsets.only(bottom: TimbreSpacing.xl),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ShelfHeader(title: title, onShuffle: onShuffle),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
SizedBox(
|
||||
height: 182,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: cards.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
itemBuilder: (_, i) => cards[i],
|
||||
),
|
||||
),
|
||||
|
|
@ -297,7 +297,7 @@ class _ShelfHeader extends StatelessWidget {
|
|||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.foreground,
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5)),
|
||||
const Spacer(),
|
||||
|
|
@ -306,9 +306,9 @@ class _ShelfHeader extends StatelessWidget {
|
|||
onTap: onShuffle,
|
||||
customBorder: const CircleBorder(),
|
||||
child: const SizedBox(
|
||||
width: RatuneSpacing.minTouchTarget,
|
||||
width: TimbreSpacing.minTouchTarget,
|
||||
height: 28,
|
||||
child: Icon(Icons.shuffle, size: 18, color: RatuneColors.dimmed),
|
||||
child: Icon(Icons.shuffle, size: 18, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -392,29 +392,29 @@ class _ArtCard extends StatelessWidget {
|
|||
width: _size,
|
||||
height: _size,
|
||||
child: ColoredBox(
|
||||
color: RatuneColors.surface,
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(artUri!,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) =>
|
||||
Icon(fallbackIcon, color: RatuneColors.dimmed))
|
||||
: Icon(fallbackIcon, color: RatuneColors.dimmed),
|
||||
Icon(fallbackIcon, color: TimbreColors.dimmed))
|
||||
: Icon(fallbackIcon, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.sm),
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: RatuneColors.dimmed, fontSize: 12)),
|
||||
const TextStyle(color: TimbreColors.dimmed, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -434,7 +434,7 @@ class _ArtCardSkeleton extends StatelessWidget {
|
|||
child: const SizedBox(
|
||||
width: _ArtCard._size,
|
||||
height: _ArtCard._size,
|
||||
child: ColoredBox(color: RatuneColors.surface),
|
||||
child: ColoredBox(color: TimbreColors.surface),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -34,16 +34,16 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
if (current == null) {
|
||||
return const Center(
|
||||
child: Text('Nothing playing.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.xl,
|
||||
RatuneSpacing.lg,
|
||||
RatuneSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.xl,
|
||||
TimbreSpacing.lg,
|
||||
TimbreSpacing.lg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -53,21 +53,21 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
Expanded(
|
||||
child: _showQueue ? const _QueuePanel() : const _AlbumArtPanel(),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.xl),
|
||||
const SizedBox(height: TimbreSpacing.xl),
|
||||
_InfoStrip(song: current, state: state, accent: accent),
|
||||
const SizedBox(height: RatuneSpacing.sm),
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
_FavRating(song: current),
|
||||
const SizedBox(height: RatuneSpacing.xs),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
_Transport(state: state, ref: ref, accent: accent),
|
||||
if (!state.supported)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: RatuneSpacing.sm),
|
||||
padding: EdgeInsets.only(top: TimbreSpacing.sm),
|
||||
child: Text(
|
||||
'Audio output unavailable on this platform — test on Android/iOS.',
|
||||
style: TextStyle(color: RatuneColors.dimmed, fontSize: 11),
|
||||
style: TextStyle(color: TimbreColors.dimmed, fontSize: 11),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.sm),
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
_QueueToggle(
|
||||
showQueue: _showQueue,
|
||||
queueLength: state.queue.length,
|
||||
|
|
@ -102,8 +102,8 @@ class _QueueToggle extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: RatuneSpacing.lg,
|
||||
vertical: RatuneSpacing.sm,
|
||||
horizontal: TimbreSpacing.lg,
|
||||
vertical: TimbreSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -111,13 +111,13 @@ class _QueueToggle extends StatelessWidget {
|
|||
Icon(
|
||||
showQueue ? Icons.album_outlined : Icons.queue_music,
|
||||
size: 16,
|
||||
color: showQueue ? accent : RatuneColors.dimmed,
|
||||
color: showQueue ? accent : TimbreColors.dimmed,
|
||||
),
|
||||
const SizedBox(width: RatuneSpacing.sm),
|
||||
const SizedBox(width: TimbreSpacing.sm),
|
||||
Text(
|
||||
showQueue ? 'Album art' : 'Queue ($queueLength)',
|
||||
style: TextStyle(
|
||||
color: showQueue ? accent : RatuneColors.foreground,
|
||||
color: showQueue ? accent : TimbreColors.foreground,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -140,7 +140,7 @@ class _QueuePanel extends ConsumerWidget {
|
|||
return HairlinePanel(
|
||||
title: 'Queue',
|
||||
trailing: '(${state.queue.length})',
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.md),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.queue.length,
|
||||
|
|
@ -153,15 +153,15 @@ class _QueuePanel extends ConsumerWidget {
|
|||
.playSongs(state.queue, startIndex: i),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: RatuneSpacing.lg,
|
||||
vertical: RatuneSpacing.xs,
|
||||
horizontal: TimbreSpacing.lg,
|
||||
vertical: TimbreSpacing.xs,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text('${i + 1}',
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
|
@ -169,23 +169,23 @@ class _QueuePanel extends ConsumerWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: isCurrent ? accent : RatuneColors.foreground,
|
||||
color: isCurrent ? accent : TimbreColors.foreground,
|
||||
fontWeight:
|
||||
isCurrent ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(_fmt(song.duration),
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
ref.read(playbackProvider.notifier).removeAt(i),
|
||||
customBorder: const CircleBorder(),
|
||||
child: const SizedBox(
|
||||
width: RatuneSpacing.minTouchTarget,
|
||||
height: RatuneSpacing.minTouchTarget,
|
||||
width: TimbreSpacing.minTouchTarget,
|
||||
height: TimbreSpacing.minTouchTarget,
|
||||
child: Icon(Icons.close,
|
||||
size: 18, color: RatuneColors.dimmed),
|
||||
size: 18, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -216,11 +216,11 @@ class _AlbumArtPanel extends ConsumerWidget {
|
|||
|
||||
return HairlinePanel(
|
||||
title: 'Album Art',
|
||||
padding: const EdgeInsets.all(RatuneSpacing.md),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.md),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ColoredBox(
|
||||
color: RatuneColors.surface,
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(
|
||||
artUri,
|
||||
|
|
@ -263,15 +263,15 @@ class _FavRating extends ConsumerWidget {
|
|||
onTap: () => ref.read(favoritesProvider.notifier).toggleSong(song),
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.sm),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.sm),
|
||||
child: Icon(
|
||||
starred ? Icons.favorite : Icons.favorite_border,
|
||||
color: starred ? accent : RatuneColors.dimmed,
|
||||
color: starred ? accent : TimbreColors.dimmed,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
for (int i = 1; i <= 5; i++)
|
||||
GestureDetector(
|
||||
onTap: () => ref
|
||||
|
|
@ -282,7 +282,7 @@ class _FavRating extends ConsumerWidget {
|
|||
child: Icon(
|
||||
i <= rating ? Icons.star : Icons.star_border,
|
||||
size: 18,
|
||||
color: i <= rating ? accent : RatuneColors.dimmed,
|
||||
color: i <= rating ? accent : TimbreColors.dimmed,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -291,9 +291,9 @@ class _FavRating extends ConsumerWidget {
|
|||
onTap: () => showAddToPlaylistSheet(context, songs: [song]),
|
||||
customBorder: const CircleBorder(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(RatuneSpacing.sm),
|
||||
padding: EdgeInsets.all(TimbreSpacing.sm),
|
||||
child: Icon(Icons.playlist_add,
|
||||
size: 22, color: RatuneColors.dimmed),
|
||||
size: 22, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
|
|
@ -305,7 +305,7 @@ class _FavRating extends ConsumerWidget {
|
|||
},
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.sm),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.sm),
|
||||
child: isDownloading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
|
|
@ -317,7 +317,7 @@ class _FavRating extends ConsumerWidget {
|
|||
? Icons.download_done
|
||||
: Icons.download_outlined,
|
||||
size: 22,
|
||||
color: isDownloaded ? accent : RatuneColors.dimmed,
|
||||
color: isDownloaded ? accent : TimbreColors.dimmed,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -348,19 +348,19 @@ class _InfoStrip extends StatelessWidget {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: accent, fontWeight: FontWeight.w700)),
|
||||
Text(song.artist ?? 'Unknown artist',
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
if (album.isNotEmpty)
|
||||
Text(album, style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
Text(album, style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
Row(
|
||||
children: [
|
||||
Text(_fmtDur(state.position),
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Expanded(child: BlockProgressBar(progress: state.progress)),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Text(_fmtDur(state.duration),
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -387,20 +387,20 @@ class _Transport extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_btn(Icons.shuffle, controller.toggleShuffle,
|
||||
color: state.shuffle ? accent : RatuneColors.dimmed),
|
||||
color: state.shuffle ? accent : TimbreColors.dimmed),
|
||||
_btn(Icons.skip_previous, controller.previous),
|
||||
_btn(state.playing ? Icons.pause : Icons.play_arrow,
|
||||
controller.togglePlayPause,
|
||||
color: accent, size: 40),
|
||||
_btn(Icons.skip_next, controller.next),
|
||||
_btn(loopIcon, controller.cycleLoop,
|
||||
color: state.loop != LoopMode.off ? accent : RatuneColors.dimmed),
|
||||
color: state.loop != LoopMode.off ? accent : TimbreColors.dimmed),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _btn(IconData icon, VoidCallback onTap,
|
||||
{Color color = RatuneColors.foreground, double size = 28}) {
|
||||
{Color color = TimbreColors.foreground, double size = 28}) {
|
||||
return IconButton(
|
||||
onPressed: onTap,
|
||||
icon: Icon(icon, color: color, size: size),
|
||||
|
|
@ -413,7 +413,7 @@ class _ArtFallback extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) => const Center(
|
||||
child: Icon(Icons.album_outlined,
|
||||
color: RatuneColors.dimmed, size: 48),
|
||||
color: TimbreColors.dimmed, size: 48),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ class PlaylistsScreen extends ConsumerWidget {
|
|||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.lg),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.lg),
|
||||
child: HairlinePanel(
|
||||
title: 'Playlists',
|
||||
active: true,
|
||||
trailing:
|
||||
state.playlists.isEmpty ? null : '(${state.playlists.length})',
|
||||
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.md),
|
||||
child: state.playlists.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
|
|
@ -48,7 +48,7 @@ class PlaylistsScreen extends ConsumerWidget {
|
|||
? 'No playlists yet. Tap + to create one.'
|
||||
: 'Connect to a server to see playlists.',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: RatuneColors.dimmed),
|
||||
style: const TextStyle(color: TimbreColors.dimmed),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
|
|
@ -99,7 +99,7 @@ class PlaylistsScreen extends ConsumerWidget {
|
|||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: RatuneColors.surface,
|
||||
backgroundColor: TimbreColors.surface,
|
||||
title: Text('Delete "${p.name}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
@ -136,12 +136,12 @@ class _PlaylistRow extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.lg),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: TimbreSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.queue_music, size: 18, color: RatuneColors.dimmed),
|
||||
const SizedBox(width: RatuneSpacing.md),
|
||||
const Icon(Icons.queue_music, size: 18, color: TimbreColors.dimmed),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -150,19 +150,19 @@ class _PlaylistRow extends StatelessWidget {
|
|||
Text(name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.dimmed, fontSize: 12)),
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onRename != null || onDelete != null)
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
size: 20, color: RatuneColors.dimmed),
|
||||
color: RatuneColors.surface,
|
||||
size: 20, color: TimbreColors.dimmed),
|
||||
color: TimbreColors.surface,
|
||||
onSelected: (v) {
|
||||
if (v == 'rename') onRename?.call();
|
||||
if (v == 'delete') onDelete?.call();
|
||||
|
|
@ -247,16 +247,16 @@ class _PlaylistDetailScreenState extends ConsumerState<PlaylistDetailScreen> {
|
|||
child: detail == null
|
||||
? const Center(
|
||||
child: Text('Loading…',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
: songs.isEmpty
|
||||
? const Center(
|
||||
child: Text('This playlist is empty.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: RatuneSpacing.md),
|
||||
vertical: TimbreSpacing.md),
|
||||
itemCount: songs.length,
|
||||
itemBuilder: (context, i) {
|
||||
final song = songs[i];
|
||||
|
|
@ -302,14 +302,14 @@ class _TrackRow extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: RatuneSpacing.lg),
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.only(left: TimbreSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text('$index',
|
||||
style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
|
@ -319,20 +319,20 @@ class _TrackRow extends StatelessWidget {
|
|||
Text(song.title ?? 'Untitled',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: RatuneColors.foreground)),
|
||||
style: const TextStyle(color: TimbreColors.foreground)),
|
||||
if (song.artist != null)
|
||||
Text(song.artist!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: RatuneColors.dimmed, fontSize: 12)),
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
size: 20, color: RatuneColors.dimmed),
|
||||
color: RatuneColors.surface,
|
||||
size: 20, color: TimbreColors.dimmed),
|
||||
color: TimbreColors.surface,
|
||||
onSelected: (v) {
|
||||
switch (v) {
|
||||
case 'next':
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||
controller: _controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.search,
|
||||
style: const TextStyle(color: RatuneColors.foreground),
|
||||
style: const TextStyle(color: TimbreColors.foreground),
|
||||
cursorColor: accent,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search artists, albums, songs…',
|
||||
hintStyle: TextStyle(color: RatuneColors.dimmed),
|
||||
hintStyle: TextStyle(color: TimbreColors.dimmed),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (v) => setState(() => _query = v),
|
||||
|
|
@ -46,7 +46,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||
child: _query.trim().isEmpty
|
||||
? const Center(
|
||||
child: Text('Type and press search.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
)
|
||||
: _Results(query: _query),
|
||||
),
|
||||
|
|
@ -71,13 +71,13 @@ class _Results extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Text('$e', style: const TextStyle(color: RatuneColors.dimmed)),
|
||||
child: Text('$e', style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
data: (r) {
|
||||
if (r.artists.isEmpty && r.albums.isEmpty && r.songs.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No results.',
|
||||
style: TextStyle(color: RatuneColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(RatuneSpacing.lg),
|
||||
padding: const EdgeInsets.all(TimbreSpacing.lg),
|
||||
children: [
|
||||
HairlinePanel(
|
||||
title: 'Streaming',
|
||||
|
|
@ -32,7 +32,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||
children: [
|
||||
const _Caption(
|
||||
'Quality used when playing tracks that are not downloaded.'),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
_ChoiceChips<int>(
|
||||
label: 'Max bitrate',
|
||||
values: AppSettings.bitrateChoices,
|
||||
|
|
@ -43,7 +43,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.xl),
|
||||
const SizedBox(height: TimbreSpacing.xl),
|
||||
HairlinePanel(
|
||||
title: 'Downloads',
|
||||
active: true,
|
||||
|
|
@ -53,7 +53,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||
const _Caption(
|
||||
'Quality used when saving tracks for offline playback. '
|
||||
'"Original" keeps the source file (best quality, largest).'),
|
||||
const SizedBox(height: RatuneSpacing.md),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
_ChoiceChips<int>(
|
||||
label: 'Max bitrate',
|
||||
values: AppSettings.bitrateChoices,
|
||||
|
|
@ -61,7 +61,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||
labelFor: AppSettings.bitrateLabel,
|
||||
onSelect: controller.setDownloadMaxBitRate,
|
||||
),
|
||||
const SizedBox(height: RatuneSpacing.lg),
|
||||
const SizedBox(height: TimbreSpacing.lg),
|
||||
_ChoiceChips<String?>(
|
||||
label: 'Format',
|
||||
values: AppSettings.formatChoices,
|
||||
|
|
@ -85,7 +85,7 @@ class _Caption extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
text,
|
||||
style: const TextStyle(color: RatuneColors.dimmed, fontSize: 12),
|
||||
style: const TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -112,11 +112,11 @@ class _ChoiceChips<T> extends StatelessWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: RatuneColors.foreground)),
|
||||
const SizedBox(height: RatuneSpacing.sm),
|
||||
Text(label, style: const TextStyle(color: TimbreColors.foreground)),
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
Wrap(
|
||||
spacing: RatuneSpacing.sm,
|
||||
runSpacing: RatuneSpacing.sm,
|
||||
spacing: TimbreSpacing.sm,
|
||||
runSpacing: TimbreSpacing.sm,
|
||||
children: [
|
||||
for (final v in values)
|
||||
_Chip(
|
||||
|
|
@ -150,21 +150,21 @@ class _Chip extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: RatuneSpacing.minTouchTarget),
|
||||
constraints: const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: RatuneSpacing.lg,
|
||||
vertical: RatuneSpacing.md,
|
||||
horizontal: TimbreSpacing.lg,
|
||||
vertical: TimbreSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: active ? accent : RatuneColors.border,
|
||||
color: active ? accent : TimbreColors.border,
|
||||
),
|
||||
color: active ? accent.withValues(alpha: 0.12) : RatuneColors.surface,
|
||||
color: active ? accent.withValues(alpha: 0.12) : TimbreColors.surface,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: active ? accent : RatuneColors.foreground,
|
||||
color: active ? accent : TimbreColors.foreground,
|
||||
fontWeight: active ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue