mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-23 07:32:18 +00:00
Merge branch 'main' into QoF-webtoon
This commit is contained in:
commit
bc19e2d7cb
46 changed files with 732 additions and 1014 deletions
|
|
@ -39,6 +39,7 @@ import 'package:mangayomi/services/download_manager/m_downloader.dart';
|
|||
import 'package:mangayomi/src/rust/frb_generated.dart';
|
||||
import 'package:mangayomi/utils/discord_rpc.dart';
|
||||
import 'package:mangayomi/utils/log/logger.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:mangayomi/utils/url_protocol/api.dart';
|
||||
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart';
|
||||
import 'package:mangayomi/modules/library/providers/file_scanner.dart';
|
||||
|
|
@ -84,7 +85,7 @@ void main(List<String> args) async {
|
|||
await RustLib.init();
|
||||
await imgCropIsolate.start();
|
||||
await getIsolateService.start();
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
if (!isMobile) {
|
||||
await windowManager.ensureInitialized();
|
||||
await WindowGeometry.restore();
|
||||
}
|
||||
|
|
@ -120,13 +121,10 @@ void main(List<String> args) async {
|
|||
Future<void> _postLaunchInit(StorageProvider storage) async {
|
||||
await AppLogger.init();
|
||||
unawaited(MDownloader.initializeIsolatePool(poolSize: 6));
|
||||
final hivePath = (Platform.isIOS || Platform.isMacOS)
|
||||
? "databases"
|
||||
: p.join("Mangayomi", "databases");
|
||||
final hivePath = isApple ? "databases" : p.join("Mangayomi", "databases");
|
||||
await Hive.initFlutter(Platform.isAndroid ? "" : hivePath);
|
||||
Hive.registerAdapter(TrackSearchAdapter());
|
||||
if ((Platform.isMacOS || Platform.isLinux || Platform.isWindows) &&
|
||||
!kDebugMode) {
|
||||
if (isDesktop && !kDebugMode) {
|
||||
discordRpc = DiscordRPC(applicationId: "1395040506677039157");
|
||||
await discordRpc?.initialize();
|
||||
}
|
||||
|
|
@ -151,9 +149,7 @@ class _MyAppState extends ConsumerState<MyApp>
|
|||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
if (!isMobile) windowManager.addListener(this);
|
||||
initializeDateFormatting();
|
||||
customDns = ref.read(customDnsStateProvider);
|
||||
_checkTrackerRefresh();
|
||||
|
|
@ -210,7 +206,7 @@ class _MyAppState extends ConsumerState<MyApp>
|
|||
builder: (context, child) {
|
||||
child = BotToastInit()(context, child);
|
||||
final appChild = child;
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
if (!isMobile) {
|
||||
child = _MouseBackButtonHandler(router: router, child: appChild);
|
||||
} else {
|
||||
child = appChild;
|
||||
|
|
@ -240,7 +236,7 @@ class _MyAppState extends ConsumerState<MyApp>
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
if (!isMobile) {
|
||||
windowManager.removeListener(this);
|
||||
WindowGeometry.save();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1261,6 +1261,18 @@ enum ReaderMode {
|
|||
horizontalContinuousRTL,
|
||||
}
|
||||
|
||||
extension ReaderModeExtension on ReaderMode {
|
||||
bool get isContinuous => isVerticalContinuous || isHorizontalContinuous;
|
||||
bool get isVertical => this == ReaderMode.vertical || isVerticalContinuous;
|
||||
bool get isVerticalContinuous =>
|
||||
this == ReaderMode.verticalContinuous || this == ReaderMode.webtoon;
|
||||
bool get isHorizontalContinuous =>
|
||||
this == ReaderMode.horizontalContinuous ||
|
||||
this == ReaderMode.horizontalContinuousRTL;
|
||||
bool get isRTL =>
|
||||
this == ReaderMode.rtl || this == ReaderMode.horizontalContinuousRTL;
|
||||
}
|
||||
|
||||
enum NovelTextAlign { left, center, right, block }
|
||||
|
||||
enum PageMode { onePage, doublePage }
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ 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:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:mangayomi/utils/system_ui.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';
|
||||
|
|
@ -56,8 +58,6 @@ import 'package:window_manager/window_manager.dart' show windowManager;
|
|||
|
||||
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});
|
||||
|
|
@ -72,16 +72,13 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
|||
bool desktopFullScreenPlayer = false;
|
||||
@override
|
||||
void dispose() {
|
||||
if (_isDesktop) {
|
||||
if (isDesktop) {
|
||||
setFullScreen(value: desktopFullScreenPlayer);
|
||||
}
|
||||
for (var infoHash in _infoHashList) {
|
||||
MTorrentServer().removeTorrent(infoHash);
|
||||
}
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -129,10 +126,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
|||
title: const Text(''),
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
|
|
@ -148,10 +142,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
|||
leading: BackButton(
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
|
|
@ -316,7 +307,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
discordRpc?.updateChapterTimestamp(_currentPosition.value, duration);
|
||||
});
|
||||
|
||||
bool get hasNextEpisode => _streamController.getEpisodeIndex().$1 != 0;
|
||||
bool get hasNextEpisode => _streamController.hasNextEpisode;
|
||||
|
||||
late final StreamSubscription<bool> _completed = _player.stream.completed
|
||||
.listen((val) {
|
||||
|
|
@ -327,7 +318,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
}
|
||||
// If the last episode of an Anime has ended, exit fullscreen mode
|
||||
final isFullScreen = ref.read(fullscreenProvider);
|
||||
if (!hasNextEpisode && val && _isDesktop && isFullScreen) {
|
||||
if (!hasNextEpisode && val && isDesktop && isFullScreen) {
|
||||
setFullScreen(value: false);
|
||||
ref.read(fullscreenProvider.notifier).state = false;
|
||||
widget.desktopFullScreenPlayer.call(false);
|
||||
|
|
@ -362,6 +353,23 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
}
|
||||
}
|
||||
|
||||
String? _readMpvString(Pointer<generated.mpv_node> value) {
|
||||
if (value.ref.format != generated.mpv_format.MPV_FORMAT_STRING) return null;
|
||||
final text = value.ref.u.string.cast<Utf8>().toDartString();
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
Future<void> _seekTo(int absoluteSeconds) async {
|
||||
_tempPosition.value = Duration(seconds: absoluteSeconds);
|
||||
await _player.seek(Duration(seconds: absoluteSeconds));
|
||||
_tempPosition.value = null;
|
||||
}
|
||||
|
||||
Future<void> _seekBy(int deltaSeconds) async {
|
||||
final pos = _currentPosition.value.inSeconds + deltaSeconds;
|
||||
await _seekTo(pos);
|
||||
}
|
||||
|
||||
Future<void> _handleMpvNodeEvents(
|
||||
String propName,
|
||||
Pointer<generated.mpv_node> value,
|
||||
|
|
@ -369,272 +377,230 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) break;
|
||||
final data = int.parse(text.replaceAll("\"", ""));
|
||||
await _seekBy(data);
|
||||
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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) break;
|
||||
final data = int.parse(text.replaceAll("\"", ""));
|
||||
await _seekTo(data);
|
||||
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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) break;
|
||||
final data = text.split("|");
|
||||
await _seekBy(int.parse(data[0].replaceAll("\"", "")));
|
||||
(_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", "");
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) break;
|
||||
final data = text.split("|");
|
||||
await _seekTo(int.parse(data[0].replaceAll("\"", "")));
|
||||
(_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,
|
||||
final text = _readMpvString(value);
|
||||
if (text == null) 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: [
|
||||
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),
|
||||
),
|
||||
NumberPicker(
|
||||
value: currentValue,
|
||||
minValue: start,
|
||||
maxValue: stop,
|
||||
step: step,
|
||||
haptics: true,
|
||||
textMapper: (numberText) =>
|
||||
data[1].replaceAll("%d", numberText),
|
||||
onChanged: (value) =>
|
||||
setState(() => currentValue = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
nativePlayer.setProperty("user-data/aniyomi/launch_int_picker", "");
|
||||
}
|
||||
),
|
||||
),
|
||||
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) {
|
||||
|
|
@ -653,10 +619,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
}
|
||||
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;
|
||||
}
|
||||
final text = _readMpvString(value);
|
||||
_selectedShader.value = text ?? '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -875,7 +839,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
"$defaultSkipIntroLength",
|
||||
);
|
||||
} catch (_) {}
|
||||
if (_isDesktop && _firstTime) {
|
||||
if (isDesktop && _firstTime) {
|
||||
final globalFullscreen = ref.read(fullScreenPlayerStateProvider);
|
||||
// Delay fullscreen until after the first frame so the window is ready.
|
||||
// On Windows, calling setFullScreen before the widget tree is built
|
||||
|
|
@ -887,7 +851,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
});
|
||||
_firstTime = false;
|
||||
}
|
||||
if (!_isDesktop) {
|
||||
if (!isDesktop) {
|
||||
final forceLandscape = ref.read(forceLandscapePlayerStateProvider);
|
||||
if (forceLandscape) {
|
||||
_setLandscapeMode(true);
|
||||
|
|
@ -1006,7 +970,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
_skipPhase.dispose();
|
||||
_subDelayController.dispose();
|
||||
_subSpeedController.dispose();
|
||||
if (!_isDesktop) _setLandscapeMode(false);
|
||||
if (!isDesktop) _setLandscapeMode(false);
|
||||
discordRpc?.showIdleText();
|
||||
discordRpc?.showOriginalTimestamp();
|
||||
_streamController.keepAliveLink?.close();
|
||||
|
|
@ -1524,21 +1488,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
? 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;
|
||||
},
|
||||
() async => await _seekBy(defaultSkipIntroLength),
|
||||
onLongPress: value?.onLongPress,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
|
@ -1619,11 +1569,6 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
@ -1633,7 +1578,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (hasPrevEpisode)
|
||||
if (_streamController.hasPreviousEpisode)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
pushToNewEpisode(
|
||||
|
|
@ -1643,10 +1588,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
},
|
||||
icon: const Icon(Icons.skip_previous, color: Colors.white),
|
||||
),
|
||||
CustomPlayOrPauseButton(
|
||||
controller: _controller,
|
||||
isDesktop: _isDesktop,
|
||||
),
|
||||
CustomPlayOrPauseButton(controller: _controller),
|
||||
if (hasNextEpisode)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
|
|
@ -1661,19 +1603,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
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;
|
||||
},
|
||||
onPressed: () async => await _seekBy(-skipDuration),
|
||||
icon: Stack(
|
||||
children: [
|
||||
const Positioned.fill(
|
||||
|
|
@ -1705,19 +1635,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
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;
|
||||
},
|
||||
onPressed: () async => await _seekBy(skipDuration),
|
||||
icon: Stack(
|
||||
children: [
|
||||
const Positioned.fill(
|
||||
|
|
@ -1875,7 +1793,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: _isDesktop ? EdgeInsets.zero : const EdgeInsets.all(5),
|
||||
padding: isDesktop ? EdgeInsets.zero : const EdgeInsets.all(5),
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(Icons.video_settings, color: Colors.white),
|
||||
),
|
||||
|
|
@ -1902,7 +1820,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
_changeFitLabel(ref);
|
||||
},
|
||||
),
|
||||
if (_isDesktop)
|
||||
if (isDesktop)
|
||||
CustomMaterialDesktopFullscreenButton(
|
||||
controller: _controller,
|
||||
desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
|
||||
|
|
@ -1926,24 +1844,19 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
final fullScreen = ref.watch(fullscreenProvider);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: !_isDesktop && !fullScreen
|
||||
? MediaQuery.of(context).padding.top
|
||||
: 0,
|
||||
top: !isDesktop && !fullScreen ? MediaQuery.of(context).padding.top : 0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
BackButton(
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
if (_isDesktop && fullScreen) {
|
||||
if (isDesktop && fullScreen) {
|
||||
setFullScreen(value: !fullScreen);
|
||||
ref.read(fullscreenProvider.notifier).state = !fullScreen;
|
||||
widget.desktopFullScreenPlayer.call(!fullScreen);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
}
|
||||
if (mounted) {
|
||||
// Set variable to true, so the player uses the global
|
||||
|
|
@ -2024,7 +1937,10 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
);
|
||||
}
|
||||
|
||||
BoxFit? _lastFit;
|
||||
void _resize(BoxFit fit) async {
|
||||
if (fit == _lastFit) return;
|
||||
_lastFit = fit;
|
||||
// Wait for the widget tree to settle before updating fit
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
if (mounted) {
|
||||
|
|
@ -2052,7 +1968,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
|||
),
|
||||
fit: fit,
|
||||
key: _key,
|
||||
controls: (state) => _isDesktop
|
||||
controls: (state) => isDesktop
|
||||
? DesktopControllerWidget(
|
||||
videoController: _controller,
|
||||
topButtonBarWidget: _topButtonBar(context),
|
||||
|
|
@ -2446,6 +2362,5 @@ mixin _AlwaysOnTopStateMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
// Whether the platform support AlwaysOnTop feature.
|
||||
bool _supportAlwaysOnTop() =>
|
||||
!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows);
|
||||
bool _supportAlwaysOnTop() => !kIsWeb && isDesktop;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.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';
|
||||
|
|
@ -44,6 +44,9 @@ class AnimeStreamController extends _$AnimeStreamController
|
|||
Chapter getPrevEpisode() => getPrevChapter();
|
||||
Chapter getNextEpisode() => getNextChapter();
|
||||
|
||||
bool get hasPreviousEpisode => hasPreviousChapter;
|
||||
bool get hasNextEpisode => hasNextChapter;
|
||||
|
||||
int getEpisodesLength(bool isInFilterList) =>
|
||||
getChaptersLength(isInFilterList);
|
||||
|
||||
|
|
@ -127,7 +130,7 @@ class AnimeStreamController extends _$AnimeStreamController
|
|||
.read(aniSkipProvider.notifier)
|
||||
.getResult(
|
||||
id,
|
||||
ChapterRecognition().parseChapterNumber(
|
||||
ChapterRecognition().parseEpisodeNumber(
|
||||
episode.manga.value!.name!,
|
||||
episode.name!,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/custom_track_shape.dart';
|
||||
|
|
@ -61,7 +61,6 @@ class CustomSeekBarState extends State<CustomSeekBar> {
|
|||
buffer = player.state.buffer;
|
||||
}
|
||||
|
||||
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxValue = max(duration.inMilliseconds.toDouble(), 0).toDouble();
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@ List<Widget> mobilePrimaryButtonBar(
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CustomPlayOrPauseButton(controller: controller, isDesktop: false),
|
||||
CustomPlayOrPauseButton(controller: controller),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: hasNextEpisode
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
|
@ -7,13 +8,8 @@ import 'package:media_kit_video/media_kit_video.dart';
|
|||
/// A material design play/pause button.
|
||||
class CustomPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoController controller;
|
||||
final bool isDesktop;
|
||||
|
||||
const CustomPlayOrPauseButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.isDesktop,
|
||||
});
|
||||
const CustomPlayOrPauseButton({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
CustomPlayOrPauseButtonState createState() => CustomPlayOrPauseButtonState();
|
||||
|
|
@ -29,7 +25,7 @@ class CustomPlayOrPauseButtonState extends State<CustomPlayOrPauseButton>
|
|||
|
||||
StreamSubscription<bool>? subscription;
|
||||
|
||||
double get iconSize => widget.isDesktop ? 25 : 65;
|
||||
double get iconSize => isDesktop ? 25 : 65;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:mangayomi/services/http/m_client.dart';
|
|||
import 'package:mangayomi/services/http/rhttp/src/model/settings.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/log/logger.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: TextFormField(
|
||||
onTap: () {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
if (isMobile) {
|
||||
setState(() {
|
||||
hide = true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import 'package:mangayomi/providers/l10n_providers.dart';
|
|||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/utils/date.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/modules/widgets/error_text.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'library_state_provider.g.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:mangayomi/models/history.dart';
|
|||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
|
||||
class ContinueReaderButton extends ConsumerWidget {
|
||||
final Manga entry;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
|
|
@ -76,7 +76,6 @@ class LibraryAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
),
|
||||
);
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
final isMobile = Platform.isIOS || Platform.isAndroid;
|
||||
|
||||
if (isLongPressed) {
|
||||
return manga.when(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -544,7 +544,7 @@ class _DownloadedOnlyBar extends StatelessWidget {
|
|||
return Material(
|
||||
child: AnimatedContainer(
|
||||
height: downloadedOnly
|
||||
? Platform.isAndroid || Platform.isIOS
|
||||
? isMobile
|
||||
? MediaQuery.of(context).padding.top * 2
|
||||
: 50
|
||||
: 0,
|
||||
|
|
@ -583,7 +583,7 @@ class _IncognitoModeBar extends StatelessWidget {
|
|||
return Material(
|
||||
child: AnimatedContainer(
|
||||
height: incognitoMode
|
||||
? Platform.isAndroid || Platform.isIOS
|
||||
? isMobile
|
||||
? MediaQuery.of(context).padding.top * 2
|
||||
: 50
|
||||
: 0,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ import 'package:mangayomi/modules/library/providers/local_archive.dart';
|
|||
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
|
||||
|
|
@ -107,22 +108,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
late final isLocalArchive = widget.manga!.isLocalArchive ?? false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scanlators = ref.watch(scanlatorsFilterStateProvider(widget.manga!));
|
||||
final reverse = ref
|
||||
.watch(sortChapterStateProvider(mangaId: widget.manga!.id!))
|
||||
.reverse!;
|
||||
final filterUnread = ref.watch(
|
||||
chapterFilterUnreadStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final filterBookmarked = ref.watch(
|
||||
chapterFilterBookmarkedStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final filterDownloaded = ref.watch(
|
||||
chapterFilterDownloadedStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final sortChapter =
|
||||
ref.watch(sortChapterStateProvider(mangaId: widget.manga!.id!)).index
|
||||
as int;
|
||||
// Watch all sort/filter providers so the list rebuilds whenever
|
||||
// the user changes settings in _showDraggableMenu().
|
||||
ref.watch(scanlatorsFilterStateProvider(widget.manga!));
|
||||
ref.watch(sortChapterStateProvider(mangaId: widget.manga!.id!));
|
||||
ref.watch(chapterFilterUnreadStateProvider(mangaId: widget.manga!.id!));
|
||||
ref.watch(chapterFilterBookmarkedStateProvider(mangaId: widget.manga!.id!));
|
||||
ref.watch(chapterFilterDownloadedStateProvider(mangaId: widget.manga!.id!));
|
||||
final chapters = ref.watch(
|
||||
getChaptersStreamProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
|
|
@ -137,134 +129,22 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
return true;
|
||||
},
|
||||
child: chapters.when(
|
||||
data: (data) {
|
||||
List<Chapter> chapters = _filterAndSortChapter(
|
||||
data: data.reversed.toList(),
|
||||
filterUnread: filterUnread,
|
||||
filterBookmarked: filterBookmarked,
|
||||
filterDownloaded: filterDownloaded,
|
||||
sortChapter: sortChapter,
|
||||
filterScanlator: scanlators.$2,
|
||||
);
|
||||
data: (_) {
|
||||
List<Chapter> chapters = widget.manga!.getFilteredChapterList();
|
||||
ref.read(chaptersListttStateProvider.notifier).set(chapters);
|
||||
return _buildWidget(chapters: chapters, reverse: reverse);
|
||||
return _buildWidget(chapters: chapters);
|
||||
},
|
||||
error: (Object error, StackTrace stackTrace) {
|
||||
return ErrorText(error);
|
||||
},
|
||||
loading: () {
|
||||
return _buildWidget(
|
||||
chapters: widget.manga!.chapters.toList().reversed.toList(),
|
||||
reverse: reverse,
|
||||
);
|
||||
return _buildWidget(chapters: widget.manga!.chapters.toList());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Chapter> _getFilteredAndSortedChapters() {
|
||||
final filterScanlator = ref.read(
|
||||
scanlatorsFilterStateProvider(widget.manga!),
|
||||
);
|
||||
final filterUnread = ref.read(
|
||||
chapterFilterUnreadStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final filterBookmarked = ref.read(
|
||||
chapterFilterBookmarkedStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final filterDownloaded = ref.read(
|
||||
chapterFilterDownloadedStateProvider(mangaId: widget.manga!.id!),
|
||||
);
|
||||
final sortChapter =
|
||||
ref.read(sortChapterStateProvider(mangaId: widget.manga!.id!)).index
|
||||
as int;
|
||||
final chapters = isar.chapters
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.mangaIdEqualTo(widget.manga!.id!)
|
||||
.findAllSync();
|
||||
return _filterAndSortChapter(
|
||||
data: chapters,
|
||||
filterUnread: filterUnread,
|
||||
filterBookmarked: filterBookmarked,
|
||||
filterDownloaded: filterDownloaded,
|
||||
sortChapter: sortChapter,
|
||||
filterScanlator: filterScanlator.$2,
|
||||
);
|
||||
}
|
||||
|
||||
List<Chapter> _filterAndSortChapter({
|
||||
required List<Chapter> data,
|
||||
required int filterUnread,
|
||||
required int filterBookmarked,
|
||||
required int filterDownloaded,
|
||||
required int sortChapter,
|
||||
required List<String> filterScanlator,
|
||||
}) {
|
||||
List<Chapter>? chapterList;
|
||||
chapterList = data
|
||||
.where(
|
||||
(element) => filterUnread == 1
|
||||
? element.isRead == false
|
||||
: filterUnread == 2
|
||||
? element.isRead == true
|
||||
: true,
|
||||
)
|
||||
.where(
|
||||
(element) => filterBookmarked == 1
|
||||
? element.isBookmarked == true
|
||||
: filterBookmarked == 2
|
||||
? element.isBookmarked == false
|
||||
: true,
|
||||
)
|
||||
.where((element) {
|
||||
final modelChapDownload = isar.downloads
|
||||
.filter()
|
||||
.idEqualTo(element.id)
|
||||
.findAllSync();
|
||||
return filterDownloaded == 1
|
||||
? modelChapDownload.isNotEmpty &&
|
||||
modelChapDownload.first.isDownload == true
|
||||
: filterDownloaded == 2
|
||||
? !(modelChapDownload.isNotEmpty &&
|
||||
modelChapDownload.first.isDownload == true)
|
||||
: true;
|
||||
})
|
||||
.where((element) => !filterScanlator.contains(element.scanlator))
|
||||
.toList();
|
||||
List<Chapter> chapters = sortChapter == 1
|
||||
? chapterList.reversed.toList()
|
||||
: chapterList;
|
||||
if (sortChapter == 0) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.scanlator == null ||
|
||||
b.scanlator == null ||
|
||||
a.dateUpload == null ||
|
||||
b.dateUpload == null)
|
||||
? 0
|
||||
: a.scanlator!.compareTo(b.scanlator!) |
|
||||
a.dateUpload!.compareTo(b.dateUpload!);
|
||||
});
|
||||
} else if (sortChapter == 2) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.dateUpload == null || b.dateUpload == null)
|
||||
? 0
|
||||
: int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!));
|
||||
});
|
||||
} else if (sortChapter == 3) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.name == null || b.name == null)
|
||||
? 0
|
||||
: a.name!.compareTo(b.name!);
|
||||
});
|
||||
}
|
||||
return chapterList;
|
||||
}
|
||||
|
||||
Widget _buildWidget({
|
||||
required List<Chapter> chapters,
|
||||
required bool reverse,
|
||||
}) {
|
||||
Widget _buildWidget({required List<Chapter> chapters}) {
|
||||
final chapterList = ref.watch(chaptersListStateProvider);
|
||||
final isLongPressed = ref.watch(isLongPressedStateProvider);
|
||||
final checkCategoryList = isar.categorys
|
||||
|
|
@ -492,8 +372,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
final chapters =
|
||||
_getFilteredAndSortedChapters();
|
||||
final chapters = widget.manga!
|
||||
.getFilteredChapterList();
|
||||
if (value == 0 ||
|
||||
value == 1 ||
|
||||
value == 2 ||
|
||||
|
|
@ -549,13 +429,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
ref.watch(processDownloadsProvider());
|
||||
}
|
||||
} else if (value == 4) {
|
||||
final List<Chapter> unreadChapters =
|
||||
_getFilteredAndSortedChapters()
|
||||
.where(
|
||||
(element) =>
|
||||
!(element.isRead ?? false),
|
||||
)
|
||||
.toList();
|
||||
final List<Chapter> unreadChapters = widget
|
||||
.manga!
|
||||
.getFilteredChapterList()
|
||||
.where(
|
||||
(element) => !(element.isRead ?? false),
|
||||
)
|
||||
.toList();
|
||||
isar.chapters
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
|
|
@ -577,8 +457,9 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
}
|
||||
ref.watch(processDownloadsProvider());
|
||||
} else if (value == 5) {
|
||||
final List<Chapter> allChapters =
|
||||
_getFilteredAndSortedChapters();
|
||||
final List<Chapter> allChapters = widget
|
||||
.manga!
|
||||
.getFilteredChapterList();
|
||||
for (var chapter in allChapters) {
|
||||
final entry = isar.downloads
|
||||
.filter()
|
||||
|
|
@ -900,17 +781,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
chapterLength: chapters.length,
|
||||
);
|
||||
}
|
||||
int reverseIndex =
|
||||
chapters.length -
|
||||
chapters.reversed.toList().indexOf(
|
||||
chapters.reversed.toList()[finalIndex],
|
||||
) -
|
||||
1;
|
||||
final indexx = reverse
|
||||
? reverseIndex
|
||||
: finalIndex;
|
||||
return ChapterListTileWidget(
|
||||
chapter: chapters[indexx],
|
||||
chapter: chapters[finalIndex],
|
||||
chapterList: chapterList,
|
||||
allChapters: chapters,
|
||||
sourceExist: widget.sourceExist,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import 'package:mangayomi/utils/constant.dart';
|
|||
import 'package:mangayomi/modules/manga/detail/manga_detail_view.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
|
||||
class MangaDetailsView extends ConsumerStatefulWidget {
|
||||
final Manga manga;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:mangayomi/eval/model/m_bridge.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/update.dart';
|
||||
|
|
@ -20,7 +20,12 @@ Future<dynamic> updateMangaDetail(
|
|||
}) async {
|
||||
try {
|
||||
final manga = isar.mangas.getSync(mangaId!);
|
||||
if ((manga!.isLocalArchive ?? false) ||
|
||||
if (manga == null) return;
|
||||
|
||||
// loadSync() so .isNotEmpty is reliable (IsarLinks are lazy by default).
|
||||
manga.chapters.loadSync();
|
||||
|
||||
if ((manga.isLocalArchive ?? false) ||
|
||||
(manga.chapters.isNotEmpty && isInit)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -30,10 +35,10 @@ Future<dynamic> updateMangaDetail(
|
|||
manga.sourceId,
|
||||
installedOnly: true,
|
||||
);
|
||||
MManga getManga;
|
||||
if (source == null) return;
|
||||
|
||||
getManga = await ref.read(
|
||||
getDetailProvider(url: manga.link!, source: source!).future,
|
||||
final getManga = await ref.read(
|
||||
getDetailProvider(url: manga.link!, source: source).future,
|
||||
);
|
||||
|
||||
final genre =
|
||||
|
|
@ -45,6 +50,8 @@ Future<dynamic> updateMangaDetail(
|
|||
[];
|
||||
|
||||
final imgUrl = getManga.imageUrl.trimmedOrDefault(manga.imageUrl);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
manga
|
||||
..imageUrl = imgUrl == null
|
||||
? null
|
||||
|
|
@ -64,90 +71,124 @@ Future<dynamic> updateMangaDetail(
|
|||
..source = manga.source
|
||||
..lang = manga.lang
|
||||
..itemType = source.itemType
|
||||
..lastUpdate = DateTime.now().millisecondsSinceEpoch
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch;
|
||||
final checkManga = isar.mangas.getSync(mangaId);
|
||||
if (checkManga!.chapters.isNotEmpty && isInit) {
|
||||
return;
|
||||
}
|
||||
isar.writeTxnSync(() {
|
||||
final mangaId = isar.mangas.putSync(manga);
|
||||
manga.lastUpdate = DateTime.now().millisecondsSinceEpoch;
|
||||
..lastUpdate = now
|
||||
..updatedAt = now;
|
||||
|
||||
List<Chapter> chapters = [];
|
||||
final chaps = getManga.chapters;
|
||||
|
||||
final chaps = getManga.chapters;
|
||||
if (chaps!.isNotEmpty && chaps.length > manga.chapters.length) {
|
||||
int newChapsIndex = chaps.length - manga.chapters.length;
|
||||
manga.lastUpdate = DateTime.now().millisecondsSinceEpoch;
|
||||
for (var i = 0; i < newChapsIndex; i++) {
|
||||
final chapter = Chapter(
|
||||
name: chaps[i].name!,
|
||||
url: chaps[i].url!.trim(),
|
||||
dateUpload: chaps[i].dateUpload == null
|
||||
? DateTime.now().millisecondsSinceEpoch.toString()
|
||||
: chaps[i].dateUpload.toString(),
|
||||
scanlator: chaps[i].scanlator ?? '',
|
||||
mangaId: mangaId,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
isFiller: chaps[i].isFiller,
|
||||
thumbnailUrl: chaps[i].thumbnailUrl,
|
||||
description: chaps[i].description,
|
||||
downloadSize: chaps[i].downloadSize,
|
||||
duration: chaps[i].duration,
|
||||
await isar.writeTxn(() async {
|
||||
// Persist updated manga metadata.
|
||||
final savedMangaId = await isar.mangas.put(manga);
|
||||
|
||||
if (chaps == null || chaps.isEmpty) return;
|
||||
|
||||
// loadSync() was called before the transaction; the set is still valid
|
||||
// here because we haven't written to chapters yet.
|
||||
final existingChapters = manga.chapters.toList();
|
||||
final existingByUrl = <String, Chapter>{
|
||||
for (final c in existingChapters)
|
||||
if (c.url?.isNotEmpty == true) c.url!.trim(): c,
|
||||
};
|
||||
|
||||
// Build a chapterNumber -> isRead map so that when a new scanlator covers
|
||||
// a chapter the user has already read, the new entry is pre-marked read.
|
||||
// The value is true if ANY existing chapter at that number is read.
|
||||
final recognition = ChapterRecognition();
|
||||
final readByNumber = <int, bool>{};
|
||||
for (final c in existingChapters) {
|
||||
if (c.name == null) continue;
|
||||
final num = recognition.parseChapterNumber(manga.name ?? '', c.name!);
|
||||
if (num > 0) {
|
||||
readByNumber[num] =
|
||||
(readByNumber[num] ?? false) || (c.isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
final newChapters = <Chapter>[];
|
||||
|
||||
for (final chap in chaps) {
|
||||
final url = chap.url?.trim();
|
||||
if (url == null || url.isEmpty) continue;
|
||||
final existing = existingByUrl[url];
|
||||
|
||||
if (existing == null) {
|
||||
// Determine whether this chapter number has already been read under
|
||||
// a different scanlator, so we don't show it as unread to the user.
|
||||
final chapNum = chap.name != null
|
||||
? recognition.parseChapterNumber(manga.name!, chap.name!)
|
||||
: 0;
|
||||
final alreadyRead = chapNum > 0 && (readByNumber[chapNum] ?? false);
|
||||
|
||||
final newChapter = Chapter(
|
||||
name: chap.name!,
|
||||
url: url,
|
||||
dateUpload: chap.dateUpload == null
|
||||
? now.toString()
|
||||
: chap.dateUpload.toString(),
|
||||
scanlator: chap.scanlator ?? '',
|
||||
mangaId: savedMangaId,
|
||||
updatedAt: now,
|
||||
isFiller: chap.isFiller,
|
||||
thumbnailUrl: chap.thumbnailUrl,
|
||||
description: chap.description,
|
||||
downloadSize: chap.downloadSize,
|
||||
duration: chap.duration,
|
||||
)..manga.value = manga;
|
||||
chapters.add(chapter);
|
||||
}
|
||||
}
|
||||
if (chapters.isNotEmpty) {
|
||||
for (var chap in chapters.reversed.toList()) {
|
||||
isar.chapters.putSync(chap);
|
||||
chap.manga.saveSync();
|
||||
if (manga.chapters.isNotEmpty) {
|
||||
final update = Update(
|
||||
mangaId: mangaId,
|
||||
chapterName: chap.name,
|
||||
date: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
)..chapter.value = chap;
|
||||
isar.updates.putSync(update);
|
||||
update.chapter.saveSync();
|
||||
|
||||
// Carry over read state if another scanlator's version was read.
|
||||
if (alreadyRead) {
|
||||
newChapter.isRead = alreadyRead;
|
||||
newChapter.lastPageRead = "1";
|
||||
}
|
||||
|
||||
newChapters.add(newChapter);
|
||||
} else {
|
||||
// Existing chapter - refresh metadata only.
|
||||
existing
|
||||
..name = chap.name
|
||||
..scanlator = chap.scanlator
|
||||
..updatedAt = now
|
||||
..isFiller = chap.isFiller
|
||||
..thumbnailUrl = chap.thumbnailUrl
|
||||
..description = chap.description
|
||||
..downloadSize = chap.downloadSize
|
||||
..duration = chap.duration;
|
||||
await isar.chapters.put(existing);
|
||||
}
|
||||
}
|
||||
final oldChapers = isar.mangas
|
||||
.getSync(mangaId)!
|
||||
.chapters
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
if (oldChapers.length == chaps.length) {
|
||||
for (var i = 0; i < oldChapers.length; i++) {
|
||||
final oldChap = oldChapers[i];
|
||||
final newChap = chaps[i];
|
||||
oldChap.name = newChap.name;
|
||||
oldChap.url = newChap.url;
|
||||
oldChap.scanlator = newChap.scanlator;
|
||||
oldChap.updatedAt = DateTime.now().millisecondsSinceEpoch;
|
||||
oldChap.isFiller = newChap.isFiller;
|
||||
oldChap.thumbnailUrl = newChap.thumbnailUrl;
|
||||
oldChap.description = newChap.description;
|
||||
oldChap.downloadSize = newChap.downloadSize;
|
||||
oldChap.duration = newChap.duration;
|
||||
isar.chapters.putSync(oldChap);
|
||||
oldChap.manga.saveSync();
|
||||
|
||||
// Insert new chapters oldest-first (API typically returns newest-first).
|
||||
if (newChapters.isNotEmpty) {
|
||||
final hasExisting = existingChapters.isNotEmpty;
|
||||
for (final chap in newChapters.reversed) {
|
||||
await isar.chapters.put(chap);
|
||||
await chap.manga.save();
|
||||
|
||||
// Only create an Update entry for genuinely new (unread) chapters,
|
||||
// so that pre-read cross-scanlator chapters don't spam the updates feed.
|
||||
if (hasExisting && !(chap.isRead ?? false)) {
|
||||
final update = Update(
|
||||
mangaId: savedMangaId,
|
||||
chapterName: chap.name,
|
||||
date: now.toString(),
|
||||
updatedAt: now,
|
||||
)..chapter.value = chap;
|
||||
await isar.updates.put(update);
|
||||
await update.chapter.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Calculate fetch interval:
|
||||
// median of gaps between recent distinct chapter dates, clamped [1, 28].
|
||||
final allChapters = isar.mangas.getSync(mangaId)!.chapters.toList();
|
||||
final allChapters = newChapters.isEmpty
|
||||
? existingChapters
|
||||
: [...existingChapters, ...newChapters];
|
||||
if (allChapters.isNotEmpty) {
|
||||
final interval = FetchInterval.calculateInterval(allChapters);
|
||||
isar.mangas.putSync(
|
||||
manga
|
||||
..id = mangaId
|
||||
..smartUpdateDays = interval,
|
||||
);
|
||||
manga
|
||||
..id = savedMangaId
|
||||
..smartUpdateDays = interval;
|
||||
await isar.mangas.put(manga);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
|
|
@ -156,6 +197,5 @@ Future<dynamic> updateMangaDetail(
|
|||
} else {
|
||||
rethrow;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'package:mangayomi/models/manga.dart';
|
|||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/date.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/download/download_page_widget.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
|
|
@ -210,7 +209,7 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: TextFormField(
|
||||
onTap: () {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
if (isMobile) {
|
||||
setState(() {
|
||||
hide = true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:mangayomi/models/download.dart';
|
|||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/download/providers/download_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/utils/global_style.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import 'package:mangayomi/services/http/m_client.dart';
|
|||
import 'package:mangayomi/services/download_manager/m3u8/m3u8_downloader.dart';
|
||||
import 'package:mangayomi/services/download_manager/m3u8/models/download.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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/utils/extensions/manga.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga_extensions.dart';
|
||||
|
||||
/// Shared navigation and history logic used by [ReaderController],
|
||||
/// [NovelReaderController], and [AnimeStreamController].
|
||||
|
|
@ -33,6 +33,13 @@ mixin ChapterControllerMixin {
|
|||
// (which is more efficient since incognito status never changes mid-session).
|
||||
bool get incognitoMode => isar.settings.getSync(227)!.incognitoMode!;
|
||||
|
||||
bool get hasNextChapter {
|
||||
final idx = getChapterIndex();
|
||||
return idx.$1 < getChaptersLength(idx.$2) - 1;
|
||||
}
|
||||
|
||||
bool get hasPreviousChapter => getChapterIndex().$1 > 0;
|
||||
|
||||
Settings getIsarSetting() => isar.settings.getSync(227)!;
|
||||
|
||||
String getMangaName() => getManga().name!;
|
||||
|
|
@ -44,8 +51,8 @@ mixin ChapterControllerMixin {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
(int, bool) getChapterIndex() => _chapterIndexWithOffset(0);
|
||||
Chapter getPrevChapter() => _chapterWithOffset(1);
|
||||
Chapter getNextChapter() => _chapterWithOffset(-1);
|
||||
Chapter getPrevChapter() => _chapterWithOffset(-1);
|
||||
Chapter getNextChapter() => _chapterWithOffset(1);
|
||||
|
||||
/// Finds this [chapter] in either the filtered list or the raw list and
|
||||
/// returns [index + offset]. The boolean indicates whether the filtered list
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.d
|
|||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/downloads/providers/downloads_state_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'reader_controller_provider.g.dart';
|
||||
|
||||
|
|
@ -187,9 +187,7 @@ class ReaderController extends _$ReaderController
|
|||
if (chapter.isRead! || incognitoMode) return;
|
||||
if (!save && newIndex == _lastSavedIndex) return;
|
||||
_lastSavedIndex = newIndex;
|
||||
final isContinuousLike =
|
||||
getReaderMode() == ReaderMode.verticalContinuous ||
|
||||
getReaderMode() == ReaderMode.webtoon;
|
||||
final isContinuousLike = getReaderMode().isVerticalContinuous;
|
||||
final isRead = isContinuousLike
|
||||
? (newIndex + 2) >= getPageLength([]) - 1
|
||||
: (newIndex + 2) >= getPageLength([]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -37,6 +37,7 @@ import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
|
|||
import 'package:mangayomi/modules/manga/reader/providers/manga_reader_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/image_view_webtoon.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
import 'package:mangayomi/utils/system_ui.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
|
@ -102,10 +103,7 @@ class _MangaReaderViewState extends ConsumerState<MangaReaderView> {
|
|||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (restoreUi) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
|
@ -152,7 +150,6 @@ class _MangaChapterPageGalleryState
|
|||
readerControllerProvider(chapter: chapter).notifier,
|
||||
);
|
||||
|
||||
bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
final ValueNotifier<bool> _isScrolling = ValueNotifier(false);
|
||||
Timer? _scrollIdleTimer;
|
||||
final Stopwatch _readingStopwatch = Stopwatch();
|
||||
|
|
@ -179,6 +176,7 @@ class _MangaChapterPageGalleryState
|
|||
_currentPageDisplayIndex.dispose();
|
||||
_scrollIdleTimer?.cancel();
|
||||
_isScrolling.dispose();
|
||||
_keyboardFocusNode.dispose();
|
||||
_itemPositionsListener.itemPositions.removeListener(_readProgressListener);
|
||||
_photoViewController.dispose();
|
||||
_photoViewScaleStateController.dispose();
|
||||
|
|
@ -189,10 +187,7 @@ class _MangaChapterPageGalleryState
|
|||
} else if (isDesktop) {
|
||||
setFullScreen(value: false);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
}
|
||||
discordRpc?.showIdleText();
|
||||
final actualIdx = _pageViewToActualIndexSync(_currentIndex!);
|
||||
|
|
@ -304,6 +299,7 @@ class _MangaChapterPageGalleryState
|
|||
final _currentReaderMode = StateProvider<ReaderMode?>(() => null);
|
||||
PageMode? _pageMode;
|
||||
bool _isView = false;
|
||||
final _keyboardFocusNode = FocusNode();
|
||||
|
||||
/// Cached reader mode to safely access in dispose without ref.read()
|
||||
ReaderMode? _cachedReaderMode;
|
||||
|
|
@ -351,14 +347,33 @@ class _MangaChapterPageGalleryState
|
|||
ref.read(fullScreenReaderStateProvider.notifier).set(!value!);
|
||||
}
|
||||
|
||||
/// Goes to either next or previous chapter
|
||||
///
|
||||
/// The [next] parameter determines the navigation direction:
|
||||
/// - `true` -> navigate to next chapter
|
||||
/// - `false` -> navigate to previous chapter
|
||||
///
|
||||
/// If the reader is already at the first or last chapter (depending on
|
||||
/// the direction), the method returns without navigating.
|
||||
void _goToChapter(bool next) {
|
||||
if (next && !_readerController.hasNextChapter) return;
|
||||
if (!next && !_readerController.hasPreviousChapter) return;
|
||||
_isNavigatingToChapter = true;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: next
|
||||
? _readerController.getNextChapter()
|
||||
: _readerController.getPrevChapter(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = ref.watch(backgroundColorStateProvider);
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
final readerMode = ref.watch(_currentReaderMode);
|
||||
final bool isHorizontalContinuous =
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
if (readerMode == null) return const SizedBox.shrink();
|
||||
final bool isHorizontalContinuous = readerMode.isHorizontalContinuous;
|
||||
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
return ReaderKeyboardHandler(
|
||||
|
|
@ -366,32 +381,11 @@ class _MangaChapterPageGalleryState
|
|||
onNextPage: () => _handlePageNavigation(forward: true),
|
||||
onEscape: () => _goBack(context),
|
||||
onFullScreen: () => _setFullScreen(),
|
||||
onNextChapter: () {
|
||||
bool hasNextChapter = _readerController.getChapterIndex().$1 != 0;
|
||||
if (hasNextChapter) {
|
||||
_isNavigatingToChapter = true;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getNextChapter(),
|
||||
);
|
||||
}
|
||||
},
|
||||
onPreviousChapter: () {
|
||||
bool hasPrevChapter =
|
||||
_readerController.getChapterIndex().$1 + 1 !=
|
||||
_readerController.getChaptersLength(
|
||||
_readerController.getChapterIndex().$2,
|
||||
);
|
||||
if (hasPrevChapter) {
|
||||
_isNavigatingToChapter = true;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getPrevChapter(),
|
||||
);
|
||||
}
|
||||
},
|
||||
onNextChapter: () => _goToChapter(true),
|
||||
onPreviousChapter: () => _goToChapter(false),
|
||||
).wrapWithKeyboardListener(
|
||||
isReverseHorizontal: _isReverseHorizontal,
|
||||
focusNode: _keyboardFocusNode,
|
||||
child: NotificationListener<UserScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.direction == ScrollDirection.idle) {
|
||||
|
|
@ -411,7 +405,7 @@ class _MangaChapterPageGalleryState
|
|||
builder: (context, failedToLoadImage, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
_isContinuousMode()
|
||||
readerMode.isContinuous
|
||||
? ImageViewWebtoon(
|
||||
pages: pages,
|
||||
itemScrollController: _itemScrollController,
|
||||
|
|
@ -752,7 +746,7 @@ class _MangaChapterPageGalleryState
|
|||
navigationLayout: navigationLayout,
|
||||
isRTL: _isReverseHorizontal,
|
||||
hasImageError: failedToLoadImage,
|
||||
isContinuousMode: _isContinuousMode(),
|
||||
isContinuousMode: readerMode.isContinuous,
|
||||
onToggleUI: _isViewFunction,
|
||||
onPreviousPage: () =>
|
||||
_handlePageNavigation(forward: false),
|
||||
|
|
@ -794,27 +788,10 @@ class _MangaChapterPageGalleryState
|
|||
ReaderBottomBar(
|
||||
chapter: chapter,
|
||||
isVisible: _isView,
|
||||
hasPreviousChapter:
|
||||
_readerController.getChapterIndex().$1 + 1 !=
|
||||
_readerController.getChaptersLength(
|
||||
_readerController.getChapterIndex().$2,
|
||||
),
|
||||
hasNextChapter:
|
||||
_readerController.getChapterIndex().$1 != 0,
|
||||
onPreviousChapter: () {
|
||||
_isNavigatingToChapter = true;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getPrevChapter(),
|
||||
);
|
||||
},
|
||||
onNextChapter: () {
|
||||
_isNavigatingToChapter = true;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getNextChapter(),
|
||||
);
|
||||
},
|
||||
hasPreviousChapter: _readerController.hasPreviousChapter,
|
||||
hasNextChapter: _readerController.hasNextChapter,
|
||||
onPreviousChapter: () => _goToChapter(false),
|
||||
onNextChapter: () => _goToChapter(true),
|
||||
onSliderChanged: (value, ref) {
|
||||
_currentPageDisplayIndex.value = value;
|
||||
ref
|
||||
|
|
@ -918,7 +895,7 @@ class _MangaChapterPageGalleryState
|
|||
formatCurrentIndex: _currentIndexLabel,
|
||||
),
|
||||
ReaderAutoScrollButton(
|
||||
isContinuousMode: _isContinuousMode(),
|
||||
isContinuousMode: readerMode.isContinuous,
|
||||
isUiVisible: _isView,
|
||||
autoScrollPage: _autoScrollPage,
|
||||
autoScroll: _autoScroll,
|
||||
|
|
@ -1485,10 +1462,7 @@ class _MangaChapterPageGalleryState
|
|||
}
|
||||
|
||||
void _goBack(BuildContext context) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
|
@ -1501,10 +1475,7 @@ class _MangaChapterPageGalleryState
|
|||
}
|
||||
if (fullScreenReader) {
|
||||
if (_isView) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
|
|
@ -1569,11 +1540,8 @@ class _MangaChapterPageGalleryState
|
|||
int get _pageViewPageCount =>
|
||||
_isDoublePageActive ? (pages.length / 2).ceil() : pages.length;
|
||||
|
||||
bool _isContinuousMode() {
|
||||
final readerMode = ref.read(_currentReaderMode);
|
||||
return readerMode == ReaderMode.verticalContinuous ||
|
||||
readerMode == ReaderMode.webtoon ||
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
bool _isContinuousMode([ReaderMode? mode]) {
|
||||
final readerMode = mode ?? ref.read(_currentReaderMode);
|
||||
return readerMode!.isContinuous;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class PageNavigationService {
|
|||
}) {
|
||||
if (index < 0) return;
|
||||
|
||||
if (_isContinuousMode(readerMode)) {
|
||||
if (readerMode.isContinuous) {
|
||||
_navigateContinuous(index, animate);
|
||||
} else {
|
||||
_navigatePaged(index, animate);
|
||||
|
|
@ -70,7 +70,7 @@ class PageNavigationService {
|
|||
void jumpToPage({required int index, required ReaderMode readerMode}) {
|
||||
if (index < 0) return;
|
||||
|
||||
if (_isContinuousMode(readerMode)) {
|
||||
if (readerMode.isContinuous) {
|
||||
itemScrollController.jumpTo(index: index);
|
||||
} else {
|
||||
if (extendedController.hasClients) {
|
||||
|
|
@ -104,13 +104,6 @@ class PageNavigationService {
|
|||
extendedController.jumpToPage(index);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isContinuousMode(ReaderMode mode) {
|
||||
return mode == ReaderMode.verticalContinuous ||
|
||||
mode == ReaderMode.webtoon ||
|
||||
mode == ReaderMode.horizontalContinuous ||
|
||||
mode == ReaderMode.horizontalContinuousRTL;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixin to add page navigation capabilities to reader state.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:mangayomi/main.dart';
|
|||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga_extensions.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/date.dart';
|
||||
|
|
|
|||
|
|
@ -17,20 +17,11 @@ class ChapterTransitionPage extends StatelessWidget {
|
|||
required this.readerMode,
|
||||
});
|
||||
|
||||
bool get _isVertical =>
|
||||
readerMode == ReaderMode.vertical ||
|
||||
readerMode == ReaderMode.verticalContinuous ||
|
||||
readerMode == ReaderMode.webtoon;
|
||||
|
||||
bool get _isRTL =>
|
||||
readerMode == ReaderMode.rtl ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: _isVertical
|
||||
child: readerMode.isVertical
|
||||
? _buildVerticalScaffold(context)
|
||||
: _buildHorizontalScaffold(context),
|
||||
);
|
||||
|
|
@ -174,7 +165,9 @@ class ChapterTransitionPage extends StatelessWidget {
|
|||
|
||||
final Widget arrowIcon = Icon(
|
||||
nextChapter != null
|
||||
? (_isRTL ? Icons.keyboard_arrow_left : Icons.keyboard_arrow_right)
|
||||
? (readerMode.isRTL
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right)
|
||||
: Icons.check_circle_outline,
|
||||
size: 36,
|
||||
color: nextChapter != null
|
||||
|
|
@ -214,7 +207,7 @@ class ChapterTransitionPage extends StatelessWidget {
|
|||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _isRTL
|
||||
children: readerMode.isRTL
|
||||
? [
|
||||
Expanded(child: nextCard),
|
||||
const SizedBox(width: 12),
|
||||
|
|
@ -315,10 +308,17 @@ class ChapterTransitionPage extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.last_page,
|
||||
size: 24,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
RotatedBox(
|
||||
quarterTurns: readerMode.isVertical
|
||||
? 1 // turn 90° clockwise, so Icon is pointing down
|
||||
: readerMode.isRTL
|
||||
? 2 // turn 180°, so Icon is pointing left
|
||||
: 0, // no rotation, Icon points to the right.
|
||||
child: Icon(
|
||||
Icons.last_page,
|
||||
size: 24,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:mangayomi/modules/manga/reader/providers/color_filter_provider.d
|
|||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
|
||||
// ── Color matrix utilities (5×4 row-major, 20 elements) ──
|
||||
|
||||
|
|
@ -197,7 +198,7 @@ Widget customColorFilterListTile(
|
|||
Expanded(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: context.isDesktop ? null : 3,
|
||||
trackHeight: isDesktop ? null : 3,
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr
|
|||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:mangayomi/utils/utils.dart';
|
||||
|
||||
/// The app bar for the manga reader.
|
||||
|
|
@ -65,8 +66,6 @@ class ReaderAppBar extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
final isDesktop =
|
||||
Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
final isLocalArchive = chapter.manga.value?.isLocalArchive ?? false;
|
||||
|
||||
double height = isVisible
|
||||
|
|
|
|||
|
|
@ -107,9 +107,8 @@ class ReaderBottomBar extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final readerMode = ref.watch(currentReaderModeProvider);
|
||||
final isHorizontalContinuous =
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
if (readerMode == null) return const SizedBox.shrink();
|
||||
final isHorizontalContinuous = readerMode.isHorizontalContinuous;
|
||||
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
|
|
|
|||
|
|
@ -131,12 +131,6 @@ class _ReadingModeTab extends ConsumerWidget {
|
|||
final showPageGaps = ref.watch(showPageGapsStateProvider);
|
||||
final webtoonSidePadding = ref.watch(webtoonSidePaddingStateProvider);
|
||||
|
||||
final isContinuousMode =
|
||||
readerMode == ReaderMode.verticalContinuous ||
|
||||
readerMode == ReaderMode.webtoon ||
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
|
|
@ -206,7 +200,7 @@ class _ReadingModeTab extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Show Page Gaps (only for continuous modes)
|
||||
if (isContinuousMode)
|
||||
if (readerMode.isContinuous)
|
||||
SwitchListTile(
|
||||
value: showPageGaps,
|
||||
title: Text(
|
||||
|
|
@ -224,7 +218,7 @@ class _ReadingModeTab extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Webtoon Side Padding (only for continuous modes)
|
||||
if (isContinuousMode)
|
||||
if (readerMode.isContinuous)
|
||||
ListTile(
|
||||
title: Text(
|
||||
'${l10n.webtoon_side_padding}: $webtoonSidePadding%',
|
||||
|
|
@ -249,7 +243,7 @@ class _ReadingModeTab extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Auto-scroll (only for continuous modes)
|
||||
if (isContinuousMode)
|
||||
if (readerMode.isContinuous)
|
||||
ValueListenableBuilder(
|
||||
valueListenable: autoScrollPage,
|
||||
builder: (context, valueT, child) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
|
@ -114,16 +114,13 @@ class _CategoriesTabState extends ConsumerState<CategoriesTab>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
final bool _isDesktop =
|
||||
Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
|
||||
/// Moves a category from `index` to `newIndex` in the list,
|
||||
/// swaps their positions in memory, and persists the change in Isar.
|
||||
Future<void> _moveCategory(int index, int newIndex) async {
|
||||
// Prevent invalid moves (out of bounds)
|
||||
if (newIndex < 0 || newIndex >= _entries.length) return;
|
||||
|
||||
if (_isDesktop && mounted) {
|
||||
if (isDesktop && mounted) {
|
||||
setState(() {
|
||||
_animatingFromIndex = index;
|
||||
_animatingToIndex = newIndex;
|
||||
|
|
@ -185,7 +182,7 @@ class _CategoriesTabState extends ConsumerState<CategoriesTab>
|
|||
|
||||
Widget itemWidget = _buildCategoryCard(context, category, index);
|
||||
|
||||
if (_isDesktop &&
|
||||
if (isDesktop &&
|
||||
_animatingFromIndex != null &&
|
||||
_animatingToIndex != null) {
|
||||
if (index == _animatingFromIndex ||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:mangayomi/models/download.dart';
|
|||
import 'package:mangayomi/modules/manga/detail/widgets/custom_floating_action_btn.dart';
|
||||
import 'package:mangayomi/modules/manga/download/providers/download_provider.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/global_style.dart';
|
||||
|
||||
class DownloadQueueScreen extends ConsumerWidget {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -59,12 +58,12 @@ class BrowseSScreen extends ConsumerWidget {
|
|||
ListTile(
|
||||
onTap: () => context.push('/extensionServer'),
|
||||
title: Text(
|
||||
Platform.isAndroid || Platform.isIOS
|
||||
isMobile
|
||||
? l10n.android_proxy_server
|
||||
: l10n.android_proxy_server_mihon,
|
||||
),
|
||||
subtitle: Text(
|
||||
Platform.isAndroid || Platform.isIOS
|
||||
isMobile
|
||||
? l10n.apkbridge_description
|
||||
: l10n.android_proxy_server_mihon_description,
|
||||
style: TextStyle(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:mangayomi/providers/l10n_providers.dart';
|
|||
import 'package:mangayomi/services/fetch_sources_list.dart';
|
||||
import 'package:mangayomi/services/m_extension_server.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
@ -56,14 +57,11 @@ class _ExtensionServerScreenState extends ConsumerState<ExtensionServerScreen> {
|
|||
|
||||
bool get _requiresJre => !Platform.isIOS;
|
||||
|
||||
bool get _showExtensionServerSection =>
|
||||
!Platform.isAndroid && !Platform.isIOS;
|
||||
bool get _showExtensionServerSection => !isMobile;
|
||||
|
||||
bool get _showAndroidProxyServerSection =>
|
||||
Platform.isAndroid || Platform.isIOS;
|
||||
bool get _showAndroidProxyServerSection => isMobile;
|
||||
|
||||
bool get _showDesktopAdvancedApkBridgeSection =>
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||
bool get _showDesktopAdvancedApkBridgeSection => isDesktop;
|
||||
|
||||
bool get _isInstalled => _serverExists && (!_requiresJre || _jreExists);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
|
|
@ -367,7 +366,7 @@ class ReaderScreen extends ConsumerWidget {
|
|||
style: TextStyle(fontSize: 11, color: context.secondaryColor),
|
||||
),
|
||||
),
|
||||
if (!(Platform.isAndroid || Platform.isIOS))
|
||||
if (!isMobile)
|
||||
SwitchListTile(
|
||||
value: fullScreenReader,
|
||||
title: Text(context.l10n.fullscreen),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import 'package:mangayomi/main.dart';
|
|||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/mixins/reader_gestures.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/auto_scroll_button.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/reader_app_bar.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
import 'package:mangayomi/modules/novel/novel_reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/novel/tts/novel_tts_service.dart';
|
||||
|
|
@ -24,6 +26,8 @@ import 'package:mangayomi/providers/l10n_providers.dart';
|
|||
import 'package:mangayomi/services/get_html_content.dart';
|
||||
import 'package:mangayomi/src/rust/api/epub.dart';
|
||||
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:mangayomi/utils/system_ui.dart';
|
||||
import 'package:mangayomi/utils/utils.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
|
@ -72,7 +76,6 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
double offset = 0;
|
||||
double maxOffset = 0;
|
||||
int fontSize = 14;
|
||||
bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
bool get _ttsSupported => !Platform.isLinux;
|
||||
|
||||
final Stopwatch _readingStopwatch = Stopwatch();
|
||||
|
|
@ -99,6 +102,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
_autoScroll.value = false;
|
||||
_autoScroll.dispose();
|
||||
_autoScrollPage.dispose();
|
||||
_keyboardFocusNode.dispose();
|
||||
_ttsIndexSub?.cancel();
|
||||
_ttsStateSub?.cancel();
|
||||
_ttsWordSub?.cancel();
|
||||
|
|
@ -108,10 +112,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
if (isDesktop) {
|
||||
setFullScreen(value: false);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
}
|
||||
discordRpc?.showIdleText();
|
||||
super.dispose();
|
||||
|
|
@ -168,7 +169,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
late bool _isBookmarked = _readerController.getChapterBookmarked();
|
||||
|
||||
bool _isView = false;
|
||||
|
||||
final _keyboardFocusNode = FocusNode();
|
||||
bool _showTts = false;
|
||||
String? _currentHtmlContent;
|
||||
final ValueNotifier<({int paragraph, int wordStart, int wordEnd})>
|
||||
|
|
@ -234,61 +235,35 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
);
|
||||
}
|
||||
|
||||
/// Goes to either next or previous chapter
|
||||
///
|
||||
/// The [next] parameter determines the navigation direction:
|
||||
/// - `true` -> navigate to next chapter
|
||||
/// - `false` -> navigate to previous chapter
|
||||
///
|
||||
/// If the reader is already at the first or last chapter (depending on
|
||||
/// the direction), the method returns without navigating.
|
||||
void _goToChapter(bool next) {
|
||||
if (next && !_readerController.hasNextChapter) return;
|
||||
if (!next && !_readerController.hasPreviousChapter) return;
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: next
|
||||
? _readerController.getNextChapter()
|
||||
: _readerController.getPrevChapter(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = ref.watch(backgroundColorStateProvider);
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
return KeyboardListener(
|
||||
autofocus: true,
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent: (event) {
|
||||
bool isLogicalKeyPressed(LogicalKeyboardKey key) =>
|
||||
HardwareKeyboard.instance.isLogicalKeyPressed(key);
|
||||
bool hasNextChapter = _readerController.getChapterIndex().$1 != 0;
|
||||
bool hasPrevChapter =
|
||||
_readerController.getChapterIndex().$1 + 1 !=
|
||||
_readerController.getChaptersLength(
|
||||
_readerController.getChapterIndex().$2,
|
||||
);
|
||||
final action = switch (event.logicalKey) {
|
||||
LogicalKeyboardKey.f11 =>
|
||||
(!isLogicalKeyPressed(LogicalKeyboardKey.f11))
|
||||
? _setFullScreen()
|
||||
: null,
|
||||
LogicalKeyboardKey.escape =>
|
||||
(!isLogicalKeyPressed(LogicalKeyboardKey.escape))
|
||||
? _goBack(context)
|
||||
: null,
|
||||
LogicalKeyboardKey.backspace =>
|
||||
(!isLogicalKeyPressed(LogicalKeyboardKey.backspace))
|
||||
? _goBack(context)
|
||||
: null,
|
||||
LogicalKeyboardKey.keyN || LogicalKeyboardKey.pageDown =>
|
||||
((!isLogicalKeyPressed(LogicalKeyboardKey.keyN) ||
|
||||
!isLogicalKeyPressed(LogicalKeyboardKey.pageDown)))
|
||||
? switch (hasNextChapter) {
|
||||
true => pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getNextChapter(),
|
||||
),
|
||||
_ => null,
|
||||
}
|
||||
: null,
|
||||
LogicalKeyboardKey.keyP || LogicalKeyboardKey.pageUp =>
|
||||
((!isLogicalKeyPressed(LogicalKeyboardKey.keyP) ||
|
||||
!isLogicalKeyPressed(LogicalKeyboardKey.pageUp)))
|
||||
? switch (hasPrevChapter) {
|
||||
true => pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _readerController.getPrevChapter(),
|
||||
),
|
||||
_ => null,
|
||||
}
|
||||
: null,
|
||||
_ => null,
|
||||
};
|
||||
action;
|
||||
},
|
||||
return ReaderKeyboardHandler(
|
||||
onEscape: () => _goBack(context),
|
||||
onFullScreen: () => _setFullScreen(),
|
||||
onNextChapter: () => _goToChapter(true),
|
||||
onPreviousChapter: () => _goToChapter(false),
|
||||
).wrapWithKeyboardListener(
|
||||
child: NotificationListener<UserScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.direction == ScrollDirection.idle) {
|
||||
|
|
@ -769,7 +744,16 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
_gestureTopBottom(ref.watch(novelTapToScrollStateProvider)),
|
||||
_appBar(),
|
||||
_bottomBar(backgroundColor),
|
||||
_autoScrollPlayPauseBtn(),
|
||||
ReaderAutoScrollButton(
|
||||
isContinuousMode: true,
|
||||
isUiVisible: _isView,
|
||||
autoScrollPage: _autoScrollPage,
|
||||
autoScroll: _autoScroll,
|
||||
onToggle: () {
|
||||
_autoPagescroll();
|
||||
_autoScroll.value = !_autoScroll.value;
|
||||
},
|
||||
),
|
||||
if (_ttsSupported &&
|
||||
_showTts &&
|
||||
_currentHtmlContent != null)
|
||||
|
|
@ -799,32 +783,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _autoScrollPlayPauseBtn() {
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: !_isView
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: _autoScrollPage,
|
||||
builder: (context, valueT, child) => valueT
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: _autoScroll,
|
||||
builder: (context, value, child) => IconButton(
|
||||
onPressed: () {
|
||||
_autoPagescroll();
|
||||
_autoScroll.value = !value;
|
||||
},
|
||||
icon: Icon(
|
||||
value ? Icons.pause_circle : Icons.play_circle,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
focusNode: _keyboardFocusNode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -840,10 +799,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (restoreUi) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
|
@ -854,10 +810,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
}
|
||||
|
||||
void _goBack(BuildContext context) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
|
@ -944,117 +897,49 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
}
|
||||
|
||||
Widget _appBar() {
|
||||
if (!_isView && Platform.isIOS) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
double height = _isView
|
||||
? Platform.isIOS
|
||||
? 120
|
||||
: !fullScreenReader && !isDesktop
|
||||
? 55
|
||||
: 80
|
||||
: 0;
|
||||
return Positioned(
|
||||
top: 0,
|
||||
child: AnimatedContainer(
|
||||
width: context.width(1),
|
||||
height: height,
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height),
|
||||
child: AppBar(
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: ListTile(
|
||||
dense: true,
|
||||
title: SizedBox(
|
||||
width: context.width(0.8),
|
||||
child: Text(
|
||||
'${_readerController.getMangaName()} ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: SizedBox(
|
||||
width: context.width(0.8),
|
||||
child: Text(
|
||||
_readerController.getChapterTitle(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
btnToShowChapterListDialog(
|
||||
context,
|
||||
context.l10n.chapters,
|
||||
widget.chapter,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_readerController.setChapterBookmarked();
|
||||
setState(() {
|
||||
_isBookmarked = !_isBookmarked;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_isBookmarked
|
||||
? Icons.bookmark
|
||||
: Icons.bookmark_border_outlined,
|
||||
),
|
||||
),
|
||||
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final manga = chapter.manga.value!;
|
||||
final source = getSource(
|
||||
manga.lang!,
|
||||
manga.source!,
|
||||
manga.sourceId,
|
||||
)!;
|
||||
String url = chapter.url!.startsWith('/')
|
||||
? "${source.baseUrl}/${chapter.url!}"
|
||||
: chapter.url!;
|
||||
Map<String, dynamic> data = {
|
||||
'url': url,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': chapter.name!,
|
||||
};
|
||||
if (Platform.isLinux) {
|
||||
final urll = Uri.parse(url);
|
||||
if (!await launchUrl(
|
||||
urll,
|
||||
mode: LaunchMode.inAppBrowserView,
|
||||
)) {
|
||||
if (!await launchUrl(
|
||||
urll,
|
||||
mode: LaunchMode.externalApplication,
|
||||
)) {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.push("/mangawebview", extra: data);
|
||||
}
|
||||
return ReaderAppBar(
|
||||
chapter: chapter,
|
||||
mangaName: _readerController.getMangaName(),
|
||||
chapterTitle: _readerController.getChapterTitle(),
|
||||
isVisible: _isView,
|
||||
isBookmarked: _isBookmarked,
|
||||
backgroundColor: _backgroundColor,
|
||||
onBackPressed: () => Navigator.pop(context),
|
||||
onBookmarkPressed: () {
|
||||
_readerController.setChapterBookmarked();
|
||||
setState(() => _isBookmarked = !_isBookmarked);
|
||||
},
|
||||
onWebViewPressed: (chapter.manga.value!.isLocalArchive ?? false)
|
||||
? null
|
||||
: () async {
|
||||
final manga = chapter.manga.value!;
|
||||
final source = getSource(
|
||||
manga.lang!,
|
||||
manga.source!,
|
||||
manga.sourceId,
|
||||
)!;
|
||||
final url = chapter.url!.startsWith('/')
|
||||
? '${source.baseUrl}/${chapter.url!}'
|
||||
: chapter.url!;
|
||||
if (Platform.isLinux) {
|
||||
final uri = Uri.parse(url);
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.inAppBrowserView,
|
||||
).catchError(
|
||||
(_) => launchUrl(uri, mode: LaunchMode.externalApplication),
|
||||
);
|
||||
} else {
|
||||
context.push(
|
||||
'/mangawebview',
|
||||
extra: {
|
||||
'url': url,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': chapter.name!,
|
||||
},
|
||||
icon: const Icon(Icons.public),
|
||||
),
|
||||
],
|
||||
backgroundColor: _backgroundColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1062,12 +947,8 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
if (!_isView && Platform.isIOS) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
bool hasPrevChapter =
|
||||
_readerController.getChapterIndex().$1 + 1 !=
|
||||
_readerController.getChaptersLength(
|
||||
_readerController.getChapterIndex().$2,
|
||||
);
|
||||
bool hasNextChapter = _readerController.getChapterIndex().$1 != 0;
|
||||
bool hasPrevChapter = _readerController.hasPreviousChapter;
|
||||
bool hasNextChapter = _readerController.hasNextChapter;
|
||||
final bodyLargeColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
|
|
@ -1414,10 +1295,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
}
|
||||
if (fullScreenReader) {
|
||||
if (_isView) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
restoreSystemUI();
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/modules/manga/download/download_page_widget.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter_extensions.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
|
||||
class UpdateChapterListTileWidget extends ConsumerWidget {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -295,7 +295,7 @@ class RouterNotifier extends ChangeNotifier {
|
|||
return child!;
|
||||
}
|
||||
},
|
||||
pageBuilder: (Platform.isIOS || Platform.isMacOS)
|
||||
pageBuilder: isApple
|
||||
? (context, state) {
|
||||
final pageChild = builder != null
|
||||
? builder(state.extra as T)
|
||||
|
|
@ -312,7 +312,7 @@ Page transitionPage({required LocalKey key, required child}) {
|
|||
}
|
||||
|
||||
Route createRoute({required Widget page}) {
|
||||
return (Platform.isIOS || Platform.isMacOS)
|
||||
return isApple
|
||||
? CupertinoPageRoute(builder: (context) => page)
|
||||
: MaterialPageRoute(builder: (context) => page);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:m_extension_server/m_extension_server.dart';
|
|||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
|
||||
class MExtensionServerPlatform {
|
||||
WidgetRef ref;
|
||||
|
|
@ -31,7 +32,7 @@ class MExtensionServerPlatform {
|
|||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final port = server.port;
|
||||
await server.close();
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
if (isDesktop) {
|
||||
final settings = isar.settings.getSync(227);
|
||||
final jrePath = settings?.jrePath;
|
||||
final serverJarPath = settings?.extensionServerPath;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:mangayomi/providers/storage_provider.dart';
|
|||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/ffi/torrent_server_ffi.dart' as libmtorrentserver_ffi;
|
||||
import 'package:mangayomi/utils/platform_utils.dart';
|
||||
|
||||
String _buildQueryString(Map<String, List<String>> parameters) {
|
||||
final segments = <String>[];
|
||||
|
|
@ -133,7 +134,7 @@ class MTorrentServer {
|
|||
final path = (await StorageProvider().getBtDirectory())!.path;
|
||||
final config = jsonEncode({"path": path, "address": "127.0.0.1:0"});
|
||||
int port = 0;
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
if (isMobile) {
|
||||
const channel = MethodChannel(
|
||||
'com.kodjodevf.mangayomi.libmtorrentserver',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,50 +1,81 @@
|
|||
class ChapterRecognition {
|
||||
final _numberPattern = r"([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?";
|
||||
|
||||
final _unwanted = RegExp(
|
||||
r"\b(?:v|ver|vol|version|volume|season|s)[^a-z]?[0-9]+",
|
||||
static final _unwanted = RegExp(
|
||||
r"\b(?:v|ver|vol|version|volume|season|staffel|saison|temporada|s)[^a-z]?[0-9]+",
|
||||
);
|
||||
static final _unwantedWhiteSpace = RegExp(r"\s(?=extra|special|omake)");
|
||||
static final _seasonKeyword = RegExp(
|
||||
r"\b(?:staffel|season|saison|temporada)\s*([0-9]+)",
|
||||
);
|
||||
static final _episodeKeyword = RegExp(
|
||||
r"\b(?:folge|episode|ep\.?)\s*([0-9]+(?:\.[0-9]+)?)",
|
||||
);
|
||||
// lookbehind for "ch." then zero or more spaces.
|
||||
static final _chNotation = RegExp(
|
||||
r"(?<=ch\.) *([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?",
|
||||
);
|
||||
static final _bareNumber = RegExp(r"([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?");
|
||||
|
||||
final _unwantedWhiteSpace = RegExp(r"\s(?=extra|special|omake)");
|
||||
/// Sort key for the UI list. Encodes season into the key so multi-season
|
||||
/// anime sort correctly: key = season * 100000 + episode.
|
||||
int parseChapterNumber(String mangaTitle, String chapterName) =>
|
||||
_parse(mangaTitle, chapterName, applySeason: true);
|
||||
|
||||
int parseChapterNumber(String mangaTitle, String chapterName) {
|
||||
var name = chapterName.toLowerCase();
|
||||
/// Episode number within a season, for tracker updates (MAL/AniList/Kitsu)
|
||||
/// and AniSkip results. The tracker entry is already season-specific,
|
||||
/// so season is stripped.
|
||||
int parseEpisodeNumber(String mangaTitle, String chapterName) =>
|
||||
_parse(mangaTitle, chapterName, applySeason: false);
|
||||
|
||||
name = name.replaceAll(mangaTitle.toLowerCase(), "").trim();
|
||||
int _parse(
|
||||
String mangaTitle,
|
||||
String chapterName, {
|
||||
required bool applySeason,
|
||||
}) {
|
||||
// Normalize the chapter name by removing title, punctuation noise, etc.
|
||||
final name = chapterName
|
||||
.toLowerCase()
|
||||
.replaceAll(mangaTitle.toLowerCase(), '')
|
||||
.trim()
|
||||
.replaceAll(',', '.')
|
||||
.replaceAll('-', '.')
|
||||
.replaceAll(_unwantedWhiteSpace, '');
|
||||
|
||||
name = name.replaceAll(',', '.').replaceAll('-', '.');
|
||||
final season = applySeason
|
||||
? int.tryParse(_seasonKeyword.firstMatch(name)?.group(1) ?? '') ?? 0
|
||||
: 0;
|
||||
|
||||
name = name.replaceAll(_unwantedWhiteSpace, "");
|
||||
|
||||
name = name.replaceAll(_unwanted, "");
|
||||
final numberPat = "*$_numberPattern";
|
||||
const ch = r"(?<=ch\.)";
|
||||
var match = RegExp("$ch $numberPat").firstMatch(name);
|
||||
if (match != null) {
|
||||
return _getChapterNumberFromMatch(match).toInt();
|
||||
final epMatch = _episodeKeyword.firstMatch(name);
|
||||
if (epMatch != null) {
|
||||
final ep = double.parse(epMatch.group(1)!).toInt();
|
||||
return _withSeason(season, ep);
|
||||
}
|
||||
|
||||
match = RegExp(_numberPattern).firstMatch(name);
|
||||
if (match != null) {
|
||||
return _getChapterNumberFromMatch(match).toInt();
|
||||
}
|
||||
|
||||
return 0;
|
||||
// strip season/volume noise, then look for ch. or bare number.
|
||||
final stripped = name.replaceAll(_unwanted, '');
|
||||
final ep = _extractNumber(stripped);
|
||||
return ep != null ? _withSeason(season, ep) : 0;
|
||||
}
|
||||
|
||||
double _getChapterNumberFromMatch(Match match) {
|
||||
final initial = double.parse(match.group(1)!);
|
||||
final subChapterDecimal = match.group(2);
|
||||
final subChapterAlpha = match.group(3);
|
||||
final addition = _checkForDecimal(subChapterDecimal, subChapterAlpha);
|
||||
return initial + addition;
|
||||
// Combines season + episode into a sortable integer.
|
||||
int _withSeason(int season, int ep) => season > 0 ? season * 100000 + ep : ep;
|
||||
|
||||
int? _extractNumber(String name) {
|
||||
final chMatch = _chNotation.firstMatch(name);
|
||||
if (chMatch != null) return _fromMatch(chMatch).toInt();
|
||||
|
||||
final numMatch = _bareNumber.firstMatch(name);
|
||||
if (numMatch != null) return _fromMatch(numMatch).toInt();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
double _checkForDecimal(String? decimal, String? alpha) {
|
||||
if (decimal != null && decimal.isNotEmpty) {
|
||||
return double.parse(decimal);
|
||||
}
|
||||
double _fromMatch(Match match) {
|
||||
final base = double.parse(match.group(1)!);
|
||||
return base + _decimalAddition(match.group(2), match.group(3));
|
||||
}
|
||||
|
||||
double _decimalAddition(String? decimal, String? alpha) {
|
||||
if (decimal != null && decimal.isNotEmpty) return double.parse(decimal);
|
||||
if (alpha != null && alpha.isNotEmpty) {
|
||||
if (alpha.contains("extra")) {
|
||||
return 0.99;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension BuildContextExtensions on BuildContext {
|
||||
|
|
@ -23,14 +21,6 @@ extension BuildContextExtensions on BuildContext {
|
|||
return isLight ? Colors.white : Colors.black;
|
||||
}
|
||||
|
||||
bool get isDesktop {
|
||||
return Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
}
|
||||
|
||||
bool get isMobile {
|
||||
return Platform.isIOS || Platform.isAndroid;
|
||||
}
|
||||
|
||||
Color get textColor {
|
||||
return themeData.textTheme.bodyLarge!.color!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:mangayomi/models/track.dart';
|
|||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga_extensions.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/download_manager/download_isolate_pool.dart';
|
||||
|
|
@ -96,7 +96,7 @@ extension ChapterExtension on Chapter {
|
|||
);
|
||||
if (!updateProgressAfterReading) return;
|
||||
final manga = this.manga.value!;
|
||||
final chapterNumber = ChapterRecognition().parseChapterNumber(
|
||||
final chapterNumber = ChapterRecognition().parseEpisodeNumber(
|
||||
manga.name!,
|
||||
name!,
|
||||
);
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/download.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
|
||||
extension MangaExtensions on Manga {
|
||||
// ── For the READER: always ascending story order, filters applied ──────────
|
||||
|
|
@ -32,12 +33,21 @@ extension MangaExtensions on Manga {
|
|||
.type!;
|
||||
|
||||
final scanlators = settings.filterScanlatorList ?? [];
|
||||
final filter = scanlators.where((e) => e.mangaId == id).toList();
|
||||
final filter = scanlators.where((e) => e.mangaId == id);
|
||||
final filterScanlator = filter.firstOrNull?.scanlators ?? [];
|
||||
final recognition = ChapterRecognition();
|
||||
final mangaTitle = name ?? '';
|
||||
|
||||
// Canonical ascending order (ch1 ... chN) — reader always moves forward.
|
||||
final data = chapters
|
||||
.toList(); // keep DB/insertion order, assumed ascending
|
||||
// Memoize so each chapter name is parsed at most once during the sort.
|
||||
final numCache = <int?, int>{};
|
||||
int chapNum(Chapter c) => numCache[c.id] ??= recognition.parseChapterNumber(
|
||||
mangaTitle,
|
||||
c.name ?? '',
|
||||
);
|
||||
|
||||
// Sort by chapter number — DB insertion order is NOT guaranteed to be ascending
|
||||
final data = chapters.toList()
|
||||
..sort((a, b) => chapNum(a).compareTo(chapNum(b)));
|
||||
|
||||
final chapterIds = data.map((c) => c.id).whereType<int>().toList();
|
||||
final downloadedIds = (filterDownloaded == 0 || chapterIds.isEmpty)
|
||||
|
|
@ -71,8 +81,6 @@ extension MangaExtensions on Manga {
|
|||
return filterDownloaded == 1 ? dl : !dl;
|
||||
})
|
||||
.where((e) => !filterScanlator.contains(e.scanlator))
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +90,7 @@ extension MangaExtensions on Manga {
|
|||
|
||||
final sortChapterEntry =
|
||||
settings.sortChapterList!.where((e) => e.mangaId == id).firstOrNull ??
|
||||
SortChapter(mangaId: id, index: 1, reverse: false);
|
||||
SortChapter(mangaId: id, index: 1);
|
||||
final sortIndex = sortChapterEntry.index!;
|
||||
final reverse = sortChapterEntry.reverse!;
|
||||
|
||||
|
|
@ -90,19 +98,23 @@ extension MangaExtensions on Manga {
|
|||
List<Chapter> list = getChapterListForReading();
|
||||
|
||||
switch (sortIndex) {
|
||||
case 0: // by scanlator, then date
|
||||
case 0: // by scanlator, then chapter number
|
||||
// Cache recognition instance — parseChapterNumber is called O(n log n)
|
||||
// times during sort, so avoid constructing it inside the comparator.
|
||||
final recognition = ChapterRecognition();
|
||||
final mangaTitle = name ?? '';
|
||||
|
||||
// Returns the parsed chapter number for a chapter, used as the primary
|
||||
// numeric sort key for cases 0 and 1.
|
||||
final numCache = <int?, int>{};
|
||||
int chapNum(Chapter c) => numCache[c.id] ??= recognition
|
||||
.parseChapterNumber(mangaTitle, c.name ?? '');
|
||||
list.sort((a, b) {
|
||||
if (a.scanlator == null || b.scanlator == null) return 0;
|
||||
final s = a.scanlator!.compareTo(b.scanlator!);
|
||||
final s = (a.scanlator ?? '').compareTo(b.scanlator ?? '');
|
||||
if (s != 0) return s;
|
||||
if (a.dateUpload == null || b.dateUpload == null) return 0;
|
||||
return (int.tryParse(a.dateUpload!) ?? 0).compareTo(
|
||||
int.tryParse(b.dateUpload!) ?? 0,
|
||||
);
|
||||
return chapNum(a).compareTo(chapNum(b));
|
||||
});
|
||||
break;
|
||||
case 1: // by chapter number - reading list is already ascending
|
||||
break;
|
||||
case 2: // by upload date
|
||||
list.sort((a, b) {
|
||||
if (a.dateUpload == null || b.dateUpload == null) return 0;
|
||||
|
|
@ -117,8 +129,12 @@ extension MangaExtensions on Manga {
|
|||
return a.name!.compareTo(b.name!);
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
// getChapterListForReading already sorted by chapter number; nothing to do.
|
||||
break;
|
||||
}
|
||||
|
||||
return reverse ? list.reversed.toList() : list;
|
||||
return reverse ? list : list.reversed.toList();
|
||||
}
|
||||
}
|
||||
11
lib/utils/platform_utils.dart
Normal file
11
lib/utils/platform_utils.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
/// macOS, Linux or Windows
|
||||
final bool isDesktop =
|
||||
Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
|
||||
/// Android or iOS
|
||||
final bool isMobile = Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
/// macOS or iOS
|
||||
final bool isApple = Platform.isMacOS || Platform.isIOS;
|
||||
6
lib/utils/system_ui.dart
Normal file
6
lib/utils/system_ui.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
void restoreSystemUI() => SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
Loading…
Reference in a new issue