fix(network): discard stale folder browsing responses

This commit is contained in:
zarzet committed 2026-10-06 22:00:14 +07:00
1 parent 7057b0c349
commit 92e3fa94ec
2 files changed
+87 -4

No files matched your search

+18 -4
View File
@@ -46,6 +46,7 @@ class _NetworkStorageScreenState extends ConsumerState<NetworkStorageScreen> {
bool _adding = false;
bool _added = false;
bool _selectingDestination = false;
int _loadGeneration = 0;
Future<void> _useForDownloads() async {
final connection = widget.connection;
@@ -123,7 +124,20 @@ class _NetworkStorageScreenState extends ConsumerState<NetworkStorageScreen> {
_load();
}
@override
void didUpdateWidget(covariant NetworkStorageScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.connection != widget.connection ||
oldWidget.path != widget.path ||
oldWidget.service != widget.service) {
_added = false;
_load();
}
}
Future<void> _load() async {
final generation = ++_loadGeneration;
bool isCurrent() => mounted && generation == _loadGeneration;
setState(() {
_loading = true;
_failed = false;
@@ -132,18 +146,18 @@ class _NetworkStorageScreenState extends ConsumerState<NetworkStorageScreen> {
final connection = widget.connection;
if (connection == null) {
final connections = await service.connections();
if (mounted) _connections = connections;
if (isCurrent()) _connections = connections;
} else {
final entries = await service.list(connection, widget.path);
if (mounted) _entries = entries;
if (isCurrent()) _entries = entries;
}
} catch (error) {
if (mounted) {
if (isCurrent()) {
_failed = true;
_failure = classifyNetworkStorageError(error);
}
} finally {
if (mounted) setState(() => _loading = false);
if (isCurrent()) setState(() => _loading = false);
}
}
+69
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
@@ -12,6 +13,59 @@ import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/app_action_button.dart';
void main() {
for (final olderFails in [false, true]) {
testWidgets('latest folder refresh wins (older fails: $olderFails)', (
tester,
) async {
final service = _DelayedListings();
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: NetworkStorageScreen(
service: service,
connection: const NetworkConnection(
id: 'nas',
name: 'NAS',
protocol: NetworkProtocol.webdav,
address: 'https://nas.test/music/',
),
),
),
),
);
service.requests.single.complete([
const NetworkEntry('Initial.flac', 'Initial.flac'),
]);
await tester.pumpAndSettle();
final refresh = find.byTooltip('Refresh');
// Two taps before the disabled state is painted can overlap requests.
await tester.tap(refresh);
await tester.tap(refresh);
expect(service.requests, hasLength(3));
service.requests.last.complete([
const NetworkEntry('Latest.flac', 'Latest.flac'),
]);
await tester.pumpAndSettle();
if (olderFails) {
service.requests[1].completeError(const SocketException('offline'));
} else {
service.requests[1].complete([
const NetworkEntry('Stale.flac', 'Stale.flac'),
]);
}
await tester.pumpAndSettle();
expect(find.text('Latest.flac'), findsOneWidget);
expect(find.text('Stale.flac'), findsNothing);
expect(
find.textContaining('The server could not be reached.'),
findsNothing,
);
expect(tester.takeException(), isNull);
});
}
testWidgets('failed sign-in stays on the form with a specific error', (
tester,
) async {
@@ -243,3 +297,18 @@ class _NetworkFixture extends NetworkStorageService {
: [const NetworkEntry('Album/Song.flac', 'Song.flac')];
}
}
class _DelayedListings extends NetworkStorageService {
_DelayedListings() : super(read: () async => null, write: (_) async {});
final requests = <Completer<List<NetworkEntry>>>[];
@override
Future<List<NetworkEntry>> list(
NetworkConnection connection, [
String path = '',
]) {
final request = Completer<List<NetworkEntry>>();
requests.add(request);
return request.future;
}
}