Merge remote-tracking branch 'upstream/main' into Correct-directory

This commit is contained in:
NBA2K1 2025-11-08 14:59:23 +01:00
commit 1b64f2650e
65 changed files with 4717 additions and 2224 deletions

View file

@ -160,9 +160,9 @@ class Parser {
} else {
// attribute without value (e.g. `disabled`)
if (this.options.onattribute) {
this.options.onattribute(attrName, null);
this.options.onattribute(attrName, "");
}
attrs[attrName] = null;
attrs[attrName] = "";
attrName = '';
}
}

View file

@ -22,6 +22,7 @@ import 'package:mangayomi/models/track.dart' as track;
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/more/settings/general/providers/general_state_provider.dart';
@ -31,6 +32,7 @@ import 'package:mangayomi/router/router.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/discord_rpc.dart';
import 'package:mangayomi/utils/log/logger.dart';
@ -52,6 +54,8 @@ void main(List<String> args) async {
if (Platform.isLinux && runWebViewTitleBarWidget(args)) return;
MediaKit.ensureInitialized();
await RustLib.init();
await imgCropIsolate.start();
await getIsolateService.start();
if (!(Platform.isAndroid || Platform.isIOS)) {
await windowManager.ensureInitialized();
}

View file

@ -244,6 +244,22 @@ class Settings {
@enumerated
late NovelTextAlign novelTextAlign;
String? novelReaderTheme;
String? novelReaderTextColor;
int? novelReaderPadding;
double? novelReaderLineHeight;
bool? novelShowScrollPercentage;
bool? novelAutoScroll;
bool? novelRemoveExtraParagraphSpacing;
bool? novelTapToScroll;
List<String>? navigationOrder;
List<String>? hideItems;
@ -392,6 +408,14 @@ class Settings {
this.novelDisplayType = DisplayType.comfortableGrid,
this.novelFontSize = 14,
this.novelTextAlign = NovelTextAlign.left,
this.novelReaderTheme = '#292832',
this.novelReaderTextColor = '#CCCCCC',
this.novelReaderPadding = 16,
this.novelReaderLineHeight = 1.5,
this.novelShowScrollPercentage = true,
this.novelAutoScroll = false,
this.novelRemoveExtraParagraphSpacing = false,
this.novelTapToScroll = false,
this.navigationOrder,
this.hideItems,
this.clearChapterCacheOnAppLaunch = false,
@ -605,6 +629,22 @@ class Settings {
}
novelTextAlign = NovelTextAlign
.values[json['novelTextAlign'] ?? NovelTextAlign.left.index];
if (json['novelReaderTheme'] != null) {
novelReaderTheme = json['novelReaderTheme'];
}
if (json['novelReaderTextColor'] != null) {
novelReaderTextColor = json['novelReaderTextColor'];
}
if (json['novelReaderPadding'] != null) {
novelReaderPadding = json['novelReaderPadding'];
}
if (json['novelReaderLineHeight'] != null) {
novelReaderLineHeight = json['novelReaderLineHeight'];
}
novelShowScrollPercentage = json['novelShowScrollPercentage'];
novelAutoScroll = json['novelAutoScroll'];
novelRemoveExtraParagraphSpacing = json['novelRemoveExtraParagraphSpacing'];
novelTapToScroll = json['novelTapToScroll'];
if (json['navigationOrder'] != null) {
navigationOrder = (json['navigationOrder'] as List).cast<String>();
}
@ -783,6 +823,14 @@ class Settings {
'novelDisplayType': novelDisplayType.index,
'novelFontSize': novelFontSize,
'novelTextAlign': novelTextAlign.index,
'novelReaderTheme': novelReaderTheme,
'novelReaderTextColor': novelReaderTextColor,
'novelReaderPadding': novelReaderPadding,
'novelReaderLineHeight': novelReaderLineHeight,
'novelShowScrollPercentage': novelShowScrollPercentage,
'novelAutoScroll': novelAutoScroll,
'novelRemoveExtraParagraphSpacing': novelRemoveExtraParagraphSpacing,
'novelTapToScroll': novelTapToScroll,
'navigationOrder': navigationOrder,
'hideItems': hideItems,
'clearChapterCacheOnAppLaunch': clearChapterCacheOnAppLaunch,

File diff suppressed because it is too large Load diff

View file

@ -59,7 +59,7 @@ final class AnimeStreamControllerProvider
}
String _$animeStreamControllerHash() =>
r'486889b2b9f71759e4d9ff147b039436572cc01e';
r'1bca3ada0f7919439500ce8c42fa39958c1c5a7b';
final class AnimeStreamControllerFamily extends $Family
with

View file

@ -260,7 +260,7 @@ final class MangaFilterDownloadedStateProvider
}
String _$mangaFilterDownloadedStateHash() =>
r'6d84bc7063be1734a0c267906a94e6b70e8b72fe';
r'7ede8df99996399e368f5074dc1b3d4d7fa5e649';
final class MangaFilterDownloadedStateFamily extends $Family
with
@ -379,7 +379,7 @@ final class MangaFilterUnreadStateProvider
}
String _$mangaFilterUnreadStateHash() =>
r'bd96c9f42a40d0610788feda3bee5fb8662afe50';
r'2bcea3aaccd923e415738d51511c0966a93a2900';
final class MangaFilterUnreadStateFamily extends $Family
with

View file

@ -40,4 +40,4 @@ final class MigrationProvider
}
}
String _$migrationHash() => r'2a82120544e693a3162da887a3ca1b3066f3799f';
String _$migrationHash() => r'43d62ddf79798d616ac7d11ce50a47551ef42c98';

View file

@ -1937,11 +1937,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
children: [
Expanded(child: widget.action!),
if (!isLocalArchive) Expanded(child: _smartUpdateDays()),
Expanded(
child: widget.itemType == ItemType.novel
? SizedBox.shrink()
: _action(),
),
if (widget.itemType != ItemType.novel) Expanded(child: _action()),
if (!isLocalArchive)
Expanded(
child: SizedBox(

View file

@ -163,9 +163,16 @@ class _MangaDetailsViewState extends ConsumerState<MangaDetailsView> {
titleDescription: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.manga.author ?? "Unknown",
style: const TextStyle(fontWeight: FontWeight.w500),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Icon(Icons.person_outline, size: 14),
const SizedBox(width: 4),
Text(
widget.manga.author ?? l10n.unknown,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,

View file

@ -701,7 +701,7 @@ final class ChapterSetIsBookmarkStateProvider
}
String _$chapterSetIsBookmarkStateHash() =>
r'091d86aebaef46d2e9f35ae9f98c12c3e423f5b3';
r'23b56105244d0aeed6ae9c27cee1897de8a306af';
final class ChapterSetIsBookmarkStateFamily extends $Family
with
@ -800,7 +800,7 @@ final class ChapterSetIsReadStateProvider
}
String _$chapterSetIsReadStateHash() =>
r'f5af852964964170905278d563fdb03eabed53b9';
r'b75796ed2dd03bf3167258bcdf064817e8fa69c9';
final class ChapterSetIsReadStateFamily extends $Family
with $ClassFamilyOverride<ChapterSetIsReadState, void, void, void, Manga> {
@ -893,7 +893,7 @@ final class ChapterSetDownloadStateProvider
}
String _$chapterSetDownloadStateHash() =>
r'2f35d274b76e28376b0089b2f6ee6d9d7ebcbeec';
r'cb89abd653c018b762eb405634c7f8ca0ee8e99b';
final class ChapterSetDownloadStateFamily extends $Family
with
@ -969,7 +969,7 @@ final class ChaptersListttStateProvider
}
String _$chaptersListttStateHash() =>
r'5f1b0d2be32fcb904c12c5735f1340c8b33400a9';
r'f45ebd9a5b1fd86b279e263813098564830c2536';
abstract class _$ChaptersListttState extends $Notifier<List<Chapter>> {
List<Chapter> build();
@ -1045,7 +1045,7 @@ final class ScanlatorsFilterStateProvider
}
String _$scanlatorsFilterStateHash() =>
r'8da89864801cd7620029d28cfb3f9bee3c67cba8';
r'f5220568e29e0c0efaac862fb0dce166f7be3172';
final class ScanlatorsFilterStateFamily extends $Family
with

View file

@ -1,4 +1,3 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
@ -21,7 +20,7 @@ class TrackState extends _$TrackState {
Track build({
Track? track,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {
return track!;
}

View file

@ -15,7 +15,7 @@ const trackStateProvider = TrackStateFamily._();
final class TrackStateProvider extends $NotifierProvider<TrackState, Track> {
const TrackStateProvider._({
required TrackStateFamily super.from,
required ({Track? track, ItemType? itemType, WidgetRef widgetRef})
required ({Track? track, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class TrackStateProvider extends $NotifierProvider<TrackState, Track> {
}
}
String _$trackStateHash() => r'cd19c5662338c7f0e508cf2f99e89c21f146d664';
String _$trackStateHash() => r'c3e386652db112f64ce5605afeb5e7a49afbc397';
final class TrackStateFamily extends $Family
with
@ -67,7 +67,7 @@ final class TrackStateFamily extends $Family
Track,
Track,
Track,
({Track? track, ItemType? itemType, WidgetRef widgetRef})
({Track? track, ItemType? itemType, dynamic widgetRef})
> {
const TrackStateFamily._()
: super(
@ -81,7 +81,7 @@ final class TrackStateFamily extends $Family
TrackStateProvider call({
Track? track,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => TrackStateProvider._(
argument: (track: track, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class TrackStateFamily extends $Family
abstract class _$TrackState extends $Notifier<Track> {
late final _$args =
ref.$arg as ({Track? track, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({Track? track, ItemType? itemType, dynamic widgetRef});
Track? get track => _$args.track;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
Track build({
Track? track,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -19,7 +19,7 @@ class ListTileChapterSort extends StatelessWidget {
iconColor: Theme.of(context).primaryColor,
dense: true,
leading: Icon(
reverse ? Icons.arrow_downward_sharp : Icons.arrow_upward_sharp,
!reverse ? Icons.arrow_downward_sharp : Icons.arrow_upward_sharp,
color: showLeading
? Theme.of(context).primaryColor
: Colors.transparent,

View file

@ -0,0 +1,555 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
typedef StringCallback = void Function(String value);
class ExpandableText extends StatefulWidget {
const ExpandableText(
this.text, {
super.key,
required this.expandText,
this.collapseText,
this.expanded = false,
this.onExpandedChanged,
this.onLinkTap,
this.linkColor,
this.linkEllipsis = true,
this.linkStyle,
this.prefixText,
this.prefixStyle,
this.onPrefixTap,
this.urlStyle,
this.onUrlTap,
this.hashtagStyle,
this.onHashtagTap,
this.mentionStyle,
this.onMentionTap,
this.expandOnTextTap = false,
this.collapseOnTextTap = false,
this.style,
this.textDirection,
this.textAlign,
this.textScaler,
this.maxLines = 2,
this.animation = false,
this.animationDuration,
this.animationCurve,
this.semanticsLabel,
this.showGradientOverlay = false,
this.gradientOverlayHeight = 30.0,
this.showExpandCollapseIcon = false,
this.expandIcon,
this.collapseIcon,
}) : assert(maxLines > 0);
final String text;
final String expandText;
final String? collapseText;
final bool expanded;
final ValueChanged<bool>? onExpandedChanged;
final VoidCallback? onLinkTap;
final Color? linkColor;
final bool linkEllipsis;
final TextStyle? linkStyle;
final String? prefixText;
final TextStyle? prefixStyle;
final VoidCallback? onPrefixTap;
final TextStyle? urlStyle;
final StringCallback? onUrlTap;
final TextStyle? hashtagStyle;
final StringCallback? onHashtagTap;
final TextStyle? mentionStyle;
final StringCallback? onMentionTap;
final bool expandOnTextTap;
final bool collapseOnTextTap;
final TextStyle? style;
final TextDirection? textDirection;
final TextAlign? textAlign;
final TextScaler? textScaler;
final int maxLines;
final bool animation;
final Duration? animationDuration;
final Curve? animationCurve;
final String? semanticsLabel;
final bool showGradientOverlay;
final double gradientOverlayHeight;
final bool showExpandCollapseIcon;
final IconData? expandIcon;
final IconData? collapseIcon;
@override
ExpandableTextState createState() => ExpandableTextState();
}
class ExpandableTextState extends State<ExpandableText>
with TickerProviderStateMixin {
bool _expanded = false;
late TapGestureRecognizer _linkTapGestureRecognizer;
late TapGestureRecognizer _prefixTapGestureRecognizer;
List<TextSegment> _textSegments = [];
final List<TapGestureRecognizer> _textSegmentsTapGestureRecognizers = [];
@override
void initState() {
super.initState();
_expanded = widget.expanded;
_linkTapGestureRecognizer = TapGestureRecognizer()..onTap = _linkTapped;
_prefixTapGestureRecognizer = TapGestureRecognizer()..onTap = _prefixTapped;
_updateText();
}
@override
void didUpdateWidget(ExpandableText oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.text != widget.text ||
oldWidget.onUrlTap != widget.onUrlTap ||
oldWidget.onHashtagTap != widget.onHashtagTap ||
oldWidget.onMentionTap != widget.onMentionTap) {
_updateText();
}
}
@override
void dispose() {
_linkTapGestureRecognizer.dispose();
_prefixTapGestureRecognizer.dispose();
for (var recognizer in _textSegmentsTapGestureRecognizers) {
recognizer.dispose();
}
super.dispose();
}
void _linkTapped() {
if (widget.onLinkTap != null) {
widget.onLinkTap!();
return;
}
final toggledExpanded = !_expanded;
setState(() => _expanded = toggledExpanded);
widget.onExpandedChanged?.call(toggledExpanded);
}
void _prefixTapped() {
widget.onPrefixTap?.call();
}
@override
Widget build(BuildContext context) {
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
var effectiveTextStyle = widget.style;
if (widget.style == null || widget.style!.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
}
final linkText =
(_expanded ? widget.collapseText : widget.expandText) ?? '';
final linkColor =
widget.linkColor ??
widget.linkStyle?.color ??
Theme.of(context).colorScheme.secondary;
final linkTextStyle = effectiveTextStyle!
.merge(widget.linkStyle)
.copyWith(color: linkColor);
final prefixText =
widget.prefixText != null && widget.prefixText!.isNotEmpty
? '${widget.prefixText} '
: '';
final link = TextSpan(
children: [
if (!_expanded)
TextSpan(
text: '\u2026 ',
style: widget.linkEllipsis ? linkTextStyle : effectiveTextStyle,
recognizer: widget.linkEllipsis ? _linkTapGestureRecognizer : null,
),
if (linkText.isNotEmpty)
TextSpan(
style: effectiveTextStyle,
children: <TextSpan>[
if (_expanded) const TextSpan(text: ' '),
TextSpan(
text: linkText,
style: linkTextStyle,
recognizer: _linkTapGestureRecognizer,
),
],
),
],
);
final prefix = TextSpan(
text: prefixText,
style: effectiveTextStyle.merge(widget.prefixStyle),
recognizer: _prefixTapGestureRecognizer,
);
final text = _textSegments.isNotEmpty
? TextSpan(
children: _buildTextSpans(_textSegments, effectiveTextStyle, null),
)
: TextSpan(text: widget.text);
final content = TextSpan(
children: <TextSpan>[prefix, text],
style: effectiveTextStyle,
);
Widget result = LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
assert(constraints.hasBoundedWidth);
final double maxWidth = constraints.maxWidth;
final textAlign =
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start;
final textDirection =
widget.textDirection ?? Directionality.of(context);
final textScaler =
widget.textScaler ?? MediaQuery.textScalerOf(context);
final locale = Localizations.maybeLocaleOf(context);
TextPainter textPainter = TextPainter(
text: link,
textAlign: textAlign,
textDirection: textDirection,
textScaler: textScaler,
maxLines: widget.maxLines,
locale: locale,
);
textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
final linkSize = textPainter.size;
textPainter.text = content;
textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
final textSize = textPainter.size;
final bool hasExceededMaxLines = textPainter.didExceedMaxLines;
TextSpan textSpan;
if (hasExceededMaxLines) {
final position = textPainter.getPositionForOffset(
Offset(textSize.width - linkSize.width, textSize.height),
);
final endOffset =
(textPainter.getOffsetBefore(position.offset) ?? 0) -
prefixText.length;
final recognizer =
(_expanded ? widget.collapseOnTextTap : widget.expandOnTextTap)
? _linkTapGestureRecognizer
: null;
final text = _textSegments.isNotEmpty
? TextSpan(
children: _buildTextSpans(
_expanded
? _textSegments
: parseText(
widget.text.substring(0, max(endOffset, 0)),
),
effectiveTextStyle!,
recognizer,
),
)
: TextSpan(
text: _expanded
? widget.text
: widget.text.substring(0, max(endOffset, 0)),
recognizer: recognizer,
);
textSpan = TextSpan(
style: effectiveTextStyle,
children: <TextSpan>[prefix, text, link],
);
} else {
textSpan = content;
}
final selectableText = SelectableText.rich(
textSpan,
textDirection: textDirection,
textAlign: textAlign,
textScaler: textScaler,
);
Widget textWidget = selectableText;
if (widget.animation) {
textWidget = AnimatedSize(
duration:
widget.animationDuration ?? const Duration(milliseconds: 200),
curve: widget.animationCurve ?? Curves.fastLinearToSlowEaseIn,
alignment: Alignment.topLeft,
child: textWidget,
);
}
// Wrap with Stack to add gradient overlay and icons
if (widget.showGradientOverlay || widget.showExpandCollapseIcon) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Stack(
children: [
textWidget,
// Gradient overlay when collapsed and text exceeds max lines
if (widget.showGradientOverlay &&
!_expanded &&
hasExceededMaxLines)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: GestureDetector(
onTap: _linkTapped,
child: Container(
height: widget.gradientOverlayHeight,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).scaffoldBackgroundColor
.withValues(alpha: 0.2),
Theme.of(context).scaffoldBackgroundColor,
],
stops: const [0, 0.9],
),
),
child: Align(
alignment: Alignment.bottomCenter,
child: Icon(
widget.expandIcon ??
Icons.keyboard_arrow_down_sharp,
color: Theme.of(context).iconTheme.color,
),
),
),
),
),
],
),
// Collapse icon when expanded
if (widget.showExpandCollapseIcon &&
_expanded &&
hasExceededMaxLines)
GestureDetector(
onTap: _linkTapped,
child: SizedBox(
height: 20,
child: Icon(
widget.collapseIcon ?? Icons.keyboard_arrow_up_sharp,
color: Theme.of(context).iconTheme.color,
),
),
),
],
);
}
return textWidget;
},
);
if (widget.semanticsLabel != null) {
result = Semantics(
textDirection: widget.textDirection,
label: widget.semanticsLabel,
child: ExcludeSemantics(child: result),
);
}
return result;
}
void _updateText() {
for (var recognizer in _textSegmentsTapGestureRecognizers) {
recognizer.dispose();
}
_textSegmentsTapGestureRecognizers.clear();
if (widget.onUrlTap == null &&
widget.onHashtagTap == null &&
widget.onMentionTap == null) {
_textSegments.clear();
return;
}
_textSegments = parseText(widget.text);
for (var element in _textSegments) {
if (element.isUrl && widget.onUrlTap != null) {
final recognizer = TapGestureRecognizer()
..onTap = () {
widget.onUrlTap!(element.name!);
};
_textSegmentsTapGestureRecognizers.add(recognizer);
} else if (element.isHashtag && widget.onHashtagTap != null) {
final recognizer = TapGestureRecognizer()
..onTap = () {
widget.onHashtagTap!(element.name!);
};
_textSegmentsTapGestureRecognizers.add(recognizer);
} else if (element.isMention && widget.onMentionTap != null) {
final recognizer = TapGestureRecognizer()
..onTap = () {
widget.onMentionTap!(element.name!);
};
_textSegmentsTapGestureRecognizers.add(recognizer);
}
}
}
List<TextSpan> _buildTextSpans(
List<TextSegment> segments,
TextStyle textStyle,
TapGestureRecognizer? textTapRecognizer,
) {
final spans = <TextSpan>[];
var index = 0;
for (var segment in segments) {
TextStyle? style;
TapGestureRecognizer? recognizer;
if (segment.isUrl && widget.onUrlTap != null) {
style = textStyle.merge(widget.urlStyle);
recognizer = _textSegmentsTapGestureRecognizers[index++];
} else if (segment.isMention && widget.onMentionTap != null) {
style = textStyle.merge(widget.mentionStyle);
recognizer = _textSegmentsTapGestureRecognizers[index++];
} else if (segment.isHashtag && widget.onHashtagTap != null) {
style = textStyle.merge(widget.hashtagStyle);
recognizer = _textSegmentsTapGestureRecognizers[index++];
}
final span = TextSpan(
text: segment.text,
style: style,
recognizer: recognizer ?? textTapRecognizer,
);
spans.add(span);
}
return spans;
}
}
class TextSegment {
String text;
final String? name;
final bool isHashtag;
final bool isMention;
final bool isUrl;
bool get isText => !isHashtag && !isMention && !isUrl;
TextSegment(
this.text, [
this.name,
this.isHashtag = false,
this.isMention = false,
this.isUrl = false,
]);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TextSegment &&
runtimeType == other.runtimeType &&
text == other.text &&
name == other.name &&
isHashtag == other.isHashtag &&
isMention == other.isMention &&
isUrl == other.isUrl;
@override
int get hashCode =>
text.hashCode ^
name.hashCode ^
isHashtag.hashCode ^
isMention.hashCode ^
isUrl.hashCode;
}
/// Split the string into multiple instances of [TextSegment] for mentions, hashtags, URLs and regular text.
///
/// Mentions are all words that start with @, e.g. @mention.
/// Hashtags are all words that start with #, e.g. #hashtag.
List<TextSegment> parseText(String? text) {
final segments = <TextSegment>[];
if (text == null || text.isEmpty) {
return segments;
}
// parse urls and words starting with @ (mention) or # (hashtag)
RegExp exp = RegExp(
r'(?<keyword>(#|@)([\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]+)|(?<url>(?:(?:https?|ftp):\/\/)?[-a-z0-9@:%._\+~#=]{1,256}\.[a-z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?))',
unicode: true,
);
final matches = exp.allMatches(text);
var start = 0;
for (var match in matches) {
// text before the keyword
if (match.start > start) {
if (segments.isNotEmpty && segments.last.isText) {
segments.last.text += text.substring(start, match.start);
} else {
segments.add(TextSegment(text.substring(start, match.start)));
}
start = match.start;
}
final url = match.namedGroup('url');
final keyword = match.namedGroup('keyword');
if (url != null) {
segments.add(TextSegment(url, url, false, false, true));
} else if (keyword != null) {
final isWord =
match.start == 0 ||
[' ', '\n'].contains(text.substring(match.start - 1, start));
if (!isWord) {
continue;
}
final isHashtag = keyword.startsWith('#');
final isMention = keyword.startsWith('@');
segments.add(
TextSegment(keyword, keyword.substring(1), isHashtag, isMention),
);
}
start = match.end;
}
// text after the last keyword or the whole text if it does not contain any keywords
if (start < text.length) {
if (segments.isNotEmpty && segments.last.isText) {
segments.last.text += text.substring(start);
} else {
segments.add(TextSegment(text.substring(start)));
}
}
return segments;
}

View file

@ -1,14 +1,13 @@
import 'package:expandable_text/expandable_text.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/detail/widgets/expandable_text.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class ReadMoreWidget extends StatefulWidget {
const ReadMoreWidget({
super.key,
required this.text,
required this.onChanged,
this.initExpanded = true,
this.initExpanded = false,
});
final Function(bool) onChanged;
final String text;
@ -29,63 +28,29 @@ class ReadMoreWidgetState extends State<ReadMoreWidget>
mainAxisAlignment: MainAxisAlignment.center,
children: [Text(l10n.no_description)],
)
: Column(
children: [
Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: ExpandableText(
animationDuration: const Duration(milliseconds: 500),
onExpandedChanged: (value) {
setState(() => expanded = value);
widget.onChanged(value);
},
expandOnTextTap: true,
widget.text.trim(),
expandText: '',
maxLines: 3,
expanded: expanded,
linkColor: Theme.of(context).scaffoldBackgroundColor,
animation: true,
collapseOnTextTap: true,
prefixText: '',
),
),
if (!expanded)
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Container(
width: context.width(1),
height: 30,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(
context,
).scaffoldBackgroundColor.withValues(alpha: 0.2),
Theme.of(context).scaffoldBackgroundColor,
],
stops: const [0, .9],
),
),
child: const Icon(Icons.keyboard_arrow_down_sharp),
),
),
],
),
if (expanded)
SizedBox(
width: context.width(1),
height: 20,
child: const Icon(Icons.keyboard_arrow_up_sharp),
),
],
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: ExpandableText(
widget.text.trim(),
expandText: '',
maxLines: 3,
expanded: expanded,
linkColor: Colors.transparent,
animation: true,
animationDuration: const Duration(milliseconds: 500),
expandOnTextTap: true,
collapseOnTextTap: true,
prefixText: '',
showGradientOverlay: true,
gradientOverlayHeight: 30,
showExpandCollapseIcon: true,
expandIcon: Icons.keyboard_arrow_down_sharp,
collapseIcon: Icons.keyboard_arrow_up_sharp,
onExpandedChanged: (value) {
setState(() => expanded = value);
widget.onChanged(value);
},
),
);
}
}

View file

@ -136,7 +136,7 @@ final class DownloadChapterProvider
}
}
String _$downloadChapterHash() => r'5eb401736efdfb2990fda6e2d97160aaeb94aec1';
String _$downloadChapterHash() => r'0eb04602246bb3c4dcc88397d97039dea3047cc6';
final class DownloadChapterFamily extends $Family
with
@ -215,7 +215,7 @@ final class ProcessDownloadsProvider
}
}
String _$processDownloadsHash() => r'ef5107f9674f2175a7aa18b8e4fc4555f3b6b584';
String _$processDownloadsHash() => r'caebad3bb681d7b38de4d09325310fc08bc1cd0a';
final class ProcessDownloadsFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<void>, bool?> {

View file

@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
/// Main widget for virtual reading that replaces ScrollablePositionedList
class ImageViewWebtoon extends StatelessWidget {
final List<UChapDataPreload> pages;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
final ItemPositionsListener itemPositionsListener;
final Axis scrollDirection;
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
final bool isHorizontalContinuous;
final ReaderMode readerMode;
final PhotoViewController photoViewController;
final PhotoViewScaleStateController photoViewScaleStateController;
final Alignment scalePosition;
final Function(ScaleEndDetails) onScaleEnd;
final Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
const ImageViewWebtoon({
super.key,
required this.pages,
required this.itemScrollController,
required this.scrollOffsetController,
required this.itemPositionsListener,
required this.scrollDirection,
required this.minCacheExtent,
required this.initialScrollIndex,
required this.physics,
required this.onLongPressData,
required this.onFailedToLoadImage,
required this.backgroundColor,
required this.isDoublePageMode,
required this.isHorizontalContinuous,
required this.readerMode,
required this.photoViewController,
required this.photoViewScaleStateController,
required this.scalePosition,
required this.onScaleEnd,
required this.onDoubleTapDown,
required this.onDoubleTap,
});
@override
Widget build(BuildContext context) {
return PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) => PhotoViewGalleryPageOptions.customChild(
controller: photoViewController,
scaleStateController: photoViewScaleStateController,
basePosition: scalePosition,
onScaleEnd: (context, details, controllerValue) => onScaleEnd(details),
child: ScrollablePositionedList.separated(
scrollDirection: scrollDirection,
minCacheExtent: minCacheExtent,
initialScrollIndex: initialScrollIndex,
itemCount: pages.length,
physics: physics,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => _buildItem(context, index),
separatorBuilder: _buildSeparator,
),
),
);
}
Widget _buildItem(BuildContext context, int index) {
if (isDoublePageMode && !isHorizontalContinuous) {
return _buildDoublePageItem(context, index);
} else {
return _buildSinglePageItem(context, index);
}
}
Widget _buildSinglePageItem(BuildContext context, int index) {
final currentPage = pages[index];
if (currentPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: TransitionViewVertical(data: currentPage),
);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: ImageViewVertical(
data: currentPage,
failedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
isHorizontal: isHorizontalContinuous,
),
);
}
Widget _buildDoublePageItem(BuildContext context, int index) {
final pageLength = pages.length;
if (index >= pageLength) {
return const SizedBox.shrink();
}
final int index1 = index * 2 - 1;
final int index2 = index1 + 1;
final List<UChapDataPreload?> datas = index == 0
? [pages[0], null]
: [
index1 < pageLength ? pages[index1] : null,
index2 < pageLength ? pages[index2] : null,
];
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: DoubleColummVerticalView(
datas: datas,
backgroundColor: backgroundColor,
isFailedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
),
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(backgroundColor),
width: 6,
);
} else {
return Divider(color: getBackgroundColor(backgroundColor), height: 6);
}
}
}

