[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

+269
View File
@@ -0,0 +1,269 @@
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/catalog.dart';
import 'package:shared_preferences/shared_preferences.dart';
class CatalogApi extends ApiService {
int homeCalls = 0;
bool failHome = false;
@override
Future<Map<String, dynamic>> home() async {
homeCalls++;
if (failHome) throw Exception('Offline');
return {
'genres': [
{
'id': 2,
'title': 'From the server',
'posters': [const TitleRecord(id: 7, title: 'Real fixture').toJson()],
},
],
};
}
@override
Future<void> initialize({bool discover = true}) async {}
bool series = false;
@override
Future<TitleRecord> poster(int id) async => TitleRecord(
id: id,
title: 'Fixture detail',
type: series ? 'serie' : 'movie',
description: 'Actual fixture synopsis',
genres: [const Tag(id: 2, title: 'Action')],
sources: [
const MediaSource(
id: 1,
quality: '1080p',
type: 'mp4',
url: 'https://media.example/movie.mp4',
),
const MediaSource(
id: 2,
quality: 'Trailer',
type: 'mp4',
url: 'https://media.example/trailer.mp4',
),
],
);
@override
Future<List<SeasonRecord>> seasons(int id) async => [
const SeasonRecord(
id: 1,
title: 'Season 1',
episodes: [
EpisodeRecord(
id: 51,
title: 'Episode 1',
sources: [
MediaSource(
id: 1,
quality: '720p',
url: 'https://media.example/1.mp4',
),
],
),
EpisodeRecord(
id: 52,
title: 'Episode 2',
sources: [
MediaSource(
id: 2,
quality: '720p',
url: 'https://media.example/2.mp4',
),
],
),
],
),
];
@override
Future<List<Map<String, dynamic>>> roles(int posterId) async => [];
int commentCalls = 0;
@override
Future<List<Map<String, dynamic>>> comments(int posterId) async {
commentCalls++;
return [{'id': 1, 'user': 'Viewer', 'content': 'SGVsbG8='}];
}
@override
Future<List<Map<String, dynamic>>> subtitles(String kind, int id) async => [];
final catalogCalls = <Map<String, dynamic>>[];
final searchCalls = <Map<String, dynamic>>[];
@override
Future<List<Tag>> genres() async => [const Tag(id: 3, title: 'Animation')];
@override
Future<List<Tag>> countries() async => [const Tag(id: 1, title: 'Country')];
@override
Future<List<Tag>> categories() async => [];
@override
Future<List<TitleRecord>> catalog({
String kind = 'movie',
int genre = 0,
int country = 0,
String order = 'created',
int page = 0,
}) async {
catalogCalls.add({
'kind': kind,
'genre': genre,
'country': country,
'order': order,
'page': page,
});
return page < 2 ? [TitleRecord(id: page + 20, title: 'Page $page')] : [];
}
@override
Future<List<String>> trending() async => ['سرچ'];
@override
Future<List<TitleRecord>> search(
String query, {
int page = 1,
bool english = false,
}) async {
searchCalls.add({'q': query, 'page': page, 'english': english});
return CatalogPage(
page == 1 ? [TitleRecord(id: 41, title: 'Result $query')] : [],
nextPage: null,
);
}
@override
Future<List<TitleRecord>> recommended(int genre, {int page = 1}) async => [];
}
Future<AppState> mountCatalog(
WidgetTester tester,
CatalogApi api,
Widget page,
) async {
SharedPreferences.setMockInitialValues({});
final state = AppState(
api: api,
storage: await SharedPreferences.getInstance(),
);
await state.initialize(discover: false);
await tester.pumpWidget(
AppScope(
state: state,
child: MaterialApp(theme: ThemeData.dark(useMaterial3: true), home: page),
),
);
await tester.pumpAndSettle();
return state;
}
void main() {
testWidgets(
'movie detail exposes sources and local watchlist without public comments',
(tester) async {
final api = CatalogApi();
final state = await mountCatalog(
tester,
api,
const TitlePage(title: TitleRecord(id: 7, title: 'Fixture')),
);
expect(find.text('Fixture detail'), findsWidgets);
await tester.ensureVisible(find.text('Add to watchlist'));
await tester.tap(find.text('Add to watchlist'));
await tester.pumpAndSettle();
expect(state.hasSaved(7), isTrue);
await tester.ensureVisible(find.text('1080p'));
expect(find.text('1080p'), findsOneWidget);
await tester.drag(find.byType(Scrollable).first, const Offset(0, -1500));
await tester.pumpAndSettle();
expect(find.text('Comments'), findsNothing);
expect(find.text('نظرات'), findsNothing);
expect(find.text('Replies'), findsNothing);
expect(api.commentCalls, 0);
expect(find.textContaining('Sign in'), findsNothing);
expect(find.byType(TextField), findsNothing);
},
);
testWidgets('series detail uses nested real seasons and episode sources', (
tester,
) async {
final api = CatalogApi()..series = true;
await mountCatalog(
tester,
api,
const TitlePage(
title: TitleRecord(id: 7, title: 'Fixture', type: 'serie'),
),
);
await tester.ensureVisible(find.text('Season 1'));
await tester.tap(find.text('Season 1'));
await tester.pumpAndSettle();
expect(find.text('Episode 1'), findsOneWidget);
expect(find.text('Episode 2'), findsOneWidget);
await tester.ensureVisible(find.text('Episode 1'));
await tester.tap(find.text('Episode 1'));
await tester.pumpAndSettle();
expect(find.text('720p'), findsOneWidget);
expect(find.byTooltip('Play'), findsWidgets);
});
testWidgets(
'browse anime preserves mixed genre 3 across automatically loaded pages',
(tester) async {
final api = CatalogApi();
await mountCatalog(
tester,
api,
const BrowsePage(initialKind: 'mixed', initialGenre: 3),
);
// Small real-result fixture pages are prefetched until the viewport is
// filled or the upstream empty page is reached, without changing filters.
expect(api.catalogCalls.map((call) => call['page']), [0, 1, 2]);
for (final call in api.catalogCalls) {
expect(call['kind'], 'mixed');
expect(call['genre'], 3);
}
expect(find.text('Page 0'), findsOneWidget);
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('You’re all caught up'), findsOneWidget);
},
);
testWidgets(
'search submits Persian query and switches to English endpoint explicitly',
(tester) async {
final api = CatalogApi();
await mountCatalog(tester, api, const SearchPage());
await tester.scrollUntilVisible(
find.text('سرچ'),
200,
scrollable: find.byType(Scrollable).last,
);
expect(find.text('سرچ'), findsOneWidget);
await tester.enterText(find.byType(TextField).first, 'فیلم');
await tester.testTextInput.receiveAction(TextInputAction.search);
await tester.pumpAndSettle();
expect(api.searchCalls.single, {
'q': 'فیلم',
'page': 1,
'english': false,
});
await tester.tap(find.text('Advanced filters'));
await tester.pumpAndSettle();
await tester.ensureVisible(find.text('English search'));
await tester.tap(find.text('English search'));
await tester.pumpAndSettle();
expect(api.searchCalls.last['english'], true);
expect(find.text('Result فیلم'), findsOneWidget);
},
);
testWidgets('Discover displays server rail without refetching on rebuild', (
tester,
) async {
final api = CatalogApi();
final state = await mountCatalog(tester, api, const DiscoverPage());
expect(find.text('From the server'), findsOneWidget);
expect(find.text('Real fixture'), findsWidgets);
expect(api.homeCalls, 1);
await state.toggleSaved(const TitleRecord(id: 12, title: 'Saved'));
await tester.pumpAndSettle();
expect(api.homeCalls, 1);
});
}