fix(player): Change vimeo player with chewie for bette performance

This commit is contained in:
BisratHailu 2026-05-01 15:25:31 +03:00
parent 539d8bf6c2
commit beee3717ba
25 changed files with 220 additions and 2213 deletions

View File

@ -52,6 +52,7 @@ import 'package:yimaru_app/services/in_app_update_service.dart';
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart'; import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart'; import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart'; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -112,6 +113,7 @@ import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
LazySingleton(classType: AudioPlayerService), LazySingleton(classType: AudioPlayerService),
LazySingleton(classType: VoiceRecorderService), LazySingleton(classType: VoiceRecorderService),
LazySingleton(classType: InAppUpdateService), LazySingleton(classType: InAppUpdateService),
LazySingleton(classType: VimeoService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

@ -26,6 +26,7 @@ import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart'; import '../services/secure_storage_service.dart';
import '../services/smart_auth_service.dart'; import '../services/smart_auth_service.dart';
import '../services/status_checker_service.dart'; import '../services/status_checker_service.dart';
import '../services/vimeo_service.dart';
import '../services/voice_recorder_service.dart'; import '../services/voice_recorder_service.dart';
final locator = StackedLocator.instance; final locator = StackedLocator.instance;
@ -55,4 +56,5 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => AudioPlayerService()); locator.registerLazySingleton(() => AudioPlayerService());
locator.registerLazySingleton(() => VoiceRecorderService()); locator.registerLazySingleton(() => VoiceRecorderService());
locator.registerLazySingleton(() => InAppUpdateService()); locator.registerLazySingleton(() => InAppUpdateService());
locator.registerLazySingleton(() => VimeoService());
} }

View File