View file

@ -23,12 +23,119 @@ Future<Uint8List?> cropBorders(
return null;
}
return await Isolate.run(() async {
await RustLib.init();
final imageRes = processCropImage(image: imageBytes!);
RustLib.dispose();
return imageRes;
});
return imgCropIsolate.process(imageBytes);
}
return null;
}
class ImageCropIsolate {
bool _isRunning = false;
Isolate? _rustIsolate;
ReceivePort? _receivePort;
SendPort? _sendPort;
Future<void> start() async {
if (!_isRunning) {
try {
await _initRustIsolate();
} catch (_) {
await stop();
}
}
}
Future<void> _initRustIsolate() async {
_receivePort = ReceivePort();
_rustIsolate = await Isolate.spawn(
_rustIsolateEntryPoint,
_receivePort!.sendPort,
);
final completer = Completer<SendPort>();
_receivePort!.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
_sendPort = await completer.future;
_isRunning = true;
}
static Future<void> _rustIsolateEntryPoint(SendPort mainSendPort) async {
await RustLib.init();
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
await for (var message in receivePort) {
if (message is Map<String, dynamic>) {
try {
final imageBytes = message['imageBytes'] as Uint8List;
final responsePort = message['responsePort'] as SendPort;
final croppedImage = processCropImage(image: imageBytes);
responsePort.send({'success': true, 'data': croppedImage});
} catch (e) {
final responsePort = message['responsePort'] as SendPort;
responsePort.send({'success': false, 'error': e.toString()});
}
} else if (message == 'dispose') {
RustLib.dispose();
break;
}
}
}
Future<Uint8List?> process(Uint8List imageBytes) async {
await start();
if (_sendPort == null) {
if (kDebugMode) {
print('Image crop isolate is not running');
}
return null;
}
final responsePort = ReceivePort();
final completer = Completer<Uint8List?>();
responsePort.listen((response) {
responsePort.close();
if (response is Map<String, dynamic>) {
if (response['success'] == true) {
completer.complete(response['data'] as Uint8List);
} else {
if (kDebugMode) {
print('Image cropping failed: ${response['error']}');
}
completer.complete(Future.value(null));
}
}
});
_sendPort!.send({
'imageBytes': imageBytes,
'responsePort': responsePort.sendPort,
});
return completer.future;
}
Future<void> stop() async {
if (!_isRunning) {
return;
}
_sendPort?.send('dispose');
_rustIsolate?.kill(priority: Isolate.immediate);
_receivePort?.close();
_sendPort = null;
_rustIsolate = null;
_receivePort = null;
_isRunning = false;
}
}
final imgCropIsolate = ImageCropIsolate();

View file

@ -68,7 +68,7 @@ final class CropBordersProvider
}
}
String _$cropBordersHash() => r'04b24357737d6cc75caa38feca77bb5d41f00aa6';
String _$cropBordersHash() => r'f60987c3f38afd5e10263f3d6935e6007ff942f0';
final class CropBordersFamily extends $Family
with

View file

@ -148,7 +148,7 @@ final class ReaderControllerProvider
}
}
String _$readerControllerHash() => r'25b13bbbbd961a5c3dbae3cc0ea58017d7bb5ce8';
String _$readerControllerHash() => r'23eece0ca4e7b6cbf425488636ef942fe0d4c2bc';
final class ReaderControllerFamily extends $Family
with

View file

