mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
feat(metadata): remove ReplayGain from individual and selected tracks
This commit is contained in:
1 parent
c24132cb9b
commit
9d530cea9e
19 files changed
+835
-171
No files matched your search
@@ -21,102 +21,124 @@ void main() {
|
||||
|
||||
for (final mornye in [false, true]) {
|
||||
for (final downloaded in [false, true]) {
|
||||
testWidgets(
|
||||
'ReplayGain survives hiding the album selection (Mornye: $mornye, downloaded: $downloaded)',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
var attempts = 0;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'safCopyToTemp') attempts++;
|
||||
// Simulate an inaccessible file. Reaching this call and showing
|
||||
// the result proves confirmation actually starts the operation.
|
||||
return null;
|
||||
});
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
const path = 'content://library/document/track.flac';
|
||||
final item = DownloadHistoryItem(
|
||||
id: 'track',
|
||||
trackName: 'Track',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: path,
|
||||
service: 'provider-a',
|
||||
downloadedAt: DateTime(2026),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
downloadedAlbumTracksProvider(
|
||||
const DownloadedAlbumTracksRequest(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
for (final remove in [false, true]) {
|
||||
testWidgets(
|
||||
'ReplayGain survives hiding the album selection (Mornye: $mornye, downloaded: $downloaded, remove: $remove)',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
var attempts = 0;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'safCopyToTemp') attempts++;
|
||||
// Simulate an inaccessible file. Reaching this call and showing
|
||||
// the result proves confirmation actually starts the operation.
|
||||
return null;
|
||||
});
|
||||
addTearDown(
|
||||
() => messenger.setMockMethodCallHandler(channel, null),
|
||||
);
|
||||
const path = 'content://library/document/track.flac';
|
||||
final item = DownloadHistoryItem(
|
||||
id: 'track',
|
||||
trackName: 'Track',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: path,
|
||||
service: 'provider-a',
|
||||
downloadedAt: DateTime(2026),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
downloadedAlbumTracksProvider(
|
||||
const DownloadedAlbumTracksRequest(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
),
|
||||
).overrideWith((ref) async => [item]),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: mornye
|
||||
? MornyeTheme.build(Brightness.light)
|
||||
: AppTheme.light(),
|
||||
localizationsDelegates:
|
||||
AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: SelectionOverlayHost(
|
||||
child: downloaded
|
||||
? const DownloadedAlbumScreen(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
)
|
||||
: LocalAlbumScreen(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
tracks: [
|
||||
LocalLibraryItem(
|
||||
id: 'track',
|
||||
trackName: 'Track',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: path,
|
||||
scannedAt: DateTime(2026),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).overrideWith((ref) async => [item]),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: mornye
|
||||
? MornyeTheme.build(Brightness.light)
|
||||
: AppTheme.light(),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: SelectionOverlayHost(
|
||||
child: downloaded
|
||||
? const DownloadedAlbumScreen(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
)
|
||||
: LocalAlbumScreen(
|
||||
albumName: 'Album',
|
||||
artistName: 'Artist',
|
||||
tracks: [
|
||||
LocalLibraryItem(
|
||||
id: 'track',
|
||||
trackName: 'Track',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: path,
|
||||
scannedAt: DateTime(2026),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(find.text('Track'), 200);
|
||||
await tester.longPress(find.text('Track'));
|
||||
await tester.pumpAndSettle();
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(SelectionBottomBar)),
|
||||
);
|
||||
Future<void> openConfirmation() async {
|
||||
await tester.tap(find.text(l10n.selectionReplayGainCount(1)));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(find.text('Track'), 200);
|
||||
await tester.longPress(find.text('Track'));
|
||||
await tester.pumpAndSettle();
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(SelectionBottomBar)),
|
||||
);
|
||||
Future<void> openConfirmation() async {
|
||||
final action = find.text(
|
||||
remove
|
||||
? l10n.selectionRemoveReplayGainCount(1)
|
||||
: l10n.selectionReplayGainCount(1),
|
||||
);
|
||||
await tester.ensureVisible(action);
|
||||
await tester.tap(action);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SelectionBottomBar), findsNothing);
|
||||
expect(find.byType(AppAlertDialog), findsOneWidget);
|
||||
}
|
||||
|
||||
await openConfirmation();
|
||||
await tester.tap(find.text(l10n.dialogCancel));
|
||||
await tester.pumpAndSettle();
|
||||
expect(attempts, 0);
|
||||
expect(find.byType(SelectionBottomBar), findsOneWidget);
|
||||
|
||||
await openConfirmation();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AppDialogAction),
|
||||
matching: find.text(
|
||||
remove
|
||||
? l10n.trackRemoveReplayGain
|
||||
: l10n.replayGainBatchConfirmTitle,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(attempts, 1);
|
||||
expect(
|
||||
find.text(
|
||||
remove
|
||||
? l10n.replayGainRemoveBatchSuccess(0, 1)
|
||||
: l10n.replayGainBatchSuccess(0, 1),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(SelectionBottomBar), findsNothing);
|
||||
expect(find.byType(AppAlertDialog), findsOneWidget);
|
||||
}
|
||||
|
||||
await openConfirmation();
|
||||
await tester.tap(find.text(l10n.dialogCancel));
|
||||
await tester.pumpAndSettle();
|
||||
expect(attempts, 0);
|
||||
expect(find.byType(SelectionBottomBar), findsOneWidget);
|
||||
|
||||
await openConfirmation();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AppDialogAction),
|
||||
matching: find.text(l10n.replayGainBatchConfirmTitle),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(attempts, 1);
|
||||
expect(find.text(l10n.replayGainBatchSuccess(0, 1)), findsOneWidget);
|
||||
expect(find.byType(SelectionBottomBar), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,4 +194,78 @@ void main() {
|
||||
expect(metadata['replaygain_track_peak'], '1.258925');
|
||||
expect(calls, ['editFileMetadata', 'readFileMetadata']);
|
||||
});
|
||||
|
||||
test('removal clears both scopes and verifies before saving SAF', () async {
|
||||
metadata['title'] = 'Preserved title';
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('content://music/document/42'),
|
||||
isTrue,
|
||||
);
|
||||
expect(editedFields, {
|
||||
'replaygain_track_gain': '',
|
||||
'replaygain_track_peak': '',
|
||||
'replaygain_album_gain': '',
|
||||
'replaygain_album_peak': '',
|
||||
});
|
||||
expect(metadata['title'], 'Preserved title');
|
||||
expect(calls, [
|
||||
'safCopyToTemp',
|
||||
'editFileMetadata',
|
||||
'readFileMetadata',
|
||||
'writeTempToSaf',
|
||||
]);
|
||||
expect(await File(tempPath).exists(), isFalse);
|
||||
});
|
||||
|
||||
test('removal refuses to save when the gain tags remain', () async {
|
||||
applyEdits = false;
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('content://music/document/42'),
|
||||
isFalse,
|
||||
);
|
||||
expect(calls, ['safCopyToTemp', 'editFileMetadata', 'readFileMetadata']);
|
||||
expect(await File(tempPath).exists(), isFalse);
|
||||
});
|
||||
|
||||
test('removal does not treat a fallback instruction as success', () async {
|
||||
method = 'ffmpeg';
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('content://music/document/42'),
|
||||
isFalse,
|
||||
);
|
||||
expect(calls, ['safCopyToTemp', 'editFileMetadata']);
|
||||
expect(await File(tempPath).exists(), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'removal reports SAF save failure and cleans its temporary copy',
|
||||
() async {
|
||||
saveSaf = false;
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('content://music/document/42'),
|
||||
isFalse,
|
||||
);
|
||||
expect(calls.last, 'writeTempToSaf');
|
||||
expect(await File(tempPath).exists(), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('removal rejects an empty metadata response', () async {
|
||||
metadata = {};
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('content://music/document/42'),
|
||||
isFalse,
|
||||
);
|
||||
expect(calls, ['safCopyToTemp', 'editFileMetadata', 'readFileMetadata']);
|
||||
});
|
||||
|
||||
test('removal is idempotent for an untagged local file', () async {
|
||||
method = 'native';
|
||||
metadata = {'audio_codec': 'flac'};
|
||||
expect(
|
||||
await ReplayGainService.removeFromFile('${directory.path}/song.flac'),
|
||||
isTrue,
|
||||
);
|
||||
expect(calls, ['editFileMetadata', 'readFileMetadata']);
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
import 'package:spotiflac_android/widgets/app_alert_dialog.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -28,10 +29,26 @@ void main() {
|
||||
'replaygain_album_peak': '1.234567',
|
||||
}
|
||||
: {'replaygain_track_gain': ' ', 'replaygain_album_peak': null};
|
||||
var edits = 0;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'editFileMetadata') {
|
||||
edits++;
|
||||
final args = call.arguments as Map;
|
||||
final fields = Map<String, String>.from(
|
||||
jsonDecode(args['metadata_json'] as String) as Map,
|
||||
);
|
||||
metadata.addAll(fields);
|
||||
return jsonEncode({'success': true, 'method': 'native'});
|
||||
}
|
||||
return switch (call.method) {
|
||||
'safStat' => jsonEncode({'exists': true, 'size': 100}),
|
||||
'readAudioMetadata' => jsonEncode(metadata),
|
||||
'readFileMetadata' => jsonEncode({
|
||||
...metadata,
|
||||
'audio_codec': 'flac',
|
||||
}),
|
||||
'safCopyToTemp' => 'temporary-song.flac',
|
||||
'writeTempToSaf' => jsonEncode({'success': true}),
|
||||
'getLyricsLRCWithSource' => jsonEncode({'lyrics': '', 'source': ''}),
|
||||
'getSafFileModTimes' => '{}',
|
||||
_ => null,
|
||||
@@ -74,6 +91,36 @@ void main() {
|
||||
]) {
|
||||
expect(find.text(text), hasTags ? findsOneWidget : findsNothing);
|
||||
}
|
||||
Future<void> openRemoval() async {
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Remove ReplayGain'));
|
||||
await tester.tap(find.text('Remove ReplayGain'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AppAlertDialog), findsOneWidget);
|
||||
}
|
||||
|
||||
await openRemoval();
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(edits, 0);
|
||||
if (hasTags) expect(find.text('-6.20 dB'), findsOneWidget);
|
||||
|
||||
await openRemoval();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AppDialogAction),
|
||||
matching: find.text('Remove ReplayGain'),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
expect(edits, 1);
|
||||
expect(find.text('ReplayGain tags removed'), findsOneWidget);
|
||||
expect(find.text('ReplayGain Track Gain'), findsNothing);
|
||||
expect(find.text('ReplayGain Album Gain'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user