feat: add resolve API with SongLink fallback, fix multi-artist tags (#288), and cleanup

Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers

Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks

Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
This commit is contained in:
zarzet
2026-04-13 23:32:14 +07:00
parent 7c4309955e
commit d034144e9c
63 changed files with 425 additions and 1044 deletions
-36
View File
@@ -1,9 +1,5 @@
import 'package:flutter/material.dart';
// ─────────────────────────────────────────────────────────────────────────────
// 1. Staggered List Item fade + slide-up entrance with index-based delay
// ─────────────────────────────────────────────────────────────────────────────
/// Wraps a child in a staggered fade-in + slide-up animation.
///
/// [index] controls the stagger delay (each item delayed by [staggerDelay]).
@@ -31,7 +27,6 @@ class StaggeredListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!animate || index >= maxAnimatedItems) return child;
// Cap the delay so very long lists don't have absurd wait times.
final cappedIndex = index.clamp(0, maxAnimatedItems - 1);
final delay = staggerDelay * cappedIndex;
final totalDuration = duration + delay;
@@ -42,7 +37,6 @@ class StaggeredListItem extends StatelessWidget {
duration: totalDuration,
curve: Curves.easeOutCubic,
builder: (context, value, child) {
// Compute the effective progress after the stagger delay.
final delayFraction = totalDuration.inMilliseconds > 0
? delay.inMilliseconds / totalDuration.inMilliseconds
: 0.0;
@@ -62,10 +56,6 @@ class StaggeredListItem extends StatelessWidget {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 2. Animated State Switcher crossfade between loading / content / empty / error
// ─────────────────────────────────────────────────────────────────────────────
/// A convenience wrapper around [AnimatedSwitcher] that crossfades between
/// different widget states (loading, content, empty, error).
///
@@ -94,10 +84,6 @@ class AnimatedStateSwitcher extends StatelessWidget {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 3. Shared Page Route consistent slide-from-right transition
// ─────────────────────────────────────────────────────────────────────────────
/// Creates a platform-aware material route.
///
/// This intentionally defers route transitions to Flutter's material route and
@@ -107,10 +93,6 @@ Route<T> slidePageRoute<T>({required Widget page}) {
return MaterialPageRoute<T>(builder: (context) => page);
}
// ─────────────────────────────────────────────────────────────────────────────
// 4. Shimmer / Skeleton Loading Widget
// ─────────────────────────────────────────────────────────────────────────────
/// A shimmer effect widget that can wrap skeleton placeholders.
class ShimmerLoading extends StatefulWidget {
final Widget child;
@@ -682,10 +664,6 @@ class HomeSearchSkeleton extends StatelessWidget {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 5. Animated Selection Checkbox scales in when entering selection mode
// ─────────────────────────────────────────────────────────────────────────────
/// An animated selection indicator that scales in/out and crossfades the
/// checked/unchecked state.
class AnimatedSelectionCheckbox extends StatelessWidget {
@@ -746,10 +724,6 @@ class AnimatedSelectionCheckbox extends StatelessWidget {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 6. Download Success Animation green flash + checkmark
// ─────────────────────────────────────────────────────────────────────────────
/// A widget that briefly flashes a success color behind its child and shows
/// an animated checkmark when [showSuccess] transitions to true.
class DownloadSuccessOverlay extends StatefulWidget {
@@ -775,8 +749,6 @@ class _DownloadSuccessOverlayState extends State<DownloadSuccessOverlay>
@override
void initState() {
super.initState();
// Initialise from the current widget value so items that are already
// completed when first built do not play the flash animation.
_wasSuccess = widget.showSuccess;
_controller = AnimationController(
vsync: this,
@@ -821,10 +793,6 @@ class _DownloadSuccessOverlayState extends State<DownloadSuccessOverlay>
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 7. Badge Bump Animation scales the badge when count changes
// ─────────────────────────────────────────────────────────────────────────────
/// Wraps a [Badge] child and plays a brief scale-bump whenever [count] changes.
class AnimatedBadge extends StatefulWidget {
final int count;
@@ -877,10 +845,6 @@ class _AnimatedBadgeState extends State<AnimatedBadge>
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 8. Animated Removal Item fade + slide out when removed from a list
// ─────────────────────────────────────────────────────────────────────────────
/// Build a removal animation for [AnimatedList] items.
/// Use as the `builder` callback in [AnimatedListState.removeItem].
Widget buildRemovalAnimation(Widget child, Animation<double> animation) {
-7
View File
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
/// Progress state communicated from caller to dialog via [ValueNotifier].
class _BatchProgress {
final int current;
final String? detail;
@@ -54,11 +53,8 @@ class BatchProgressDialog extends StatefulWidget {
required ValueNotifier<_BatchProgress> progressNotifier,
}) : _progressNotifier = progressNotifier;
// ── Static bookkeeping ──────────────────────────────────────────────
static ValueNotifier<_BatchProgress>? _activeNotifier;
/// Show the dialog. Call [update] to push progress, [dismiss] to close.
static void show({
required BuildContext context,
required String title,
@@ -82,13 +78,10 @@ class BatchProgressDialog extends StatefulWidget {
);
}
/// Update the progress of the currently visible dialog.
/// No [BuildContext] needed communicates via [ValueNotifier].
static void update({required int current, String? detail}) {
_activeNotifier?.value = _BatchProgress(current: current, detail: detail);
}
/// Dismiss the dialog and clean up.
static void dismiss(BuildContext context) {
_activeNotifier = null;
Navigator.of(context, rootNavigator: true).pop();
-4
View File
@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
/// A collapsing header widget
/// Title collapses from large to small when scrolling
class CollapsingHeader extends StatelessWidget {
final String title;
final bool showBackButton;
@@ -100,7 +98,6 @@ class CollapsingHeader extends StatelessWidget {
}
}
/// Section header for settings
class SettingsSection extends StatelessWidget {
final String title;
const SettingsSection({super.key, required this.title});
@@ -120,7 +117,6 @@ class SettingsSection extends StatelessWidget {
}
}
/// Info card widget (like version info)
class InfoCard extends StatelessWidget {
final IconData icon;
final String title;
-8
View File
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
/// Custom painted icons for donate platforms
class KofiIcon extends StatelessWidget {
final double size;
final Color color;
@@ -46,7 +44,6 @@ class _KofiPainter extends CustomPainter {
..quadraticBezierTo(s * 0.92, s * 0.68, s * 0.70, s * 0.68);
canvas.drawPath(handlePath, handlePaint);
// Heart on cup
final heartPaint = Paint()
..color = const Color(0xFFFF5E5B)
..style = PaintingStyle.fill;
@@ -62,7 +59,6 @@ class _KofiPainter extends CustomPainter {
..close();
canvas.drawPath(heart, heartPaint);
// Steam lines
final steamPaint = Paint()
..color = color.withValues(alpha: 0.6)
..style = PaintingStyle.stroke
@@ -108,12 +104,9 @@ class _GitHubPainter extends CustomPainter {
..color = color
..style = PaintingStyle.fill;
// GitHub octocat silhouette (simplified mark)
// Based on the GitHub logo path, scaled to fit
final scale = s / 24.0;
final path = Path();
// Outer circle/head shape
path.moveTo(12 * scale, 0.5 * scale);
path.cubicTo(
5.37 * scale, 0.5 * scale,
@@ -135,7 +128,6 @@ class _GitHubPainter extends CustomPainter {
9.01 * scale, 22.01 * scale,
9.01 * scale, 21.01 * scale,
);
// Left arm
path.cubicTo(
5.67 * scale, 21.71 * scale,
4.97 * scale, 19.56 * scale,
-7
View File
@@ -5,7 +5,6 @@ import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
/// Built-in service info with quality options
class BuiltInService {
final String id;
final String label;
@@ -22,7 +21,6 @@ class BuiltInService {
});
}
/// Default quality options for each built-in service
const _builtInServices = [
BuiltInService(
id: 'tidal',
@@ -99,7 +97,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
ConsumerState<DownloadServicePicker> createState() =>
_DownloadServicePickerState();
/// Show the download service picker as a modal bottom sheet
static void show(
BuildContext context, {
String? trackName,
@@ -135,7 +132,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
@override
void initState() {
super.initState();
// Default to recommended service if available, otherwise use default
final recommended = widget.recommendedService;
if (recommended != null && recommended.isNotEmpty) {
_selectedService = recommended;
@@ -144,7 +140,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
}
}
/// Get quality options for the selected service
List<QualityOption> _getQualityOptions() {
final builtIn = _builtInServices
.where((s) => s.id == _selectedService)
@@ -161,8 +156,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
return ext.qualityOptions;
}
// Extensions without quality options use Tidal's options as default
// since the download will fall back to built-in providers anyway.
return _builtInServices.firstWhere((s) => s.id == 'tidal').qualityOptions;
}
-9
View File
@@ -29,10 +29,6 @@ class ReEnrichFieldSelection {
bool get isAll => fields.length == ReEnrichFields.all.length;
}
/// Shows a bottom sheet that lets the user pick which metadata fields to update
/// during a bulk re-enrich operation.
///
/// Returns `null` when cancelled, or a [ReEnrichFieldSelection] when confirmed.
Future<ReEnrichFieldSelection?> showReEnrichFieldDialog(
BuildContext context, {
required int selectedCount,
@@ -128,7 +124,6 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 4),
child: Text(
@@ -138,7 +133,6 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
),
),
),
// Subtitle
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 4),
child: Text(
@@ -158,7 +152,6 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
),
),
const Divider(height: 1),
// Select All
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(
@@ -171,7 +164,6 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
controlAffinity: ListTileControlAffinity.leading,
),
const Divider(height: 1, indent: 16, endIndent: 16),
// Individual fields
for (final field in ReEnrichFields.all)
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
@@ -182,7 +174,6 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 8),
// Confirm button
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
+1 -10
View File
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
/// A grouped settings card that connects items together like Android Settings
/// Items are connected with no gap between them, only separated when changing groups
class SettingsGroup extends StatelessWidget {
final List<Widget> children;
final EdgeInsetsGeometry? margin;
@@ -17,14 +15,10 @@ class SettingsGroup extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
// Use a more contrasting color for cards
// In dark mode with dynamic color, surfaceContainerHighest can be too similar to surface
// So we add a slight white overlay to make it more visible
// In light mode with dynamic color, we add a slight black overlay for the same reason
final cardColor = isDark
? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface)
: Color.alphaBlend(Colors.black.withValues(alpha: 0.04), colorScheme.surface);
return Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
@@ -44,7 +38,6 @@ class SettingsGroup extends StatelessWidget {
}
class SettingsItem extends StatelessWidget {
final IconData? icon;
final String title;
final String? subtitle;
@@ -126,7 +119,6 @@ class SettingsItem extends StatelessWidget {
}
class SettingsSwitchItem extends StatelessWidget {
final IconData? icon;
final String title;
final String? subtitle;
@@ -214,7 +206,6 @@ class SettingsSwitchItem extends StatelessWidget {
}
class SettingsSectionHeader extends StatelessWidget {
final String title;
const SettingsSectionHeader({super.key, required this.title});
@@ -74,7 +74,6 @@ class _TrackOptionsSheet extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header with drag handle + track info (matches _TrackInfoHeader)
Column(
children: [
const SizedBox(height: 8),
@@ -163,7 +162,6 @@ class _TrackOptionsSheet extends ConsumerWidget {
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
// Action items (matches _QualityOption style)
_OptionTile(
icon: isLoved ? Icons.favorite : Icons.favorite_border,
iconColor: isLoved ? colorScheme.error : null,
@@ -234,7 +232,6 @@ class _TrackOptionsSheet extends ConsumerWidget {
}
}
/// Styled like _QualityOption in download_service_picker.dart
class _OptionTile extends StatelessWidget {
final IconData icon;
final Color? iconColor;