@ -40,10 +40,9 @@ import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicat
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart';
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/virtual_scrolling/virtual_reader_view.dart';
import 'package:mangayomi/modules/manga/reader/image_view_webtoon.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:share_plus/share_plus.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
@ -243,11 +242,12 @@ class _MangaChapterPageGalleryState
WidgetsBinding.instance.addObserver(this);
}
final double _horizontalScaleValue = 1.0;
// final double _horizontalScaleValue = 1.0;
bool _isNextChapterPreloading = false;
late int pagePreloadAmount = ref.read(pagePreloadAmountStateProvider);
late bool _isBookmarked = _readerController.getChapterBookmarked();
bool _isLastPageTransition = false;
final _currentReaderMode = StateProvider<ReaderMode?>(() => null);
PageMode? _pageMode;
bool _isView = false;
@ -471,7 +471,7 @@ class _MangaChapterPageGalleryState
if (cropBorders) {
_processCropBorders();
}
final usePageTapZones = ref.watch(usePageTapZonesStateProvider);
final l10n = l10nLocalizations(context)!;
return KeyboardListener(
autofocus: true,
@ -564,97 +564,45 @@ class _MangaChapterPageGalleryState
return Stack(
children: [
_isVerticalOrHorizontalContinous()
? PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) =>
PhotoViewGalleryPageOptions.customChild(
controller: _photoViewController,
scaleStateController:
_photoViewScaleStateController,
basePosition: _scalePosition,
onScaleEnd: _onScaleEnd,
child: VirtualReaderView(
pages: _uChapDataPreload,
itemScrollController: _itemScrollController,
scrollOffsetController:
_pageOffsetController,
itemPositionsListener:
_itemPositionsListener,
scrollDirection: isHorizontalContinuaous
? Axis.horizontal
: Axis.vertical,
minCacheExtent:
pagePreloadAmount * context.height(1),
initialScrollIndex: _readerController
.getPageIndex(),
physics: const ClampingScrollPhysics(),
onLongPressData: (data) =>
_onLongPressImageDialog(data, context),
onFailedToLoadImage: (value) {
// Handle failed image loading
if (_failedToLoadImage.value != value &&
context.mounted) {
_failedToLoadImage.value = value;
}
},
backgroundColor: backgroundColor,
isDoublePageMode:
_pageMode == PageMode.doublePage &&
!isHorizontalContinuaous,
isHorizontalContinuous:
isHorizontalContinuaous,
readerMode: ref.watch(_currentReaderMode)!,
photoViewController: _photoViewController,
photoViewScaleStateController:
_photoViewScaleStateController,
scalePosition: _scalePosition,
onScaleEnd: (details) => _onScaleEnd(
context,
details,
_photoViewController.value,
),
onDoubleTapDown: (offset) =>
_toggleScale(offset),
onDoubleTap: () {},
// Chapter transition callbacks
onChapterChanged: (newChapter) {
// Update the current chapter when a chapter change is detected
if (newChapter.id != chapter.id) {
if (mounted) {
setState(() {
_readerController = ref.read(
readerControllerProvider(
chapter: newChapter,
).notifier,
);
chapter = newChapter;
_isBookmarked = _readerController
.getChapterBookmarked();
});
}
}
},
onReachedLastPage: (lastPageIndex) {
try {
ref
.watch(
getChapterPagesProvider(
chapter: _readerController
.getNextChapter(),
).future,
)
.then(
(value) => _preloadNextChapter(
value,
chapter,
),
);
} on RangeError {
_addLastPageTransition(chapter);
}
},
),
),
? ImageViewWebtoon(
pages: _uChapDataPreload,
itemScrollController: _itemScrollController,
scrollOffsetController: _pageOffsetController,
itemPositionsListener: _itemPositionsListener,
scrollDirection: isHorizontalContinuaous
? Axis.horizontal
: Axis.vertical,
minCacheExtent:
pagePreloadAmount * context.height(1),
initialScrollIndex: _readerController
.getPageIndex(),
physics: const ClampingScrollPhysics(),
onLongPressData: (data) =>
_onLongPressImageDialog(data, context),
onFailedToLoadImage: (value) {
// Handle failed image loading
if (_failedToLoadImage.value != value &&
context.mounted) {
_failedToLoadImage.value = value;
}
},
backgroundColor: backgroundColor,
isDoublePageMode:
_pageMode == PageMode.doublePage &&
!isHorizontalContinuaous,
isHorizontalContinuous: isHorizontalContinuaous,
readerMode: ref.watch(_currentReaderMode)!,
photoViewController: _photoViewController,
photoViewScaleStateController:
_photoViewScaleStateController,
scalePosition: _scalePosition,
onScaleEnd: (details) => _onScaleEnd(
context,
details,
_photoViewController.value,
),
onDoubleTapDown: (offset) => _toggleScale(offset),
onDoubleTap: () {},
)
: Material(
color: getBackgroundColor(backgroundColor),
@ -941,8 +889,28 @@ class _MangaChapterPageGalleryState
onPageChanged: _onPageChanged,
),
),
_gestureRightLeft(failedToLoadImage, usePageTapZones),
_gestureTopBottom(failedToLoadImage, usePageTapZones),
Consumer(
builder: (context, ref, child) {
final usePageTapZones = ref.watch(
usePageTapZonesStateProvider,
);
return _gestureRightLeft(
failedToLoadImage,
usePageTapZones,
);
},
),
Consumer(
builder: (context, ref, child) {
final usePageTapZones = ref.watch(
usePageTapZonesStateProvider,
);
return _gestureTopBottom(
failedToLoadImage,
usePageTapZones,
);
},
),
_appBar(),
_bottomBar(),
_showPage(),
@ -1020,16 +988,23 @@ class _MangaChapterPageGalleryState
});
}
}
if (itemPositions.last.index == pagesLength - 1) {
if ((itemPositions.last.index == pagesLength - 1) &&
!_isLastPageTransition) {
if (_isNextChapterPreloading) return;
try {
_isNextChapterPreloading = true;
ref
.watch(
getChapterPagesProvider(
chapter: _readerController.getNextChapter(),
).future,
)
.then((value) => _preloadNextChapter(value, chapter));
.then((value) {
_preloadNextChapter(value, chapter);
_isNextChapterPreloading = false;
});
} on RangeError {
_isNextChapterPreloading = false;
_addLastPageTransition(chapter);
}
}
@ -1042,6 +1017,7 @@ class _MangaChapterPageGalleryState
}
void _addLastPageTransition(Chapter chap) {
if (_isLastPageTransition) return;
try {
if (!mounted || (_uChapDataPreload.last.isLastChapter ?? false)) return;
final currentLength = _uChapDataPreload.length;
@ -1056,6 +1032,7 @@ class _MangaChapterPageGalleryState
if (mounted) {
setState(() {
_uChapDataPreload.add(transitionPage);
_isLastPageTransition = true;
});
}
} catch (_) {}
@ -1195,16 +1172,23 @@ class _MangaChapterPageGalleryState
.setCurrentIndex(_uChapDataPreload[index].index!);
}
if (_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) {
if ((_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) &&
!_isLastPageTransition) {
if (_isNextChapterPreloading) return;
try {
_isNextChapterPreloading = true;
ref
.watch(
getChapterPagesProvider(
chapter: _readerController.getNextChapter(),
).future,
)
.then((value) => _preloadNextChapter(value, chapter));
.then((value) {
_preloadNextChapter(value, chapter);
_isNextChapterPreloading = false;
});
} on RangeError {
_isNextChapterPreloading = false;
_addLastPageTransition(chapter);
}
}
@ -1413,6 +1397,7 @@ class _MangaChapterPageGalleryState
}
void _processCropBorders() async {
if (_cropBorderCheckList.length == _uChapDataPreload.length) return;
for (var i = 0; i < _uChapDataPreload.length; i++) {
if (!_cropBorderCheckList.contains(i)) {
_cropBorderCheckList.add(i);
@ -1985,7 +1970,7 @@ class _MangaChapterPageGalleryState
void _isViewFunction() {
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
if (mounted) {
if (context.mounted) {
setState(() {
_isView = !_isView;
});

View file

@ -1,343 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
/// Widget for displaying manga pages in a virtual scrolling list
class VirtualMangaList extends ConsumerStatefulWidget {
final VirtualPageManager pageManager;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
final ItemPositionsListener itemPositionsListener;
final Axis scrollDirection;
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
final bool isHorizontalContinuous;
final ReaderMode readerMode;
final Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
final Function(Chapter chapter)? onChapterChanged;
final Function(int lastPageIndex)? onReachedLastPage;
final Function(int index)? onPageChanged;
const VirtualMangaList({
super.key,
required this.pageManager,
required this.itemScrollController,
required this.scrollOffsetController,
required this.itemPositionsListener,
required this.scrollDirection,
required this.minCacheExtent,
required this.initialScrollIndex,
required this.physics,
required this.onLongPressData,
required this.onFailedToLoadImage,
required this.backgroundColor,
required this.isDoublePageMode,
required this.isHorizontalContinuous,
required this.readerMode,
required this.onDoubleTapDown,
required this.onDoubleTap,
this.onChapterChanged,
this.onReachedLastPage,
this.onPageChanged,
});
@override
ConsumerState<VirtualMangaList> createState() => _VirtualMangaListState();
}
class _VirtualMangaListState extends ConsumerState<VirtualMangaList> {
Chapter? _currentChapter;
int? _currentIndex;
@override
void initState() {
super.initState();
// Listen to item positions to update virtual page manager
widget.itemPositionsListener.itemPositions.addListener(_onPositionChanged);
// Initialize current chapter
if (widget.pageManager.pageCount > 0) {
final firstPage = widget.pageManager.getOriginalPage(
widget.initialScrollIndex,
);
_currentChapter = firstPage?.chapter;
}
}
@override
void dispose() {
widget.itemPositionsListener.itemPositions.removeListener(
_onPositionChanged,
);
super.dispose();
}
void _onPositionChanged() {
final positions = widget.itemPositionsListener.itemPositions.value;
if (positions.isNotEmpty) {
// Get the first visible item
final firstVisibleIndex = positions.first.index;
final lastVisibleIndex = positions.last.index;
// Update virtual page manager
widget.pageManager.updateVisibleIndex(firstVisibleIndex);
// Calculate actual page lengths considering page mode
int pagesLength =
widget.isDoublePageMode && !widget.isHorizontalContinuous
? (widget.pageManager.pageCount / 2).ceil() + 1
: widget.pageManager.pageCount;
// Check if index is valid
if (firstVisibleIndex >= 0 && firstVisibleIndex < pagesLength) {
final currentPage = widget.pageManager.getOriginalPage(
firstVisibleIndex,
);
if (currentPage != null) {
// Check for chapter change
if (_currentChapter?.id != currentPage.chapter?.id &&
currentPage.chapter != null) {
_currentChapter = currentPage.chapter;
widget.onChapterChanged?.call(currentPage.chapter!);
}
// Update current index
if (_currentIndex != firstVisibleIndex) {
_currentIndex = firstVisibleIndex;
widget.onPageChanged?.call(firstVisibleIndex);
}
}
// Check if reached last page to trigger next chapter preload
if (lastVisibleIndex >= pagesLength - 1) {
widget.onReachedLastPage?.call(lastVisibleIndex);
}
}
}
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.pageManager,
builder: (context, child) {
final itemCount =
widget.isDoublePageMode && !widget.isHorizontalContinuous
? (widget.pageManager.pageCount / 2).ceil() + 1
: widget.pageManager.pageCount;
return ScrollablePositionedList.separated(
scrollDirection: widget.scrollDirection,
minCacheExtent: widget.minCacheExtent,
initialScrollIndex: widget.initialScrollIndex,
itemCount: itemCount,
physics: widget.physics,
itemScrollController: widget.itemScrollController,
scrollOffsetController: widget.scrollOffsetController,
itemPositionsListener: widget.itemPositionsListener,
itemBuilder: (context, index) => _buildItem(context, index),
separatorBuilder: _buildSeparator,
);
},
);
}
Widget _buildItem(BuildContext context, int index) {
if (widget.isDoublePageMode && !widget.isHorizontalContinuous) {
return _buildDoublePageItem(context, index);
} else {
return _buildSinglePageItem(context, index);
}
}
Widget _buildSinglePageItem(BuildContext context, int index) {
final originalPage = widget.pageManager.getOriginalPage(index);
if (originalPage == null) {
return const SizedBox.shrink();
}
// Check if page should be loaded
final pageInfo = widget.pageManager.getPageInfo(index);
final shouldLoad = widget.pageManager.shouldPageBeLoaded(index);
if (!shouldLoad &&
(pageInfo?.loadState == PageLoadState.notLoaded || pageInfo == null)) {
// Return placeholder for unloaded pages
return _buildPlaceholder(context);
}
if (originalPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: TransitionViewVertical(data: originalPage),
);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: ImageViewVertical(
data: originalPage,
failedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
isHorizontal: widget.isHorizontalContinuous,
),
);
}
Widget _buildDoublePageItem(BuildContext context, int index) {
if (index >= widget.pageManager.pageCount) {
return const SizedBox.shrink();
}
final int index1 = index * 2 - 1;
final int index2 = index1 + 1;
final List<UChapDataPreload?> datas = index == 0
? [widget.pageManager.getOriginalPage(0), null]
: [
index1 < widget.pageManager.pageCount
? widget.pageManager.getOriginalPage(index1)
: null,
index2 < widget.pageManager.pageCount
? widget.pageManager.getOriginalPage(index2)
: null,
];
// Check if pages should be loaded
final shouldLoad1 = index1 >= 0
? widget.pageManager.shouldPageBeLoaded(index1)
: false;
final shouldLoad2 = index2 < widget.pageManager.pageCount
? widget.pageManager.shouldPageBeLoaded(index2)
: false;
if (!shouldLoad1 && !shouldLoad2) {
return _buildPlaceholder(context);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: DoubleColummVerticalView(
datas: datas,
backgroundColor: widget.backgroundColor,
isFailedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
),
);
}
Widget _buildPlaceholder(BuildContext context) {
return Container(
height: context.height(0.8),
color: getBackgroundColor(widget.backgroundColor),
child: const Center(child: CircularProgressIndicator()),
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (widget.readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (widget.isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(widget.backgroundColor),
width: 6,
);
} else {
return Divider(
color: getBackgroundColor(widget.backgroundColor),
height: 6,
);
}
}
}
/// Debug widget to show virtual page manager statistics
class VirtualPageManagerDebugInfo extends ConsumerWidget {
final VirtualPageManager pageManager;
const VirtualPageManagerDebugInfo({super.key, required this.pageManager});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListenableBuilder(
listenable: pageManager,
builder: (context, child) {
final stats = pageManager.getMemoryStats();
return Positioned(
top: 100,
right: 10,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Virtual Page Manager',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Current: ${stats['currentIndex']}/${stats['totalPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Loaded: ${stats['loadedPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Cached: ${stats['cachedPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Errors: ${stats['errorPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Queue: ${stats['preloadQueueSize']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
);
},
);
}
}

View file

@ -1,285 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
/// Page loading states for virtual scrolling
enum PageLoadState { notLoaded, loading, loaded, error, cached }
/// Virtual page information for tracking state
class VirtualPageInfo {
final int index;
final UChapDataPreload originalData;
PageLoadState loadState;
DateTime? lastAccessTime;
Object? error;
VirtualPageInfo({
required this.index,
required this.originalData,
this.loadState = PageLoadState.notLoaded,
this.lastAccessTime,
this.error,
});
bool get isVisible =>
loadState == PageLoadState.loaded || loadState == PageLoadState.cached;
bool get needsLoading => loadState == PageLoadState.notLoaded;
bool get isLoading => loadState == PageLoadState.loading;
bool get hasError => loadState == PageLoadState.error;
void markAccessed() {
lastAccessTime = DateTime.now();
}
Duration get timeSinceAccess {
if (lastAccessTime == null) return Duration.zero;
return DateTime.now().difference(lastAccessTime!);
}
}
/// Configuration for virtual page manager
class VirtualPageConfig {
final int preloadDistance;
final int maxCachedPages;
final Duration cacheTimeout;
final bool enableMemoryOptimization;
const VirtualPageConfig({
this.preloadDistance = 3,
this.maxCachedPages = 10,
this.cacheTimeout = const Duration(minutes: 5),
this.enableMemoryOptimization = true,
});
}
/// Manages virtual page loading and memory optimization
class VirtualPageManager extends ChangeNotifier {
final List<UChapDataPreload> _originalPages;
final VirtualPageConfig config;
final Map<int, VirtualPageInfo> _pageInfoMap = {};
final Set<int> _preloadQueue = {};
int _currentVisibleIndex = 0;
Timer? _cleanupTimer;
VirtualPageManager({
required List<UChapDataPreload> pages,
this.config = const VirtualPageConfig(),
}) : _originalPages = List.from(pages) {
_initializePages();
_startCleanupTimer();
}
void _initializePages() {
for (int i = 0; i < _originalPages.length; i++) {
_pageInfoMap[i] = VirtualPageInfo(
index: i,
originalData: _originalPages[i],
);
}
}
void _startCleanupTimer() {
_cleanupTimer?.cancel();
_cleanupTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => _performMemoryCleanup(),
);
}
@override
void dispose() {
_cleanupTimer?.cancel();
super.dispose();
}
/// Get page count
int get pageCount => _originalPages.length;
/// Get current visible index
int get currentVisibleIndex => _currentVisibleIndex;
/// Get page info for a specific index
VirtualPageInfo? getPageInfo(int index) {
if (index < 0 || index >= _originalPages.length) return null;
return _pageInfoMap[index];
}
/// Get original page data
UChapDataPreload? getOriginalPage(int index) {
if (index < 0 || index >= _originalPages.length) return null;
return _originalPages[index];
}
/// Update visible page index and trigger preloading
void updateVisibleIndex(int index) {
if (index == _currentVisibleIndex) return;
_currentVisibleIndex = index.clamp(0, _originalPages.length - 1);
_pageInfoMap[_currentVisibleIndex]?.markAccessed();
_schedulePreloading();
notifyListeners();
}
/// Check if a page should be visible/loaded
bool shouldPageBeLoaded(int index) {
final distance = (index - _currentVisibleIndex).abs();
return distance <= config.preloadDistance;
}
/// Get priority for a page (higher = more important)
int getPagePriority(int index) {
final distance = (index - _currentVisibleIndex).abs();
if (distance == 0) return 1000; // Current page has highest priority
return max(0, 100 - distance * 10);
}
/// Schedule preloading for nearby pages
void _schedulePreloading() {
_preloadQueue.clear();
// Add pages within preload distance
for (int i = 0; i < _originalPages.length; i++) {
if (shouldPageBeLoaded(i)) {
final pageInfo = _pageInfoMap[i]!;
if (pageInfo.needsLoading) {
_preloadQueue.add(i);
}
}
}
// Process preload queue
_processPreloadQueue();
}
/// Process the preload queue
void _processPreloadQueue() {
final sortedQueue = _preloadQueue.toList()
..sort((a, b) => getPagePriority(b).compareTo(getPagePriority(a)));
for (final index in sortedQueue.take(3)) {
// Limit concurrent loading
_loadPage(index);
}
}
/// Load a specific page
Future<void> _loadPage(int index) async {
final pageInfo = _pageInfoMap[index];
if (pageInfo == null || pageInfo.isLoading) return;
pageInfo.loadState = PageLoadState.loading;
notifyListeners();
try {
// For now, we just mark as loaded since the actual image loading
// is handled by the ImageView widgets
await Future.delayed(const Duration(milliseconds: 10));
pageInfo.loadState = PageLoadState.loaded;
pageInfo.markAccessed();
} catch (error) {
pageInfo.loadState = PageLoadState.error;
pageInfo.error = error;
}
notifyListeners();
}
/// Perform memory cleanup
void _performMemoryCleanup() {
if (!config.enableMemoryOptimization) return;
final pageEntries = _pageInfoMap.entries.toList();
// Sort by last access time and distance from current page
pageEntries.sort((a, b) {
final aDistance = (a.key - _currentVisibleIndex).abs();
final bDistance = (b.key - _currentVisibleIndex).abs();
final aTime =
a.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0);
final bTime =
b.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0);
// First sort by distance, then by access time
final distanceComparison = aDistance.compareTo(bDistance);
return distanceComparison != 0
? distanceComparison
: aTime.compareTo(bTime);
});
int cachedCount = pageEntries.where((e) => e.value.isVisible).length;
// Remove old cached pages if we exceed the limit
for (final entry in pageEntries) {
if (cachedCount <= config.maxCachedPages) break;
final pageInfo = entry.value;
final distance = (entry.key - _currentVisibleIndex).abs();
// Don't unload pages within preload distance
if (distance <= config.preloadDistance) continue;
// Don't unload recently accessed pages
if (pageInfo.timeSinceAccess < config.cacheTimeout) continue;
if (pageInfo.isVisible) {
pageInfo.loadState = PageLoadState.notLoaded;
pageInfo.error = null;
cachedCount--;
}
}
if (cachedCount != pageEntries.where((e) => e.value.isVisible).length) {
notifyListeners();
}
}
/// Force load a page immediately
Future<void> forceLoadPage(int index) async {
await _loadPage(index);
}
/// Get memory usage statistics
Map<String, dynamic> getMemoryStats() {
final loadedCount = _pageInfoMap.values
.where((p) => p.loadState == PageLoadState.loaded)
.length;
final cachedCount = _pageInfoMap.values
.where((p) => p.loadState == PageLoadState.cached)
.length;
final errorCount = _pageInfoMap.values.where((p) => p.hasError).length;
return {
'totalPages': _originalPages.length,
'loadedPages': loadedCount,
'cachedPages': cachedCount,
'errorPages': errorCount,
'currentIndex': _currentVisibleIndex,
'preloadQueueSize': _preloadQueue.length,
};
}
/// Preload a range of pages
Future<void> preloadRange(int startIndex, int endIndex) async {
for (int i = startIndex; i <= endIndex && i < _originalPages.length; i++) {
if (i >= 0) {
await _loadPage(i);
}
}
}
/// Clear all cached pages
void clearCache() {
for (final pageInfo in _pageInfoMap.values) {
if (pageInfo.loadState != PageLoadState.loading) {
pageInfo.loadState = PageLoadState.notLoaded;
pageInfo.error = null;
}
}
notifyListeners();
}
}

View file

@ -1,223 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/utils/riverpod.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart';
/// Provides virtual page manager instances
final virtualPageManagerProvider =
Provider.family<VirtualPageManager, List<UChapDataPreload>>((ref, pages) {
return VirtualPageManager(pages: pages);
});
/// Main widget for virtual reading that replaces ScrollablePositionedList
class VirtualReaderView extends ConsumerStatefulWidget {
final List<UChapDataPreload> pages;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
final ItemPositionsListener itemPositionsListener;
final Axis scrollDirection;
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
final bool isHorizontalContinuous;
final ReaderMode readerMode;
final PhotoViewController photoViewController;
final PhotoViewScaleStateController photoViewScaleStateController;
final Alignment scalePosition;
final Function(ScaleEndDetails) onScaleEnd;
final Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
final bool showDebugInfo;
// Callbacks pour gérer les transitions entre chapitres
final Function(Chapter chapter)? onChapterChanged;
final Function(int lastPageIndex)? onReachedLastPage;
const VirtualReaderView({
super.key,
required this.pages,
required this.itemScrollController,
required this.scrollOffsetController,
required this.itemPositionsListener,
required this.scrollDirection,
required this.minCacheExtent,
required this.initialScrollIndex,
required this.physics,
required this.onLongPressData,
required this.onFailedToLoadImage,
required this.backgroundColor,
required this.isDoublePageMode,
required this.isHorizontalContinuous,
required this.readerMode,
required this.photoViewController,
required this.photoViewScaleStateController,
required this.scalePosition,
required this.onScaleEnd,
required this.onDoubleTapDown,
required this.onDoubleTap,
this.showDebugInfo = false,
this.onChapterChanged,
this.onReachedLastPage,
});
@override
ConsumerState<VirtualReaderView> createState() => _VirtualReaderViewState();
}
class _VirtualReaderViewState extends ConsumerState<VirtualReaderView> {
late VirtualPageManager _pageManager;
@override
void initState() {
super.initState();
_pageManager = VirtualPageManager(pages: widget.pages);
// Set initial visible index
_pageManager.updateVisibleIndex(widget.initialScrollIndex);
}
@override
void didUpdateWidget(VirtualReaderView oldWidget) {
super.didUpdateWidget(oldWidget);
// Update page manager if pages changed
if (widget.pages != oldWidget.pages) {
_pageManager.dispose();
_pageManager = VirtualPageManager(pages: widget.pages);
_pageManager.updateVisibleIndex(widget.initialScrollIndex);
}
}
@override
void dispose() {
_pageManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_pageManager.pageCount < widget.pages.length) {
_pageManager = VirtualPageManager(pages: widget.pages);
}
return Stack(
children: [
PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) => PhotoViewGalleryPageOptions.customChild(
controller: widget.photoViewController,
scaleStateController: widget.photoViewScaleStateController,
basePosition: widget.scalePosition,
onScaleEnd: (context, details, controllerValue) =>
widget.onScaleEnd(details),
child: VirtualMangaList(
pageManager: _pageManager,
itemScrollController: widget.itemScrollController,
scrollOffsetController: widget.scrollOffsetController,
itemPositionsListener: widget.itemPositionsListener,
scrollDirection: widget.scrollDirection,
minCacheExtent: widget.minCacheExtent,
initialScrollIndex: widget.initialScrollIndex,
physics: widget.physics,
onLongPressData: widget.onLongPressData,
onFailedToLoadImage: widget.onFailedToLoadImage,
backgroundColor: widget.backgroundColor,
isDoublePageMode: widget.isDoublePageMode,
isHorizontalContinuous: widget.isHorizontalContinuous,
readerMode: widget.readerMode,
onDoubleTapDown: widget.onDoubleTapDown,
onDoubleTap: widget.onDoubleTap,
// Passer les callbacks pour les transitions entre chapitres
onChapterChanged: widget.onChapterChanged,
onReachedLastPage: widget.onReachedLastPage,
onPageChanged: (index) {
// Ici on peut ajouter une logique supplémentaire si nécessaire
// Par exemple, précaching d'images
_pageManager.updateVisibleIndex(index);
},
),
),
),
// Debug info overlay
if (widget.showDebugInfo)
VirtualPageManagerDebugInfo(pageManager: _pageManager),
],
);
}
}
/// Mixin to add virtual page manager capabilities to existing widgets
mixin VirtualPageManagerMixin<T extends ConsumerStatefulWidget>
on ConsumerState<T> {
VirtualPageManager? _virtualPageManager;
VirtualPageManager get virtualPageManager {
_virtualPageManager ??= VirtualPageManager(pages: getPages());
return _virtualPageManager!;
}
/// Override this method to provide the pages list
List<UChapDataPreload> getPages();
/// Call this when pages change
void updateVirtualPages(List<UChapDataPreload> newPages) {
_virtualPageManager?.dispose();
_virtualPageManager = VirtualPageManager(pages: newPages);
}
/// Call this when the visible page changes
void updateVisiblePage(int index) {
virtualPageManager.updateVisibleIndex(index);
}
@override
void dispose() {
_virtualPageManager?.dispose();
super.dispose();
}
}
/// Configuration provider for virtual page manager
final virtualPageConfigProvider = Provider<VirtualPageConfig>((ref) {
// Get user preferences for virtual scrolling configuration
final preloadAmount = ref.watch(readerPagePreloadAmountStateProvider);
return VirtualPageConfig(
preloadDistance: preloadAmount,
maxCachedPages: preloadAmount * 3,
cacheTimeout: const Duration(minutes: 5),
enableMemoryOptimization: true,
);
});
/// Provider for page preload amount (renamed to avoid conflicts)
final readerPagePreloadAmountStateProvider = StateProvider<int>(() => 3);
/// Extension to convert ReaderMode to virtual scrolling parameters
extension ReaderModeExtension on ReaderMode {
bool get isContinuous {
return this == ReaderMode.verticalContinuous ||
this == ReaderMode.webtoon ||
this == ReaderMode.horizontalContinuous;
}
Axis get scrollDirection {
return this == ReaderMode.horizontalContinuous
? Axis.horizontal
: Axis.vertical;
}
bool get isHorizontalContinuous {
return this == ReaderMode.horizontalContinuous;
}
}

View file

@ -65,7 +65,7 @@ final class DoBackUpProvider
}
}
String _$doBackUpHash() => r'd16d5b6e5ed2c20988fa2d49842524d70ac0ed0d';
String _$doBackUpHash() => r'e0d28adf6b592e34f26fd6b566151f3691f1946a';
final class DoBackUpFamily extends $Family
with

