topic_picture_bloc.dart 11.7 KB
import 'dart:async';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wow_english/common/request/dao/listen_dao.dart';
import 'package:wow_english/common/request/exception.dart';
import 'package:wow_english/models/course_process_entity.dart';
import 'package:wow_english/pages/section/subsection/base_section/bloc.dart';
import 'package:wow_english/pages/section/subsection/base_section/event.dart';
import 'package:wow_english/pages/section/subsection/base_section/state.dart';
import 'package:wow_english/utils/audio_player_util.dart';
import 'package:wow_english/utils/toast_util.dart';

import '../../../common/permission/permissionRequester.dart';
import '../../../common/utils/show_star_reward_dialog.dart';
import '../../../models/voice_result_type.dart';
import '../../../route/route.dart';
import '../../../utils/log_util.dart';

part 'topic_picture_event.dart';

part 'topic_picture_state.dart';

enum VoicePlayState {
  ///未知
  unKnow,

  ///播放中
  playing,

  ///播放完成
  completed,

  ///播放终止
  stop
}

class TopicPictureBloc
    extends BaseSectionBloc<TopicPictureEvent, TopicPictureState> with WidgetsBindingObserver {
  final PageController pageController;

  final String courseLessonId;

  int _currentPage = 0;

  int _optionSelectItem = -1;

  CourseProcessEntity? _entity;

  CourseProcessEntity? get entity => _entity;

  ///正在评测
  bool _isRecording = false;

  ///正在播放音频
  VoicePlayState _voicePlayState = VoicePlayState.unKnow;

  int get currentPage => _currentPage + 1;

  /// 选择题选中项
  int get optionSelectItem => _optionSelectItem;

  bool get isRecording => _isRecording;

  VoicePlayState get voicePlayState => _voicePlayState;

  late MethodChannel methodChannel;

  late AudioPlayer audioPlayer;

  final BuildContext context;

  final Color? moduleColor;

  TopicPictureBloc(
      this.context, this.pageController, this.courseLessonId, this.moduleColor)
      : super(TopicPictureInitial()) {
    on<CurrentPageIndexChangeEvent>(_pageControllerChange);
    on<VoicePlayStateChangeEvent>(_voicePlayStateChange);
    on<XSVoiceResultEvent>(_voiceXsResult);
    on<XSVoiceInitEvent>(_initVoiceSdk);
    on<SelectItemEvent>(_selectItemLoad);
    on<SelectItemResetEvent>(_selectItemReset);
    on<RequestDataEvent>(_requestData);
    on<XSVoiceStartEvent>(_voiceXsStart);
    on<XSVoiceStopEvent>(_voiceXsStop);
    on<VoicePlayEvent>(_questionVoicePlay);
    on<InitBlocEvent>((event, emit) {
      //音频播放器
      audioPlayer = AudioPlayer();
      audioPlayer.onPlayerStateChanged.listen((event) async {
        debugPrint('播放状态变化 _voicePlayState=$_voicePlayState event=$event');
        if (event == PlayerState.completed) {
          debugPrint('播放完成');
          _voicePlayState = VoicePlayState.completed;
        }
        if (event == PlayerState.stopped) {
          debugPrint('播放结束');
          _voicePlayState = VoicePlayState.stop;
        }

        if (event == PlayerState.playing) {
          debugPrint('正在播放中');
          _voicePlayState = VoicePlayState.playing;
        }
        if (isClosed) {
          return;
        }
        add(VoicePlayStateChangeEvent());
      });

      methodChannel =
          const MethodChannel('wow_english/sing_sound_method_channel');
      methodChannel.setMethodCallHandler((call) async {
        if (call.method == 'voiceResult') {
          //评测结果
          await audioPlayer.setAudioContext(AudioContext());
          await audioPlayer.setBalance(0.0);
          add(XSVoiceResultEvent(call.arguments));
          return;
        }

        if (call.method == 'voiceStart') {
          //评测开始
          if (kDebugMode) {
            print('评测开始');
          }
          return;
        }

        if (call.method == 'voiceEnd') {
          await audioPlayer.setAudioContext(AudioContext());
          await audioPlayer.setBalance(0.0);
          //评测结束
          if (kDebugMode) {
            print('评测结束');
          }
          return;
        }

        if (call.method == 'voiceFail') {
          //评测失败
          await audioPlayer.setAudioContext(AudioContext());
          await audioPlayer.setBalance(0.0);

          EasyLoading.showToast('评测失败');
          return;
        }
      });

      WidgetsBinding.instance.addObserver(this);
    });
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    super.didChangeAppLifecycleState(state);
    Log.d('TopicPictureBloc didChangeAppLifecycleState state=$state');
    if (state == AppLifecycleState.paused) {
      ///切到后台暂停音频播放、录音等
      if (audioPlayer.state == PlayerState.playing) {
        await audioPlayer.pause();
      }
      _voiceXsCancel();
    }
  }

  @override
  Future<void> close() {
    pageController.dispose();
    audioPlayer.release();
    audioPlayer.dispose();
    _voiceXsCancel();
    WidgetsBinding.instance.removeObserver(this);
    return super.close();
  }

  ///请求数据
  void _requestData(
      RequestDataEvent event, Emitter<TopicPictureState> emitter) async {
    try {
      _entity = await ListenDao.process(courseLessonId);
      emitter(RequestDataState());
    } catch (e) {
      if (e is ApiException) {
        showToast(e.message ?? '请求失败,请检查网络连接');
      }
    }
  }

  ///页面切换
  void _pageControllerChange(CurrentPageIndexChangeEvent event,
      Emitter<TopicPictureState> emitter) async {
    await pageResetIfNeed();
    debugPrint('翻页 $_currentPage->${event.pageIndex}');
    if (_currentPage == _entity?.topics?.length) {
      return;
    }
    _currentPage = event.pageIndex;
    final topics = _entity?.topics?[_currentPage];
    if (topics?.type != 3 && topics?.type != 4) {
      if (topics?.audioUrl != null) {
        final urlStr = topics?.audioUrl ?? '';
        if (urlStr.isNotEmpty) {
          debugPrint(urlStr);
          await audioPlayer.play(UrlSource(urlStr));
        }
      }
    }
    emitter(CurrentPageIndexState());
  }

  ///选择
  void _selectItemLoad(
      SelectItemEvent event, Emitter<TopicPictureState> emitter) async {
    _optionSelectItem = event.selectIndex;
    emitter(SelectItemChangeState());
    if (checkAnswerRight(_optionSelectItem) == true) {
      /// 如果选择题答(选)对后题目没播完,则暂停播放题目。答错的话继续播放体验也不错
      await closePlayerResource();

      /// right音频长度比动效短,所以等动效完了再翻页
      AudioPlayerUtil.getInstance().playAudio(AudioPlayerUtilType.right);
      await showStarRewardDialog(context, onDismiss: () {
        _resetSelectItem();
        autoFlipPage();
      });
    } else {
      await AudioPlayerUtil.getInstance().playAudio(AudioPlayerUtilType.wrong);
      _resetSelectItem();
    }
  }

  ///重置选择
  void _selectItemReset(
      SelectItemResetEvent event, Emitter<TopicPictureState> emitter) async {
    ///用于一次完整的选择后重置视图
    emitter(SelectItemResetState());
  }

  ///为空则数据异常,用于是否晃动时需要
  bool? checkAnswerRight(int selectIndex) {
    CourseProcessTopics? topics = _entity?.topics?[_currentPage];
    if (topics == null ||
        topics.topicAnswerList == null ||
        selectIndex < 0 ||
        selectIndex >= topics.topicAnswerList!.length) {
      return null;
    }
    CourseProcessTopicsTopicAnswerList? answerList =
        topics.topicAnswerList?[selectIndex];
    return answerList?.correct != 0;
  }

  ///初始化SDK
  _initVoiceSdk(
      XSVoiceInitEvent event, Emitter<TopicPictureState> emitter) async {
    methodChannel.invokeMethod('initVoiceSdk', event.data);
  }

  ///先声测试
  void _voiceXsStart(
      XSVoiceStartEvent event, Emitter<TopicPictureState> emitter) async {
    await audioPlayer.stop();
    // 调用封装好的权限检查和请求方法
    bool result = await requestPermission(
        context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈");
    if (result) {
      methodChannel.invokeMethod('startVoice', {
        'word': event.testWord,
        'type': event.type,
        'userId': event.userId.toString()
      });
      _isRecording = true;
      emitter(XSVoiceTestState());
    }
  }

  ///终止评测
  Future<void> _voiceXsStop(
      XSVoiceStopEvent event, Emitter<TopicPictureState> emitter) async {
    methodChannel.invokeMethod('stopVoice');
  }

  ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作)
  Future<void> _voiceXsCancel({bool force = false}) async {
    if (_isRecording || force) {
      methodChannel.invokeMethod('cancelVoice');
    }
  }

  ///先声评测结果
  void _voiceXsResult(
      XSVoiceResultEvent event, Emitter<TopicPictureState> emitter) async {
    _isRecording = false;
    emitter(XSVoiceTestState());
    final Map args = event.message as Map;
    final result = args['result'] as Map;
    final overall = result['overall'].toString();
    int score = int.parse(overall);
    final voiceResult = VoiceResultType.fromScore(score);
    if (voiceResult.lottieFilePath != null) {
      AudioPlayerUtil.getInstance().playAudio(voiceResult.audioType);
      await showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!, onDismiss: () {
        autoFlipPageByVoice(score);
      });
    } else {
      await AudioPlayerUtil.getInstance().playAudio(voiceResult.audioType);
      autoFlipPageByVoice(score);
    }
  }

  // 暂时没用上
  void _voicePlayStateChange(VoicePlayStateChangeEvent event,
      Emitter<TopicPictureState> emitter) async {
    emitter(VoicePlayStateChange());
  }

  // 题目音频播放
  void _questionVoicePlay(
      VoicePlayEvent event, Emitter<TopicPictureState> emitter) async {
    await pageResetIfNeed();
    final topics = _entity?.topics?[_currentPage];
    final urlStr = topics?.audioUrl ?? '';
    await audioPlayer.play(UrlSource(urlStr),
        balance: 0.0, ctx: AudioContext());
  }

  /// 重置状态,音频播放、录音以及一些变量等。用于翻页,打断等场景
  Future<void> pageResetIfNeed() async {
    _optionSelectItem = -1;
    _isRecording = false;
    _voicePlayState = VoicePlayState.stop;

    await closePlayerResource();
    await _voiceXsCancel();
  }

  void _resetSelectItem() {
    _optionSelectItem = -1;
    add(SelectItemResetEvent());
  }

  Future<void> closePlayerResource() async {
    if (voicePlayState == VoicePlayState.playing) {
      await audioPlayer.stop();
    }
  }

  ///是否是最后一页
  bool isLastPage() {
    return currentPage == _entity?.topics?.length;
  }

  ///自动翻页
  void autoFlipPage() {
    if (isLastPage()) {
      showStepPage();
    } else {
      // 答对后且播放完自动翻页
      pageController.nextPage(
        duration: const Duration(milliseconds: 250),
        curve: Curves.ease,
      );
    }
  }

  ///语音题自动翻页
  ///90分以上自动翻页
  void autoFlipPageByVoice(int score) {
    if (score >= 90) {
      autoFlipPage();
    } else {
      if (isLastPage()) {
        showStepPage();
      }
    }
  }

  ///展示过渡页
  void showStepPage() {
    ///如果最后一页是语音问答题,评测完后自动翻页
    sectionComplete(() {
      popPage(data: {
        'currentStep': currentPage,
        'courseLessonId': courseLessonId,
        'isCompleted': true,
        'nextSection': true
      });
    }, againSectionTap: () {
      pageController.jumpToPage(0);
    });
  }
}