@ -0,0 +1,85 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class VimeoService {
Future<String?> getVideoUrl(String vimeoUrl) async {
final videoId = _extractVideoId(vimeoUrl);
if (videoId == null) {
throw Exception('Invalid Vimeo URL');
}
final html = await _fetchHtml(videoId);
final config = _extractConfig(html);
if (config == null) {
throw Exception('Failed to extract Vimeo config');
}
return _extractHlsUrl(config);
}
Future<String> _fetchHtml(String videoId) async {
final response = await http.get(
Uri.parse('https://player.vimeo.com/video/$videoId'),
headers: {
'Origin': 'https://vimeo.com',
'Referer': 'https://vimeo.com/',
'Accept': 'text/html,application/xhtml+xml',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
);
return response.body;
}
static Map<String, dynamic>? _extractConfig(String html) {
final regex = RegExp(
r'window\.playerConfig\s*=\s*({.*?})\s*</script>',
dotAll: true,
);
final match = regex.firstMatch(html);
if (match == null) return null;
final jsonString = match.group(1);
if (jsonString == null) return null;
return jsonDecode(jsonString);
}
String? _extractHlsUrl(Map<String, dynamic> config) {
try {
final files = config['request']?['files'];
final hls = files?['hls'];
if (hls == null) return null;
final cdns = hls['cdns'] as Map<String, dynamic>;
// Prefer fastly_skyfire or fallback to first CDN
final cdn = cdns['fastly_skyfire'] ?? cdns.values.first;
return cdn['url'];
} catch (_) {
return null;
}
}
String? _extractVideoId(String url) {
try {
final uri = Uri.parse(url);
if (uri.pathSegments.isNotEmpty) {
return uri.pathSegments.last;
}
return null;
} catch (_) {
return null;
}
}
}

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';

View File

@ -312,9 +312,8 @@ TextStyle style14MG400 = const TextStyle(
TextStyle style14DG500 = TextStyle style14DG500 =
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500); const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
TextStyle style18MG500 = TextStyle style18MG500 = const TextStyle(
const TextStyle(fontSize: 18,color: kcMediumGrey, fontWeight: FontWeight.w500); fontSize: 18, color: kcMediumGrey, fontWeight: FontWeight.w500);
TextStyle style14DG400 = const TextStyle( TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey, color: kcDarkGrey,

View File

@ -69,17 +69,17 @@ class AssessmentViewModel extends BaseViewModel {
Map<String, dynamic> get userData => _userData; Map<String, dynamic> get userData => _userData;
// Assessment // Assessment
Future<void> setFirstAssessment()async{ Future<void> setFirstAssessment() async {
_proficiencyLevel = null; _proficiencyLevel = null;
_selectedAnswers.clear(); _selectedAnswers.clear();
_currentQuestionIndex = 0; _currentQuestionIndex = 0;
_pageController.jumpToPage(_currentQuestionIndex); _pageController.jumpToPage(_currentQuestionIndex);
_currentAssessment = assessments[currentAssessmentIndex]; _currentAssessment = assessments[currentAssessmentIndex];
await getAssessmentQuestions( await getAssessmentQuestions(_currentAssessment?.id ?? 0);
_currentAssessment?.id ?? 0);
next(); next();
} }
Map<String, dynamic> evaluateAssessment() { Map<String, dynamic> evaluateAssessment() {
bool levelPassed = canPassLevel(); bool levelPassed = canPassLevel();
if (levelPassed) { if (levelPassed) {
@ -165,9 +165,8 @@ class AssessmentViewModel extends BaseViewModel {
_selectedAnswers.clear(); _selectedAnswers.clear();
_currentQuestionIndex = 0; _currentQuestionIndex = 0;
_proficiencyLevel = response['level']; _proficiencyLevel = response['level'];
_currentAssessment = assessments[currentAssessmentIndex]; _currentAssessment = assessments[currentAssessmentIndex];
await getAssessmentQuestions( await getAssessmentQuestions(_currentAssessment?.id ?? 0);
_currentAssessment?.id ?? 0);
_pageController.jumpToPage(_currentQuestionIndex); _pageController.jumpToPage(_currentQuestionIndex);
} else { } else {
_proficiencyLevel = response['level']; _proficiencyLevel = response['level'];
@ -274,9 +273,8 @@ class AssessmentViewModel extends BaseViewModel {
} }
// Assessments // Assessments
Future<void> getAssessments() async => Future<void> getAssessments() async => await runBusyFuture(_getAssessments(),
await runBusyFuture(_getAssessments(), busyObject: StateObjects.assessments);
busyObject: StateObjects.assessments);
Future<void> _getAssessments() async { Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {

View File

@ -139,7 +139,8 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
onTap: viewModel.selectedAnswers.containsKey(question.toString()) onTap: viewModel.selectedAnswers.containsKey(question.toString())
? () => viewModel.nextQuestion() ? () => viewModel.nextQuestion()
: null, : null,
text: viewModel.currentQuestionIndex == viewModel.assessmentQuestions.length - 1 text: viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length - 1
? 'Finish Level' ? 'Finish Level'
: 'Continue', : 'Continue',
); );

View File

@ -86,9 +86,7 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
); );
Widget _buildIconWrapper(AssessmentViewModel viewModel) => Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
viewModel.proficiencyLevel != null viewModel.proficiencyLevel != null ? _buildIcon(viewModel) : Container();
? _buildIcon(viewModel)
: Container();
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset( Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
'assets/icons/${viewModel.proficiencyLevel?.substring(0, 1).toLowerCase()}_${viewModel.proficiencyLevel?.substring(1).toLowerCase()}.svg'); 'assets/icons/${viewModel.proficiencyLevel?.substring(0, 1).toLowerCase()}_${viewModel.proficiencyLevel?.substring(1).toLowerCase()}.svg');

View File

@ -1,11 +1,14 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:vimeo_video_player/vimeo_video_player.dart'; import 'package:vimeo_video_player/vimeo_video_player.dart';
import 'package:yimaru_app/models/learn_lesson.dart'; import 'package:yimaru_app/models/learn_lesson.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart'; import '../../widgets/custom_elevated_button.dart';
import '../../widgets/empty_video_player.dart';
import '../../widgets/small_app_bar.dart'; import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart'; import 'learn_lesson_detail_viewmodel.dart';
@ -20,6 +23,12 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
await viewModel.navigateToLearnPractice(lesson.id ?? 0); await viewModel.navigateToLearnPractice(lesson.id ?? 0);
} }
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer(lesson.videoUrl ?? '');
super.onViewModelReady(viewModel);
}
@override @override
void onDispose(LearnLessonDetailViewModel viewModel) { void onDispose(LearnLessonDetailViewModel viewModel) {
viewModel.close(); viewModel.close();
@ -120,19 +129,23 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
height: 200, height: 200,
color: kcBlack, color: kcBlack,
width: double.maxFinite, width: double.maxFinite,
child: _buildVideoPlayer(viewModel), child: _buildVideoPlayerState(viewModel),
); );
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) => Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildVimeoPlayer(viewModel); _buildChewiePlayer(viewModel);
Widget _buildVimeoPlayer(LearnLessonDetailViewModel viewModel) => Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
VimeoVideoPlayer( Chewie(controller: viewModel.chewieController!);
isAutoPlay: true,
onInAppWebViewCreated: (controller) => Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
viewModel.initializePlayer(controller),
videoId: lesson.videoUrl?.split('/').last ?? '',
);
Widget _buildDescriptionWrapper() => Padding( Widget _buildDescriptionWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),