View file

@ -302,3 +302,165 @@ class NovelTextAlignState extends _$NovelTextAlignState {
);
}
}
@riverpod
class NovelReaderThemeState extends _$NovelReaderThemeState {
@override
String build() {
return isar.settings.getSync(227)!.novelReaderTheme ?? '#292832';
}
void set(String value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelReaderTheme = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelReaderTextColorState extends _$NovelReaderTextColorState {
@override
String build() {
return isar.settings.getSync(227)!.novelReaderTextColor ?? '#CCCCCC';
}
void set(String value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelReaderTextColor = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelReaderPaddingState extends _$NovelReaderPaddingState {
@override
int build() {
return isar.settings.getSync(227)!.novelReaderPadding ?? 16;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelReaderPadding = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelReaderLineHeightState extends _$NovelReaderLineHeightState {
@override
double build() {
return isar.settings.getSync(227)!.novelReaderLineHeight ?? 1.5;
}
void set(double value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelReaderLineHeight = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelShowScrollPercentageState extends _$NovelShowScrollPercentageState {
@override
bool build() {
return isar.settings.getSync(227)!.novelShowScrollPercentage ?? true;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelShowScrollPercentage = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelAutoScrollState extends _$NovelAutoScrollState {
@override
bool build() {
return isar.settings.getSync(227)!.novelAutoScroll ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelAutoScroll = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelRemoveExtraParagraphSpacingState
extends _$NovelRemoveExtraParagraphSpacingState {
@override
bool build() {
return isar.settings.getSync(227)!.novelRemoveExtraParagraphSpacing ??
false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelRemoveExtraParagraphSpacing = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NovelTapToScrollState extends _$NovelTapToScrollState {
@override
bool build() {
return isar.settings.getSync(227)!.novelTapToScroll ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..novelTapToScroll = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}

View file

@ -764,3 +764,440 @@ abstract class _$NovelTextAlignState extends $Notifier<NovelTextAlign> {
element.handleValue(ref, created);
}
}
@ProviderFor(NovelReaderThemeState)
const novelReaderThemeStateProvider = NovelReaderThemeStateProvider._();
final class NovelReaderThemeStateProvider
extends $NotifierProvider<NovelReaderThemeState, String> {
const NovelReaderThemeStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelReaderThemeStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelReaderThemeStateHash();
@$internal
@override
NovelReaderThemeState create() => NovelReaderThemeState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String>(value),
);
}
}
String _$novelReaderThemeStateHash() =>
r'3149f8ea16353f770b57cce9f27f3e63d062ee7b';
abstract class _$NovelReaderThemeState extends $Notifier<String> {
String build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<String, String>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<String, String>,
String,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelReaderTextColorState)
const novelReaderTextColorStateProvider = NovelReaderTextColorStateProvider._();
final class NovelReaderTextColorStateProvider
extends $NotifierProvider<NovelReaderTextColorState, String> {
const NovelReaderTextColorStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelReaderTextColorStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelReaderTextColorStateHash();
@$internal
@override
NovelReaderTextColorState create() => NovelReaderTextColorState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String>(value),
);
}
}
String _$novelReaderTextColorStateHash() =>
r'28a1987b49a9b0a209c4848dfa4c8c730432c75d';
abstract class _$NovelReaderTextColorState extends $Notifier<String> {
String build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<String, String>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<String, String>,
String,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelReaderPaddingState)
const novelReaderPaddingStateProvider = NovelReaderPaddingStateProvider._();
final class NovelReaderPaddingStateProvider
extends $NotifierProvider<NovelReaderPaddingState, int> {
const NovelReaderPaddingStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelReaderPaddingStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelReaderPaddingStateHash();
@$internal
@override
NovelReaderPaddingState create() => NovelReaderPaddingState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$novelReaderPaddingStateHash() =>
r'572f1a7134c499a9a5107d29552beca9a5fd55ea';
abstract class _$NovelReaderPaddingState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelReaderLineHeightState)
const novelReaderLineHeightStateProvider =
NovelReaderLineHeightStateProvider._();
final class NovelReaderLineHeightStateProvider
extends $NotifierProvider<NovelReaderLineHeightState, double> {
const NovelReaderLineHeightStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelReaderLineHeightStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelReaderLineHeightStateHash();
@$internal
@override
NovelReaderLineHeightState create() => NovelReaderLineHeightState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(double value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<double>(value),
);
}
}
String _$novelReaderLineHeightStateHash() =>
r'cc21fb550eecf8d7869c076ab47647afd2873996';
abstract class _$NovelReaderLineHeightState extends $Notifier<double> {
double build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<double, double>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<double, double>,
double,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelShowScrollPercentageState)
const novelShowScrollPercentageStateProvider =
NovelShowScrollPercentageStateProvider._();
final class NovelShowScrollPercentageStateProvider
extends $NotifierProvider<NovelShowScrollPercentageState, bool> {
const NovelShowScrollPercentageStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelShowScrollPercentageStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelShowScrollPercentageStateHash();
@$internal
@override
NovelShowScrollPercentageState create() => NovelShowScrollPercentageState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$novelShowScrollPercentageStateHash() =>
r'adc9cb5def293fa4ed8b367929e7538f6f056b76';
abstract class _$NovelShowScrollPercentageState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelAutoScrollState)
const novelAutoScrollStateProvider = NovelAutoScrollStateProvider._();
final class NovelAutoScrollStateProvider
extends $NotifierProvider<NovelAutoScrollState, bool> {
const NovelAutoScrollStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelAutoScrollStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelAutoScrollStateHash();
@$internal
@override
NovelAutoScrollState create() => NovelAutoScrollState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$novelAutoScrollStateHash() =>
r'80f717515844fa97396dffc6f45ee0b7b9e6f96d';
abstract class _$NovelAutoScrollState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelRemoveExtraParagraphSpacingState)
const novelRemoveExtraParagraphSpacingStateProvider =
NovelRemoveExtraParagraphSpacingStateProvider._();
final class NovelRemoveExtraParagraphSpacingStateProvider
extends $NotifierProvider<NovelRemoveExtraParagraphSpacingState, bool> {
const NovelRemoveExtraParagraphSpacingStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelRemoveExtraParagraphSpacingStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() =>
_$novelRemoveExtraParagraphSpacingStateHash();
@$internal
@override
NovelRemoveExtraParagraphSpacingState create() =>
NovelRemoveExtraParagraphSpacingState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$novelRemoveExtraParagraphSpacingStateHash() =>
r'5c784a57ce5ee57524317dd00d4b40020e5e0582';
abstract class _$NovelRemoveExtraParagraphSpacingState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(NovelTapToScrollState)
const novelTapToScrollStateProvider = NovelTapToScrollStateProvider._();
final class NovelTapToScrollStateProvider
extends $NotifierProvider<NovelTapToScrollState, bool> {
const NovelTapToScrollStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'novelTapToScrollStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$novelTapToScrollStateHash();
@$internal
@override
NovelTapToScrollState create() => NovelTapToScrollState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$novelTapToScrollStateHash() =>
r'4ad09be8c324b019bd1d94cd8d77ef6077bd2100';
abstract class _$NovelTapToScrollState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View file

@ -1,10 +1,10 @@
import 'package:isar_community/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'novel_reader_controller_provider.g.dart';
@ -188,115 +188,3 @@ class NovelReaderController extends _$NovelReaderController {
return chapter.name!;
}
}
extension MangaExtensions on Manga {
List<Chapter> getFilteredChapterList() {
final data = this.chapters.toList().toList();
final filterUnread =
(isar.settings
.getSync(227)!
.chapterFilterUnreadList!
.where((element) => element.mangaId == id)
.toList()
.firstOrNull ??
ChapterFilterUnread(mangaId: id, type: 0))
.type!;
final filterBookmarked =
(isar.settings
.getSync(227)!
.chapterFilterBookmarkedList!
.where((element) => element.mangaId == id)
.toList()
.firstOrNull ??
ChapterFilterBookmarked(mangaId: id, type: 0))
.type!;
final filterDownloaded =
(isar.settings
.getSync(227)!
.chapterFilterDownloadedList!
.where((element) => element.mangaId == id)
.toList()
.firstOrNull ??
ChapterFilterDownloaded(mangaId: id, type: 0))
.type!;
final sortChapter =
(isar.settings
.getSync(227)!
.sortChapterList!
.where((element) => element.mangaId == id)
.toList()
.firstOrNull ??
SortChapter(mangaId: id, index: 1, reverse: false))
.index;
final filterScanlator = _getFilterScanlator(this) ?? [];
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()
.idIsNotNull()
.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;
}
}
List<String>? _getFilterScanlator(Manga manga) {
final scanlators = isar.settings.getSync(227)!.filterScanlatorList ?? [];
final filter = scanlators
.where((element) => element.mangaId == manga.id)
.toList();
return filter.firstOrNull?.scanlators;
}

View file

@ -16,6 +16,8 @@ import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.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/widgets/novel_reader_settings_sheet.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/get_html_content.dart';
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
@ -24,7 +26,7 @@ import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:html/dom.dart' as dom;
import 'package:flutter/widgets.dart' as widgets;
@ -73,6 +75,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
if (_scrollController.hasClients) {
offset = _scrollController.offset;
maxOffset = _scrollController.position.maxScrollExtent;
_rebuildDetail.add(offset);
}
}
@ -203,119 +206,254 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
bottom: false,
child: Stack(
children: [
widget.result.when(
data: (data) {
epubBook = data.$2;
Future.delayed(const Duration(milliseconds: 1000), () {
if (!scrolled && _scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent *
(double.tryParse(chapter.lastPageRead!) ?? 0),
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
);
scrolled = true;
}
});
return Scrollbar(
controller: _scrollController,
interactive: true,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_isViewFunction();
},
child: CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
HtmlWidget(
data.$1,
customWidgetBuilder: (element) =>
_buildCustomWidgets(element),
customStylesBuilder: (element) {
switch (backgroundColor) {
case BackgroundColor.black:
return {'background-color': 'black'};
default:
return {'background-color': '#F0F0F0'};
}
},
onTapUrl: (url) {
context.push(
"/mangawebview",
extra: {'url': url, 'title': url},
);
return true;
},
renderMode: RenderMode.sliverList,
textStyle: TextStyle(
color: backgroundColor == BackgroundColor.white
? Colors.black
: Colors.white,
fontSize: fontSize.toDouble(),
),
),
SliverToBoxAdapter(
child: Center(
heightFactor: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 5,
children: [
IconButton(
padding: const EdgeInsets.all(5),
onPressed: () =>
pushReplacementMangaReaderView(
context: context,
chapter: _readerController
.getPrevChapter(),
),
icon: Icon(
size: 32,
Icons.arrow_back,
color:
backgroundColor ==
BackgroundColor.white
? Colors.black
: Colors.white,
Column(
children: [
Flexible(
child: widget.result.when(
data: (data) {
epubBook = data.$2;
final padding = ref.watch(
novelReaderPaddingStateProvider,
);
final lineHeight = ref.watch(
novelReaderLineHeightStateProvider,
);
final textAlign = ref.watch(
novelTextAlignStateProvider,
);
final removeExtraSpacing = ref.watch(
novelRemoveExtraParagraphSpacingStateProvider,
);
final customBackgroundColor = ref.watch(
novelReaderThemeStateProvider,
);
final customTextColor = ref.watch(
novelReaderTextColorStateProvider,
);
Color parseColor(String hex) {
final hexColor = hex.replaceAll('#', '');
return Color(int.parse('FF$hexColor', radix: 16));
}
TextAlign getTextAlign() {
switch (textAlign) {
case NovelTextAlign.left:
return TextAlign.left;
case NovelTextAlign.center:
return TextAlign.center;
case NovelTextAlign.right:
return TextAlign.right;
case NovelTextAlign.block:
return TextAlign.justify;
}
}
Future.delayed(const Duration(milliseconds: 10), () {
if (!scrolled && _scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent *
(double.tryParse(chapter.lastPageRead!) ??
0),
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
);
scrolled = true;
}
});
return Consumer(
builder: (context, ref, _) {
final fontSize = ref.read(
novelFontSizeStateProvider,
);
return Scrollbar(
controller: _scrollController,
interactive: true,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_isViewFunction();
},
child: CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Html(
data: data.$1,
style: {
"body": Style(
fontSize: FontSize(
fontSize.toDouble(),
),
color: parseColor(
customTextColor,
),
backgroundColor: parseColor(
customBackgroundColor,
),
margin: Margins.zero,
padding: HtmlPaddings.all(
padding.toDouble(),
),
lineHeight: LineHeight(
lineHeight,
),
textAlign: getTextAlign(),
),
"p": Style(
margin: removeExtraSpacing
? Margins.only(bottom: 4)
: Margins.only(bottom: 8),
fontSize: FontSize(
fontSize.toDouble(),
),
lineHeight: LineHeight(
lineHeight,
),
textAlign: getTextAlign(),
),
"div": Style(
fontSize: FontSize(
fontSize.toDouble(),
),
lineHeight: LineHeight(
lineHeight,
),
textAlign: getTextAlign(),
),
"span": Style(
fontSize: FontSize(
fontSize.toDouble(),
),
lineHeight: LineHeight(
lineHeight,
),
),
"h1, h2, h3, h4, h5, h6": Style(
color: parseColor(
customTextColor,
),
lineHeight: LineHeight(
lineHeight,
),
textAlign: getTextAlign(),
),
"a": Style(
color: Colors.blue,
textDecoration:
TextDecoration.underline,
),
"img": Style(
width: Width(100, Unit.percent),
height: Height.auto(),
),
},
extensions: [
TagExtension(
tagsToExtend: {"img"},
builder: (extensionContext) {
final element =
extensionContext.node
as dom.Element;
final customWidget =
_buildCustomWidgets(
element,
);
if (customWidget != null) {
return customWidget;
}
return const SizedBox.shrink();
},
),
],
onLinkTap:
(url, attributes, element) {
if (url != null) {
context.push(
"/mangawebview",
extra: {
'url': url,
'title': url,
},
);
}
},
),
),
),
IconButton(
padding: const EdgeInsets.all(5),
onPressed: () =>
pushReplacementMangaReaderView(
context: context,
chapter: _readerController
.getNextChapter(),
),
icon: Icon(
size: 32,
Icons.arrow_forward,
color:
backgroundColor ==
BackgroundColor.white
? Colors.black
: Colors.white,
),
),
],
],
),
),
),
),
],
);
},
);
},
loading: () => scaffoldWith(
context,
Center(child: CircularProgressIndicator()),
),
error: (err, stack) => scaffoldWith(
context,
Center(child: Text(err.toString())),
),
),
);
},
loading: () => scaffoldWith(
context,
Center(child: CircularProgressIndicator()),
),
error: (err, stack) => scaffoldWith(
context,
Center(child: Text(err.toString())),
),
),
if (ref.watch(novelShowScrollPercentageStateProvider))
StreamBuilder(
stream: _rebuildDetail.stream,
builder: (context, asyncSnapshot) {
return Consumer(
builder: (context, ref, child) {
final customBackgroundColor = ref.watch(
novelReaderThemeStateProvider,
);
final customTextColor = ref.watch(
novelReaderTextColorStateProvider,
);
final scrollPercentage = maxOffset > 0
? ((offset / maxOffset) * 100)
.clamp(0, 100)
.toInt()
: 0;
return Row(
children: [
Expanded(
child: Container(
color: Color(
int.parse(
'FF${customBackgroundColor.replaceAll('#', '')}',
radix: 16,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
'$scrollPercentage %',
style: TextStyle(
color: Color(
int.parse(
'FF${customTextColor.replaceAll('#', '')}',
radix: 16,
),
),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
],
);
},
);
},
),
],
),
_appBar(),
_bottomBar(backgroundColor),
@ -485,70 +623,186 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
_readerController.getChapterIndex().$2,
);
bool hasNextChapter = _readerController.getChapterIndex().$1 != 0;
// final novelTextAlign = ref.watch(novelTextAlignStateProvider); // TODO. The variable is never used/modified
final bodyLargeColor = Theme.of(context).textTheme.bodyLarge!.color;
return Positioned(
bottom: 0,
child: AnimatedContainer(
curve: Curves.ease,
duration: const Duration(milliseconds: 300),
width: context.width(1),
height: (_isView ? 130 : 0),
height: (_isView ? 140 : 0),
child: Column(
children: [
Flexible(
child: Transform.scale(
scaleX: 1,
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
radius: 23,
backgroundColor: _backgroundColor(context),
child: IconButton(
onPressed: hasPrevChapter
? () {
pushReplacementMangaReaderView(
context: context,
chapter: _readerController.getPrevChapter(),
);
}
: null,
icon: Transform.scale(
scaleX: 1,
child: Icon(
Icons.skip_previous_rounded,
color: hasPrevChapter
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
.bodyLarge!
.color!
.withValues(alpha: 0.4),
),
if (_isView)
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
radius: 21,
backgroundColor: _backgroundColor(context),
child: IconButton(
onPressed: hasPrevChapter
? () {
pushReplacementMangaReaderView(
context: context,
chapter: _readerController.getPrevChapter(),
);
}
: null,
icon: Icon(
Icons.skip_previous_rounded,
color: hasPrevChapter
? bodyLargeColor
: bodyLargeColor!.withValues(alpha: 0.4),
),
),
),
),
Flexible(
child: Container(
height: 40,
decoration: BoxDecoration(
color: _backgroundColor(context),
borderRadius: BorderRadius.circular(50),
),
child: StreamBuilder(
stream: _rebuildDetail.stream,
builder: (context, asyncSnapshot) {
return Consumer(
builder: (context, ref, child) {
final scrollPercentage = maxOffset > 0
? ((offset / maxOffset) * 100)
.clamp(0, 100)
.toInt()
: 0;
return Row(
children: [
SizedBox(width: 10),
Padding(
padding: const EdgeInsets.all(4),
child: Text(
scrollPercentage.toInt().toString(),
style: TextStyle(
color: bodyLargeColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
if (_isView)
Expanded(
flex: 14,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 2.0,
thumbShape:
const RoundSliderThumbShape(
enabledThumbRadius: 6.0,
),
overlayShape:
const RoundSliderOverlayShape(
overlayRadius: 12.0,
),
),
child: Slider(
onChanged: (value) {
_scrollController.jumpTo(
_scrollController
.position
.maxScrollExtent *
value,
);
},
value: scrollPercentage / 100,
min: 0,
max: 1,
),
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
'100',
style: TextStyle(
color: bodyLargeColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 10),
],
);
},
);
},
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
radius: 21,
backgroundColor: _backgroundColor(context),
child: IconButton(
onPressed: hasNextChapter
? () {
pushReplacementMangaReaderView(
context: context,
chapter: _readerController.getNextChapter(),
);
}
: null,
icon: Transform.scale(
scaleX: 1,
child: Icon(
Icons.skip_next_rounded,
color: hasNextChapter
? bodyLargeColor
: bodyLargeColor!.withValues(alpha: 0.4),
),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
height: 70,
decoration: BoxDecoration(
color: _backgroundColor(context),
borderRadius: BorderRadius.circular(25),
),
),
],
),
if (_isView)
Expanded(
child: Container(
color: _backgroundColor(context),
child: Row(
children: [
Flexible(
child: SizedBox(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Transform.scale(
scaleX: 1,
child: SizedBox(
width: 55,
child: Center(
child: IconButton(
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
border: Border.all(
color: bodyLargeColor!,
width: 0.2,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Text Size :',
style: TextStyle(
fontWeight: FontWeight.bold,
color: bodyLargeColor,
),
),
IconButton(
onPressed: () {
final newFontSize = max(
4,
@ -564,58 +818,52 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
fontSize = newFontSize;
});
},
icon: const Icon(Icons.text_decrease),
icon: Icon(Icons.text_decrease),
iconSize: 20,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
),
),
),
),
if (_isView)
Flexible(
flex: 14,
child: Consumer(
builder: (context, ref, child) {
final currentFontSize = ref.watch(
novelFontSizeStateProvider,
);
return SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape:
const RoundSliderOverlayShape(
overlayRadius: 5.0,
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(
8,
),
),
child: Consumer(
builder: (context, ref, child) {
final currentFontSize = ref.watch(
novelFontSizeStateProvider,
);
return Text(
"$currentFontSize px",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
child: Slider(
onChanged: (value) {
ref
.read(
novelFontSizeStateProvider
.notifier,
)
.set(value.toInt());
);
},
onChangeEnd: (newValue) {
try {
setState(() {
fontSize = newValue.toInt();
});
} catch (_) {}
},
divisions: 36,
value: currentFontSize.toDouble(),
label: "$currentFontSize",
min: 4,
max: 40,
),
);
},
),
),
Transform.scale(
scaleX: 1,
child: SizedBox(
width: 55,
child: Center(
child: IconButton(
),
),
IconButton(
onPressed: () {
final newFontSize = min(
40,
@ -632,106 +880,42 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
});
},
icon: const Icon(Icons.text_increase),
iconSize: 20,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
),
),
],
),
),
IconButton(
onPressed: () {
customDraggableTabBar(
tabs: [
Tab(text: context.l10n.reader),
Tab(text: context.l10n.general),
],
children: [
ReaderSettingsTab(),
GeneralSettingsTab(),
],
context: context,
vsync: this,
);
},
icon: const Icon(Icons.settings),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
radius: 23,
backgroundColor: _backgroundColor(context),
child: IconButton(
onPressed: hasNextChapter
? () {
pushReplacementMangaReaderView(
context: context,
chapter: _readerController.getNextChapter(),
);
}
: null,
icon: Transform.scale(
scaleX: 1,
child: Icon(
Icons.skip_next_rounded,
color: hasNextChapter
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
.bodyLarge!
.color!
.withValues(alpha: 0.4),
// size: 17,
),
),
),
),
),
],
],
),
),
),
),
/*Flexible(
child: Container(
height: 65,
color: _backgroundColor(context),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
PopupMenuButton(
popUpAnimationStyle: popupAnimationStyle,
color: Colors.black,
child: const Icon(
Icons.format_align_center_outlined,
),
onSelected: (value) {
ref
.read(novelTextAlignStateProvider.notifier)
.set(value);
},
itemBuilder: (context) => [
for (var mode in NovelTextAlign.values)
PopupMenuItem(
value: mode,
child: Row(
children: [
Icon(
Icons.check,
color: novelTextAlign == mode
? Colors.white
: Colors.transparent,
),
const SizedBox(
width: 7,
),
Text(
mode.name,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
)),
],
),
IconButton(
onPressed: () {
// _showModalSettings();
},
icon: const Icon(
Icons.settings_rounded,
),
),
],
),
),
),*/
],
),
),

View file

@ -0,0 +1,591 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
class ReaderSettingsTab extends ConsumerWidget {
const ReaderSettingsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final padding = ref.watch(novelReaderPaddingStateProvider);
final lineHeight = ref.watch(novelReaderLineHeightStateProvider);
final textAlign = ref.watch(novelTextAlignStateProvider);
final backgroundColor = ref.watch(novelReaderThemeStateProvider);
final textColor = ref.watch(novelReaderTextColorStateProvider);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_SettingSection(
title: 'Theme',
child: Column(
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_ThemeButton(
backgroundColor: '#292832',
textColor: '#CCCCCC',
label: 'Dark',
isSelected: backgroundColor == '#292832',
onTap: () {
ref
.read(novelReaderThemeStateProvider.notifier)
.set('#292832');
ref
.read(novelReaderTextColorStateProvider.notifier)
.set('#CCCCCC');
},
),
_ThemeButton(
backgroundColor: '#FFFFFF',
textColor: '#000000',
label: 'Light',
isSelected: backgroundColor == '#FFFFFF',
onTap: () {
ref
.read(novelReaderThemeStateProvider.notifier)
.set('#FFFFFF');
ref
.read(novelReaderTextColorStateProvider.notifier)
.set('#000000');
},
),
_ThemeButton(
backgroundColor: '#000000',
textColor: '#FFFFFF',
label: 'Black',
isSelected: backgroundColor == '#000000',
onTap: () {
ref
.read(novelReaderThemeStateProvider.notifier)
.set('#000000');
ref
.read(novelReaderTextColorStateProvider.notifier)
.set('#FFFFFF');
},
),
_ThemeButton(
backgroundColor: '#F5E6D3',
textColor: '#5F4B32',
label: 'Sepia',
isSelected: backgroundColor == '#F5E6D3',
onTap: () {
ref
.read(novelReaderThemeStateProvider.notifier)
.set('#F5E6D3');
ref
.read(novelReaderTextColorStateProvider.notifier)
.set('#5F4B32');
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _ColorPicker(
label: 'Background',
color: backgroundColor,
onColorChanged: (color) {
ref
.read(novelReaderThemeStateProvider.notifier)
.set(color);
},
),
),
const SizedBox(width: 12),
Expanded(
child: _ColorPicker(
label: 'Text',
color: textColor,
onColorChanged: (color) {
ref
.read(novelReaderTextColorStateProvider.notifier)
.set(color);
},
),
),
],
),
],
),
),
const SizedBox(height: 16),
_SettingSection(
title: 'Text Align',
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_AlignButton(
icon: Icons.format_align_left,
isSelected: textAlign == NovelTextAlign.left,
onTap: () {
ref
.read(novelTextAlignStateProvider.notifier)
.set(NovelTextAlign.left);
},
),
_AlignButton(
icon: Icons.format_align_center,
isSelected: textAlign == NovelTextAlign.center,
onTap: () {
ref
.read(novelTextAlignStateProvider.notifier)
.set(NovelTextAlign.center);
},
),
_AlignButton(
icon: Icons.format_align_right,
isSelected: textAlign == NovelTextAlign.right,
onTap: () {
ref
.read(novelTextAlignStateProvider.notifier)
.set(NovelTextAlign.right);
},
),
_AlignButton(
icon: Icons.format_align_justify,
isSelected: textAlign == NovelTextAlign.block,
onTap: () {
ref
.read(novelTextAlignStateProvider.notifier)
.set(NovelTextAlign.block);
},
),
],
),
),
const SizedBox(height: 16),
_SettingSection(
title: 'Padding',
child: Column(
children: [
Row(
children: [
const Icon(Icons.space_bar, size: 20),
Expanded(
child: Slider(
value: padding.toDouble(),
min: 0,
max: 50,
divisions: 50,
label: '$padding px',
onChanged: (value) {
ref
.read(novelReaderPaddingStateProvider.notifier)
.set(value.toInt());
},
),
),
Text('${padding}px'),
],
),
],
),
),
const SizedBox(height: 16),
_SettingSection(
title: 'Line Height',
child: Column(
children: [
Row(
children: [
const Icon(Icons.height, size: 20),
Expanded(
child: Slider(
value: lineHeight,
min: 1.0,
max: 3.0,
divisions: 20,
label: lineHeight.toStringAsFixed(1),
onChanged: (value) {
ref
.read(novelReaderLineHeightStateProvider.notifier)
.set(value);
},
),
),
Text(lineHeight.toStringAsFixed(1)),
],
),
],
),
),
],
),
);
}
}
class GeneralSettingsTab extends ConsumerWidget {
const GeneralSettingsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_SwitchListTileSetting(
title: 'Show Scroll Percentage',
subtitle: 'Display reading progress percentage',
value: ref.watch(novelShowScrollPercentageStateProvider),
onChanged: (value) {
ref
.read(novelShowScrollPercentageStateProvider.notifier)
.set(value);
},
),
// _SwitchListTileSetting(
// title: 'Auto Scroll',
// subtitle: 'Automatically scroll through pages',
// value: ref.watch(novelAutoScrollStateProvider),
// onChanged: (value) {
// ref.read(novelAutoScrollStateProvider.notifier).set(value);
// },
// ),
_SwitchListTileSetting(
title: 'Remove Extra Paragraph Spacing',
subtitle: 'Reduce spacing between paragraphs',
value: ref.watch(novelRemoveExtraParagraphSpacingStateProvider),
onChanged: (value) {
ref
.read(novelRemoveExtraParagraphSpacingStateProvider.notifier)
.set(value);
},
),
// _SwitchListTileSetting(
// title: 'Tap to Scroll',
// subtitle: 'Tap screen to scroll up/down',
// value: ref.watch(novelTapToScrollStateProvider),
// onChanged: (value) {
// ref.read(novelTapToScrollStateProvider.notifier).set(value);
// },
// ),
],
),
);
}
}
class _SettingSection extends StatelessWidget {
final String title;
final Widget child;
const _SettingSection({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.titleLarge?.color,
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
child: child,
),
),
],
);
}
}
class _SwitchListTileSetting extends StatelessWidget {
final String title;
final String subtitle;
final bool value;
final ValueChanged<bool> onChanged;
const _SwitchListTileSetting({
required this.title,
required this.subtitle,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(title),
subtitle: Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
value: value,
onChanged: onChanged,
);
}
}
class _ThemeButton extends StatelessWidget {
final String backgroundColor;
final String textColor;
final String label;
final bool isSelected;
final VoidCallback onTap;
const _ThemeButton({
required this.backgroundColor,
required this.textColor,
required this.label,
required this.isSelected,
required this.onTap,
});
Color _parseColor(String hex) {
final hexColor = hex.replaceAll('#', '');
return Color(int.parse('FF$hexColor', radix: 16));
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 70,
height: 60,
decoration: BoxDecoration(
color: _parseColor(backgroundColor),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey.withValues(alpha: 0.3),
width: isSelected ? 3 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Aa',
style: TextStyle(
color: _parseColor(textColor),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(color: _parseColor(textColor), fontSize: 10),
),
],
),
),
);
}
}
class _ColorPicker extends StatelessWidget {
final String label;
final String color;
final ValueChanged<String> onColorChanged;
const _ColorPicker({
required this.label,
required this.color,
required this.onColorChanged,
});
Color _parseColor(String hex) {
final hexColor = hex.replaceAll('#', '');
return Color(int.parse('FF$hexColor', radix: 16));
}
String _colorToHex(Color color) {
return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
}
void _showColorPickerDialog(BuildContext context) {
Color selectedColor = _parseColor(color);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Select $label Color'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_colorOption(context, Colors.white, selectedColor),
_colorOption(context, Colors.black, selectedColor),
_colorOption(
context,
const Color(0xFF292832),
selectedColor,
),
_colorOption(
context,
const Color(0xFFF5E6D3),
selectedColor,
),
_colorOption(
context,
const Color(0xFF5F4B32),
selectedColor,
),
_colorOption(
context,
const Color(0xFFCCCCCC),
selectedColor,
),
_colorOption(context, Colors.grey[800]!, selectedColor),
_colorOption(context, Colors.grey[300]!, selectedColor),
_colorOption(context, Colors.brown[100]!, selectedColor),
_colorOption(context, Colors.blue[100]!, selectedColor),
_colorOption(context, Colors.green[100]!, selectedColor),
_colorOption(context, Colors.amber[100]!, selectedColor),
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
],
);
},
);
}
Widget _colorOption(
BuildContext context,
Color optionColor,
Color selectedColor,
) {
final isSelected = optionColor.toARGB32() == selectedColor.toARGB32();
return GestureDetector(
onTap: () {
onColorChanged(_colorToHex(optionColor));
Navigator.of(context).pop();
},
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: optionColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
width: isSelected ? 3 : 1,
),
),
child: isSelected
? Icon(
Icons.check,
color: optionColor.computeLuminance() > 0.5
? Colors.black
: Colors.white,
)
: null,
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showColorPickerDialog(context),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: _parseColor(color),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12)),
Text(
color,
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
),
],
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
}
class _AlignButton extends StatelessWidget {
final IconData icon;
final bool isSelected;
final VoidCallback onTap;
const _AlignButton({
required this.icon,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor.withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey.withValues(alpha: 0.3),
width: isSelected ? 2 : 1,
),
),
child: Icon(
icon,
color: isSelected
? Theme.of(context).primaryColor
: Theme.of(context).iconTheme.color,
),
),
);
}
}

