mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-31 00:07:25 +02:00
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:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
Reference in New Issue
Block a user