mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
feat: Added AniSkip feature
This commit is contained in:
parent
9ee32a2380
commit
7c2fc50a47
25 changed files with 1288 additions and 340 deletions
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "تحديث التقدم بعد القراءة",
|
||||
"no_sources_installed": "لم يتم تثبيت مصادر!",
|
||||
"show_extensions": "عرض الإضافات",
|
||||
"default_skip_forward_skip_length": "طول التخطي الافتراضي للأمام"
|
||||
"default_skip_forward_skip_length": "طول التخطي الافتراضي للأمام",
|
||||
"aniskip_requires_info": "AniSkip يتطلب تتبع الأنمي باستخدام MAL أو Anilist للعمل.",
|
||||
"enable_aniskip": "تمكين AniSkip",
|
||||
"enable_auto_skip": "تمكين التخطي التلقائي",
|
||||
"aniskip_button_timeout": "مهلة زر"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Fortschritt nach dem Lesen aktualisieren",
|
||||
"no_sources_installed": "Keine Quellen installiert!",
|
||||
"show_extensions": "Erweiterungen anzeigen",
|
||||
"default_skip_forward_skip_length": "Standardmäßige Länge des Vorwärtsspringens"
|
||||
"default_skip_forward_skip_length": "Standardmäßige Länge des Vorwärtsspringens",
|
||||
"aniskip_requires_info": "AniSkip erfordert, dass der Anime mit MAL oder Anilist verfolgt wird, um zu funktionieren.",
|
||||
"enable_aniskip": "AniSkip aktivieren",
|
||||
"enable_auto_skip": "Automatisches Überspringen aktivieren",
|
||||
"aniskip_button_timeout": "Timeout für Taste"
|
||||
}
|
||||
|
|
@ -276,5 +276,11 @@
|
|||
"updateProgressAfterReading": "Update progress after reading",
|
||||
"no_sources_installed": "No sources installed!",
|
||||
"show_extensions": "Show extensions",
|
||||
"default_skip_forward_skip_length": "Default skip forward skip length"
|
||||
"default_skip_forward_skip_length": "Default skip forward skip length",
|
||||
"aniskip_requires_info": "AniSkip requires the anime to be tracked with MAL or Anilist to work.",
|
||||
"enable_aniskip": "Enable AniSkip",
|
||||
"enable_auto_skip": "Enable auto skip",
|
||||
"aniskip_button_timeout": "Button timeout",
|
||||
"skip_opening": "Skip opening",
|
||||
"skip_ending": "Skip ending"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Actualizar el progreso después de leer",
|
||||
"no_sources_installed": "¡No hay fuentes instaladas!",
|
||||
"show_extensions": "Mostrar extensiones",
|
||||
"default_skip_forward_skip_length": "Longitud de salto hacia adelante predeterminada"
|
||||
"default_skip_forward_skip_length": "Longitud de salto hacia adelante predeterminada",
|
||||
"aniskip_requires_info": "AniSkip requiere que el anime esté registrado en MAL o Anilist para funcionar.",
|
||||
"enable_aniskip": "Habilitar AniSkip",
|
||||
"enable_auto_skip": "Habilitar salto automático",
|
||||
"aniskip_button_timeout": "Tiempo de espera del botón"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Actualizar el progreso después de leer",
|
||||
"no_sources_installed": "¡No hay fuentes instaladas!",
|
||||
"show_extensions": "Mostrar extensiones",
|
||||
"default_skip_forward_skip_length": "Longitud de salto hacia adelante predeterminada"
|
||||
"default_skip_forward_skip_length": "Longitud de salto hacia adelante predeterminada",
|
||||
"aniskip_requires_info": "AniSkip requiere que el anime esté registrado en MAL o Anilist para funcionar.",
|
||||
"enable_aniskip": "Habilitar AniSkip",
|
||||
"enable_auto_skip": "Habilitar salto automático",
|
||||
"aniskip_button_timeout": "Tiempo de espera del botón"
|
||||
}
|
||||
|
|
@ -276,5 +276,11 @@
|
|||
"updateProgressAfterReading": "Synchroniser la progression après lecture",
|
||||
"no_sources_installed": "Aucune source installée !",
|
||||
"show_extensions": "Afficher les extensions",
|
||||
"default_skip_forward_skip_length": "Longueur de saut par défaut"
|
||||
"default_skip_forward_skip_length": "Longueur de saut par défaut",
|
||||
"aniskip_requires_info": "AniSkip nécessite que l'anime soit suivi sur MAL ou Anilist pour fonctionner.",
|
||||
"enable_aniskip": "Activer AniSkip",
|
||||
"enable_auto_skip": "Activer le saut automatique",
|
||||
"aniskip_button_timeout": "Délai du bouton",
|
||||
"skip_opening": "Passer l'opening",
|
||||
"skip_ending": "Passer l'ending"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Perbarui kemajuan setelah membaca",
|
||||
"no_sources_installed": "Tidak ada sumber yang terpasang!",
|
||||
"show_extensions": "Tampilkan ekstensi",
|
||||
"default_skip_forward_skip_length": "Panjang lompatan maju default"
|
||||
"default_skip_forward_skip_length": "Panjang lompatan maju default",
|
||||
"aniskip_requires_info": "AniSkip memerlukan informasi anime dilacak dengan MAL atau Anilist untuk berfungsi.",
|
||||
"enable_aniskip": "Aktifkan AniSkip",
|
||||
"enable_auto_skip": "Aktifkan pengabaian otomatis",
|
||||
"aniskip_button_timeout": "Timeout tombol"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Aggiorna il progresso dopo aver letto",
|
||||
"no_sources_installed": "Nessuna fonte installata!",
|
||||
"show_extensions": "Mostra estensioni",
|
||||
"default_skip_forward_skip_length": "Lunghezza predefinita del salto in avanti"
|
||||
"default_skip_forward_skip_length": "Lunghezza predefinita del salto in avanti",
|
||||
"aniskip_requires_info": "AniSkip richiede che l'anime sia tracciato con MAL o Anilist per funzionare.",
|
||||
"enable_aniskip": "Abilita AniSkip",
|
||||
"enable_auto_skip": "Abilita l'auto-skip",
|
||||
"aniskip_button_timeout": "Timeout del pulsante"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Atualizar progresso após a leitura",
|
||||
"no_sources_installed": "Nenhuma fonte instalada!",
|
||||
"show_extensions": "mostrar extensões",
|
||||
"default_skip_forward_skip_length": "Comprimento padrão do salto para frente"
|
||||
"default_skip_forward_skip_length": "Comprimento padrão do salto para frente",
|
||||
"aniskip_requires_info": "AniSkip requer que o anime seja rastreado com o MAL ou Anilist para funcionar.",
|
||||
"enable_aniskip": "Ativar AniSkip",
|
||||
"enable_auto_skip": "Ativar auto skip",
|
||||
"aniskip_button_timeout": "Tempo limite do botão"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Atualize o progresso após a leitura",
|
||||
"no_sources_installed": "Nenhuma fonte instalada!",
|
||||
"show_extensions": "Mostrar extensões",
|
||||
"default_skip_forward_skip_length": "Comprimento padrão do salto para frente"
|
||||
"default_skip_forward_skip_length": "Comprimento padrão do salto para frente",
|
||||
"aniskip_requires_info": "AniSkip requer que o anime seja rastreado com MAL ou Anilist para funcionar.",
|
||||
"enable_aniskip": "Habilitar AniSkip",
|
||||
"enable_auto_skip": "Habilitar auto skip",
|
||||
"aniskip_button_timeout": "Timeout do botão"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Обновить прогресс после чтения",
|
||||
"no_sources_installed": "Источники не установлены!",
|
||||
"show_extensions": "Показать расширения",
|
||||
"default_skip_forward_skip_length": "Длина пропуска вперед по умолчанию"
|
||||
"default_skip_forward_skip_length": "Длина пропуска вперед по умолчанию",
|
||||
"aniskip_requires_info": "AniSkip требует отслеживания аниме с использованием MAL или Anilist для работы.",
|
||||
"enable_aniskip": "Включить AniSkip",
|
||||
"enable_auto_skip": "Включить автоматическое пропускание",
|
||||
"aniskip_button_timeout": "Тайм-аут кнопки"
|
||||
}
|
||||
|
|
@ -276,5 +276,9 @@
|
|||
"updateProgressAfterReading": "Okuduktan Sonra İlerlemeyi Güncelle",
|
||||
"no_sources_installed": "Hiçbir kaynak yüklü değil!",
|
||||
"show_extensions": "uzantıları göster",
|
||||
"default_skip_forward_skip_length": "Varsayılan ileri atlama atlama uzunluğu"
|
||||
"default_skip_forward_skip_length": "Varsayılan ileri atlama atlama uzunluğu",
|
||||
"aniskip_requires_info": "AniSkip, çalışması için animenin MAL veya Anilist ile takip edilmesini gerektirir.",
|
||||
"enable_aniskip": "AniSkip'i Etkinleştir",
|
||||
"enable_auto_skip": "Otomatik Atla'yı Etkinleştir",
|
||||
"aniskip_button_timeout": "Düğme Zaman Aşımı"
|
||||
}
|
||||
|
|
@ -276,5 +276,11 @@
|
|||
"updateProgressAfterReading": "阅读后更新进度",
|
||||
"no_sources_installed": "未安装任何来源!",
|
||||
"show_extensions": "显示扩展",
|
||||
"default_skip_forward_skip_length": "默认向前跳过长度"
|
||||
"default_skip_forward_skip_length": "默认向前跳过长度",
|
||||
"aniskip_requires_info": "AniSkip需要跟踪使用MAL或Anilist进行的动漫才能工作。",
|
||||
"enable_aniskip": "启用AniSkip",
|
||||
"enable_auto_skip": "启用自动跳过",
|
||||
"aniskip_button_timeout": "按钮超时",
|
||||
"skip_opening": "跳过开头",
|
||||
"skip_ending": "跳过结尾"
|
||||
}
|
||||
|
|
@ -153,6 +153,12 @@ class Settings {
|
|||
|
||||
bool? updateProgressAfterReading;
|
||||
|
||||
bool? enableAniSkip;
|
||||
|
||||
bool? enableAutoSkip;
|
||||
|
||||
int? aniSkipTimeoutLength;
|
||||
|
||||
Settings(
|
||||
{this.id = 227,
|
||||
this.displayType = DisplayType.compactGrid,
|
||||
|
|
@ -219,7 +225,10 @@ class Settings {
|
|||
this.defaultSkipIntroLength = 85,
|
||||
this.defaultDoubleTapToSkipLength = 10,
|
||||
this.defaultPlayBackSpeed = 1.0,
|
||||
this.updateProgressAfterReading = true});
|
||||
this.updateProgressAfterReading = true,
|
||||
this.enableAniSkip,
|
||||
this.enableAutoSkip,
|
||||
this.aniSkipTimeoutLength});
|
||||
|
||||
Settings.fromJson(Map<String, dynamic> json) {
|
||||
animatePageTransitions = json['animatePageTransitions'];
|
||||
|
|
@ -342,6 +351,9 @@ class Settings {
|
|||
defaultDoubleTapToSkipLength = json['defaultDoubleTapToSkipLength'];
|
||||
defaultPlayBackSpeed = json['defaultPlayBackSpeed'];
|
||||
updateProgressAfterReading = json['updateProgressAfterReading'];
|
||||
enableAniSkip = json['enableAniSkip'];
|
||||
enableAutoSkip = json['enableAutoSkip'];
|
||||
aniSkipTimeoutLength = json['aniSkipTimeoutLength'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
|
@ -432,7 +444,10 @@ class Settings {
|
|||
'defaultSkipIntroLength': defaultSkipIntroLength,
|
||||
'defaultDoubleTapToSkipLength': defaultDoubleTapToSkipLength,
|
||||
'defaultPlayBackSpeed': defaultPlayBackSpeed,
|
||||
'updateProgressAfterReading': updateProgressAfterReading
|
||||
'updateProgressAfterReading': updateProgressAfterReading,
|
||||
'enableAniSkip': enableAniSkip,
|
||||
'enableAutoSkip': enableAutoSkip,
|
||||
'aniSkipTimeoutLength': aniSkipTimeoutLength
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,12 +9,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
|
|||
import 'package:mangayomi/models/chapter.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/mobile.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:mangayomi/services/get_video_list.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
|
@ -211,7 +213,12 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
_currentTotalDuration.value = duration;
|
||||
},
|
||||
);
|
||||
|
||||
Results? _openingResult;
|
||||
Results? _endingResult;
|
||||
bool _hasOpeningSkip = false;
|
||||
bool _hasEndingSkip = false;
|
||||
final ValueNotifier<bool> _showAniSkipOpeningButton = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _showAniSkipEndingButton = ValueNotifier(false);
|
||||
@override
|
||||
void initState() {
|
||||
_setCurrentPosition(true);
|
||||
|
|
@ -220,9 +227,31 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
_player.open(Media(_video.value!.videoTrack!.id,
|
||||
httpHeaders: _video.value!.headers));
|
||||
_setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider));
|
||||
_initAniSkip();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
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() {
|
||||
_setCurrentPosition(true);
|
||||
|
|
@ -838,7 +867,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
skipDuration.toString(),
|
||||
style: const TextStyle(fontSize: 9),
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
),
|
||||
|
|
@ -874,7 +904,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
skipDuration.toString(),
|
||||
style: const TextStyle(fontSize: 9),
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
),
|
||||
|
|
@ -1017,48 +1048,126 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
Widget _videoPlayer(BuildContext context) {
|
||||
final fit = _fit.value;
|
||||
_resize(fit);
|
||||
return Video(
|
||||
subtitleViewConfiguration: const SubtitleViewConfiguration(
|
||||
style: TextStyle(
|
||||
fontSize: 50,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: "",
|
||||
shadows: [Shadow(offset: Offset(0.2, 0.0), blurRadius: 7.0)],
|
||||
backgroundColor: Colors.transparent),
|
||||
),
|
||||
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(),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
Video(
|
||||
subtitleViewConfiguration: const SubtitleViewConfiguration(
|
||||
style: TextStyle(
|
||||
fontSize: 50,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: "",
|
||||
shadows: [Shadow(offset: Offset(0.2, 0.0), blurRadius: 7.0)],
|
||||
backgroundColor: Colors.transparent),
|
||||
),
|
||||
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;
|
||||
},
|
||||
)
|
||||
: MobileControllerWidget(
|
||||
videoController: _controller,
|
||||
topButtonBarWidget: _topButtonBar(context),
|
||||
videoStatekey: _key,
|
||||
bottomButtonBarWidget: _mobileBottomButtonBar(context),
|
||||
streamController: _streamController,
|
||||
),
|
||||
),
|
||||
tempDuration: (value) {
|
||||
_tempPosition.value = value;
|
||||
},
|
||||
)
|
||||
: MobileControllerWidget(
|
||||
videoController: _controller,
|
||||
topButtonBarWidget: _topButtonBar(context),
|
||||
videoStatekey: _key,
|
||||
bottomButtonBarWidget: _mobileBottomButtonBar(context),
|
||||
streamController: _streamController,
|
||||
),
|
||||
controller: _controller,
|
||||
width: context.mediaWidth(1),
|
||||
height: context.mediaHeight(1),
|
||||
resumeUponEnteringForegroundMode: true,
|
||||
controller: _controller,
|
||||
width: context.mediaWidth(1),
|
||||
height: context.mediaHeight(1),
|
||||
resumeUponEnteringForegroundMode: true,
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 80,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _currentPosition,
|
||||
builder: (context, value, child) {
|
||||
if (_hasOpeningSkip || _hasEndingSkip) {
|
||||
if (_hasOpeningSkip) {
|
||||
if (_openingResult!.interval!.startTime!.ceil() <=
|
||||
value.inSeconds &&
|
||||
_openingResult!.interval!.endTime!.toInt() >
|
||||
value.inSeconds) {
|
||||
_showAniSkipOpeningButton.value = true;
|
||||
_showAniSkipEndingButton.value = false;
|
||||
} else {
|
||||
_showAniSkipOpeningButton.value = false;
|
||||
}
|
||||
}
|
||||
if (_hasEndingSkip) {
|
||||
if (_endingResult!.interval!.startTime!.ceil() <=
|
||||
value.inSeconds &&
|
||||
_endingResult!.interval!.endTime!.toInt() >
|
||||
value.inSeconds) {
|
||||
_showAniSkipEndingButton.value = true;
|
||||
_showAniSkipOpeningButton.value = false;
|
||||
}
|
||||
} else {
|
||||
_showAniSkipEndingButton.value = false;
|
||||
}
|
||||
}
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
late final enableAniSkip =
|
||||
ref.watch(enableAniSkipStateProvider);
|
||||
late final enableAutoSkip =
|
||||
ref.watch(enableAutoSkipStateProvider);
|
||||
late final aniSkipTimeoutLength =
|
||||
ref.watch(aniSkipTimeoutLengthStateProvider);
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _showAniSkipOpeningButton,
|
||||
builder: (context, showAniSkipOpENINGButton, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _showAniSkipEndingButton,
|
||||
builder: (context, showAniSkipENDINGButton, child) {
|
||||
return showAniSkipOpENINGButton
|
||||
? Container(
|
||||
key: const Key('skip_opening'),
|
||||
child: AniSkipCountDownButton(
|
||||
active: enableAniSkip,
|
||||
autoSkip: enableAutoSkip,
|
||||
timeoutLength: aniSkipTimeoutLength,
|
||||
skipTypeText: context.l10n.skip_opening,
|
||||
player: _player,
|
||||
aniSkipResult: _openingResult,
|
||||
))
|
||||
: showAniSkipENDINGButton
|
||||
? Container(
|
||||
key: const Key('skip_ending'),
|
||||
child: AniSkipCountDownButton(
|
||||
active: enableAniSkip,
|
||||
autoSkip: enableAutoSkip,
|
||||
timeoutLength: aniSkipTimeoutLength,
|
||||
skipTypeText: context.l10n.skip_ending,
|
||||
player: _player,
|
||||
aniSkipResult: _endingResult,
|
||||
))
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/history.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'anime_player_controller_provider.g.dart';
|
||||
|
||||
|
|
@ -168,5 +171,42 @@ class AnimeStreamController extends _$AnimeStreamController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(int, int)? _getTrackId() {
|
||||
final malId = isar.tracks
|
||||
.filter()
|
||||
.syncIdEqualTo(1)
|
||||
.mangaIdEqualTo(episode.manga.value!.id!)
|
||||
.findFirstSync()
|
||||
?.mediaId;
|
||||
final aniId = isar.tracks
|
||||
.filter()
|
||||
.syncIdEqualTo(2)
|
||||
.mangaIdEqualTo(episode.manga.value!.id!)
|
||||
.findFirstSync()
|
||||
?.mediaId;
|
||||
return switch (malId) {
|
||||
!= null => (malId, 1),
|
||||
== null => switch (aniId) { != null => (aniId, 2), _ => null },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<Results>?> getAniSkipResults(
|
||||
Function(List<Results>) result) async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (ref.watch(enableAniSkipStateProvider)) {
|
||||
final id = _getTrackId();
|
||||
if (id != null) {
|
||||
final res = await ref.read(aniSkipProvider.notifier).getResult(
|
||||
id,
|
||||
ChapterRecognition()
|
||||
.parseChapterNumber(episode.manga.value!.name!, episode.name!),
|
||||
0);
|
||||
result.call(res ?? []);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'anime_player_controller_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$animeStreamControllerHash() =>
|
||||
r'bf0730758333f74352257fb6afd2d0d0a319044f';
|
||||
r'7fc848faf2b337f34561ec4ff519b516ab9954a3';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
139
lib/modules/anime/widgets/aniskip_countdown_btn.dart
Normal file
139
lib/modules/anime/widgets/aniskip_countdown_btn.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
class AniSkipCountDownButton extends ConsumerStatefulWidget {
|
||||
final bool active;
|
||||
final bool autoSkip;
|
||||
final int timeoutLength;
|
||||
final String skipTypeText;
|
||||
final Results? aniSkipResult;
|
||||
final Player player;
|
||||
const AniSkipCountDownButton(
|
||||
{super.key,
|
||||
required this.skipTypeText,
|
||||
required this.aniSkipResult,
|
||||
required this.player,
|
||||
required this.active,
|
||||
required this.autoSkip,
|
||||
required this.timeoutLength});
|
||||
|
||||
@override
|
||||
ConsumerState<AniSkipCountDownButton> createState() =>
|
||||
_AniSkipCountDownButtonState();
|
||||
}
|
||||
|
||||
class _AniSkipCountDownButtonState extends ConsumerState<AniSkipCountDownButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
@override
|
||||
void initState() {
|
||||
_controller = AnimationController(
|
||||
vsync: this, duration: Duration(seconds: widget.timeoutLength))
|
||||
..forward();
|
||||
super.initState();
|
||||
if (widget.active) {
|
||||
if (widget.autoSkip) {
|
||||
_seekTo();
|
||||
} else {
|
||||
_controller.addListener(() {
|
||||
if (_controller.isCompleted) {
|
||||
setState(() {
|
||||
_isCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _seekTo() {
|
||||
setState(() {
|
||||
_isCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
widget.player.seek(
|
||||
Duration(seconds: widget.aniSkipResult!.interval!.endTime!.ceil()));
|
||||
}
|
||||
|
||||
bool _isCompleted = false;
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.active && !widget.autoSkip
|
||||
? _isCompleted
|
||||
? const SizedBox.shrink()
|
||||
: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: MaterialButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
onPressed: () {
|
||||
_seekTo();
|
||||
},
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
width: 200,
|
||||
child: Stack(
|
||||
children: [
|
||||
RotatedBox(
|
||||
quarterTurns: 0,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(200, 40),
|
||||
child: LinearProgressIndicator(
|
||||
color: Colors.red,
|
||||
value: 1 - _controller.value,
|
||||
backgroundColor: Colors.transparent)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
widget.skipTypeText.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text((widget.timeoutLength -
|
||||
(_controller.duration! *
|
||||
_controller.value)
|
||||
.inSeconds)
|
||||
.toString()),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$updateMangaDetailHash() => r'753f7db14ba8284ede82e9c8baec4344b28acedb';
|
||||
String _$updateMangaDetailHash() => r'fae71a657e084879e6bbef7b343291a1f014f790';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ class PlayerScreen extends ConsumerWidget {
|
|||
ref.watch(defaultDoubleTapToSkipLengthStateProvider);
|
||||
final defaultPlayBackSpeed = ref.watch(defaultPlayBackSpeedStateProvider);
|
||||
|
||||
final enableAniSkip = ref.watch(enableAniSkipStateProvider);
|
||||
final enableAutoSkip = ref.watch(enableAutoSkipStateProvider);
|
||||
final aniSkipTimeoutLength = ref.watch(aniSkipTimeoutLengthStateProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.player),
|
||||
|
|
@ -265,6 +269,103 @@ class PlayerScreen extends ConsumerWidget {
|
|||
style: TextStyle(fontSize: 11, color: context.secondaryColor),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
color: context.secondaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Text(context.l10n.aniskip_requires_info,
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: context.secondaryColor)),
|
||||
),
|
||||
ExpansionTile(
|
||||
title: Text(context.l10n.enable_aniskip),
|
||||
shape: const StarBorder(),
|
||||
initiallyExpanded: enableAniSkip,
|
||||
trailing: IgnorePointer(
|
||||
child: Switch(
|
||||
value: enableAniSkip,
|
||||
onChanged: (_) {},
|
||||
),
|
||||
),
|
||||
onExpansionChanged: (value) =>
|
||||
ref.read(enableAniSkipStateProvider.notifier).set(value),
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: enableAutoSkip,
|
||||
title: Text(context.l10n.enable_auto_skip),
|
||||
onChanged: (value) {
|
||||
ref.read(enableAutoSkipStateProvider.notifier).set(value);
|
||||
}),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
final values = [5, 6, 7, 8, 9, 10];
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
context.l10n.default_playback_speed_length),
|
||||
content: SizedBox(
|
||||
width: context.mediaWidth(0.8),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RadioListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
value: values[index],
|
||||
groupValue: aniSkipTimeoutLength,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(
|
||||
aniSkipTimeoutLengthStateProvider
|
||||
.notifier)
|
||||
.set(value!);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
title: Row(
|
||||
children: [Text("${values[index]}s")],
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.cancel,
|
||||
style: TextStyle(
|
||||
color: context.primaryColor),
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
title: Text(context.l10n.aniskip_button_timeout),
|
||||
subtitle: Text(
|
||||
"${aniSkipTimeoutLength}s",
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: context.secondaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -63,3 +63,48 @@ class DefaultPlayBackSpeedState extends _$DefaultPlayBackSpeedState {
|
|||
() => isar.settings.putSync(settings!..defaultPlayBackSpeed = value));
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class EnableAniSkipState extends _$EnableAniSkipState {
|
||||
@override
|
||||
bool build() {
|
||||
return isar.settings.getSync(227)!.enableAniSkip ?? false;
|
||||
}
|
||||
|
||||
void set(bool value) {
|
||||
final settings = isar.settings.getSync(227);
|
||||
state = value;
|
||||
isar.writeTxnSync(
|
||||
() => isar.settings.putSync(settings!..enableAniSkip = value));
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class EnableAutoSkipState extends _$EnableAutoSkipState {
|
||||
@override
|
||||
bool build() {
|
||||
return isar.settings.getSync(227)!.enableAutoSkip ?? false;
|
||||
}
|
||||
|
||||
void set(bool value) {
|
||||
final settings = isar.settings.getSync(227);
|
||||
state = value;
|
||||
isar.writeTxnSync(
|
||||
() => isar.settings.putSync(settings!..enableAutoSkip = value));
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class AniSkipTimeoutLengthState extends _$AniSkipTimeoutLengthState {
|
||||
@override
|
||||
int build() {
|
||||
return isar.settings.getSync(227)!.aniSkipTimeoutLength ?? 5;
|
||||
}
|
||||
|
||||
void set(int value) {
|
||||
final settings = isar.settings.getSync(227);
|
||||
state = value;
|
||||
isar.writeTxnSync(
|
||||
() => isar.settings.putSync(settings!..aniSkipTimeoutLength = value));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,5 +74,56 @@ final defaultPlayBackSpeedStateProvider =
|
|||
);
|
||||
|
||||
typedef _$DefaultPlayBackSpeedState = AutoDisposeNotifier<double>;
|
||||
String _$enableAniSkipStateHash() =>
|
||||
r'1b448453e54f2a261820d40ca2d82971d165372a';
|
||||
|
||||
/// See also [EnableAniSkipState].
|
||||
@ProviderFor(EnableAniSkipState)
|
||||
final enableAniSkipStateProvider =
|
||||
AutoDisposeNotifierProvider<EnableAniSkipState, bool>.internal(
|
||||
EnableAniSkipState.new,
|
||||
name: r'enableAniSkipStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$enableAniSkipStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$EnableAniSkipState = AutoDisposeNotifier<bool>;
|
||||
String _$enableAutoSkipStateHash() =>
|
||||
r'5f4d5e669cadf98396afe52635e2ec5f2ee7ff2f';
|
||||
|
||||
/// See also [EnableAutoSkipState].
|
||||
@ProviderFor(EnableAutoSkipState)
|
||||
final enableAutoSkipStateProvider =
|
||||
AutoDisposeNotifierProvider<EnableAutoSkipState, bool>.internal(
|
||||
EnableAutoSkipState.new,
|
||||
name: r'enableAutoSkipStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$enableAutoSkipStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$EnableAutoSkipState = AutoDisposeNotifier<bool>;
|
||||
String _$aniSkipTimeoutLengthStateHash() =>
|
||||
r'fc1c16c22fb129e1a2ea5434282baf2dcfa79c82';
|
||||
|
||||
/// See also [AniSkipTimeoutLengthState].
|
||||
@ProviderFor(AniSkipTimeoutLengthState)
|
||||
final aniSkipTimeoutLengthStateProvider =
|
||||
AutoDisposeNotifierProvider<AniSkipTimeoutLengthState, int>.internal(
|
||||
AniSkipTimeoutLengthState.new,
|
||||
name: r'aniSkipTimeoutLengthStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$aniSkipTimeoutLengthStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AniSkipTimeoutLengthState = AutoDisposeNotifier<int>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
|
|
|||
98
lib/services/aniskip.dart
Normal file
98
lib/services/aniskip.dart
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'aniskip.g.dart';
|
||||
|
||||
// credits: https://github.com/aniyomiorg/aniyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/util/AniSkipApi.kt
|
||||
@riverpod
|
||||
class AniSkip extends _$AniSkip {
|
||||
final _client = http.Client();
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
Future<List<Results>?> getResult(
|
||||
(int, int) id, int episodeNumber, double episodeLength) async {
|
||||
try {
|
||||
final malId = await _getMalId(id);
|
||||
|
||||
final url =
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength";
|
||||
final response = await _client.get(Uri.parse(url));
|
||||
final res = AniSkipResponse.fromJson(json.decode(response.body));
|
||||
|
||||
return (res.found ?? false) ? res.results : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _getMalId((int, int) id) async {
|
||||
if (id.$2 == 1) return id.$1;
|
||||
if (id.$2 != 2) throw "";
|
||||
|
||||
final query = """
|
||||
query{
|
||||
Media(id:${id.$1}){idMal}
|
||||
}
|
||||
""";
|
||||
|
||||
final response = await _client.post(
|
||||
Uri.parse("https://graphql.anilist.co"),
|
||||
body: json.encode({"query": query}),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
);
|
||||
|
||||
return jsonDecode(response.body)["data"]["Media"]["idMal"] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
class AniSkipResponse {
|
||||
bool? found;
|
||||
List<Results>? results;
|
||||
String? message;
|
||||
int? statusCode;
|
||||
|
||||
AniSkipResponse({this.found, this.results, this.message, this.statusCode});
|
||||
|
||||
factory AniSkipResponse.fromJson(Map<String, dynamic> json) {
|
||||
return AniSkipResponse(
|
||||
found: json['found'],
|
||||
results:
|
||||
(json['results'] as List?)?.map((e) => Results.fromJson(e)).toList(),
|
||||
message: json['message'],
|
||||
statusCode: json['statusCode'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Results {
|
||||
Interval? interval;
|
||||
String? skipType;
|
||||
String? skipId;
|
||||
double? episodeLength;
|
||||
|
||||
Results({this.interval, this.skipType, this.skipId, this.episodeLength});
|
||||
|
||||
factory Results.fromJson(Map<String, dynamic> json) {
|
||||
return Results(
|
||||
interval: Interval.fromJson(json['interval']),
|
||||
skipType: json['skipType'],
|
||||
skipId: json['skipId'],
|
||||
episodeLength: json['episodeLength'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Interval {
|
||||
double? startTime;
|
||||
double? endTime;
|
||||
|
||||
Interval({this.startTime, this.endTime});
|
||||
|
||||
factory Interval.fromJson(Map<String, dynamic> json) {
|
||||
return Interval(
|
||||
startTime: json['startTime'],
|
||||
endTime: json['endTime'],
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/services/aniskip.g.dart
Normal file
24
lib/services/aniskip.g.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'aniskip.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$aniSkipHash() => r'58cfba5bb87e0a5f34009391a204d99bf773c858';
|
||||
|
||||
/// See also [AniSkip].
|
||||
@ProviderFor(AniSkip)
|
||||
final aniSkipProvider = AutoDisposeNotifierProvider<AniSkip, void>.internal(
|
||||
AniSkip.new,
|
||||
name: r'aniSkipProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$aniSkipHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AniSkip = AutoDisposeNotifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
Loading…
Reference in a new issue