View file

@ -14,6 +14,130 @@ import 'package:path_provider/path_provider.dart';
import 'package:extended_image_library/src/network/extended_network_image_provider.dart'
as image_provider;
/// LRU Memory Cache for decoded image data
class _LRUCache<K, V> {
final int _maxSize;
final _cache = <K, V>{};
int _currentSize = 0;
final int Function(V)? _sizeOf;
_LRUCache({required int maxSize, int Function(V)? sizeOf})
: _maxSize = maxSize,
_sizeOf = sizeOf;
V? get(K key) {
final value = _cache.remove(key);
if (value != null) {
_cache[key] = value; // Move to end (most recently used)
}
return value;
}
void put(K key, V value) {
_cache.remove(key); // Remove if exists
_cache[key] = value; // Add to end
if (_sizeOf != null) {
_currentSize += _sizeOf(value);
while (_currentSize > _maxSize && _cache.isNotEmpty) {
final oldest = _cache.entries.first;
_currentSize -= _sizeOf(oldest.value);
_cache.remove(oldest.key);
}
} else {
while (_cache.length > _maxSize) {
_cache.remove(_cache.keys.first);
}
}
}
void remove(K key) {
final value = _cache.remove(key);
if (value != null && _sizeOf != null) {
_currentSize -= _sizeOf(value);
}
}
void clear() {
_cache.clear();
_currentSize = 0;
}
int get length => _cache.length;
int get currentSize => _currentSize;
}
/// Global memory cache (100 images max, ~50MB)
final _memoryCache = _LRUCache<String, Uint8List>(
maxSize: 50 * 1024 * 1024, // 50MB
sizeOf: (data) => data.length,
);
/// Cache metadata for LRU eviction
class _CacheMetadata {
final String path;
final int size;
final DateTime lastAccessed;
_CacheMetadata({
required this.path,
required this.size,
required this.lastAccessed,
});
}
/// Global cache manager
class _CacheManager {
static const _maxCacheSize = 500 * 1024 * 1024; // 500MB
static Future<int> getCacheSize(Directory cacheDir) async {
if (!await cacheDir.exists()) return 0;
int totalSize = 0;
await for (final entity in cacheDir.list(recursive: true)) {
if (entity is File) {
totalSize += await entity.length();
}
}
return totalSize;
}
static Future<void> evictOldestIfNeeded(Directory cacheDir) async {
final size = await getCacheSize(cacheDir);
if (size <= _maxCacheSize) return;
// Collect all cache files with metadata
final List<_CacheMetadata> files = [];
await for (final entity in cacheDir.list()) {
if (entity is File) {
final stat = await entity.stat();
files.add(
_CacheMetadata(
path: entity.path,
size: stat.size,
lastAccessed: stat.accessed,
),
);
}
}
// Sort by last accessed (oldest first)
files.sort((a, b) => a.lastAccessed.compareTo(b.lastAccessed));
// Delete until under limit
int currentSize = size;
for (final file in files) {
if (currentSize <= _maxCacheSize) break;
try {
await File(file.path).delete();
currentSize -= file.size;
} catch (e) {
if (kDebugMode) print('Failed to delete cache file: $e');
}
}
}
}
class CustomExtendedNetworkImageProvider
extends ImageProvider<image_provider.ExtendedNetworkImageProvider>
with ExtendedImageProvider<image_provider.ExtendedNetworkImageProvider>
@ -187,6 +311,12 @@ class CustomExtendedNetworkImageProvider
StreamController<ImageChunkEvent>? chunkEvents,
String md5Key,
) async {
// Check memory cache first
final cachedData = _memoryCache.get(md5Key);
if (cachedData != null) {
return cachedData;
}
final Directory cacheImagesDirectory = Directory(
join(
(await getTemporaryDirectory()).path,
@ -197,6 +327,7 @@ class CustomExtendedNetworkImageProvider
Uint8List? data;
await StorageProvider().createDirectorySafely(cacheImagesDirectory.path);
final File cacheFile = File(join(cacheImagesDirectory.path, md5Key));
// exist, try to find cache image file
if (cacheFile.existsSync()) {
if (key.cacheMaxAge != null) {
@ -206,17 +337,28 @@ class CustomExtendedNetworkImageProvider
cacheFile.deleteSync();
} else {
data = await cacheFile.readAsBytes();
// Store in memory cache
_memoryCache.put(md5Key, data);
}
} else {
data = await cacheFile.readAsBytes();
// Store in memory cache
_memoryCache.put(md5Key, data);
}
}
// load from network
if (data == null) {
data = await _loadNetwork(key, chunkEvents);
if (data != null) {
// Evict old cache if needed before writing
await _CacheManager.evictOldestIfNeeded(cacheImagesDirectory);
// cache image file
await File(join(cacheImagesDirectory.path, md5Key)).writeAsBytes(data);
// Store in memory cache
_memoryCache.put(md5Key, data);
}
}
@ -231,32 +373,37 @@ class CustomExtendedNetworkImageProvider
try {
final Uri resolved = Uri.base.resolve(key.url);
final StreamedResponse? response = await _tryGetResponse(resolved);
List<int> bytes = [];
final int total = response!.contentLength ?? 0;
if (response.statusCode == HttpStatus.ok) {
int received = 0;
response.stream.asBroadcastStream();
await for (var chunk in response.stream) {
bytes.addAll(chunk);
try {
received += chunk.length;
if (chunkEvents != null) {}
chunkEvents!.add(
ImageChunkEvent(
cumulativeBytesLoaded: received,
expectedTotalBytes: total,
),
);
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
} else {
if (response == null || response.statusCode != HttpStatus.ok) {
return null;
}
// Pre-allocate list if content length is known
final int total = response.contentLength ?? 0;
final List<int> bytes = total > 0
? List<int>.filled(total, 0, growable: true)
: [];
int received = 0;
response.stream.asBroadcastStream();
await for (var chunk in response.stream) {
if (total > 0 && received + chunk.length <= total) {
// Copy directly to pre-allocated list
bytes.setRange(received, received + chunk.length, chunk);
} else {
// Fallback for unknown size
bytes.addAll(chunk);
}
received += chunk.length;
chunkEvents?.add(
ImageChunkEvent(
cumulativeBytesLoaded: received,
expectedTotalBytes: total,
),
);
}
if (bytes.isEmpty) {
return Future<Uint8List>.error(
StateError('NetworkImage is an empty file: $resolved'),
@ -281,11 +428,20 @@ class CustomExtendedNetworkImageProvider
Future<StreamedResponse> _getResponse(Uri resolved) async {
var request = Request('GET', resolved);
request.headers.addAll(headers ?? {});
// Optimize headers for better caching and compression
final optimizedHeaders = {
...?headers,
'Accept-Encoding': 'gzip, deflate, br',
'Accept': 'image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Connection': 'keep-alive',
};
request.headers.addAll(optimizedHeaders);
StreamedResponse response = await MClient.init(
showCloudFlareError: showCloudFlareError,
).send(request);
if (response.statusCode != 200) {
final res = await MClient.init(
reqcopyWith: {'useDartHttpClient': true},
@ -297,20 +453,40 @@ class CustomExtendedNetworkImageProvider
return response;
}
// Http get with cancel, delay try again
// Http get with cancel, exponential backoff retry
Future<StreamedResponse?> _tryGetResponse(Uri resolved) async {
cancelToken?.throwIfCancellationRequested();
return await RetryHelper.tryRun<StreamedResponse>(
() {
return CancellationTokenSource.register(
int attempt = 0;
while (attempt < retries) {
try {
return await CancellationTokenSource.register(
cancelToken,
_getResponse(resolved),
);
},
cancelToken: cancelToken,
timeRetry: timeRetry,
retries: retries,
);
} catch (e) {
attempt++;
if (attempt >= retries) {
rethrow;
}
// Exponential backoff: 100ms, 200ms, 400ms, 800ms, etc.
final backoffDelay = Duration(
milliseconds: timeRetry.inMilliseconds * (1 << attempt),
);
if (kDebugMode) {
print(
'Retry attempt $attempt/$retries after ${backoffDelay.inMilliseconds}ms',
);
}
await Future.delayed(backoffDelay);
cancelToken?.throwIfCancellationRequested();
}
}
return null;
}
@override

View file

@ -15,13 +15,16 @@ Future<void> fetchItemSourcesList(
if (ref.watch(checkForExtensionsUpdateStateProvider) || reFresh) {
final repos = ref.watch(extensionsRepoStateProvider(itemType));
for (Repo repo in repos) {
await fetchSourcesList(
repo: repo,
refresh: reFresh,
id: id,
ref: ref,
itemType: itemType,
);
try {
await fetchSourcesList(
repo: repo,
refresh: reFresh,
id: id,
androidProxyServer: ref.watch(androidProxyServerStateProvider),
autoUpdateExtensions: ref.watch(autoUpdateExtensionsStateProvider),
itemType: itemType,
);
} catch (_) {}
}
}
}

View file

@ -65,7 +65,7 @@ final class FetchItemSourcesListProvider
}
String _$fetchItemSourcesListHash() =>
r'16238be20517fddacf52a2694fbd50cafbfa7496';
r'219aed67d2329f03101f2270e2f344bf70eff128';
final class FetchItemSourcesListFamily extends $Family
with

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
@ -9,14 +8,14 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:package_info_plus/package_info_plus.dart';
Future<void> fetchSourcesList({
int? id,
required bool refresh,
required Ref ref,
required String androidProxyServer,
required bool autoUpdateExtensions,
required ItemType itemType,
required Repo? repo,
}) async {
@ -126,21 +125,21 @@ Future<void> fetchSourcesList({
orElse: () => Source(),
);
if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) {
await _updateSource(matchingSource, ref, repo, itemType);
await _updateSource(matchingSource, androidProxyServer, repo, itemType);
}
} else {
for (var source in sourceList) {
final existingSource = await isar.sources.get(source.id!);
if (existingSource == null) {
await _addNewSource(source, ref, repo, itemType);
await _addNewSource(source, repo, itemType);
continue;
}
final shouldUpdate =
existingSource.isAdded! &&
compareVersions(existingSource.version!, source.version!) < 0;
if (!shouldUpdate) continue;
if (ref.read(autoUpdateExtensionsStateProvider)) {
await _updateSource(source, ref, repo, itemType);
if (autoUpdateExtensions) {
await _updateSource(source, androidProxyServer, repo, itemType);
} else {
await isar.writeTxn(() async {
isar.sources.put(existingSource..versionLast = source.version);
@ -149,12 +148,12 @@ Future<void> fetchSourcesList({
}
}
checkIfSourceIsObsolete(sourceList, repo!, itemType, ref);
checkIfSourceIsObsolete(sourceList, repo!, itemType);
}
Future<void> _updateSource(
Source source,
Ref ref,
String androidProxyServer,
Repo? repo,
ItemType itemType,
) async {
@ -163,7 +162,7 @@ Future<void> _updateSource(
final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon
? base64.encode(req.bodyBytes)
: req.body;
final androidProxyServer = ref.read(androidProxyServerStateProvider);
Map<String, String> headers = {};
bool? supportLatest;
FilterList? filterList;
@ -232,12 +231,7 @@ Future<void> _updateSource(
await isar.writeTxn(() async => isar.sources.put(updatedSource));
}
Future<void> _addNewSource(
Source source,
Ref ref,
Repo? repo,
ItemType itemType,
) async {
Future<void> _addNewSource(Source source, Repo? repo, ItemType itemType) async {
final newSource = Source()
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
@ -269,7 +263,6 @@ Future<void> checkIfSourceIsObsolete(
List<Source> sourceList,
Repo repo,
ItemType itemType,
Ref ref,
) async {
if (sourceList.isEmpty) return;

View file

@ -66,7 +66,7 @@ final class GetChapterPagesProvider
}
}
String _$getChapterPagesHash() => r'129624607a92b6d3a896a03b450862ce1e941ff6';
String _$getChapterPagesHash() => r'dab1776f81d5ef5003d4d4515fe634f56c14b795';
final class GetChapterPagesFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<GetChapterPagesModel>, Chapter> {

View file

@ -1,7 +1,8 @@
import 'package:mangayomi/eval/lib.dart';
import 'dart:async';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_detail.g.dart';
@ -11,8 +12,12 @@ Future<MManga> getDetail(
required String url,
required Source source,
}) async {
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).getDetail(url);
final proxyServer = ref.read(androidProxyServerStateProvider);
return getIsolateService.get<MManga>(
url: url,
source: source,
serviceType: 'getDetail',
proxyServer: proxyServer,
);
}

View file

@ -58,7 +58,7 @@ final class GetDetailProvider
}
}
String _$getDetailHash() => r'6b758b79281cb00a7df2fe1903d4a67068052bca';
String _$getDetailHash() => r'7eab7d00e6ad61a9bafaee855eae1f49b127af9f';
final class GetDetailFamily extends $Family
with

View file

@ -66,7 +66,7 @@ final class GetHtmlContentProvider
}
}
String _$getHtmlContentHash() => r'fa74506c0adebbdb7a0dda5a8d16a784466b79bb';
String _$getHtmlContentHash() => r'a5763e11960bfe0dbd38ce2b2a3f4b51fefc976e';
final class GetHtmlContentFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<(String, EpubBook?)>, Chapter> {

View file

@ -1,13 +1,12 @@
import 'dart:math';
import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_latest_updates.g.dart';
@ -38,8 +37,10 @@ Future<MPages?> getLatestUpdates(
.toList();
return MPages(list: result, hasNextPage: true);
}
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).getLatestUpdates(page);
return getIsolateService.get<MPages?>(
page: page,
source: source,
serviceType: 'getLatestUpdates',
proxyServer: ref.read(androidProxyServerStateProvider),
);
}

View file

@ -58,7 +58,7 @@ final class GetLatestUpdatesProvider
}
}
String _$getLatestUpdatesHash() => r'7a3c06c469c77ec933cf2f4dd7d39780d993f0ea';
String _$getLatestUpdatesHash() => r'6f99dfe1d4aa950b6852110ec23f92b5c73c413c';
final class GetLatestUpdatesFamily extends $Family
with

View file

@ -1,13 +1,12 @@
import 'dart:math';
import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_popular.g.dart';
@ -38,8 +37,11 @@ Future<MPages?> getPopular(
.toList();
return MPages(list: result, hasNextPage: true);
}
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).getPopular(page);
return getIsolateService.get<MPages?>(
page: page,
source: source,
serviceType: 'getPopular',
proxyServer: ref.read(androidProxyServerStateProvider),
);
}

View file

@ -58,7 +58,7 @@ final class GetPopularProvider
}
}
String _$getPopularHash() => r'f169b6a9ba76d9dd9237ba9c21805151a1419843';
String _$getPopularHash() => r'7e1139bc0f6a3a495fa0dc59d450bc7fd70f36a8';
final class GetPopularFamily extends $Family
with

View file

@ -86,7 +86,7 @@ Future<(List<Video>, bool, List<String>, Directory?)> getVideoList(
episode.url,
episode.archivePath,
);
result = (videos, false, [infohash ?? ""], mpvDirectory);
return (videos, false, [infohash ?? ""], mpvDirectory);
}
try {
@ -112,7 +112,7 @@ Future<(List<Video>, bool, List<String>, Directory?)> getVideoList(
}
}
}
result = (torrentList, false, infoHashes, mpvDirectory);
return (torrentList, false, infoHashes, mpvDirectory);
}
List<Video> list = await getExtensionService(

View file

@ -65,7 +65,7 @@ final class GetVideoListProvider
}
}
String _$getVideoListHash() => r'c54f7294e15eeede933a6e04cd9b761d82b5f74c';
String _$getVideoListHash() => r'd374dccf3edc478c883cb79b9f1be724342b9ac7';
final class GetVideoListFamily extends $Family
with

View file

@ -68,7 +68,7 @@ class MClient {
);
return InterceptedClient.build(
client: httpClient(settings: clientSettings, reqcopyWith: reqcopyWith),
retryPolicy: ResolveCloudFlareChallenge(showCloudFlareError),
interceptors: [
MCookieManager(reqcopyWith),
LoggerInterceptor(showCloudFlareError),
@ -235,11 +235,15 @@ class LoggerInterceptor extends InterceptorContract {
Logger.add(LoggerLevel.info, content);
}
if (cloudflare) {
botToast(
"${response.statusCode} Failed to bypass Cloudflare",
hasCloudFlare: cloudflare,
url: response.request!.url.toString(),
);
try {
botToast(
"${response.statusCode} Failed to bypass Cloudflare",
hasCloudFlare: cloudflare,
url: response.request!.url.toString(),
);
} catch (e) {
throw "${response.statusCode} Failed to bypass Cloudflare";
}
}
}
@ -252,78 +256,78 @@ bool isCloudflare(BaseResponse response) {
["cloudflare-nginx", "cloudflare"].contains(response.headers["server"]);
}
class ResolveCloudFlareChallenge extends RetryPolicy {
bool showCloudFlareError;
ResolveCloudFlareChallenge(this.showCloudFlareError);
@override
int get maxRetryAttempts => 2;
@override
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
if (!showCloudFlareError || Platform.isLinux) return false;
flutter_inappwebview.HeadlessInAppWebView? headlessWebView;
int time = 0;
bool timeOut = false;
bool isCloudFlare = isCloudflare(response);
if (isCloudFlare) {
headlessWebView = flutter_inappwebview.HeadlessInAppWebView(
webViewEnvironment: webViewEnvironment,
initialUrlRequest: flutter_inappwebview.URLRequest(
url: flutter_inappwebview.WebUri(response.request!.url.toString()),
),
onLoadStop: (controller, url) async {
try {
isCloudFlare = await controller.platform.evaluateJavascript(
source:
"document.head.innerHTML.includes('#challenge-success-text')",
);
} catch (_) {
isCloudFlare = false;
}
// class ResolveCloudFlareChallenge extends RetryPolicy {
// bool showCloudFlareError;
// ResolveCloudFlareChallenge(this.showCloudFlareError);
// @override
// int get maxRetryAttempts => 2;
// @override
// Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
// if (!showCloudFlareError || Platform.isLinux) return false;
// flutter_inappwebview.HeadlessInAppWebView? headlessWebView;
// int time = 0;
// bool timeOut = false;
// bool isCloudFlare = isCloudflare(response);
// if (isCloudFlare) {
// headlessWebView = flutter_inappwebview.HeadlessInAppWebView(
// webViewEnvironment: webViewEnvironment,
// initialUrlRequest: flutter_inappwebview.URLRequest(
// url: flutter_inappwebview.WebUri(response.request!.url.toString()),
// ),
// onLoadStop: (controller, url) async {
// try {
// isCloudFlare = await controller.platform.evaluateJavascript(
// source:
// "document.head.innerHTML.includes('#challenge-success-text')",
// );
// } catch (_) {
// isCloudFlare = false;
// }
await Future.doWhile(() async {
if (!timeOut && isCloudFlare) {
try {
isCloudFlare = await controller.platform.evaluateJavascript(
source:
"document.head.innerHTML.includes('#challenge-success-text')",
);
} catch (_) {
isCloudFlare = false;
}
}
if (isCloudFlare) await Future.delayed(Duration(milliseconds: 300));
// await Future.doWhile(() async {
// if (!timeOut && isCloudFlare) {
// try {
// isCloudFlare = await controller.platform.evaluateJavascript(
// source:
// "document.head.innerHTML.includes('#challenge-success-text')",
// );
// } catch (_) {
// isCloudFlare = false;
// }
// }
// if (isCloudFlare) await Future.delayed(Duration(milliseconds: 300));
return isCloudFlare;
});
if (!timeOut) {
final ua =
await controller.evaluateJavascript(
source: "navigator.userAgent",
) ??
"";
await MClient.setCookie(url.toString(), ua, controller);
}
},
);
// return isCloudFlare;
// });
// if (!timeOut) {
// final ua =
// await controller.evaluateJavascript(
// source: "navigator.userAgent",
// ) ??
// "";
// await MClient.setCookie(url.toString(), ua, controller);
// }
// },
// );
headlessWebView.run();
// headlessWebView.run();
await Future.doWhile(() async {
timeOut = time == 15;
if (!isCloudFlare || timeOut) {
return false;
}
await Future.delayed(const Duration(seconds: 1));
time++;
return true;
});
try {
headlessWebView.dispose();
} catch (_) {}
// await Future.doWhile(() async {
// timeOut = time == 15;
// if (!isCloudFlare || timeOut) {
// return false;
// }
// await Future.delayed(const Duration(seconds: 1));
// time++;
// return true;
// });
// try {
// headlessWebView.dispose();
// } catch (_) {}
return true;
}
// return true;
// }
return false;
}
}
// return false;
// }
// }

View file

@ -0,0 +1,183 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/providers/storage_provider.dart';
class _IsolateData {
final SendPort sendPort;
final RootIsolateToken rootIsolateToken;
_IsolateData({required this.sendPort, required this.rootIsolateToken});
}
class GetIsolateService {
bool _isRunning = false;
Isolate? _getIsolateService;
ReceivePort? _receivePort;
SendPort? _sendPort;
Future<void> start() async {
if (!_isRunning) {
try {
await _initGetIsolateService();
} catch (_) {
await stop();
}
}
}
Future<void> _initGetIsolateService() async {
_receivePort = ReceivePort();
final rootToken = RootIsolateToken.instance!;
_getIsolateService = await Isolate.spawn(
_getIsolateServiceEntryPoint,
_IsolateData(
sendPort: _receivePort!.sendPort,
rootIsolateToken: rootToken,
),
);
final completer = Completer<SendPort>();
_receivePort!.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
_sendPort = await completer.future;
_isRunning = true;
}
static Future<void> _getIsolateServiceEntryPoint(
_IsolateData isolateData,
) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(
isolateData.rootIsolateToken,
);
await initializeDateFormatting();
isar = await StorageProvider().initDB(null, inspector: kDebugMode);
final receivePort = ReceivePort();
isolateData.sendPort.send(receivePort.sendPort);
await for (var message in receivePort) {
if (message is Map<String, dynamic>) {
try {
final url = message['url'] as String?;
final page = message['page'] as int?;
final query = message['query'] as String?;
final filterList = message['filterList'] as List?;
final source = message['source'] as Source?;
final proxyServer = message['proxyServer'] as String?;
final serviceType = message['serviceType'] as String?;
final responsePort = message['responsePort'] as SendPort;
if (serviceType == 'getDetail') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getDetail(url!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getPopular') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getPopular(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getLatestUpdates') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getLatestUpdates(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'search') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).search(query!, page!, filterList!);
responsePort.send({'success': true, 'data': result});
}
} catch (e) {
final responsePort = message['responsePort'] as SendPort;
responsePort.send({'success': false, 'error': e.toString()});
}
} else if (message == 'dispose') {
break;
}
}
}
Future<T> get<T>({
int? id,
bool? refresh,
ItemType? itemType,
Repo? repo,
String? url,
int? page,
String? query,
List<dynamic>? filterList,
Source? source,
String? serviceType,
String? proxyServer,
bool? autoUpdateExtensions,
String? androidProxyServer,
}) async {
if (_sendPort == null) {
throw Exception('Isolate not running');
}
final responsePort = ReceivePort();
final completer = Completer<T>();
responsePort.listen((response) {
responsePort.close();
if (response is Map<String, dynamic>) {
if (response['success'] == true) {
completer.complete(response['data'] as T);
} else {
completer.completeError(response['error']);
}
}
});
_sendPort!.send({
'url': url,
'page': page,
'query': query,
'filterList': filterList,
'serviceType': serviceType,
'source': source,
'proxyServer': proxyServer,
'responsePort': responsePort.sendPort,
});
return completer.future;
}
Future<void> stop() async {
if (!_isRunning) {
return;
}
_sendPort?.send('dispose');
_getIsolateService?.kill(priority: Isolate.immediate);
_receivePort?.close();
_sendPort = null;
_getIsolateService = null;
_receivePort = null;
_isRunning = false;
}
}
final getIsolateService = GetIsolateService();

