redesign app phase 3
Build / Build for Android (push) Has been cancelled
Build / Build for iOS (push) Has been cancelled

This commit is contained in:
Zakaria
2026-05-19 16:41:18 -04:00
parent c36fc55102
commit 9eb6b5237c
20 changed files with 645 additions and 275 deletions
+208 -147
View File
@@ -265,91 +265,87 @@ class _MusicScreenState extends State<MusicScreen>
padding: const EdgeInsets.only(right: 8.0),
child: getFloatingActionButton(),
),
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.45),
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface,
],
),
),
child: SafeArea(
top: false,
child: Column(
children: [
if (!isSearching)
_MusicHeroHeader(
title: _finampUserHelper
.currentUser?.currentView?.name ??
AppLocalizations.of(context)!.music,
activeTab: tabs
.elementAt(_tabController!.index)
.toLocalisedString(context),
isFavourite: finampSettings.isFavourite,
isOffline: finampSettings.isOffline,
onShuffle: () async {
try {
await _audioServiceHelper.shuffleAll(
FinampSettingsHelper
.finampSettings.isFavourite,
);
} catch (e) {
errorSnackbar(e, context);
}
},
body: Stack(
children: [
const _MusicBackdrop(),
SafeArea(
top: false,
child: Column(
children: [
if (!isSearching)
_MusicHeroHeader(
title: _finampUserHelper
.currentUser?.currentView?.name ??
AppLocalizations.of(context)!.music,
activeTab: tabs
.elementAt(_tabController!.index)
.toLocalisedString(context),
isFavourite: finampSettings.isFavourite,
isOffline: finampSettings.isOffline,
onShuffle: () async {
try {
await _audioServiceHelper.shuffleAll(
FinampSettingsHelper
.finampSettings.isFavourite,
);
} catch (e) {
errorSnackbar(e, context);
}
},
),
_ModernMusicTabBar(
controller: _tabController!,
tabs: tabs.toList(),
),
_ModernMusicTabBar(
controller: _tabController!,
tabs: tabs.toList(),
),
Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(12, 8, 12, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(28),
),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.shadow
.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, -6),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 0),
child: Material(
color: Theme.of(context)
.colorScheme
.surface
.withOpacity(0.74),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(34),
),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outlineVariant
.withOpacity(0.42),
),
),
],
),
clipBehavior: Clip.antiAlias,
child: TabBarView(
controller: _tabController,
children: tabs
.map((tabType) => MusicScreenTabView(
tabContentType: tabType,
searchTerm: searchQuery,
isFavourite: finampSettings.isFavourite,
sortBy: finampSettings
.getTabSortBy(tabType),
sortOrder: finampSettings
.getSortOrder(tabType),
view: _finampUserHelper
.currentUser?.currentView,
))
.toList(),
elevation: 16,
shadowColor: Theme.of(context)
.colorScheme
.shadow
.withOpacity(0.24),
clipBehavior: Clip.antiAlias,
child: TabBarView(
controller: _tabController,
children: tabs
.map((tabType) => MusicScreenTabView(
tabContentType: tabType,
searchTerm: searchQuery,
isFavourite:
finampSettings.isFavourite,
sortBy: finampSettings
.getTabSortBy(tabType),
sortOrder: finampSettings
.getSortOrder(tabType),
view: _finampUserHelper
.currentUser?.currentView,
))
.toList(),
),
),
),
),
),
],
],
),
),
),
],
),
),
);
@@ -360,6 +356,78 @@ class _MusicScreenState extends State<MusicScreen>
}
}
class _MusicBackdrop extends StatelessWidget {
const _MusicBackdrop();
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFF081D38),
colorScheme.background,
const Color(0xFF020611),
],
),
),
child: Stack(
children: [
Positioned(
top: -110,
right: -80,
child: _GlowOrb(
color: colorScheme.primary.withOpacity(0.28),
size: 280,
),
),
Positioned(
top: 90,
left: -120,
child: _GlowOrb(
color: colorScheme.tertiary.withOpacity(0.20),
size: 260,
),
),
Positioned(
bottom: -140,
right: -130,
child: _GlowOrb(
color: colorScheme.secondary.withOpacity(0.16),
size: 320,
),
),
],
),
);
}
}
class _GlowOrb extends StatelessWidget {
const _GlowOrb({required this.color, required this.size});
final Color color;
final double size;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [color, Colors.transparent],
),
),
);
}
}
class _MusicHeroHeader extends StatelessWidget {
const _MusicHeroHeader({
required this.title,
@@ -381,73 +449,64 @@ class _MusicHeroHeader extends StatelessWidget {
final colorScheme = theme.colorScheme;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 86, 20, 12),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary.withOpacity(0.95),
colorScheme.tertiary.withOpacity(0.78),
],
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.28),
blurRadius: 34,
offset: const Offset(0, 18),
padding: const EdgeInsets.fromLTRB(24, 88, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isOffline ? 'OFFLINE MODE' : 'FINAMP MUSIC',
style: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.w800,
letterSpacing: 2.2,
),
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
),
const SizedBox(height: 10),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.displaySmall?.copyWith(
color: colorScheme.onBackground,
fontWeight: FontWeight.w900,
height: 0.98,
letterSpacing: -1.4,
),
),
const SizedBox(height: 12),
Text(
'Browse your Jellyfin collection with artwork-first shelves.',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 22),
Row(
children: [
Text(
isOffline ? 'Offline library' : 'Your Jellyfin music',
style: theme.textTheme.labelLarge?.copyWith(
color: colorScheme.onPrimary.withOpacity(0.76),
letterSpacing: 0.6,
FilledButton.icon(
onPressed: onShuffle,
icon: const Icon(Icons.shuffle_rounded),
label: const Text('Shuffle'),
style: FilledButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 14,
),
),
),
const SizedBox(height: 8),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.headlineMedium?.copyWith(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w800,
height: 1.04,
),
),
const SizedBox(height: 20),
Row(
children: [
FilledButton.icon(
onPressed: onShuffle,
icon: const Icon(Icons.shuffle_rounded),
label: const Text('Shuffle'),
style: FilledButton.styleFrom(
backgroundColor: colorScheme.onPrimary,
foregroundColor: colorScheme.primary,
),
),
const SizedBox(width: 10),
_HeroChip(
icon: isFavourite
? Icons.favorite_rounded
: Icons.library_music_rounded,
label: isFavourite ? 'Favourites' : activeTab,
),
],
const SizedBox(width: 12),
_HeroChip(
icon:
isFavourite ? Icons.favorite_rounded : Icons.album_rounded,
label: isFavourite ? 'Favourites' : activeTab,
),
],
),
),
],
),
);
}
@@ -466,19 +525,19 @@ class _HeroChip extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: colorScheme.onPrimary.withOpacity(0.14),
color: colorScheme.surfaceVariant.withOpacity(0.58),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: colorScheme.onPrimary.withOpacity(0.16)),
border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.55)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: colorScheme.onPrimary, size: 18),
Icon(icon, color: colorScheme.secondary, size: 18),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: colorScheme.onPrimary,
color: colorScheme.onSurface,
fontWeight: FontWeight.w700,
),
),
@@ -502,10 +561,10 @@ class _ModernMusicTabBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DecoratedBox(
decoration: BoxDecoration(
color: colorScheme.surface.withOpacity(0.7),
color: colorScheme.surfaceVariant.withOpacity(0.50),
borderRadius: BorderRadius.circular(999),
border:
Border.all(color: colorScheme.outlineVariant.withOpacity(0.4)),
Border.all(color: colorScheme.outlineVariant.withOpacity(0.55)),
),
child: TabBar(
controller: controller,
@@ -518,7 +577,9 @@ class _ModernMusicTabBar extends StatelessWidget {
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(999),
color: colorScheme.primary,
gradient: LinearGradient(
colors: [colorScheme.primary, colorScheme.secondary],
),
),
labelColor: colorScheme.onPrimary,
unselectedLabelColor: colorScheme.onSurfaceVariant,
+157 -3
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui';
@@ -68,6 +69,7 @@ class PlayerScreen extends StatelessWidget {
children: [
if (FinampSettingsHelper.finampSettings.showCoverAsPlayerBackground)
const _BlurredPlayerScreenBackground(),
const _PlayerAmbientVisualizer(),
SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
@@ -113,6 +115,158 @@ class PlayerScreen extends StatelessWidget {
}
}
class _PlayerAmbientVisualizer extends StatefulWidget {
const _PlayerAmbientVisualizer({Key? key}) : super(key: key);
@override
State<_PlayerAmbientVisualizer> createState() =>
_PlayerAmbientVisualizerState();
}
class _PlayerAmbientVisualizerState extends State<_PlayerAmbientVisualizer>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
StreamSubscription<PlaybackState>? _subscription;
bool _isPlaying = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1800),
);
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
_subscription = audioHandler.playbackState.listen((playbackState) {
if (_isPlaying == playbackState.playing) {
return;
}
setState(() {
_isPlaying = playbackState.playing;
});
if (_isPlaying) {
_controller.repeat();
} else {
_controller.stop();
}
});
}
@override
void dispose() {
_subscription?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return IgnorePointer(
child: AnimatedOpacity(
opacity: _isPlaying ? 1 : 0.18,
duration: const Duration(milliseconds: 450),
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
painter: _AmbientVisualizerPainter(
progress: _controller.value,
primary: colorScheme.primary,
secondary: colorScheme.secondary,
tertiary: colorScheme.tertiary,
),
size: Size.infinite,
);
},
),
),
),
);
}
}
class _AmbientVisualizerPainter extends CustomPainter {
const _AmbientVisualizerPainter({
required this.progress,
required this.primary,
required this.secondary,
required this.tertiary,
});
final double progress;
final Color primary;
final Color secondary;
final Color tertiary;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width * 0.5, size.height * 0.42);
final pulse = 0.5 + sin(progress * pi * 2) * 0.5;
_paintOrb(
canvas,
center.translate(size.width * -0.22, size.height * -0.08),
size.shortestSide * (0.34 + pulse * 0.05),
primary.withOpacity(0.16),
);
_paintOrb(
canvas,
center.translate(size.width * 0.23, size.height * 0.02),
size.shortestSide * (0.30 + (1 - pulse) * 0.07),
tertiary.withOpacity(0.14),
);
final barPaint = Paint()..style = PaintingStyle.fill;
final barCount = max(16, (size.width / 28).floor());
final maxBarHeight = size.height * 0.14;
final baseline = size.height * 0.86;
final totalWidth = min(size.width * 0.72, 620.0);
final gap = totalWidth / barCount;
final startX = (size.width - totalWidth) / 2;
for (var i = 0; i < barCount; i++) {
final wave = sin(progress * pi * 2 + i * 0.62);
final alternateWave = sin(progress * pi * 4 + i * 0.31);
final normalized = (wave + alternateWave * 0.35 + 1.35) / 2.7;
final barHeight = 12 + normalized * maxBarHeight;
final x = startX + i * gap + gap * 0.22;
final width = gap * 0.48;
final rect = RRect.fromRectAndRadius(
Rect.fromLTWH(x, baseline - barHeight, width, barHeight),
Radius.circular(width),
);
barPaint.color = Color.lerp(primary, secondary, i / barCount)!
.withOpacity(0.16 + normalized * 0.26);
canvas.drawRRect(rect, barPaint);
}
}
void _paintOrb(Canvas canvas, Offset center, double radius, Color color) {
final paint = Paint()
..shader = RadialGradient(
colors: [color, Colors.transparent],
).createShader(Rect.fromCircle(center: center, radius: radius));
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(covariant _AmbientVisualizerPainter oldDelegate) {
return progress != oldDelegate.progress ||
primary != oldDelegate.primary ||
secondary != oldDelegate.secondary ||
tertiary != oldDelegate.tertiary;
}
}
class _PlayerArtworkSurface extends StatelessWidget {
const _PlayerArtworkSurface({Key? key}) : super(key: key);
@@ -250,9 +404,9 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
// We need a post frame callback because otherwise this
// widget rebuilds on the same frame
WidgetsBinding.instance.addPostFrameCallback(
(_) => ref.read(_albumImageProvider.notifier).state =
imageProvider,
),
(_) => ref.read(_albumImageProvider.notifier).state =
imageProvider,
),
// Here we awkwardly get the next 3 queue items so that we
// can precache them (so that the image is already loaded
// when the next song comes on).