[verified] Publish ReelBox source under GPLv3

This commit is contained in:
Hermes committed 2026-11-12 23:41:13 +00:00
commit db2b505ca3
445 files changed
+105274

No files matched your search

+325
View File
@@ -0,0 +1,325 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:reelbox/data/api_service.dart';
import 'package:reelbox/data/models.dart';
import 'package:reelbox/state/app_state.dart';
import 'package:reelbox/ui/common.dart';
import 'package:reelbox/ui/library.dart';
import 'package:shared_preferences/shared_preferences.dart';
const movie = TitleRecord(id: 42, title: 'Movie');
const channel = TitleRecord(id: 42, title: 'Channel', type: 'channel');
class SettingsLocalApi extends ApiService {
int discoveryChecks = 0;
@override
Future<void> initialize({bool discover = true}) async {
if (discover) discoveryChecks++;
}
}
Future<void> mount(WidgetTester tester, AppState state, Widget page) async {
await tester.pumpWidget(
AppScope(
state: state,
child: MaterialApp(home: page),
),
);
await tester.pumpAndSettle();
}
void main() {
setUp(() => SharedPreferences.setMockInitialValues({}));
testWidgets('imported non-menu settings remain selected and editable', (
tester,
) async {
final state = AppState(api: SettingsLocalApi());
addTearDown(state.dispose);
final backup = jsonDecode(state.exportLibrary()) as Map<String, dynamic>;
backup['settings'] = {
...state.prefs.toJson(),
'defaultQuality': '2160p',
'playbackSpeed': 1.75,
'skipSeconds': 12,
'subtitleColor': 0xFF12AB34,
'subtitleBackground': 0x40123456,
};
await state.importLibrary(jsonEncode(backup));
final original = state.prefs.toJson();
await mount(tester, state, const SettingsPage());
for (final setting in <String, Object>{
'Default quality': '2160p',
'Playback speed': 1.75,
'Seek step': 12,
'Subtitle color': 0xFF12AB34,
'Subtitle background': 0x40123456,
}.entries) {
await tester.scrollUntilVisible(
find.text(setting.key),
250,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
final tile = find.ancestor(
of: find.text(setting.key),
matching: find.byType(ListTile),
);
final dropdown = tester.widget<DropdownButton<dynamic>>(
find.descendant(
of: tile,
matching: find.byWidgetPredicate(
(widget) => widget is DropdownButton,
),
),
);
expect(dropdown.value, setting.value);
expect(
dropdown.items!.where((item) => item.value == setting.value).length,
1,
);
}
expect(state.prefs.toJson(), original);
await tester.scrollUntilVisible(
find.text('Playback speed'),
-250,
scrollable: find.byType(Scrollable).first,
);
final speedTile = find.ancestor(
of: find.text('Playback speed'),
matching: find.byType(ListTile),
);
await tester.ensureVisible(speedTile);
await tester.tap(
find.descendant(
of: speedTile,
matching: find.byWidgetPredicate((widget) => widget is DropdownButton),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('1×').last);
await tester.pumpAndSettle();
expect(state.prefs.playbackSpeed, 1.0);
expect(state.prefs.defaultQuality, '2160p');
});
for (final locale in ['en', 'fa']) {
testWidgets(
'settings keep useful controls without endpoint or technical clutter in $locale',
(tester) async {
final api = SettingsLocalApi();
final state = AppState(api: api);
addTearDown(state.dispose);
await state.initialize(discover: false);
await state.updateSettings(state.prefs.copyWith(locale: locale));
await mount(tester, state, const SettingsPage());
final labels = <String>{};
final scrollable = find.byType(Scrollable).first;
for (var step = 0; step < 40; step++) {
labels.addAll(
tester.widgetList<Text>(find.byType(Text)).map((w) => w.data ?? ''),
);
expect(find.byKey(const Key('catalog-api-endpoint')), findsNothing);
final position = tester.state<ScrollableState>(scrollable).position;
if (position.pixels >= position.maxScrollExtent) break;
await tester.drag(scrollable, const Offset(0, -400));
await tester.pumpAndSettle();
}
final copy = labels.join('\n');
expect(copy, isNot(contains(ApiService.iranBaseUrl)));
expect(
copy,
isNot(
matches(
RegExp(
r'API|Iran Catalogue|فهرست ایران|Parallel download segments|بخش‌های همزمان دانلود|Player volume|بلندی صدای پخش',
),
),
),
);
for (final label
in locale == 'en'
? [
'Language',
'Theme',
'Resume from last position',
'Enable subtitles',
'Download on Wi-Fi only',
'Export local library',
'Import local library',
'Clear disposable cache',
]
: [
'زبان',
'پوسته',
'ادامه از آخرین موقعیت',
'نمایش زیرنویس',
'دانلود فقط با وای‌فای',
'خروجی کتابخانه محلی',
'ورود کتابخانه محلی',
'پاک کردن حافظه موقت',
]) {
expect(labels, contains(label));
}
for (final label
in locale == 'en'
? ['Export local library', 'Import local library']
: ['خروجی کتابخانه محلی', 'ورود کتابخانه محلی']) {
final tile = find.ancestor(
of: find.text(label),
matching: find.byType(ListTile),
);
expect(tester.widget<ListTile>(tile).onTap, isNotNull);
}
expect(api.discoveryChecks, 0);
expect(tester.takeException(), isNull);
},
);
}
testWidgets('More opens a factual privacy and local-data explanation', (
tester,
) async {
final state = AppState(api: SettingsLocalApi());
addTearDown(state.dispose);
await mount(tester, state, const MorePage());
await tester.scrollUntilVisible(
find.text('Privacy & your data'),
300,
scrollable: find.byType(Scrollable).first,
);
await tester.tap(find.text('Privacy & your data'));
await tester.pumpAndSettle();
expect(find.text('External services'), findsOneWidget);
final disclosures = <String>{};
for (final topic in [
'On this device',
'External services',
'Sharing & backups',
'Deleting local data',
'Unencrypted media',
]) {
await tester.scrollUntilVisible(
find.text(topic),
250,
scrollable: find.byType(Scrollable).first,
);
disclosures.addAll(
tester.widgetList<Text>(find.byType(Text)).map((w) => w.data ?? ''),
);
expect(find.text(topic), findsOneWidget);
}
final copy = disclosures.join('\n');
for (final disclosure in [
'No account needed',
'recent searches',
'IP address',
'access tokens',
'outside Reelbox’s control',
'completed downloads',
'unencrypted HTTP',
'observe or modify',
]) {
expect(copy, contains(disclosure));
}
expect(copy, isNot(contains('API')));
expect(tester.takeException(), isNull);
});
testWidgets('notifications explain new-task scope and Android requirements', (
tester,
) async {
final state = AppState(api: SettingsLocalApi());
addTearDown(state.dispose);
await mount(tester, state, const SettingsPage());
await tester.scrollUntilVisible(
find.text('Notifications for new downloads'),
400,
scrollable: find.byType(Scrollable).first,
);
expect(find.text('Notifications for new downloads'), findsOneWidget);
expect(
find.textContaining('Running downloads keep their notification settings'),
findsOneWidget,
);
expect(
find.textContaining(
'Android may still require a foreground notification',
),
findsOneWidget,
);
});
testWidgets(
'legacy channel history is pruned while same-ID movie remains removable',
(tester) async {
final state = AppState(api: SettingsLocalApi());
addTearDown(state.dispose);
final backup = jsonDecode(state.exportLibrary()) as Map<String, dynamic>;
backup['history'] = [
WatchEntry(title: channel, positionSeconds: 30).toJson(),
WatchEntry(title: movie, positionSeconds: 15).toJson(),
];
await state.importLibrary(jsonEncode(backup));
await mount(tester, state, const LibraryPage());
expect(find.text('Watchlist (0)'), findsOneWidget);
expect(find.text('Continue (1)'), findsOneWidget);
await tester.tap(find.text('Continue (1)'));
await tester.pumpAndSettle();
expect(find.text('Channel'), findsNothing);
expect(find.text('Movie'), findsOneWidget);
expect(state.history.single.title.toJson(), movie.toJson());
expect(state.resumeSeconds(42), 15);
expect(state.resumeSeconds(42, kind: 'channel'), 0);
await tester.tap(find.byTooltip('Remove history'));
await tester.pumpAndSettle();
expect(state.history, isEmpty);
expect(find.text('Continue (0)'), findsOneWidget);
expect(find.text('Pick up where you left off'), findsOneWidget);
},
);
testWidgets(
'removed channel card has no bookmark action and preserves the same-ID movie',
(tester) async {
final state = AppState(api: SettingsLocalApi());
addTearDown(state.dispose);
await state.toggleSaved(movie);
await mount(
tester,
state,
const Scaffold(
body: SizedBox(height: 244, child: PosterCard(title: channel)),
),
);
expect(find.text('Channel'), findsNothing);
expect(find.byTooltip('Watchlist'), findsNothing);
expect(find.byIcon(Icons.bookmark_border_rounded), findsNothing);
expect(find.byIcon(Icons.bookmark_rounded), findsNothing);
expect(state.saved.single.toJson(), movie.toJson());
expect(state.hasSaved(42, kind: 'channel'), isFalse);
// Allowed cards still expose the real bookmark action for this ID.
await mount(
tester,
state,
const Scaffold(
body: SizedBox(height: 244, child: PosterCard(title: movie)),
),
);
expect(find.text('Movie'), findsWidgets);
expect(find.byIcon(Icons.bookmark_rounded), findsOneWidget);
await tester.tap(find.byTooltip('Watchlist'));
await tester.pumpAndSettle();
expect(state.saved, isEmpty);
expect(find.byIcon(Icons.bookmark_border_rounded), findsOneWidget);
await tester.tap(find.byTooltip('Watchlist'));
await tester.pumpAndSettle();
expect(state.saved.single.toJson(), movie.toJson());
expect(find.byIcon(Icons.bookmark_rounded), findsOneWidget);
},
);
}