content: import 'dart:async';
  import 'dart:convert';
  import 'dart:ffi';
  import 'dart:io';
  import 'dart:math';
  import 'package:bot_toast/bot_toast.dart';
  import 'package:ffi/ffi.dart';
  import 'package:file_picker/file_picker.dart';
  import 'package:flutter/foundation.dart';
  import 'package:flutter/material.dart';
  import 'package:flutter/services.dart';
  import 'package:flutter_qjs/quickjs/ffi.dart';
  import 'package:flutter_riverpod/flutter_riverpod.dart';
  import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
  import 'package:mangayomi/eval/model/m_bridge.dart';
  import 'package:mangayomi/main.dart';
  import 'package:mangayomi/models/chapter.dart';
  import 'package:mangayomi/models/custom_button.dart';
  import 'package:mangayomi/models/manga.dart';
  import 'package:mangayomi/models/settings.dart';
  import 'package:mangayomi/models/video.dart' as vid;
  import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
  import 'package:mangayomi/modules/anime/widgets/aniskip_countdown_btn.dart';
  import 'package:mangayomi/modules/anime/widgets/desktop.dart';
  import 'package:mangayomi/modules/anime/widgets/play_or_pause_button.dart';
  import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
  import 'package:mangayomi/modules/anime/widgets/mobile.dart';
  import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart';
  import 'package:mangayomi/modules/anime/widgets/subtitle_setting_widget.dart';
  import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
  import 'package:mangayomi/modules/more/settings/player/providers/custom_buttons_provider.dart';
  import 'package:mangayomi/modules/more/settings/player/providers/player_audio_state_provider.dart';
  import 'package:mangayomi/modules/more/settings/player/providers/player_decoder_state_provider.dart';
  import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
  import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
  import 'package:mangayomi/modules/widgets/progress_center.dart';
  import 'package:mangayomi/providers/l10n_providers.dart';
  import 'package:mangayomi/providers/storage_provider.dart';
  import 'package:mangayomi/services/aniskip.dart';
  import 'package:mangayomi/services/fetch_subtitles.dart';
  import 'package:mangayomi/services/get_video_list.dart';
  import 'package:mangayomi/services/torrent_server.dart';
  import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
  import 'package:mangayomi/utils/language.dart';
  import 'package:media_kit/media_kit.dart';
  import 'package:media_kit/generated/libmpv/bindings.dart' as generated;
  import 'package:media_kit_video/media_kit_video.dart';
  import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
  import 'package:numberpicker/numberpicker.dart';
  import 'package:path/path.dart' as p;
  import 'package:path/path.dart' as path;
  import 'package:path_provider/path_provider.dart';
  import 'package:share_plus/share_plus.dart';
  import 'package:super_sliver_list/super_sliver_list.dart';
  
  import 'widgets/search_subtitles.dart';
  
  bool _isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
  
  class AnimePlayerView extends riv.ConsumerStatefulWidget {
    final int episodeId;
    const AnimePlayerView({super.key, required this.episodeId});
  
    @override
    riv.ConsumerState<AnimePlayerView> createState() => _AnimePlayerViewState();
  }
  
  class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
    late final Chapter episode = isar.chapters.getSync(widget.episodeId)!;
    List<String> _infoHashList = [];
    bool desktopFullScreenPlayer = false;
    @override
    void dispose() {
      if (_isDesktop) {
        setFullScreen(value: desktopFullScreenPlayer);
      }
      for (var infoHash in _infoHashList) {
        MTorrentServer().removeTorrent(infoHash);
      }
      SystemChrome.setEnabledSystemUIMode(
        SystemUiMode.manual,
        overlays: SystemUiOverlay.values,
      );
      super.dispose();
    }
  
    @override
    Widget build(BuildContext context) {
      final defaultSubtitleLang = ref.watch(defaultSubtitleLangStateProvider);
      final serversData = ref.watch(getVideoListProvider(episode: episode));
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
      return serversData.when(
        data: (data) {
          final (videos, isLocal, infoHashList, mpvDirectory) = data;
          _infoHashList = infoHashList;
          if (videos.isEmpty && !(episode.manga.value!.isLocalArchive ?? false)) {
            return Scaffold(
              backgroundColor: Theme.of(context).scaffoldBackgroundColor,
              appBar: AppBar(
                title: const Text(''),
                leading: BackButton(
                  onPressed: () {
                    Navigator.pop(context);
                  },
                ),
              ),
              body: const Center(child: Text("Video list is empty")),
            );
          }
  
          return AnimeStreamPage(
            defaultSubtitle: completeLanguageNameEnglish(
              defaultSubtitleLang.toLanguageTag(),
            ),
            episode: episode,
            videos: videos,
            isLocal: isLocal,
            isTorrent: infoHashList.isNotEmpty,
            desktopFullScreenPlayer: (value) {
              desktopFullScreenPlayer = value;
            },
            mpvDirectory: mpvDirectory,
          );
        },
        error: (error, stackTrace) => Scaffold(
          backgroundColor: Theme.of(context).scaffoldBackgroundColor,
          appBar: AppBar(
            title: const Text(''),
            leading: BackButton(
              onPressed: () {
                SystemChrome.setEnabledSystemUIMode(
                  SystemUiMode.manual,
                  overlays: SystemUiOverlay.values,
                );
                Navigator.pop(context);
              },
            ),
          ),
          body: Center(child: Text(error.toString())),
        ),
        loading: () {
          return Scaffold(
            backgroundColor: Colors.black,
            extendBodyBehindAppBar: true,
            appBar: AppBar(
              title: const Text(''),
              leading: BackButton(
                color: Colors.white,
                onPressed: () {
                  SystemChrome.setEnabledSystemUIMode(
                    SystemUiMode.manual,
                    overlays: SystemUiOverlay.values,
                  );
                  Navigator.pop(context);
                },
              ),
            ),
            body: const ProgressCenter(),
          );
        },
      );
    }
  }
  
  class AnimeStreamPage extends riv.ConsumerStatefulWidget {
    final List<vid.Video> videos;
    final Chapter episode;
    final String defaultSubtitle;
    final bool isLocal;
    final bool isTorrent;
    final Directory? mpvDirectory;
    final void Function(bool) desktopFullScreenPlayer;
    const AnimeStreamPage({
      super.key,
      required this.defaultSubtitle,
      required this.isLocal,
      required this.videos,
      required this.episode,
      required this.isTorrent,
      required this.desktopFullScreenPlayer,
      required this.mpvDirectory,
    });
  
    @override
    riv.ConsumerState<AnimeStreamPage> createState() => _AnimeStreamPageState();
  }
  
  enum _AniSkipPhase { none, opening, ending }
  
  /// When the user first opens a video (on Desktop).
  /// Only used for fullscreen/windowed behavior.
  bool _firstTime = true;
  
  class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
      with TickerProviderStateMixin, WidgetsBindingObserver {
    late final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
    late final useLibass = ref.read(useLibassStateProvider);
    late final useMpvConfig = ref.read(useMpvConfigStateProvider);
    late final useGpuNext = ref.read(useGpuNextStateProvider);
    late final debandingType = ref.read(debandingStateProvider);
    late final useYUV420P = ref.read(useYUV420PStateProvider);
    late final audioPreferredLang = ref.read(audioPreferredLangStateProvider);
    late final enableAudioPitchCorrection = ref.read(
      enableAudioPitchCorrectionStateProvider,
    );
    late final audioChannel = ref.read(audioChannelStateProvider);
    late final volumeBoostCap = ref.read(volumeBoostCapStateProvider);
    late final Player _player = Player(
      configuration: PlayerConfiguration(
        libass: useLibass,
        config: true,
        configDir: useMpvConfig ? widget.mpvDirectory?.path ?? "" : "",
        options: {
          if (debandingType == DebandingType.cpu) "vf": "gradfun=radius=12",
          if (debandingType == DebandingType.gpu) "deband": "yes",
          if (useYUV420P) "vf": "format=yuv420p",
          if (audioPreferredLang.isNotEmpty) "alang": audioPreferredLang,
          if (enableAudioPitchCorrection) "audio-pitch-correction": "yes",
          "volume-max": "${volumeBoostCap + 100}",
          if (audioChannel != AudioChannel.reverseStereo)
            "audio-channels": audioChannel.mpvName,
          if (audioChannel == AudioChannel.reverseStereo)
            "af": audioChannel.mpvName,
        },
        observeProperties: {
          "user-data/aniyomi/show_text": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/toggle_ui": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/show_panel": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/software_keyboard":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/set_button_title":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/reset_button_title":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/toggle_button": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/switch_episode":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/pause": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/seek_by": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/seek_to": generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/seek_by_with_text":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/seek_to_with_text":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/aniyomi/launch_int_picker":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/mangayomi/chapter_titles":
              generated.mpv_format.MPV_FORMAT_NODE,
          "user-data/mangayomi/current_chapter":
              generated.mpv_format.MPV_FORMAT_INT64,
          "user-data/mangayomi/selected_shader":
              generated.mpv_format.MPV_FORMAT_NODE,
        },
        eventHandler: _handleMpvEvents,
      ),
    );
    late final hwdecMode = ref.read(hwdecModeStateProvider());
    late final enableHardwareAccel = ref.read(enableHardwareAccelStateProvider);
    late final VideoController _controller;
    late final _streamController = ref.read(
      animeStreamControllerProvider(episode: widget.episode).notifier,
    );
    late final _firstVid = widget.videos.first;
    late final ValueNotifier<VideoPrefs?> _video = ValueNotifier(
      VideoPrefs(
        videoTrack: VideoTrack(
          _firstVid.originalUrl,
          _firstVid.quality,
          _firstVid.quality,
        ),
        headers: _firstVid.headers,
      ),
    );
    final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
    final ValueNotifier<bool> _isDoubleSpeed = ValueNotifier(false);
    late final ValueNotifier<Duration> _currentPosition = ValueNotifier(
      _streamController.geTCurrentPosition(),
    );
    final ValueNotifier<Duration?> _currentTotalDuration = ValueNotifier(null);
    final ValueNotifier<bool> _showFitLabel = ValueNotifier(false);
    final ValueNotifier<bool> _isCompleted = ValueNotifier(false);
    final ValueNotifier<Duration?> _tempPosition = ValueNotifier(null);
    final ValueNotifier<BoxFit> _fit = ValueNotifier(BoxFit.contain);
    final ValueNotifier<List<(String, int)>> _chapterMarks = ValueNotifier([]);
    final ValueNotifier<int?> _currentChapterMark = ValueNotifier(null);
    final ValueNotifier<String> _selectedShader = ValueNotifier("");
    final ValueNotifier<ActiveCustomButton?> _customButton = ValueNotifier(null);
    final ValueNotifier<List<CustomButton>?> _customButtons = ValueNotifier(null);
    late final ValueNotifier<_AniSkipPhase> _skipPhase = ValueNotifier(
      _AniSkipPhase.none,
    );
    Results? _openingResult;
    Results? _endingResult;
    bool _hasOpeningSkip = false;
    bool _hasEndingSkip = false;
    bool _initSubtitleAndAudio = true;
    bool _includeSubtitles = false;
    int _subDelay = 0;
    final _subDelayController = TextEditingController(text: "0");
    double _subSpeed = 1;
    final _subSpeedController = TextEditingController(text: "1");
    int lastRpcTimestampUpdate = DateTime.now().millisecondsSinceEpoch;
  
    late final StreamSubscription<Duration> _currentPositionSub;
  
    late final StreamSubscription<Duration> _currentTotalDurationSub = _player
        .stream
        .duration
        .listen((duration) {
          _currentTotalDuration.value = duration;
          discordRpc?.updateChapterTimestamp(_currentPosition.value, duration);
        });
  
    bool get hasNextEpisode => _streamController.getEpisodeIndex().$1 != 0;
  
    late final StreamSubscription<bool> _completed = _player.stream.completed
        .listen((val) {
          if (hasNextEpisode && val) {
            if (mounted) {
              pushToNewEpisode(context, _streamController.getNextEpisode());
            }
          }
          // If the last episode of an Anime has ended, exit fullscreen mode
          final isFullScreen = ref.read(fullscreenProvider);
          if (!hasNextEpisode && val && _isDesktop && isFullScreen) {
            setFullScreen(value: false);
            ref.read(fullscreenProvider.notifier).state = false;
            widget.desktopFullScreenPlayer.call(false);
          }
        });
  
    Future<void> _handleMpvEvents(Pointer<generated.mpv_event> event) async {
      try {
        if (event.ref.event_id ==
            generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) {
          final prop = event.ref.data.cast<generated.mpv_event_property>();
          final propName = prop.ref.name.cast<Utf8>().toDartString();
          if (kDebugMode) {
            if (propName.startsWith("user-data/")) {
              print("DEBUG 00: $propName - ${prop.ref.format}");
            }
          }
          if (propName.startsWith("user-data/") &&
              prop.ref.format == generated.mpv_format.MPV_FORMAT_NODE) {
            final value = prop.ref.data.cast<generated.mpv_node>();
            _handleMpvNodeEvents(propName, value);
          } else if (propName.startsWith("user-data/") &&
              prop.ref.format == generated.mpv_format.MPV_FORMAT_INT64) {
            final value = prop.ref.data.cast<Int64>().value;
            _handleMpvNumberEvents(propName, value);
          }
        }
      } catch (e) {
        if (kDebugMode) {
          debugPrint(e.toString());
        }
      }
    }
  
    Future<void> _handleMpvNodeEvents(
      String propName,
      Pointer<generated.mpv_node> value,
    ) async {
      final nativePlayer = _player.platform as NativePlayer;
      switch (propName.substring(10)) {
        case "aniyomi/show_text":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            botToast(
              text,
              alignY: -0.99,
              second: 2,
              dismissDirections: const [
                DismissDirection.vertical,
                DismissDirection.horizontal,
              ],
              showIcon: false,
            );
            nativePlayer.setProperty("user-data/aniyomi/show_text", "");
          }
          break;
        case "aniyomi/toggle_ui":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            switch (text) {
              // WIP
              case "show":
                break;
              case "hide":
                break;
              case "toggle":
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/toggle_ui", "");
          }
          break;
        case "aniyomi/show_panel":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            switch (text) {
              // WIP
              case "subtitle_settings":
                break;
              case "subtitle_delay":
                break;
              case "audio_delay":
                break;
              case "video_filters":
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/show_panel", "");
          }
          break;
        case "aniyomi/software_keyboard":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            switch (text) {
              // WIP
              case "show":
                break;
              case "hide":
                break;
              case "toggle":
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/software_keyboard", "");
          }
          break;
        case "aniyomi/set_button_title":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final temp = _customButton.value;
            if (temp == null) break;
            _customButton.value = temp..currentTitle = text;
            nativePlayer.setProperty("user-data/aniyomi/set_button_title", "");
          }
          break;
        case "aniyomi/reset_button_title":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final temp = _customButton.value;
            if (temp == null) break;
            _customButton.value = temp..currentTitle = temp.button.title ?? "";
            nativePlayer.setProperty("user-data/aniyomi/reset_button_title", "");
          }
          break;
        case "aniyomi/toggle_button":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final temp = _customButton.value;
            if (temp == null) break;
            switch (text) {
              case "show":
                _customButton.value = temp..visible = true;
                break;
              case "hide":
                _customButton.value = temp..visible = false;
                break;
              case "toggle":
                _customButton.value = temp..visible = !temp.visible;
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/toggle_button", "");
          }
          break;
        case "aniyomi/switch_episode":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            switch (text) {
              case "n":
                pushToNewEpisode(context, _streamController.getNextEpisode());
                break;
              case "p":
                pushToNewEpisode(context, _streamController.getPrevEpisode());
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/switch_episode", "");
          }
          break;
        case "aniyomi/pause":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            switch (text) {
              case "pause":
                await _player.pause();
                break;
              case "unpause":
                await _player.play();
                break;
              case "pauseunpause":
                await _player.playOrPause();
                break;
            }
            nativePlayer.setProperty("user-data/aniyomi/pause", "");
          }
          break;
        case "aniyomi/seek_by":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final data = int.parse(text.replaceAll("\"", ""));
            final pos = _currentPosition.value.inSeconds + data;
            _tempPosition.value = Duration(seconds: pos);
            await _player.seek(Duration(seconds: pos));
            _tempPosition.value = null;
            nativePlayer.setProperty("user-data/aniyomi/seek_by", "");
          }
          break;
        case "aniyomi/seek_to":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final data = int.parse(text.replaceAll("\"", ""));
            _tempPosition.value = Duration(seconds: data);
            await _player.seek(Duration(seconds: data));
            _tempPosition.value = null;
            nativePlayer.setProperty("user-data/aniyomi/seek_to", "");
          }
          break;
        case "aniyomi/seek_by_with_text":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final data = text.split("|");
            final pos =
                _currentPosition.value.inSeconds +
                int.parse(data[0].replaceAll("\"", ""));
            _tempPosition.value = Duration(seconds: pos);
            await _player.seek(Duration(seconds: pos));
            _tempPosition.value = null;
            (_player.platform as NativePlayer).command(["show-text", data[1]]);
            nativePlayer.setProperty("user-data/aniyomi/seek_by_with_text", "");
          }
          break;
        case "aniyomi/seek_to_with_text":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final data = text.split("|");
            final pos = int.parse(data[0].replaceAll("\"", ""));
            _tempPosition.value = Duration(seconds: pos);
            await _player.seek(Duration(seconds: pos));
            _tempPosition.value = null;
            (_player.platform as NativePlayer).command(["show-text", data[1]]);
            nativePlayer.setProperty("user-data/aniyomi/seek_to_with_text", "");
          }
          break;
        case "aniyomi/launch_int_picker":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            if (text.isEmpty) break;
            final data = text.split("|");
            final start = int.parse(data[2]);
            final stop = int.parse(data[3]);
            final step = int.parse(data[4]);
            int currentValue = start;
            await showDialog(
              context: context,
              builder: (context) {
                return AlertDialog(
                  title: Text(data[0]),
                  content: StatefulBuilder(
                    builder: (context, setState) => SizedBox(
                      height: 200,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          NumberPicker(
                            value: currentValue,
                            minValue: start,
                            maxValue: stop,
                            step: step,
                            haptics: true,
                            textMapper: (numberText) =>
                                data[1].replaceAll("%d", numberText),
                            onChanged: (value) =>
                                setState(() => currentValue = value),
                          ),
                        ],
                      ),
                    ),
                  ),
                  actions: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [
                        TextButton(
                          onPressed: () async {
                            Navigator.pop(context);
                          },
                          child: Text(
                            context.l10n.cancel,
                            style: TextStyle(color: context.primaryColor),
                          ),
                        ),
                        TextButton(
                          onPressed: () async {
                            final namePtr = data[5].toNativeUtf8();
                            final valuePtr = calloc<Int64>(1)
                              ..value = currentValue;
                            nativePlayer.mpv.mpv_set_property(
                              nativePlayer.ctx,
                              namePtr.cast(),
                              generated.mpv_format.MPV_FORMAT_INT64,
                              valuePtr.cast(),
                            );
                            malloc.free(namePtr);
                            malloc.free(valuePtr);
                            Navigator.pop(context);
                          },
                          child: Text(
                            context.l10n.ok,
                            style: TextStyle(color: context.primaryColor),
                          ),
                        ),
                      ],
                    ),
                  ],
                );
              },
            );
            nativePlayer.setProperty("user-data/aniyomi/launch_int_picker", "");
          }
          break;
        case "mangayomi/chapter_titles":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            final data = jsonDecode(text) as List<dynamic>;
            _chapterMarks.value = data
                .map(
                  (e) => (
                    e["title"] as String,
                    e["timestamp"] is double
                        ? (e["timestamp"] as double).toInt() * 1000
                        : (e["timestamp"] as int) * 1000,
                  ),
                )
                .toList();
          }
          break;
        case "mangayomi/selected_shader":
          if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
            final text = value.ref.u.string.cast<Utf8>().toDartString();
            _selectedShader.value = text;
          }
          break;
      }
    }
  
    Future<void> _handleMpvNumberEvents(String propName, int value) async {
      switch (propName.substring(10)) {
        case "mangayomi/current_chapter":
          _currentChapterMark.value = max(value, 0);
          break;
      }
    }
  
    Future<void> _initCustomButton() async {
      if (!useMpvConfig) return;
      final customButtons = await ref.read(getCustomButtonsStreamProvider.future);
      if (customButtons.isEmpty) return;
      final primaryButton =
          customButtons.firstWhereOrNull((e) => e.isFavourite ?? false) ??
          customButtons.first;
      final provider = StorageProvider();
      if (!(await provider.requestPermission())) {
        return;
      }
      final dir = await provider.getMpvDirectory();
      String scriptsDir = path.join(dir!.path, 'scripts');
      final mpvFile = File('$scriptsDir/init_custom_buttons.lua');
      final content = StringBuffer();
      content.writeln("""local lua_modules = mp.find_config_file('scripts')
  if lua_modules then
    package.path = package.path .. ';' .. lua_modules .. '/?.lua;' .. lua_modules .. '/?/init.lua;' .. '\${scriptsDir()!!.filePath}' .. '/?.lua'
  end
  local aniyomi = require 'init_aniyomi_functions'""");
      for (final button in customButtons) {
        content.writeln(
          """
  ${button.getButtonStartup(primaryButton.id!).trim()}
  function button${button.id}()
    ${button.getButtonPress(primaryButton.id!).trim()}
  end
  mp.register_script_message('call_button_${button.id}', button${button.id})
  function button${button.id}long()
    ${button.getButtonLongPress(primaryButton.id!).trim()}
  end
  mp.register_script_message('call_button_${button.id}_long', button${button.id}long)""",
        );
      }
      await mpvFile.writeAsString(content.toString());
      await (_player.platform as NativePlayer).command([
        "load-script",
        mpvFile.path,
      ]);
      _customButton.value = ActiveCustomButton(
        currentTitle: primaryButton.title!,
        visible: true,
        button: primaryButton,
        onPress: () => (_player.platform as NativePlayer).command([
          "script-message",
          "call_button_${primaryButton.id}",
        ]),
        onLongPress: () => (_player.platform as NativePlayer).command([
          "script-message",
          "call_button_${primaryButton.id}_long",
        ]),
      );
      _customButtons.value = customButtons;
    }
  
    void pushToNewEpisode(BuildContext context, Chapter episode) {
      widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
      if (context.mounted) {
        pushReplacementMangaReaderView(context: context, chapter: episode);
      }
    }
  
    void _unifiedPositionHandler(Duration position) {
      final currentSecs = position.inSeconds;
      _setCurrentAudSub(position, currentSecs);
      _setSkipPhase(currentSecs);
    }
  
    void _setCurrentAudSub(Duration position, int secs) {
      final totalSecs = _player.state.duration.inSeconds;
      _isCompleted.value = (totalSecs - secs) <= 10;
      _currentPosition.value = position;
      if (_initSubtitleAndAudio) {
        _initSubtitleAndAudio = false;
        if (_firstVid.subtitles?.isNotEmpty ?? false) {
          try {
            final defaultTrack = _firstVid.subtitles!.firstWhere(
              (sub) => sub.label == widget.defaultSubtitle,
              orElse: () => _firstVid.subtitles!.first,
            );
            final file = defaultTrack.file ?? "";
            final label = defaultTrack.label;
            final track = (file.startsWith("http") || file.startsWith("file"))
                ? SubtitleTrack.uri(file, title: label, language: label)
                : SubtitleTrack.data(file, title: label, language: label);
            _player.setSubtitleTrack(track);
          } catch (_) {}
          if (_firstVid.audios?.isNotEmpty ?? false) {
            try {
              final at = _firstVid.audios!.first;
              _player.setAudioTrack(
                AudioTrack.uri(
                  at.file ?? "",
                  title: at.label,
                  language: at.label,
                ),
              );
            } catch (_) {}
          }
        }
      }
    }
  
    void _setSkipPhase(int secs) {
      _AniSkipPhase newPhase;
      if (_hasOpeningSkip &&
          secs >= _openingResult!.interval!.startTime!.ceil() &&
          secs < _openingResult!.interval!.endTime!.toInt()) {
        newPhase = _AniSkipPhase.opening;
      } else if (_hasEndingSkip &&
          secs >= _endingResult!.interval!.startTime!.ceil() &&
          secs < _endingResult!.interval!.endTime!.toInt()) {
        newPhase = _AniSkipPhase.ending;
      } else {
        newPhase = _AniSkipPhase.none;
      }
      if (_skipPhase.value != newPhase) _skipPhase.value = newPhase;
    }
  
    void _updateRpcTimestamp() {
      final now = DateTime.now().millisecondsSinceEpoch;
      if (lastRpcTimestampUpdate + 5000 < now) {
        if (_currentTotalDuration.value != null) {
          discordRpc?.updateChapterTimestamp(
            _currentPosition.value,
            _currentTotalDuration.value!,
          );
        }
        lastRpcTimestampUpdate = now;
      }
    }
  
    void _onSubDelayChanged() {
      final nativePlayer = (_player.platform as NativePlayer);
      final delayMs = int.tryParse(_subDelayController.text);
      if (delayMs != null) {
        final namePtr = "sub-delay".toNativeUtf8();
        final valuePtr = calloc<Double>(1)..value = delayMs / 1000;
        nativePlayer.mpv.mpv_set_property(
          nativePlayer.ctx,
          namePtr.cast(),
          generated.mpv_format.MPV_FORMAT_DOUBLE,
          valuePtr.cast(),
        );
        malloc.free(namePtr);
        malloc.free(valuePtr);
        _subDelay = delayMs;
      }
    }
  
    void _onSubSpeedChanged() {
      final nativePlayer = (_player.platform as NativePlayer);
      final speed = double.tryParse(_subSpeedController.text);
      if (speed != null) {
        final namePtr = "sub-speed".toNativeUtf8();
        final valuePtr = calloc<Double>(1)
          ..value = speed < 0.1
              ? 0.1
              : speed > 10
              ? 10
              : speed;
        nativePlayer.mpv.mpv_set_property(
          nativePlayer.ctx,
          namePtr.cast(),
          generated.mpv_format.MPV_FORMAT_DOUBLE,
          valuePtr.cast(),
        );
        malloc.free(namePtr);
        malloc.free(valuePtr);
        _subSpeed = speed;
      }
    }
  
    @override
    void initState() {
      super.initState();
      _controller = VideoController(
        _player,
        configuration: VideoControllerConfiguration(
          hwdec: hwdecMode,
          enableHardwareAcceleration: enableHardwareAccel,
          vo: Platform.isAndroid
              ? useGpuNext
                    ? "gpu-next"
                    : "gpu"
              : "libmpv",
        ),
      );
      // If player is being launched the first time,
      // use global "Use Fullscreen" setting.
      // Else (if user already watches an episode and just changes it),
      // stay in the same mode, the user left it in.
      try {
        final defaultSkipIntroLength = ref.read(
          defaultSkipIntroLengthStateProvider,
        );
        (_player.platform as NativePlayer).setProperty(
          "user-data/current-anime/intro-length",
          "$defaultSkipIntroLength",
        );
      } catch (_) {}
      if (_isDesktop && _firstTime) {
        final globalFullscreen = ref.read(fullScreenPlayerStateProvider);
        setFullScreen(value: globalFullscreen);
        Future.microtask(() {
          ref.read(fullscreenProvider.notifier).state = globalFullscreen;
          widget.desktopFullScreenPlayer.call(globalFullscreen);
        });
        _firstTime = false;
      }
      _currentPositionSub = _player.stream.position.listen(
        _unifiedPositionHandler,
      );
      _completed;
      _currentTotalDurationSub;
      _loadAndroidFont().then((_) {
        _openMedia(_video.value!, _streamController.geTCurrentPosition());
        if (widget.isTorrent) {
          Future.delayed(const Duration(seconds: 10)).then((_) {
            if (mounted) {
              _openMedia(_video.value!, _streamController.geTCurrentPosition());
            }
          });
        }
        _setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider));
        if (ref.read(enableAniSkipStateProvider)) _initAniSkip();
      });
      _initCustomButton();
      discordRpc?.showChapterDetails(ref, widget.episode);
      _currentPosition.addListener(_updateRpcTimestamp);
      _subDelayController.addListener(_onSubDelayChanged);
      _subSpeedController.addListener(_onSubSpeedChanged);
      WidgetsBinding.instance.addObserver(this);
    }
  
    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
      if (state == AppLifecycleState.paused ||
          state == AppLifecycleState.detached) {
        _setCurrentPosition(true);
      }
    }
  
    Future<void> _openMedia(VideoPrefs prefs, [Duration? position]) {
      return _player.open(
        Media(
          prefs.videoTrack!.id,
          httpHeaders: prefs.headers,
          start: position ?? _currentPosition.value,
        ),
      );
    }
  
    Future<void> _loadAndroidFont() async {
      if (Platform.isAndroid && useLibass) {
        try {
          final subDir = await getApplicationDocumentsDirectory();
          final fontPath = p.join(subDir.path, 'subfont.ttf');
          final data = await rootBundle.load('assets/fonts/subfont.ttf');
          final bytes = data.buffer.asInt8List(
            data.offsetInBytes,
            data.lengthInBytes,
          );
          final fontFile = await File(fontPath).create(recursive: true);
          await fontFile.writeAsBytes(bytes);
          await (_player.platform as NativePlayer).setProperty(
            'sub-fonts-dir',
            subDir.path,
          );
          await (_player.platform as NativePlayer).setProperty(
            'sub-font',
            'Droid Sans Fallback',
          );
        } catch (_) {}
      }
    }
  
    Future<void> _initAniSkip() async {
      await _player.stream.buffer.first;
      _streamController.getAniSkipResults((result) {
        final openingRes = result
            .where((element) => element.skipType == "op")
            .toList();
        _hasOpeningSkip = openingRes.isNotEmpty;
        if (_hasOpeningSkip) _openingResult = openingRes.first;
        final endingRes = result
            .where((element) => element.skipType == "ed")
            .toList();
        _hasEndingSkip = endingRes.isNotEmpty;
        if (_hasEndingSkip) _endingResult = endingRes.first;
        if (mounted) {
          setState(() {});
        }
      });
    }
  
    @override
    void dispose() {
      _currentPosition.removeListener(_updateRpcTimestamp);
      WidgetsBinding.instance.removeObserver(this);
      _setCurrentPosition(true);
      _player.dispose();
      _currentPositionSub.cancel();
      _currentTotalDurationSub.cancel();
      _completed.cancel();
      _video.dispose();
      _playbackSpeed.dispose();
      _isDoubleSpeed.dispose();
      _currentTotalDuration.dispose();
      _showFitLabel.dispose();
      _isCompleted.dispose();
      _tempPosition.dispose();
      _fit.dispose();
      if (!_isDesktop) {
        _setLandscapeMode(false);
      }
      _skipPhase.dispose();
      discordRpc?.showIdleText();
      discordRpc?.showOriginalTimestamp();
      _currentPosition.dispose();
      _subDelayController.dispose();
      _subSpeedController.dispose();
      super.dispose();
    }
  
    void _setCurrentPosition(bool save) {
      _streamController.setCurrentPosition(
        _currentPosition.value,
        _currentTotalDuration.value,
        save: save,
      );
      _streamController.setAnimeHistoryUpdate();
    }
  
    void _setLandscapeMode(bool state) {
      if (state) {
        SystemChrome.setPreferredOrientations([
          DeviceOrientation.landscapeLeft,
          DeviceOrientation.landscapeRight,
        ]);
      } else {
        SystemChrome.setPreferredOrientations([
          DeviceOrientation.portraitUp,
          DeviceOrientation.portraitDown,
          DeviceOrientation.landscapeLeft,
          DeviceOrientation.landscapeRight,
        ]);
      }
    }
  
    Widget textWidget(String text, bool selected) => Row(
      children: [
        Flexible(
          child: Padding(
            padding: EdgeInsets.symmetric(
              horizontal: MediaQuery.of(context).padding.top,
            ),
            child: Text(
              text,
              style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                fontSize: 16,
                fontStyle: selected ? FontStyle.italic : null,
                color: selected ? context.primaryColor : null,
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ),
      ],
    );
  
    Widget _videoQualityWidget(BuildContext context) {
      List<VideoPrefs> videoQuality = _player.state.tracks.video
          .where(
            (element) => element.w != null && element.h != null && widget.isLocal,
          )
          .toList()
          .map((e) => VideoPrefs(videoTrack: e, isLocal: true))
          .toList();
  
      if (widget.videos.isNotEmpty && !widget.isLocal) {
        for (var video in widget.videos) {
          videoQuality.add(
            VideoPrefs(
              videoTrack: VideoTrack(video.url, video.quality, video.quality),
              headers: video.headers,
              isLocal: false,
            ),
          );
        }
      }
  
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
        child: Column(
          children: videoQuality.map((quality) {
            final selected =
                _video.value!.videoTrack!.title == quality.videoTrack!.title ||
                widget.isLocal;
            return GestureDetector(
              child: textWidget(
                widget.isLocal ? _firstVid.quality : quality.videoTrack!.title!,
                selected,
              ),
              onTap: () async {
                if (_video.value?.videoTrack?.id == quality.videoTrack?.id) {
                  Navigator.pop(context);
                  return;
                }
                _video.value = quality;
                _player.stop();
                if (quality.isLocal) {
                  if (widget.isLocal) {
                    _player.setVideoTrack(quality.videoTrack!);
                  } else {
                    _openMedia(quality);
                  }
                } else {
                  _openMedia(quality);
                }
                _initSubtitleAndAudio = true;
                Navigator.pop(context);
              },
            );
          }).toList(),
        ),
      );
    }
  
    void _videoSettingDraggableMenu(BuildContext context) async {
      final l10n = l10nLocalizations(context)!;
      bool hasSubtitleTrack = false;
      _player.pause();
      await customDraggableTabBar(
        tabs: [
          Tab(text: l10n.video_quality),
          Tab(text: l10n.video_subtitle),
          Tab(text: l10n.video_audio),
        ],
        children: [
          _videoQualityWidget(context),
          _videoSubtitle(context, (value) => hasSubtitleTrack = value),
          _videoAudios(context),
        ],
        context: context,
        vsync: this,
        fullWidth: true,
        moreWidget: IconButton(
          onPressed: () async {
            if (useLibass) {
              BotToast.showText(
                contentColor: Colors.white,
                textStyle: const TextStyle(color: Colors.black, fontSize: 20),
                onlyOne: true,
                align: const Alignment(0, 0.90),
                duration: const Duration(seconds: 2),
                text: context.l10n.libass_not_disable_message,
              );
            } else {
              await customDraggableTabBar(
                tabs: [
                  Tab(text: l10n.font),
                  Tab(text: l10n.color),
                ],
                children: [
                  FontSettingWidget(hasSubtitleTrack: hasSubtitleTrack),
                  ColorSettingWidget(hasSubtitleTrack: hasSubtitleTrack),
                ],
                context: context,
                vsync: this,
                fullWidth: true,
              );
              if (context.mounted) {
                Navigator.pop(context);
              }
            }
          },
          icon: const Icon(Icons.settings_outlined),
        ),
      );
      setState(() {});
      _player.play();
    }
  
    Widget _videoSubtitle(BuildContext context, Function(bool) hasSubtitleTrack) {
      List<VideoPrefs> videoSubtitle = _player.state.tracks.subtitle
          .toList()
          .map((e) => VideoPrefs(isLocal: true, subtitle: e))
          .toList();
  
      List<String> subs = [];
      if (widget.videos.isNotEmpty) {
        for (var video in widget.videos) {
          for (var sub in video.subtitles ?? []) {
            if (!subs.contains(sub.file)) {
              final file = sub.file!;
              final label = sub.label;
              videoSubtitle.add(
                VideoPrefs(
                  isLocal: widget.isLocal,
                  subtitle: (file.startsWith("http") || file.startsWith("file"))
                      ? SubtitleTrack.uri(file, title: label, language: label)
                      : SubtitleTrack.data(file, title: label, language: label),
                ),
              );
              subs.add(sub.file!);
            }
          }
        }
      }
      final subtitle = _player.state.track.subtitle;
      videoSubtitle = videoSubtitle
          .map((e) {
            VideoPrefs vid = e;
            vid.title =
                vid.subtitle?.title ??
                vid.subtitle?.language ??
                vid.subtitle?.channels ??
                "";
            return vid;
          })
          .toList()
          .where((element) => element.title!.isNotEmpty)
          .toList();
      videoSubtitle.sort((a, b) => a.title!.compareTo(b.title!));
      hasSubtitleTrack.call(videoSubtitle.isNotEmpty);
      videoSubtitle.insert(
        0,
        VideoPrefs(isLocal: false, subtitle: SubtitleTrack.no()),
      );
      List<VideoPrefs> videoSubtitleLast = [];
      for (var element in videoSubtitle) {
        final contains = videoSubtitleLast.any((sub) {
          return (sub.title ??
                  sub.subtitle?.title ??
                  sub.subtitle?.language ??
                  sub.subtitle?.channels ??
                  "None") ==
              (element.title ??
                  element.subtitle?.title ??
                  element.subtitle?.language ??
                  element.subtitle?.channels ??
                  "None");
        });
        if (!contains) {
          videoSubtitleLast.add(element);
        }
      }
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
        child: Column(
          children: [
            Row(
              children: [
                Text(context.l10n.subtitle_delay_text),
                IconButton(
                  onPressed: () {
                    _subDelay = 0;
                    _subDelayController.value = TextEditingValue(
                      text: "$_subDelay",
                    );
                    _subSpeed = 1;
                    _subSpeedController.value = TextEditingValue(
                      text: _subSpeed.toStringAsFixed(2),
                    );
                  },
                  icon: const Icon(Icons.refresh),
                ),
              ],
            ),
            const SizedBox(height: 15),
            Row(
              children: [
                IconButton(
                  onPressed: () {
                    _subDelay -= 50;
                    _subDelayController.value = TextEditingValue(
                      text: "$_subDelay",
                    );
                  },
                  icon: const Icon(Icons.remove_circle),
                ),
                Expanded(
                  child: TextFormField(
                    controller: _subDelayController,
                    keyboardType: TextInputType.number,
                    decoration: InputDecoration(
                      isDense: true,
                      label: Text(context.l10n.subtitle_delay),
                    ),
                  ),
                ),
                IconButton(
                  onPressed: () {
                    _subDelay += 50;
                    _subDelayController.value = TextEditingValue(
                      text: "$_subDelay",
                    );
                  },
                  icon: const Icon(Icons.add_circle),
                ),
              ],
            ),
            const SizedBox(height: 15),
            Row(
              children: [
                IconButton(
                  onPressed: () {
                    _subSpeed -= 0.01;
                    _subSpeedController.value = TextEditingValue(
                      text: _subSpeed.toStringAsFixed(2),
                    );
                  },
                  icon: const Icon(Icons.remove_circle),
                ),
                Expanded(
                  child: TextFormField(
                    controller: _subSpeedController,
                    keyboardType: TextInputType.numberWithOptions(decimal: true),
                    decoration: InputDecoration(
                      isDense: true,
                      label: Text(context.l10n.subtitle_speed),
                    ),
                  ),
                ),
                IconButton(
                  onPressed: () {
                    _subSpeed += 0.01;
                    _subSpeedController.value = TextEditingValue(
                      text: _subSpeed.toStringAsFixed(2),
                    );
                  },
                  icon: const Icon(Icons.add_circle),
                ),
              ],
            ),
            const SizedBox(height: 30),
            ...videoSubtitleLast.toSet().toList().map((sub) {
              final title =
                  sub.title ??
                  sub.subtitle?.title ??
                  sub.subtitle?.language ??
                  sub.subtitle?.channels ??
                  "None";
  
              final selected =
                  (title ==
                      (subtitle.title ??
                          subtitle.language ??
                          subtitle.channels ??
                          "None")) ||
                  (subtitle.id == "no" && title == "None");
              return GestureDetector(
                onTap: () {
                  Navigator.pop(context);
                  try {
                    _player.setSubtitleTrack(sub.subtitle!);
                  } catch (_) {}
                },
                child: textWidget(title, selected),
              );
            }),
            const SizedBox(height: 30),
            GestureDetector(
              onTap: () async {
                try {
                  FilePickerResult? result = await FilePicker.platform.pickFiles(
                    allowMultiple: false,
                  );
  
                  if (result != null && context.mounted) {
                    _player.setSubtitleTrack(
                      SubtitleTrack.uri(result.files.first.path!),
                    );
                  }
                  if (!context.mounted) return;
                  Navigator.pop(context);
                } catch (_) {
                  botToast("Error");
                  Navigator.pop(context);
                }
              },
              child: textWidget(context.l10n.load_own_subtitles, false),
            ),
            const SizedBox(height: 30),
            GestureDetector(
              onTap: () async {
                try {
                  final subtitle =
                      await subtitlesSearchraggableMenu(
                            context,
                            chapter: widget.episode,
                            isLocal: widget.isLocal,
                          )
                          as ImdbSubtitle?;
                  if (subtitle != null && context.mounted) {
                    _player.setSubtitleTrack(
                      SubtitleTrack.uri(
                        subtitle.url!,
                        title: subtitle.language,
                        language: subtitle.language,
                      ),
                    );
                  }
                  if (!context.mounted) return;
                  Navigator.pop(context);
                } catch (_) {
                  botToast("Error");
                  Navigator.pop(context);
                }
              },
              child: textWidget(context.l10n.search_subtitles, false),
            ),
          ],
        ),
      );
    }
  
    Widget _videoAudios(BuildContext context) {
      List<VideoPrefs> videoAudio = _player.state.tracks.audio
          .toList()
          .map((e) => VideoPrefs(isLocal: true, audio: e))
          .toList();
  
      List<String> audios = [];
      if (widget.videos.isNotEmpty && !widget.isLocal) {
        for (var video in widget.videos) {
          for (var audio in video.audios ?? []) {
            if (!audios.contains(audio.file)) {
              videoAudio.add(
                VideoPrefs(
                  isLocal: false,
                  audio: AudioTrack.uri(
                    audio.file!,
                    title: audio.label,
                    language: audio.label,
                  ),
                ),
              );
              audios.add(audio.file!);
            }
          }
        }
      }
      final audio = _player.state.track.audio;
      videoAudio = videoAudio
          .map((e) {
            VideoPrefs vid = e;
            vid.title =
                vid.audio?.title ??
                vid.audio?.language ??
                vid.audio?.channels ??
                "";
            return vid;
          })
          .toList()
          .where((element) => element.title!.isNotEmpty)
          .toList();
      videoAudio.sort((a, b) => a.title!.compareTo(b.title!));
      videoAudio.insert(0, VideoPrefs(isLocal: false, audio: AudioTrack.no()));
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
        child: Column(
          children: videoAudio.toSet().toList().map((aud) {
            final title =
                aud.title ??
                aud.audio?.title ??
                aud.audio?.language ??
                aud.audio?.channels ??
                "None";
            final selected =
                (aud.audio == audio) || (audio.id == "no" && title == "None");
            return GestureDetector(
              onTap: () {
                Navigator.pop(context);
                try {
                  _player.setAudioTrack(aud.audio!);
                } catch (_) {}
              },
              child: textWidget(title, selected),
            );
          }).toList(),
        ),
      );
    }
  
    Future<void> _setPlaybackSpeed(double speed) async {
      await _player.setRate(speed);
      _playbackSpeed.value = speed;
    }
  
    Future<void> _changeFitLabel(WidgetRef ref) async {
      List<BoxFit> fitList = [
        BoxFit.contain,
        BoxFit.cover,
        BoxFit.fill,
        BoxFit.fitHeight,
        BoxFit.fitWidth,
        BoxFit.scaleDown,
        BoxFit.none,
      ];
      _showFitLabel.value = true;
      BoxFit? fit;
      if (fitList.indexOf(_fit.value) < fitList.length - 1) {
        fit = fitList[fitList.indexOf(_fit.value) + 1];
      } else {
        fit = fitList[0];
      }
      _fit.value = fit;
      _key.currentState?.update(fit: fit);
      BotToast.showText(
        onlyOne: true,
        align: const Alignment(0, 0.90),
        duration: const Duration(seconds: 1),
        text: fit.name.toUpperCase(),
      );
    }
  
    Widget _seekToWidget() {
      final defaultSkipIntroLength = ref.watch(
        defaultSkipIntroLengthStateProvider,
      );
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 5),
        child: SizedBox(
          height: 35,
          child: ValueListenableBuilder(
            valueListenable: _customButton,
            builder: (context, value, child) => (value?.visible ?? true)
                ? ElevatedButton(
                    onPressed:
                        value?.onPress ??
                        () async {
                          _tempPosition.value = Duration(
                            seconds:
                                defaultSkipIntroLength +
                                _currentPosition.value.inSeconds,
                          );
                          await _player.seek(
                            Duration(
                              seconds:
                                  _currentPosition.value.inSeconds +
                                  defaultSkipIntroLength,
                            ),
                          );
                          _tempPosition.value = null;
                        },
                    onLongPress: value?.onLongPress,
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        value != null
                            ? value.currentTitle
                            : "+$defaultSkipIntroLength",
                        style: const TextStyle(fontWeight: FontWeight.w100),
                      ),
                    ),
                  )
                : Container(),
          ),
        ),
      );
    }
  
    Widget _chapterMarkWidget() {
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5),
        child: SizedBox(
          height: 35,
          child: ValueListenableBuilder(
            valueListenable: _currentChapterMark,
            builder: (context, value, child) => value != null
                ? PopupMenuButton<int>(
                    tooltip: '',
                    itemBuilder: (context) => _chapterMarks.value
                        .map(
                          (mark) => PopupMenuItem<int>(
                            value: mark.$2,
                            child: Text(
                              "${mark.$1} - ${Duration(milliseconds: mark.$2).label()}",
                            ),
                            onTap: () =>
                                _player.seek(Duration(milliseconds: mark.$2)),
                          ),
                        )
                        .toList(),
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        "${_chapterMarks.value[value].$1} - ${Duration(milliseconds: _chapterMarks.value[value].$2).label()}",
                        style: const TextStyle(
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      ),
                    ),
                  )
                : Container(),
          ),
        ),
      );
    }
  
    Widget _mobileBottomButtonBar(BuildContext context) {
      return Padding(
        padding: const EdgeInsets.only(bottom: 30),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 8),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  _seekToWidget(),
                  _chapterMarkWidget(),
                  _buildSettingsButtons(context),
                ],
              ),
            ),
          ],
        ),
      );
    }
  
    Widget _desktopBottomButtonBar(BuildContext context) {
      bool hasPrevEpisode =
          _streamController.getEpisodeIndex().$1 + 1 !=
          _streamController.getEpisodesLength(
            _streamController.getEpisodeIndex().$2,
          );
      final skipDuration = ref.watch(defaultDoubleTapToSkipLengthStateProvider);
      return Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Row(
                children: [
                  if (hasPrevEpisode)
                    IconButton(
                      onPressed: () {
                        pushToNewEpisode(
                          context,
                          _streamController.getPrevEpisode(),
                        );
                      },
                      icon: const Icon(Icons.skip_previous, color: Colors.white),
                    ),
                  CustomPlayOrPauseButton(
                    controller: _controller,
                    isDesktop: _isDesktop,
                  ),
                  if (hasNextEpisode)
                    IconButton(
                      onPressed: () async {
                        pushToNewEpisode(
                          context,
                          _streamController.getNextEpisode(),
                        );
                      },
                      icon: const Icon(Icons.skip_next, color: Colors.white),
                    ),
                  SizedBox(
                    height: 50,
                    width: 50,
                    child: IconButton(
                      onPressed: () async {
                        _tempPosition.value = Duration(
                          seconds:
                              skipDuration - _currentPosition.value.inSeconds,
                        );
                        await _player.seek(
                          Duration(
                            seconds:
                                _currentPosition.value.inSeconds - skipDuration,
                          ),
                        );
                        _tempPosition.value = null;
                      },
                      icon: Stack(
                        children: [
                          const Positioned.fill(
                            child: Icon(
                              Icons.rotate_left_outlined,
                              color: Colors.white,
                              size: 30,
                            ),
                          ),
                          Positioned.fill(
                            child: Center(
                              child: Padding(
                                padding: const EdgeInsets.only(top: 2),
                                child: Text(
                                  skipDuration.toString(),
                                  style: const TextStyle(
                                    fontSize: 9,
                                    color: Colors.white,
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                  SizedBox(
                    height: 50,
                    width: 50,
                    child: IconButton(
                      onPressed: () async {
                        _tempPosition.value = Duration(
                          seconds:
                              skipDuration + _currentPosition.value.inSeconds,
                        );
                        await _player.seek(
                          Duration(
                            seconds:
                                _currentPosition.value.inSeconds + skipDuration,
                          ),
                        );
                        _tempPosition.value = null;
                      },
                      icon: Stack(
                        children: [
                          const Positioned.fill(
                            child: Icon(
                              Icons.rotate_right_outlined,
                              color: Colors.white,
                              size: 30,
                            ),
                          ),
                          Positioned.fill(
                            child: Center(
                              child: Padding(
                                padding: const EdgeInsets.only(top: 2),
                                child: Text(
                                  skipDuration.toString(),
                                  style: const TextStyle(
                                    fontSize: 9,
                                    color: Colors.white,
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                  CustomMaterialDesktopVolumeButton(controller: _controller),
                  ValueListenableBuilder(
                    valueListenable: _tempPosition,
                    builder: (context, value, child) =>
                        CustomMaterialDesktopPositionIndicator(
                          delta: value,
                          controller: _controller,
                        ),
                  ),
                  _chapterMarkWidget(),
                ],
              ),
              _buildSettingsButtons(context),
            ],
          ),
        ],
      );
    }
  
    List<Widget> _buildMpvSettingsButton(BuildContext context) {
      return [
        PopupMenuButton<String>(
          tooltip: 'Shaders',
          icon: const Icon(Icons.high_quality, color: Colors.white),
          itemBuilder: (context) =>
              [
                    ("Anime4K: Mode A (Fast)", "set_anime_a"),
                    ("Anime4K: Mode B (Fast)", "set_anime_b"),
                    ("Anime4K: Mode C (Fast)", "set_anime_c"),
                    ("Anime4K: Mode A+A (Fast)", "set_anime_aa"),
                    ("Anime4K: Mode B+B (Fast)", "set_anime_bb"),
                    ("Anime4K: Mode C+A (Fast)", "set_anime_ca"),
                    ("Anime4K: Mode A (HQ)", "set_anime_hq_a"),
                    ("Anime4K: Mode B (HQ)", "set_anime_hq_b"),
                    ("Anime4K: Mode C (HQ)", "set_anime_hq_c"),
                    ("Anime4K: Mode A+A (HQ)", "set_anime_hq_aa"),
                    ("Anime4K: Mode B+B (HQ)", "set_anime_hq_bb"),
                    ("Anime4K: Mode C+A (HQ)", "set_anime_hq_ca"),
                    ("AMD FSR", "set_fsr"),
                    ("Luma Upscaling", "set_luma"),
                    ("Qualcomm Snapdragon GSR", "set_snapdragon"),
                    ("NVIDIA Image Scaling", "set_nvidia"),
                    ("Clear GLSL shaders", "clear_anime"),
                  ]
                  .map(
                    (mode) => PopupMenuItem<String>(
                      value: mode.$1,
                      child: Text(
                        mode.$1,
                        style: TextStyle(
                          fontWeight: _selectedShader.value == mode.$1
                              ? FontWeight.w900
                              : FontWeight.normal,
                        ),
                      ),
                      onTap: () {
                        (_player.platform as NativePlayer).command([
                          "script-message",
                          mode.$2,
                        ]);
                      },
                    ),
                  )
                  .toList(),
        ),
        PopupMenuButton<String>(
          tooltip: 'Stats',
          icon: const Icon(Icons.memory, color: Colors.white),
          itemBuilder: (context) =>
              [
                    ("Stats Toggle", "stats/display-stats-toggle"),
                    ("Stats Page 1", "stats/display-page-1"),
                    ("Stats Page 2", "stats/display-page-2"),
                    ("Stats Page 3", "stats/display-page-3"),
                    ("Stats Page 4", "stats/display-page-4"),
                    ("Stats Page 5", "stats/display-page-5"),
                  ]
                  .map(
                    (mode) => PopupMenuItem<String>(
                      value: mode.$1,
                      child: Text(
                        mode.$1,
                        style: TextStyle(
                          fontWeight: _selectedShader.value == mode.$1
                              ? FontWeight.w900
                              : FontWeight.normal,
                        ),
                      ),
                      onTap: () {
                        (_player.platform as NativePlayer).command([
                          "script-binding",
                          mode.$2,
                        ]);
                      },
                    ),
                  )
                  .toList(),
        ),
        ValueListenableBuilder(
          valueListenable: _customButtons,
          builder: (context, value, child) => value != null
              ? PopupMenuButton<String>(
                  tooltip: context.l10n.custom_buttons,
                  icon: const Icon(Icons.terminal, color: Colors.white),
                  itemBuilder: (context) => value
                      .map(
                        (btn) => PopupMenuItem<String>(
                          value: btn.title!,
                          child: Text(btn.title!),
                          onTap: () {
                            (_player.platform as NativePlayer).command([
                              "script-message",
                              "call_button_${btn.id}",
                            ]);
                          },
                        ),
                      )
                      .toList(),
                )
              : Container(),
        ),
      ];
    }
  
    /// helper method for _mobileBottomButtonBar() and _desktopBottomButtonBar()
    Widget _buildSettingsButtons(BuildContext context) {
      final isFullscreen = ref.watch(fullscreenProvider);
      return Row(
        children: [
          IconButton(
            padding: _isDesktop ? EdgeInsets.zero : const EdgeInsets.all(5),
            onPressed: () => _videoSettingDraggableMenu(context),
            icon: const Icon(Icons.video_settings, color: Colors.white),
          ),
          if (useMpvConfig) ..._buildMpvSettingsButton(context),
          PopupMenuButton<double>(
            tooltip: '', // Remove default tooltip "Show menu" for consistency
            icon: const Icon(Icons.speed, color: Colors.white),
            itemBuilder: (context) =>
                [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0]
                    .map(
                      (speed) => PopupMenuItem<double>(
                        value: speed,
                        child: Text("${speed}x"),
                        onTap: () {
                          _setPlaybackSpeed(speed);
                        },
                      ),
                    )
                    .toList(),
          ),
          IconButton(
            icon: const Icon(Icons.fit_screen_outlined, color: Colors.white),
            onPressed: () async {
              _changeFitLabel(ref);
            },
          ),
          if (_isDesktop)
            CustomMaterialDesktopFullscreenButton(
              controller: _controller,
              desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
            )
          else
            IconButton(
              icon: Icon(isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen),
              iconSize: 25,
              color: Colors.white,
              onPressed: () {
                _setLandscapeMode(!isFullscreen);
                ref.read(fullscreenProvider.notifier).state = !isFullscreen;
                widget.desktopFullScreenPlayer.call(!isFullscreen);
              },
            ),
        ],
      );
    }
  
    Widget _topButtonBar(BuildContext context) {
      final fullScreen = ref.watch(fullscreenProvider);
      return Padding(
        padding: EdgeInsets.only(
          top: !_isDesktop && !fullScreen
              ? MediaQuery.of(context).padding.top
              : 0,
        ),
        child: Row(
          children: [
            BackButton(
              color: Colors.white,
              onPressed: () {
                if (_isDesktop && fullScreen) {
                  setFullScreen(value: !fullScreen);
                  ref.read(fullscreenProvider.notifier).state = !fullScreen;
                  widget.desktopFullScreenPlayer.call(!fullScreen);
                } else {
                  SystemChrome.setEnabledSystemUIMode(
                    SystemUiMode.manual,
                    overlays: SystemUiOverlay.values,
                  );
                }
                if (mounted) {
                  // Set variable to true, so the player uses the global
                  // "Use Fullscreen" setting again.
                  _firstTime = true;
                  Navigator.pop(context);
                }
              },
            ),
            Flexible(
              child: ListTile(
                dense: true,
                title: SizedBox(
                  width: context.width(0.8),
                  child: Text(
                    widget.episode.manga.value!.name!,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                subtitle: SizedBox(
                  width: context.width(0.8),
                  child: Text(
                    widget.episode.name!,
                    style: TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.w400,
                      color: Colors.white.withValues(alpha: 0.7),
                    ),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ),
            ),
            Row(
              children: [
                btnToShowChapterListDialog(
                  context,
                  context.l10n.episodes,
                  widget.episode,
                  onChanged: (v) {
                    if (v) {
                      _player.play();
                    } else {
                      _player.pause();
                    }
                  },
                  iconColor: Colors.white,
                ),
                btnToShowShareScreenshot(
                  widget.episode,
                  onChanged: (v) {
                    if (v) {
                      _player.play();
                    } else {
                      _player.pause();
                    }
                  },
                ),
              ],
            ),
          ],
        ),
      );
    }
  
    void _resize(BoxFit fit) async {
      await Future.delayed(const Duration(milliseconds: 100));
      if (mounted) {
        _key.currentState?.update(
          fit: fit,
          width: context.width(1),
          height: context.height(1),
        );
      }
    }
  
    Widget _videoPlayer(BuildContext context) {
      final fit = _fit.value;
      _resize(fit);
      final enableAniSkip = ref.read(enableAniSkipStateProvider);
      final enableAutoSkip = ref.read(enableAutoSkipStateProvider);
      final aniSkipTimeoutLength = ref.read(aniSkipTimeoutLengthStateProvider);
      final skipIntroLength = ref.read(defaultSkipIntroLengthStateProvider);
      return Stack(
        children: [
          Video(
            subtitleViewConfiguration: SubtitleViewConfiguration(
              visible: false,
              style: subtileTextStyle(ref),
            ),
            fit: fit,
            key: _key,
            controls: (state) => _isDesktop
                ? DesktopControllerWidget(
                    videoController: _controller,
                    topButtonBarWidget: _topButtonBar(context),
                    videoStatekey: _key,
                    bottomButtonBarWidget: _desktopBottomButtonBar(context),
                    streamController: _streamController,
                    seekToWidget: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 15),
                      child: Row(children: [_seekToWidget()]),
                    ),
                    tempDuration: (value) {
                      _tempPosition.value = value;
                    },
                    doubleSpeed: (value) {
                      _isDoubleSpeed.value = value ?? false;
                    },
                    defaultSkipIntroLength: skipIntroLength,
                    desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
                    chapterMarks: _chapterMarks,
                  )
                : MobileControllerWidget(
                    videoController: _controller,
                    topButtonBarWidget: _topButtonBar(context),
                    videoStatekey: _key,
                    bottomButtonBarWidget: _mobileBottomButtonBar(context),
                    streamController: _streamController,
                    doubleSpeed: (value) {
                      _isDoubleSpeed.value = value ?? false;
                    },
                    chapterMarks: _chapterMarks,
                  ),
            controller: _controller,
            width: context.width(1),
            height: context.height(1),
            resumeUponEnteringForegroundMode: true,
          ),
          Stack(
            alignment: AlignmentDirectional.center,
            children: [
              Positioned(
                top: 30,
                child: ValueListenableBuilder<bool>(
                  valueListenable: _isDoubleSpeed,
                  builder: (context, snapshot, _) {
                    return Text.rich(
                      textAlign: TextAlign.center,
                      TextSpan(
                        style: TextStyle(
                          background: Paint()
                            ..color = Theme.of(context).scaffoldBackgroundColor
                            ..strokeWidth = 30.0
                            ..strokeJoin = StrokeJoin.round
                            ..style = PaintingStyle.stroke,
                        ),
                        children: snapshot
                            ? [
                                TextSpan(
                                  text: " 2X ",
                                  style: const TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                                WidgetSpan(
                                  alignment: PlaceholderAlignment.middle,
                                  child: Icon(Icons.fast_forward),
                                ),
                              ]
                            : [],
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
          if (enableAniSkip && (_hasOpeningSkip || _hasEndingSkip))
            Positioned(
              right: 0,
              bottom: 80,
              child: ValueListenableBuilder<_AniSkipPhase>(
                valueListenable: _skipPhase,
                builder: (context, phase, _) {
                  if (phase == _AniSkipPhase.none) return const SizedBox.shrink();
                  final isOpening = phase == _AniSkipPhase.opening;
                  final result = isOpening ? _openingResult! : _endingResult!;
                  return AniSkipCountDownButton(
                    key: Key(isOpening ? 'skip_opening' : 'skip_ending'),
                    active: true,
                    autoSkip: enableAutoSkip,
                    timeoutLength: aniSkipTimeoutLength,
                    skipTypeText: isOpening
                        ? context.l10n.skip_opening
                        : context.l10n.skip_ending,
                    player: _player,
                    aniSkipResult: result,
                  );
                },
              ),
            ),
        ],
      );
    }
  
    Widget btnToShowShareScreenshot(
      Chapter episode, {
      void Function(bool)? onChanged,
    }) {
      return IconButton(
        onPressed: () async {
          onChanged?.call(false);
          Widget button(String label, IconData icon, Function() onPressed) =>
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(15),
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.transparent,
                      elevation: 0,
                      shadowColor: Colors.transparent,
                    ),
                    onPressed: onPressed,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding(
                          padding: const EdgeInsets.all(4),
                          child: Icon(icon),
                        ),
                        Text(label),
                      ],
                    ),
                  ),
                ),
              );
          final name =
              "${episode.manga.value!.name} ${episode.name} - ${_currentPosition.value.toString()}"
                  .replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_');
          await showModalBottomSheet(
            context: context,
            constraints: BoxConstraints(maxWidth: context.width(1)),
            builder: (context) {
              return SuperListView(
                shrinkWrap: true,
                children: [
                  Container(
                    decoration: BoxDecoration(
                      borderRadius: const BorderRadius.only(
                        topLeft: Radius.circular(20),
                        topRight: Radius.circular(20),
                      ),
                      color: context.themeData.scaffoldBackgroundColor,
                    ),
                    child: Column(
                      children: [
                        Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Container(
                            height: 7,
                            width: 35,
                            decoration: BoxDecoration(
                              borderRadius: BorderRadius.circular(6),
                              color: context.secondaryColor.withValues(
                                alpha: 0.4,
                              ),
                            ),
                          ),
                        ),
                        Row(
                          children: [
                            button(
                              context.l10n.set_as_cover,
                              Icons.image_outlined,
                              () async {
                                final imageBytes = await _player.screenshot(
                                  format: "image/png",
                                  includeLibassSubtitles: _includeSubtitles,
                                );
                                if (context.mounted) {
                                  final res = await showDialog(
                                    context: context,
                                    builder: (context) {
                                      return AlertDialog(
                                        content: Text(
                                          context.l10n.use_this_as_cover_art,
                                        ),
                                        actions: [
                                          Row(
                                            mainAxisAlignment:
                                                MainAxisAlignment.end,
                                            children: [
                                              TextButton(
                                                onPressed: () {
                                                  Navigator.pop(context);
                                                },
                                                child: Text(context.l10n.cancel),
                                              ),
                                              const SizedBox(width: 15),
                                              TextButton(
                                                onPressed: () {
                                                  final manga =
                                                      episode.manga.value!;
                                                  isar.writeTxnSync(() {
                                                    isar.mangas.putSync(
                                                      manga
                                                        ..updatedAt = DateTime.now()
                                                            .millisecondsSinceEpoch
                                                        ..customCoverImage =
                                                            imageBytes,
                                                    );
                                                  });
                                                  if (context.mounted) {
                                                    Navigator.pop(context, "ok");
                                                  }
                                                },
                                                child: Text(context.l10n.ok),
                                              ),
                                            ],
                                          ),
                                        ],
                                      );
                                    },
                                  );
                                  if (res != null &&
                                      res == "ok" &&
                                      context.mounted) {
                                    Navigator.pop(context);
                                    botToast(
                                      context.l10n.cover_updated,
                                      second: 3,
                                    );
                                  }
                                }
                              },
                            ),
                            button(
                              context.l10n.share,
                              Icons.share_outlined,
                              () async {
                                final imageBytes = await _player.screenshot(
                                  format: "image/png",
                                  includeLibassSubtitles: _includeSubtitles,
                                );
                                if (context.mounted) {
                                  final box =
                                      context.findRenderObject() as RenderBox?;
                                  await Share.shareXFiles(
                                    [
                                      XFile.fromData(
                                        imageBytes!,
                                        name: name,
                                        mimeType: 'image/png',
                                      ),
                                    ],
                                    sharePositionOrigin:
                                        box!.localToGlobal(Offset.zero) &
                                        box.size,
                                  );
                                }
                              },
                            ),
                            button(
                              context.l10n.save,
                              Icons.save_outlined,
                              () async {
                                final imageBytes = await _player.screenshot(
                                  format: "image/png",
                                  includeLibassSubtitles: _includeSubtitles,
                                );
                                final dir = await StorageProvider()
                                    .getGalleryDirectory();
                                final file = File(p.join(dir!.path, "$name.png"));
                                file.writeAsBytesSync(imageBytes!);
                                if (context.mounted) {
                                  botToast(context.l10n.picture_saved, second: 3);
                                }
                              },
                            ),
                          ],
                        ),
                        SwitchListTile(
                          onChanged: (value) {
                            setState(() {
                              _includeSubtitles = value;
                            });
                          },
                          title: Text(context.l10n.include_subtitles),
                          value: _includeSubtitles,
                        ),
                      ],
                    ),
                  ),
                ],
              );
            },
          );
          onChanged?.call(true);
        },
        icon: Icon(Icons.adaptive.share, color: Colors.white),
      );
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(body: _videoPlayer(context));
    }
  }
  
  Widget seekIndicatorTextWidget(Duration duration, Duration currentPosition) {
    final swipeDuration = duration.inSeconds;
    final value = currentPosition.inSeconds + swipeDuration;
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          Duration(seconds: value).label(),
          style: const TextStyle(
            fontSize: 65.0,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
        Text(
          "[${swipeDuration > 0 ? "+${Duration(seconds: swipeDuration).label()}" : "-${Duration(seconds: swipeDuration).label()}"}]",
          style: const TextStyle(
            fontSize: 40.0,
            color: Colors.white,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    );
  }
  
  class VideoPrefs {
    String? title;
    VideoTrack? videoTrack;
    SubtitleTrack? subtitle;
    AudioTrack? audio;
    bool isLocal;
    final Map<String, String>? headers;
    VideoPrefs({
      this.videoTrack,
      this.isLocal = true,
      this.headers,
      this.subtitle,
      this.audio,
      this.title,
    });
  }
  