View File

@ -1,45 +1,67 @@
import 'package:chewie/chewie.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../app/app.router.dart'; import '../../../app/app.router.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../../services/vimeo_service.dart';
import '../../common/app_constants.dart';
import '../../common/helper_functions.dart';
import '../../common/ui_helpers.dart';
class LearnLessonDetailViewModel extends BaseViewModel { class LearnLessonDetailViewModel extends BaseViewModel {
// Dependency injection // Dependency injection
final _statusChecker = locator<StatusCheckerService>(); final _vimeoService = locator<VimeoService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Video player config // Video player config
InAppWebViewController? _webViewController; ChewieController? _chewieController;
InAppWebViewController? get webViewController => _webViewController; ChewieController? get chewieController => _chewieController;
VideoPlayerController? _videoPlayerController;
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player // Video player
void close() { void close() {
webViewController?.dispose(); _videoPlayerController?.dispose();
_chewieController?.dispose();
} }
Future<void> pause() async { Future<void> pause() async {
await webViewController?.pause(); await _chewieController?.pause();
} }
void initializePlayer(InAppWebViewController controller) { Future<void> initializePlayer(String url) async =>
_webViewController = controller; await runBusyFuture(_initializePlayer(url),
rebuildUi(); busyObject: StateObjects.loadLessonVideo);
}
void onLoadVideoStart() { Future<void> _initializePlayer(String url) async {
setBusyForObject(StateObjects.loadLessonVideo, true); final playableUrl = await _vimeoService.getVideoUrl(url);
rebuildUi();
}
void onLoadVideoComplete() { if (playableUrl == null) {
setBusyForObject(StateObjects.loadLessonVideo, false); throw Exception("Unable to load video");
rebuildUi(); }
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(playableUrl));
await _videoPlayerController?.initialize();
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController!,
autoPlay: true,
looping: true,
aspectRatio: 16 / 9,
);
notifyListeners();
} }
// Navigation // Navigation

View File

@ -61,10 +61,10 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
canPop: false, canPop: false,
onPopInvokedWithResult: (didPop, data) { onPopInvokedWithResult: (didPop, data) {
if (!didPop) { if (!didPop) {
Future.microtask(()async =>await _showSheet(context: context, viewModel: viewModel)); Future.microtask(() async =>
await _showSheet(context: context, viewModel: viewModel));
} }
}, },
child: _buildScaffoldWrapper(viewModel)); child: _buildScaffoldWrapper(viewModel));
Widget _buildSheet(LearnPracticeViewModel viewModel) => Widget _buildSheet(LearnPracticeViewModel viewModel) =>

View File