View file

@ -1,13 +1,12 @@
import 'dart:math';
import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search.g.dart';
@ -40,8 +39,12 @@ Future<MPages?> search(
.toList();
return MPages(list: result, hasNextPage: true);
}
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).search(query, page, filterList);
return getIsolateService.get<MPages?>(
query: query,
filterList: filterList,
source: source,
page: page,
serviceType: 'search',
proxyServer: ref.read(androidProxyServerStateProvider),
);
}

View file

@ -72,7 +72,7 @@ final class SearchProvider
}
}
String _$searchHash() => r'b6bac0a3af58547bb93356f3c00a04135cd5a891';
String _$searchHash() => r'03bfee6172b386c53aee05fe2429a10ce5915b18';
final class SearchFamily extends $Family
with

View file

@ -1,11 +1,11 @@
import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search_.g.dart';
@ -37,8 +37,12 @@ Future<MPages?> search(
.toList();
return MPages(list: result, hasNextPage: true);
}
return getExtensionService(
source,
ref.read(androidProxyServerStateProvider),
).search(query, page, filterList);
return getIsolateService.get<MPages?>(
query: query,
filterList: filterList,
source: source,
page: page,
serviceType: 'search',
proxyServer: ref.read(androidProxyServerStateProvider),
);
}

View file

