feat: Added AniSkip feature

This commit is contained in:
kodjomoustapha 2024-01-11 14:31:01 +01:00
parent 9ee32a2380
commit 7c2fc50a47
25 changed files with 1288 additions and 340 deletions

View file

@ -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": "مهلة زر"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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": "Тайм-аут кнопки"
}

View file

@ -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ı"
}

View file

@ -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": "跳过结尾"
}

View file

@ -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

View file

@ -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();
},
);
},
);
});
},
),
),
],
);
}

View file

@ -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;
}
}

View file

@ -7,7 +7,7 @@ part of 'anime_player_controller_provider.dart';
// **************************************************************************
String _$animeStreamControllerHash() =>
r'bf0730758333f74352257fb6afd2d0d0a319044f';
r'7fc848faf2b337f34561ec4ff519b516ab9954a3';
/// Copied from Dart SDK
class _SystemHash {

View 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();
}
}

View file

@ -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 {

View file

@ -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),
),
),
],
),
],
),
),

View file

@ -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));
}
}

View file

@ -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
View 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'],
);
}
}

View 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