@ -64,7 +64,6 @@ class LearnPracticeViewModel extends ReactiveViewModel {
return _position.inMilliseconds / _duration.inMilliseconds; return _position.inMilliseconds / _duration.inMilliseconds;
} }
// Voice recorder // Voice recorder
WaveformRecorderController get _waveController => WaveformRecorderController get _waveController =>
_voiceRecorderService.waveController; _voiceRecorderService.waveController;
@ -129,12 +128,12 @@ class LearnPracticeViewModel extends ReactiveViewModel {
// Play practice audio // Play practice audio
void _listenToAudio() { void _listenToAudio() {
_audioPlayerService.durationStream.listen((dur) { _audioPlayerService.durationStream.listen((dur) {
if (dur.inMilliseconds > 0) { if (dur.inMilliseconds > 0) {
_duration = dur; _duration = dur;
rebuildUi(); rebuildUi();
} }
}); });
_audioPlayerService.positionStream.listen((pos) { _audioPlayerService.positionStream.listen((pos) {
_position = pos; _position = pos;

View File

@ -28,8 +28,8 @@ class InteractLearnPracticeScreen
await viewModel.stopRecording(); await viewModel.stopRecording();
viewModel.pop(); viewModel.pop();
viewModel.pop(); viewModel.pop();
} }
void _reply(LearnPracticeViewModel viewModel) => void _reply(LearnPracticeViewModel viewModel) =>
viewModel.replayVoicePrompt(question); viewModel.replayVoicePrompt(question);
@ -164,10 +164,10 @@ class InteractLearnPracticeScreen
? Container() ? Container()
: viewModel.player.state == PlayerState.playing : viewModel.player.state == PlayerState.playing
? _buildSpinner() ? _buildSpinner()
: VoiceRecordingState.recording == viewModel.recordingState && : VoiceRecordingState.recording == viewModel.recordingState &&
viewModel.waveController.isRecording viewModel.waveController.isRecording
? _buildSpeakingSpinnerColumn(viewModel) ? _buildSpeakingSpinnerColumn(viewModel)
: Container(); : Container();
Widget _buildSpeakingSpinnerColumn(LearnPracticeViewModel viewModel) => Widget _buildSpeakingSpinnerColumn(LearnPracticeViewModel viewModel) =>
Column( Column(
@ -359,8 +359,7 @@ class InteractLearnPracticeScreen
onClose: viewModel.pop, onClose: viewModel.pop,
onContinue: viewModel.pop, onContinue: viewModel.pop,
user: viewModel.user?.firstName ?? '', user: viewModel.user?.firstName ?? '',
onCancel: ()async => await _cancel(viewModel), onCancel: () async => await _cancel(viewModel),
); );
Widget _buildProgressIndicatorState(LearnPracticeViewModel viewModel) => Widget _buildProgressIndicatorState(LearnPracticeViewModel viewModel) =>

View File

@ -56,9 +56,11 @@ class LearnLoadingScreen extends StatelessWidget {
Widget _buildPageIndicator() => const PageLoadingIndicator(); Widget _buildPageIndicator() => const PageLoadingIndicator();
Widget _buildRefreshButtonWrapper() => Align( Widget _buildRefreshButtonWrapper() =>
alignment: Alignment.center, Align(alignment: Alignment.center, child: _buildRefreshButton());
child:_buildRefreshButton());
Widget _buildRefreshButton()=> NoDataIndicator(title:'No practice available!' ,onTap: onTap,); Widget _buildRefreshButton() => NoDataIndicator(
title: 'No practice available!',
onTap: onTap,
);
} }

View File

@ -16,7 +16,6 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
await viewModel.stopRecording(); await viewModel.stopRecording();
viewModel.pop(); viewModel.pop();
viewModel.pop(); viewModel.pop();
} }
Future<void> _showSheet( Future<void> _showSheet(
@ -82,8 +81,7 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
onClose: viewModel.pop, onClose: viewModel.pop,
onContinue: viewModel.pop, onContinue: viewModel.pop,
user: viewModel.user?.firstName ?? '', user: viewModel.user?.firstName ?? '',
onCancel: ()async => await _cancel(viewModel), onCancel: () async => await _cancel(viewModel),
); );
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded( Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(

View File

@ -40,7 +40,8 @@ class LearnPracticeQuestionsScreen
required LearnQuestion question, required LearnQuestion question,
}) => }) =>
[ [
if(index ==1) _buildStartLearnPracticeScreen(index: index, question: question), if (index == 1)
_buildStartLearnPracticeScreen(index: index, question: question),
_buildInteractLearnPracticeScreen(index: index, question: question) _buildInteractLearnPracticeScreen(index: index, question: question)
]; ];

View File