@ -72,7 +72,7 @@ final class SearchProvider
}
}
String _$searchHash() => r'e23c6dd56549ffdfc89b5fcfa43719d90ca1760e';
String _$searchHash() => r'0fa9d882436b1b58b3420dae5a757e7622273eb5';
final class SearchFamily extends $Family
with

View file

@ -1,5 +1,4 @@
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
@ -32,7 +31,7 @@ class Anilist extends _$Anilist implements BaseTracker {
void build({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {}
Future<bool?> login() async {

View file

@ -15,7 +15,7 @@ const anilistProvider = AnilistFamily._();
final class AnilistProvider extends $NotifierProvider<Anilist, void> {
const AnilistProvider._({
required AnilistFamily super.from,
required ({int syncId, ItemType? itemType, WidgetRef widgetRef})
required ({int syncId, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class AnilistProvider extends $NotifierProvider<Anilist, void> {
}
}
String _$anilistHash() => r'2d34818d1f8c58455b0081d4ae707cff160ea91d';
String _$anilistHash() => r'c7ade80d69398d712596080cdba0c670724ac0da';
final class AnilistFamily extends $Family
with
@ -67,7 +67,7 @@ final class AnilistFamily extends $Family
void,
void,
void,
({int syncId, ItemType? itemType, WidgetRef widgetRef})
({int syncId, ItemType? itemType, dynamic widgetRef})
> {
const AnilistFamily._()
: super(
@ -81,7 +81,7 @@ final class AnilistFamily extends $Family
AnilistProvider call({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => AnilistProvider._(
argument: (syncId: syncId, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class AnilistFamily extends $Family
abstract class _$Anilist extends $Notifier<void> {
late final _$args =
ref.$arg as ({int syncId, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({int syncId, ItemType? itemType, dynamic widgetRef});
int get syncId => _$args.syncId;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
void build({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -1,4 +1,3 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:intl/intl.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
@ -40,7 +39,7 @@ class Kitsu extends _$Kitsu implements BaseTracker {
void build({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {}
Future<(bool, String)> login(String username, String password) async {

View file

@ -15,7 +15,7 @@ const kitsuProvider = KitsuFamily._();
final class KitsuProvider extends $NotifierProvider<Kitsu, void> {
const KitsuProvider._({
required KitsuFamily super.from,
required ({int syncId, ItemType? itemType, WidgetRef widgetRef})
required ({int syncId, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class KitsuProvider extends $NotifierProvider<Kitsu, void> {
}
}
String _$kitsuHash() => r'ae5836feaaa4f95953f4890be2084cd1f7a3a412';
String _$kitsuHash() => r'8a19aa11f167df8d8cb537f746cc9dc31cad1d49';
final class KitsuFamily extends $Family
with
@ -67,7 +67,7 @@ final class KitsuFamily extends $Family
void,
void,
void,
({int syncId, ItemType? itemType, WidgetRef widgetRef})
({int syncId, ItemType? itemType, dynamic widgetRef})
> {
const KitsuFamily._()
: super(
@ -81,7 +81,7 @@ final class KitsuFamily extends $Family
KitsuProvider call({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => KitsuProvider._(
argument: (syncId: syncId, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class KitsuFamily extends $Family
abstract class _$Kitsu extends $Notifier<void> {
late final _$args =
ref.$arg as ({int syncId, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({int syncId, ItemType? itemType, dynamic widgetRef});
int get syncId => _$args.syncId;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
void build({
required int syncId,
ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:intl/intl.dart';
@ -37,7 +36,7 @@ class MyAnimeList extends _$MyAnimeList implements BaseTracker {
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {}
Future<bool?> login() async {

View file

@ -15,7 +15,7 @@ const myAnimeListProvider = MyAnimeListFamily._();
final class MyAnimeListProvider extends $NotifierProvider<MyAnimeList, void> {
const MyAnimeListProvider._({
required MyAnimeListFamily super.from,
required ({int syncId, ItemType? itemType, WidgetRef widgetRef})
required ({int syncId, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class MyAnimeListProvider extends $NotifierProvider<MyAnimeList, void> {
}
}
String _$myAnimeListHash() => r'6fc4940fbc11af8b3779ecc42bb3845eadc06f0f';
String _$myAnimeListHash() => r'2aeddba481f6f97fac7ce2037f6c38f84acf755d';
final class MyAnimeListFamily extends $Family
with
@ -67,7 +67,7 @@ final class MyAnimeListFamily extends $Family
void,
void,
void,
({int syncId, ItemType? itemType, WidgetRef widgetRef})
({int syncId, ItemType? itemType, dynamic widgetRef})
> {
const MyAnimeListFamily._()
: super(
@ -81,7 +81,7 @@ final class MyAnimeListFamily extends $Family
MyAnimeListProvider call({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => MyAnimeListProvider._(
argument: (syncId: syncId, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class MyAnimeListFamily extends $Family
abstract class _$MyAnimeList extends $Notifier<void> {
late final _$args =
ref.$arg as ({int syncId, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({int syncId, ItemType? itemType, dynamic widgetRef});
int get syncId => _$args.syncId;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_qjs/quickjs/ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:mangayomi/models/manga.dart';
@ -37,7 +36,7 @@ class Simkl extends _$Simkl implements BaseTracker {
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {}
Future<bool?> login() async {

View file

@ -15,7 +15,7 @@ const simklProvider = SimklFamily._();
final class SimklProvider extends $NotifierProvider<Simkl, void> {
const SimklProvider._({
required SimklFamily super.from,
required ({int syncId, ItemType? itemType, WidgetRef widgetRef})
required ({int syncId, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class SimklProvider extends $NotifierProvider<Simkl, void> {
}
}
String _$simklHash() => r'f2770c0a3f3f6c0730aec2b5128a9bffe19eeb4d';
String _$simklHash() => r'a5311b207d0bfb5b34911633ee73d5d77ebde6cf';
final class SimklFamily extends $Family
with
@ -67,7 +67,7 @@ final class SimklFamily extends $Family
void,
void,
void,
({int syncId, ItemType? itemType, WidgetRef widgetRef})
({int syncId, ItemType? itemType, dynamic widgetRef})
> {
const SimklFamily._()
: super(
@ -81,7 +81,7 @@ final class SimklFamily extends $Family
SimklProvider call({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => SimklProvider._(
argument: (syncId: syncId, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class SimklFamily extends $Family
abstract class _$Simkl extends $Notifier<void> {
late final _$args =
ref.$arg as ({int syncId, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({int syncId, ItemType? itemType, dynamic widgetRef});
int get syncId => _$args.syncId;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
@ -38,7 +37,7 @@ class TraktTv extends _$TraktTv implements BaseTracker {
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) {}
Future<bool?> login() async {

View file

@ -15,7 +15,7 @@ const traktTvProvider = TraktTvFamily._();
final class TraktTvProvider extends $NotifierProvider<TraktTv, void> {
const TraktTvProvider._({
required TraktTvFamily super.from,
required ({int syncId, ItemType? itemType, WidgetRef widgetRef})
required ({int syncId, ItemType? itemType, dynamic widgetRef})
super.argument,
}) : super(
retry: null,
@ -58,7 +58,7 @@ final class TraktTvProvider extends $NotifierProvider<TraktTv, void> {
}
}
String _$traktTvHash() => r'25c72f51804b49771a10d9a7ad2ca5f70581082e';
String _$traktTvHash() => r'6843c07d55eb4daec6fd99a14037d2cefd51f8de';
final class TraktTvFamily extends $Family
with
@ -67,7 +67,7 @@ final class TraktTvFamily extends $Family
void,
void,
void,
({int syncId, ItemType? itemType, WidgetRef widgetRef})
({int syncId, ItemType? itemType, dynamic widgetRef})
> {
const TraktTvFamily._()
: super(
@ -81,7 +81,7 @@ final class TraktTvFamily extends $Family
TraktTvProvider call({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
}) => TraktTvProvider._(
argument: (syncId: syncId, itemType: itemType, widgetRef: widgetRef),
from: this,
@ -93,15 +93,15 @@ final class TraktTvFamily extends $Family
abstract class _$TraktTv extends $Notifier<void> {
late final _$args =
ref.$arg as ({int syncId, ItemType? itemType, WidgetRef widgetRef});
ref.$arg as ({int syncId, ItemType? itemType, dynamic widgetRef});
int get syncId => _$args.syncId;
ItemType? get itemType => _$args.itemType;
WidgetRef get widgetRef => _$args.widgetRef;
dynamic get widgetRef => _$args.widgetRef;
void build({
required int syncId,
required ItemType? itemType,
required WidgetRef widgetRef,
required dynamic widgetRef,
});
@$mustCallSuper
@override

View file

@ -4,14 +4,86 @@ import 'package:mangayomi/utils/reg_exp_matcher.dart';
import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart';
void _initPseudoSelector() {
bool nthChild(Element element, String? args) {
if (int.tryParse(args!) != null) {
final parent = element.parentNode;
return parent != null &&
(int.parse(args) as num) > 0 &&
parent.nodes.indexOf(element) == int.parse(args);
(int, int) parseNth(String arg) {
arg = arg.toLowerCase().replaceAll(' ', '');
if (arg == 'odd') return (2, 1);
if (arg == 'even') return (2, 0);
final reg = RegExp(r'^(\d*)n([+-]?\d+)?$');
final match = reg.firstMatch(arg);
if (match != null) {
final aStr = match.group(1);
final a = aStr == null || aStr.isEmpty ? 1 : int.parse(aStr);
final bStr = match.group(2);
final b = bStr == null ? 0 : int.parse(bStr);
return (a, b);
}
return true;
final n = int.tryParse(arg);
if (n != null) return (0, n);
return (0, 0);
}
bool matchesNth(int index, int a, int b) {
if (a == 0) return index == b;
final diff = index - b;
return diff % a == 0 && diff ~/ a >= 0;
}
String getWholeText(Element element) {
return element.nodes.map((node) {
if (node is Text) return node.text;
if (node is Element) return getWholeText(node);
return '';
}).join();
}
String getWholeOwnText(Element element) {
return element.nodes.whereType<Text>().map((t) => t.text).join();
}
bool nthChild(Element element, String? args) {
if (args == null) return false;
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children;
final index = siblings.indexOf(element) + 1; // 1-based
final (a, b) = parseNth(args);
return matchesNth(index, a, b);
}
bool nthLastChild(Element element, String? args) {
if (args == null) return false;
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children;
final index =
siblings.length - siblings.indexOf(element); // 1-based from end
final (a, b) = parseNth(args);
return matchesNth(index, a, b);
}
bool nthOfType(Element element, String? args) {
if (args == null) return false;
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children
.where((e) => e.localName == element.localName)
.toList();
final index = siblings.indexOf(element) + 1; // 1-based
final (a, b) = parseNth(args);
return matchesNth(index, a, b);
}
bool nthLastOfType(Element element, String? args) {
if (args == null) return false;
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children
.where((e) => e.localName == element.localName)
.toList();
final index =
siblings.length - siblings.indexOf(element); // 1-based from end
final (a, b) = parseNth(args);
return matchesNth(index, a, b);
}
bool has(Element element, String? args) {
@ -34,7 +106,74 @@ void _initPseudoSelector() {
bool contains(Element element, String? args) {
final text = args ?? '';
return element.text.contains(text);
return element.text.toLowerCase().contains(text.toLowerCase());
}
bool containsOwn(Element element, String? args) {
final text = args ?? '';
final ownText = element.nodes.whereType<Text>().map((t) => t.text).join();
return ownText.toLowerCase().contains(text.toLowerCase());
}
bool matches(Element element, String? args) {
if (args == null) return false;
try {
final reg = RegExp(args, caseSensitive: false);
return reg.hasMatch(element.text);
} catch (e) {
return false;
}
}
bool containsData(Element element, String? args) {
final data = args ?? '';
// For script and style elements
if (element.localName == 'script' || element.localName == 'style') {
return element.text.toLowerCase().contains(data.toLowerCase());
}
return false;
}
bool containsWholeText(Element element, String? args) {
final text = args ?? '';
return getWholeText(element).contains(text);
}
bool containsWholeOwnText(Element element, String? args) {
final text = args ?? '';
return getWholeOwnText(element).contains(text);
}
bool matchesWholeText(Element element, String? args) {
if (args == null) return false;
try {
final reg = RegExp(args);
return reg.hasMatch(getWholeText(element));
} catch (e) {
return false;
}
}
bool matchesWholeOwnText(Element element, String? args) {
if (args == null) return false;
try {
final reg = RegExp(args);
return reg.hasMatch(getWholeOwnText(element));
} catch (e) {
return false;
}
}
bool isSelector(Element element, String? args) {
if (args == null) return false;
final selectors = args.split(',').map((s) => s.trim()).toList();
for (final sel in selectors) {
try {
final parsed = pseudom.parse(sel);
if (parsed.selectFirst(element) != null) return true;
} catch (_) {}
}
return false;
}
bool firstChild(Element element, String? args) {
@ -45,17 +184,103 @@ void _initPseudoSelector() {
return element.nextElementSibling == null;
}
bool firstOfType(Element element, String? args) {
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children.where(
(e) => e.localName == element.localName,
);
return siblings.first == element;
}
bool lastOfType(Element element, String? args) {
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children.where(
(e) => e.localName == element.localName,
);
return siblings.last == element;
}
bool onlyChild(Element element, String? args) {
return element.nextElementSibling == null;
return element.previousElementSibling == null &&
element.nextElementSibling == null;
}
bool onlyOfType(Element element, String? args) {
final parent = element.parent;
if (parent == null) return false;
final siblings = parent.children.where(
(e) => e.localName == element.localName,
);
return siblings.length == 1;
}
bool empty(Element element, String? args) {
return element.children.isEmpty && element.text.trim().isEmpty;
}
bool root(Element element, String? args) {
return element.parent == null;
}
bool lt(Element element, String? args) {
if (args == null) return false;
final n = int.tryParse(args);
if (n == null) return false;
final parent = element.parent;
if (parent == null) return false;
final index = parent.children.indexOf(element);
return index < n;
}
bool gt(Element element, String? args) {
if (args == null) return false;
final n = int.tryParse(args);
if (n == null) return false;
final parent = element.parent;
if (parent == null) return false;
final index = parent.children.indexOf(element);
return index > n;
}
bool eq(Element element, String? args) {
if (args == null) return false;
final n = int.tryParse(args);
if (n == null) return false;
final parent = element.parent;
if (parent == null) return false;
final index = parent.children.indexOf(element);
return index == n;
}
pseudom.PseudoSelector.handlers['nth-child'] = nthChild;
pseudom.PseudoSelector.handlers['nth-last-child'] = nthLastChild;
pseudom.PseudoSelector.handlers['nth-of-type'] = nthOfType;
pseudom.PseudoSelector.handlers['nth-last-of-type'] = nthLastOfType;
pseudom.PseudoSelector.handlers['has'] = has;
pseudom.PseudoSelector.handlers['inot'] = inot;
pseudom.PseudoSelector.handlers['contains'] = contains;
pseudom.PseudoSelector.handlers['containsOwn'] = containsOwn;
pseudom.PseudoSelector.handlers['containsData'] = containsData;
pseudom.PseudoSelector.handlers['containsWholeText'] = containsWholeText;
pseudom.PseudoSelector.handlers['containsWholeOwnText'] =
containsWholeOwnText;
pseudom.PseudoSelector.handlers['matches'] = matches;
pseudom.PseudoSelector.handlers['matchesWholeText'] = matchesWholeText;
pseudom.PseudoSelector.handlers['matchesWholeOwnText'] = matchesWholeOwnText;
pseudom.PseudoSelector.handlers['is'] = isSelector;
pseudom.PseudoSelector.handlers['last-child'] = lastChild;
pseudom.PseudoSelector.handlers['first-child'] = firstChild;
pseudom.PseudoSelector.handlers['first-of-type'] = firstOfType;
pseudom.PseudoSelector.handlers['last-of-type'] = lastOfType;
pseudom.PseudoSelector.handlers['only-child'] = onlyChild;
pseudom.PseudoSelector.handlers['only-of-type'] = onlyOfType;
pseudom.PseudoSelector.handlers['empty'] = empty;
pseudom.PseudoSelector.handlers['root'] = root;
pseudom.PseudoSelector.handlers['lt'] = lt;
pseudom.PseudoSelector.handlers['gt'] = gt;
pseudom.PseudoSelector.handlers['eq'] = eq;
}
String _fixSelector(String selector) {

View file

@ -6,7 +6,6 @@ import FlutterMacOS
import Foundation
import app_links
import audio_session
import connectivity_plus
import device_info_plus
import file_picker
@ -14,7 +13,6 @@ import flutter_inappwebview_macos
import flutter_qjs
import flutter_web_auth_2
import isar_community_flutter_libs
import just_audio
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
@ -22,18 +20,14 @@ import path_provider_foundation
import screen_brightness_macos
import screen_retriever_macos
import share_plus
import sqflite_darwin
import url_launcher_macos
import video_player_avfoundation
import volume_controller
import wakelock_plus
import webview_flutter_wkwebview
import window_manager
import window_to_front
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
@ -41,7 +35,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterQjsPlugin.register(with: registry.registrar(forPlugin: "FlutterQjsPlugin"))
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
@ -49,12 +42,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
}

View file

@ -1,8 +1,6 @@
PODS:
- app_links (6.4.1):
- FlutterMacOS
- audio_session (0.0.1):
- FlutterMacOS
- connectivity_plus (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
@ -21,9 +19,6 @@ PODS:
- FlutterMacOS (1.0.0)
- isar_community_flutter_libs (1.0.0):
- FlutterMacOS
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_video (0.0.1):
@ -42,21 +37,12 @@ PODS:
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- volume_controller (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- window_manager (0.5.0):
- FlutterMacOS
- window_to_front (0.0.1):
@ -64,7 +50,6 @@ PODS:
DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
@ -74,7 +59,6 @@ DEPENDENCIES:
- flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- isar_community_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
@ -83,12 +67,9 @@ DEPENDENCIES:
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
- window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`)
@ -99,8 +80,6 @@ SPEC REPOS:
EXTERNAL SOURCES:
app_links:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
device_info_plus:
@ -119,8 +98,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
isar_community_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos
just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_video:
@ -137,18 +114,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_player_avfoundation:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
volume_controller:
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
webview_flutter_wkwebview:
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
window_to_front:
@ -156,7 +127,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
@ -166,7 +136,6 @@ SPEC CHECKSUMS:
flutter_web_auth_2: 62b08da29f15a20fa63f144234622a1488d45b65
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
isar_community_flutter_libs: a631ceb5622413b56bcd0a8bf49cb55bf3d8bb2b
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
@ -176,12 +145,9 @@ SPEC CHECKSUMS:
screen_brightness_macos: 2a3ee243f8051c340381e8e51bcedced8360f421
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
window_manager: b729e31d38fb04905235df9ea896128991cad99e
window_to_front: 9e76fd432e36700a197dac86a0011e49c89abe0a

View file

@ -105,14 +105,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
audio_session:
dependency: transitive
description:
name: audio_session
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
boolean_selector:
dependency: transitive
description:
@ -201,30 +193,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.0"
cached_network_image:
dependency: transitive
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
characters:
dependency: transitive
description:
@ -241,14 +209,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4"
chewie:
dependency: transitive
description:
name: chewie
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
url: "https://pub.dev"
source: hosted
version: "1.13.0"
cli_config:
dependency: transitive
description:
@ -451,14 +411,6 @@ packages:
url: "https://github.com/kodjodevf/epubx.dart.git"
source: git
version: "4.0.3"
expandable_text:
dependency: "direct main"
description:
name: expandable_text
sha256: "7d03ea48af6987b20ece232678b744862aa3250d4a71e2aaf1e4af90015d76b1"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
expressions:
dependency: transitive
description:
@ -560,14 +512,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_discord_rpc_fork:
dependency: "direct main"
description:
@ -577,6 +521,14 @@ packages:
url: "https://github.com/Schnitzel5/flutter-discord-rpc.git"
source: git
version: "1.0.5"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_inappwebview:
dependency: "direct main"
description:
@ -695,14 +647,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.1"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
url: "https://pub.dev"
source: hosted
version: "2.2.1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -730,22 +674,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html:
dependency: "direct main"
description:
name: flutter_widget_from_html
sha256: "7f1daefcd3009c43c7e7fb37501e6bb752d79aa7bfad0085fb0444da14e89bd0"
url: "https://pub.dev"
source: hosted
version: "0.17.1"
flutter_widget_from_html_core:
dependency: transitive
description:
name: flutter_widget_from_html_core
sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
font_awesome_flutter:
dependency: "direct main"
description:
@ -778,54 +706,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
fwfh_cached_network_image:
dependency: transitive
description:
name: fwfh_cached_network_image
sha256: "484cb5f8047f02cfac0654fca5832bfa91bb715fd7fc651c04eb7454187c4af8"
url: "https://pub.dev"
source: hosted
version: "0.16.1"
fwfh_chewie:
dependency: transitive
description:
name: fwfh_chewie
sha256: ae74fc26798b0e74f3983f7b851e74c63b9eeb2d3015ecd4b829096b2c3f8818
url: "https://pub.dev"
source: hosted
version: "0.16.1"
fwfh_just_audio:
dependency: transitive
description:
name: fwfh_just_audio
sha256: dfd622a0dfe049ac647423a2a8afa7f057d9b2b93d92710b624e3d370b1ac69a
url: "https://pub.dev"
source: hosted
version: "0.17.0"
fwfh_svg:
dependency: transitive
description:
name: fwfh_svg
sha256: "2e6bb241179eeeb1a7941e05c8c923b05d332d36a9085233e7bf110ea7deb915"
url: "https://pub.dev"
source: hosted
version: "0.16.1"
fwfh_url_launcher:
dependency: transitive
description:
name: fwfh_url_launcher
sha256: c38aa8fb373fda3a89b951fa260b539f623f6edb45eee7874cb8b492471af881
url: "https://pub.dev"
source: hosted
version: "0.16.1"
fwfh_webview:
dependency: transitive
description:
name: fwfh_webview
sha256: f71b0aa16e15d82f3c017f33560201ff5ae04e91e970cab5d12d3bcf970b870c
url: "https://pub.dev"
source: hosted
version: "0.15.6"
glob:
dependency: transitive
description:
@ -1050,30 +930,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.2"
just_audio:
dependency: transitive
description:
name: just_audio
sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908"
url: "https://pub.dev"
source: hosted
version: "0.10.5"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663"
url: "https://pub.dev"
source: hosted
version: "0.4.16"
leak_tracker:
dependency: transitive
description:
@ -1106,6 +962,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
list_counter:
dependency: transitive
description:
name: list_counter
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
url: "https://pub.dev"
source: hosted
version: "1.0.2"
logging:
dependency: transitive
description:
@ -1234,14 +1098,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.5.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nm:
dependency: transitive
description:
@ -1266,14 +1122,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_config:
dependency: transitive
description:
@ -1306,14 +1154,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
@ -1482,14 +1322,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "23.0.0"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pseudom:
dependency: "direct main"
description:
@ -1798,46 +1630,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
url: "https://pub.dev"
source: hosted
version: "2.4.2+2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@ -2046,30 +1838,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_math:
dependency: transitive
description:
@ -2078,46 +1846,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
video_player:
dependency: transitive
description:
name: video_player
sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: a8dc4324f67705de057678372bedb66cd08572fe7c495605ac68c5f503324a39
url: "https://pub.dev"
source: hosted
version: "2.8.15"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
url: "https://pub.dev"
source: hosted
version: "2.8.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
@ -2190,38 +1918,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
url: "https://pub.dev"
source: hosted
version: "4.13.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: e5201c620eb2637dca88a756961fae4a7191bb30b4f2271e08b746405ffdf3fd
url: "https://pub.dev"
source: hosted
version: "4.10.5"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
url: "https://pub.dev"
source: hosted
version: "2.14.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd
url: "https://pub.dev"
source: hosted
version: "3.23.1"
win32:
dependency: "direct main"
description:

View file

@ -16,7 +16,6 @@ dependencies:
riverpod_annotation: ^3.0.3
html: ^0.15.5
font_awesome_flutter: ^10.12.0
expandable_text: ^2.3.0
flex_color_scheme: ^8.3.1
extended_image: ^10.0.0
photo_view: ^0.15.0
@ -80,7 +79,7 @@ dependencies:
path: packages/desktop_webview_window
ref: main
screen_brightness: ^2.1.1
flutter_widget_from_html: ^0.17.1
flutter_html: ^3.0.0
convert: ^3.1.2
connectivity_plus: ^7.0.0
app_links: ^6.4.0