[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

+315
View File
@@ -0,0 +1,315 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:reelbox/data/models.dart';
import 'package:reelbox/media/media_platform.dart';
import 'package:reelbox/media/player_page.dart';
import 'package:reelbox/state/app_state.dart';
import 'package:shared_preferences/shared_preferences.dart';
const title = TitleRecord(id: 78, title: 'Film');
const source = MediaSource(
url: 'https://media.test/film.mkv?token=unchanged',
type: 'mkv',
);
Map<String, dynamic> exitAt(
int position, {
bool opened = true,
bool completed = false,
}) => {
'positionMs': position,
'durationMs': 3600000,
'opened': opened,
'completed': completed,
};
Future<AppState> mount(
WidgetTester tester, {
EpisodeRecord? episode,
List<EpisodeRecord> playlist = const [],
List<MediaSource> alternatives = const [],
String? localFile,
}) async {
SharedPreferences.setMockInitialValues({});
final state = AppState(storage: await SharedPreferences.getInstance());
await state.initialize(discover: false);
await state.updateSettings(state.prefs.copyWith(autoplay: true));
await state.recordWatch(
title,
sourceUrl: source.url,
positionSeconds: 600,
durationSeconds: 3600,
episodeId: episode?.id,
episodeTitle: episode?.title ?? '',
);
await tester.pumpWidget(
AppScope(
state: state,
child: MaterialApp(
home: PlayerPage(
title: title,
source: source,
episode: episode,
playlist: playlist,
alternatives: alternatives,
localFile: localFile,
),
),
),
);
await tester.pump();
return state;
}
void main() {
testWidgets(
'early native EOF never advances or records a completed episode',
(tester) async {
const first = EpisodeRecord(
id: 101,
title: 'Episode 1',
sources: [source],
);
const second = EpisodeRecord(
id: 102,
title: 'Episode 2',
sources: [MediaSource(url: 'https://media.test/episode2.mkv')],
);
final calls = <MethodCall>[];
final firstExit = Completer<Map<String, dynamic>>();
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
(call) async {
if (call.method != 'openNativePlayer') return null;
calls.add(call);
return calls.length == 1
? firstExit.future
: exitAt(0, opened: false);
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
null,
),
);
final state = await mount(
tester,
episode: first,
playlist: [first, second],
);
addTearDown(state.dispose);
firstExit.complete(exitAt(180000, completed: true));
await tester.pumpAndSettle();
expect(
calls,
hasLength(1),
reason: 'Three minutes is not the end of this episode',
);
expect(state.history, hasLength(1));
expect(state.history.single.episodeId, first.id);
expect(state.history.single.positionSeconds, 180);
expect(state.history.single.durationSeconds, 3600);
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets(
'auto-next recovery keeps alternatives and history on the current episode',
(tester) async {
const oldAlternative = MediaSource(
url: 'https://media.test/episode1-720.mkv',
quality: '720p',
);
const first = EpisodeRecord(
id: 101,
title: 'Episode 1',
sources: [source, oldAlternative],
);
const secondSource = MediaSource(url: 'https://media.test/episode2.mkv');
const secondAlternative = MediaSource(
url: 'https://media.test/episode2-720.mkv?token=unchanged',
quality: '720p',
);
const trailer = MediaSource(
url: 'https://media.test/episode2-trailer.mp4',
quality: 'Trailer',
);
const denied = MediaSource(
url: 'https://t.me/episode2',
quality: 'Denied',
);
const second = EpisodeRecord(
id: 102,
title: 'Episode 2',
sources: [secondSource, secondAlternative, trailer, denied],
);
final calls = <MethodCall>[];
final firstExit = Completer<Map<String, dynamic>>();
final recoveryExit = Completer<Map<String, dynamic>>();
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
(call) async {
if (call.method != 'openNativePlayer') return null;
calls.add(call);
if (calls.length == 1) return firstExit.future;
if (calls.length == 2) return exitAt(0, opened: false);
return recoveryExit.future;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
null,
),
);
final state = await mount(
tester,
episode: first,
playlist: [first, second],
alternatives: first.sources,
);
addTearDown(state.dispose);
firstExit.complete(exitAt(3600000, completed: true));
await tester.pumpAndSettle();
expect(calls.length, 2);
expect(calls.last.arguments['url'], secondSource.url);
expect(find.text('Episode 2'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
await tester.tap(find.widgetWithText(ListTile, '720p'));
await tester.pump();
expect(calls.length, 3);
// Both episodes offer 720p: the row must dispatch episode 2, not 1.
final recoveryUrl = calls.last.arguments['url'];
recoveryExit.complete(exitAt(10000));
await tester.pumpAndSettle();
final recovered = state.history.singleWhere((e) => e.episodeId == 102);
expect(recovered.episodeTitle, second.title);
expect(recovered.positionSeconds, 10);
expect(recoveryUrl, secondAlternative.url);
expect(recovered.sourceUrl, secondAlternative.url);
expect(state.history.map((e) => e.episodeId).toSet(), {101, 102});
expect(find.text('Trailer'), findsNothing);
expect(find.text('Denied'), findsNothing);
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets(
'Android default player dispatches VLC with resume and preserves source identity',
(tester) async {
final calls = <MethodCall>[];
final result = Completer<Map<String, dynamic>>();
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
(call) async {
calls.add(call);
if (call.method == 'openNativePlayer') return result.future;
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
null,
),
);
final state = await mount(tester);
addTearDown(state.dispose);
await tester.pump(const Duration(milliseconds: 100));
final call = calls.singleWhere(
(call) => call.method == 'openNativePlayer',
);
expect(call.arguments['url'], source.url);
expect(call.arguments['positionMs'], 595000);
expect(call.arguments['autoplay'], true);
expect(state.history.single.positionSeconds, 600);
result.complete(exitAt(700000));
await tester.pumpAndSettle();
expect(state.history.single.positionSeconds, 700);
expect(state.history.single.sourceUrl, source.url);
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets('failed native open never overwrites a saved checkpoint', (
tester,
) async {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
(call) async {
if (call.method == 'openNativePlayer') return exitAt(0, opened: false);
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
null,
),
);
final state = await mount(tester);
addTearDown(state.dispose);
await tester.pumpAndSettle();
expect(state.history.single.positionSeconds, 600);
expect(find.text('External player'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
expect(tester.takeException(), isNull);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets(
'natural native episode completion opens next episode without losing history',
(tester) async {
const first = EpisodeRecord(
id: 101,
title: 'Episode 1',
sources: [source],
);
const second = EpisodeRecord(
id: 102,
title: 'Episode 2',
sources: [MediaSource(url: 'https://media.test/episode2.mkv')],
);
final calls = <MethodCall>[];
final firstExit = Completer<Map<String, dynamic>>();
final secondExit = Completer<Map<String, dynamic>>();
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
(call) async {
if (call.method != 'openNativePlayer') return null;
calls.add(call);
return calls.length == 1 ? firstExit.future : secondExit.future;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
MediaPlatform.channel,
null,
),
);
final state = await mount(
tester,
episode: first,
playlist: [first, second],
);
addTearDown(state.dispose);
await tester.pump(const Duration(milliseconds: 100));
expect(calls.length, 1);
firstExit.complete(exitAt(3600000, completed: true));
await tester.pump(const Duration(milliseconds: 200));
expect(calls.length, 2);
expect(calls.last.arguments['url'], second.sources.single.url);
expect(calls.last.arguments['positionMs'], 0);
expect(state.history.single.episodeId, 101);
secondExit.complete(exitAt(10000));
await tester.pumpAndSettle();
expect(state.history.map((e) => e.episodeId).toSet(), {101, 102});
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
}