@ -23,7 +23,6 @@ class LearnPracticeResultScreen
await viewModel.stopRecording(); await viewModel.stopRecording();
viewModel.pop(); viewModel.pop();
viewModel.pop(); viewModel.pop();
} }
Future<void> _showSheet( Future<void> _showSheet(
@ -96,7 +95,7 @@ class LearnPracticeResultScreen
onClose: viewModel.pop, onClose: viewModel.pop,
onContinue: viewModel.pop, onContinue: viewModel.pop,
user: viewModel.user?.firstName ?? '', user: viewModel.user?.firstName ?? '',
onCancel: ()async => await _cancel(viewModel), onCancel: () async => await _cancel(viewModel),
); );
Widget _buildBodyWrapper(LearnPracticeViewModel viewMode) => Expanded( Widget _buildBodyWrapper(LearnPracticeViewModel viewMode) => Expanded(

View File

@ -20,7 +20,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
await viewModel.stopRecording(); await viewModel.stopRecording();
viewModel.pop(); viewModel.pop();
viewModel.pop(); viewModel.pop();
} }
void _start(LearnPracticeViewModel viewModel) { void _start(LearnPracticeViewModel viewModel) {
@ -107,7 +106,7 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
onClose: viewModel.pop, onClose: viewModel.pop,
onContinue: viewModel.pop, onContinue: viewModel.pop,
user: viewModel.user?.firstName ?? '', user: viewModel.user?.firstName ?? '',
onCancel: ()async => await _cancel(viewModel), onCancel: () async => await _cancel(viewModel),
); );
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded( Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded(

View File

@ -155,7 +155,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
children: _buildSettingsChildren(viewModel)); children: _buildSettingsChildren(viewModel));
List<Widget> _buildSettingsChildren(ProfileViewModel viewModel) => [ List<Widget> _buildSettingsChildren(ProfileViewModel viewModel) => [
// _buildDownloadsCard(viewModel), // _buildDownloadsCard(viewModel),
_buildProgressCard(viewModel), _buildProgressCard(viewModel),
_buildAccountCard(viewModel), _buildAccountCard(viewModel),
_buildSupportCard(viewModel) _buildSupportCard(viewModel)

View File

@ -47,7 +47,8 @@ class StartupViewModel extends ReactiveViewModel {
} }
// Navigation // Navigation
Future<void> replaceWithFailure() async => await _navigationService.replaceWithFailureView( Future<void> replaceWithFailure() async =>
await _navigationService.replaceWithFailureView(
label: 'Check you internet connection', label: 'Check you internet connection',
onTap: () async => await _getProfileStatus()); onTap: () async => await _getProfileStatus());

View File

@ -46,5 +46,6 @@ class LearnPracticeResultsWrapper
itemBuilder: (context, index) => _buildResult(viewModel.answers[index]), itemBuilder: (context, index) => _buildResult(viewModel.answers[index]),
); );
Widget _buildResult(Map<String, dynamic> answer) => LearnPracticeResultCard(answer: answer); Widget _buildResult(Map<String, dynamic> answer) =>
LearnPracticeResultCard(answer: answer);
} }

View File

@ -19,10 +19,9 @@ class NoDataIndicator extends StatelessWidget {
); );
List<Widget> _buildColumnChildren() => [ List<Widget> _buildColumnChildren() => [
_buildIconWrapper(), _buildIconWrapper(),
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
]; ];
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
@ -31,15 +30,13 @@ class NoDataIndicator extends StatelessWidget {
); );
Widget _buildIconWrapper() => GestureDetector( Widget _buildIconWrapper() => GestureDetector(
onTap: onTap, onTap: onTap,
child: _buildIcon(), child: _buildIcon(),
); );
Widget _buildIcon() => const Icon( Widget _buildIcon() => const Icon(
Icons.replay, Icons.replay,
size: 75, size: 75,
color: kcPrimaryColor, color: kcPrimaryColor,
); );
} }

View File

@ -17,6 +17,7 @@ import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/services/audio_player_service.dart'; import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart'; import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/services/in_app_update_service.dart'; import 'package:yimaru_app/services/in_app_update_service.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
// @stacked-import // @stacked-import
import 'test_helpers.mocks.dart'; import 'test_helpers.mocks.dart';
@ -44,6 +45,8 @@ import 'test_helpers.mocks.dart';
MockSpec<AudioPlayerService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<AudioPlayerService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VoiceRecorderService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<VoiceRecorderService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec // @stacked-mock-spec
], ],
) )
@ -66,6 +69,8 @@ void registerServices() {
getAndRegisterAudioPlayerService(); getAndRegisterAudioPlayerService();
getAndRegisterVoiceRecorderService(); getAndRegisterVoiceRecorderService();
getAndRegisterInAppUpdateService(); getAndRegisterInAppUpdateService();
getAndRegisterVimeoService();
getAndRegisterVimeoService();
// @stacked-mock-register // @stacked-mock-register
} }
@ -227,6 +232,13 @@ MockInAppUpdateService getAndRegisterInAppUpdateService() {
locator.registerSingleton<InAppUpdateService>(service); locator.registerSingleton<InAppUpdateService>(service);
return service; return service;
} }
MockVimeoService getAndRegisterVimeoService() {
_removeRegistrationIfExists<VimeoService>();
final service = MockVimeoService();
locator.registerSingleton<VimeoService>(service);
return service;
}
// @stacked-mock-create // @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() { void _removeRegistrationIfExists<T extends Object>() {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('VimeoServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}