mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
Merge remote-tracking branch 'upstream/main' into Correct-directory
This commit is contained in:
commit
1b64f2650e
65 changed files with 4717 additions and 2224 deletions
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -59,7 +59,7 @@ final class AnimeStreamControllerProvider
|
|||
}
|
||||
|
||||
String _$animeStreamControllerHash() =>
|
||||
r'486889b2b9f71759e4d9ff147b039436572cc01e';
|
||||
r'1bca3ada0f7919439500ce8c42fa39958c1c5a7b';
|
||||
|
||||
final class AnimeStreamControllerFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ final class MigrationProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$migrationHash() => r'2a82120544e693a3162da887a3ca1b3066f3799f';
|
||||
String _$migrationHash() => r'43d62ddf79798d616ac7d11ce50a47551ef42c98';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
555
lib/modules/manga/detail/widgets/expandable_text.dart
Normal file
555
lib/modules/manga/detail/widgets/expandable_text.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?> {
|
||||
|
|
|
|||
160
lib/modules/manga/reader/image_view_webtoon.dart
Normal file
160
lib/modules/manga/reader/image_view_webtoon.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ final class CropBordersProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$cropBordersHash() => r'04b24357737d6cc75caa38feca77bb5d41f00aa6';
|
||||
String _$cropBordersHash() => r'f60987c3f38afd5e10263f3d6935e6007ff942f0';
|
||||
|
||||
final class CropBordersFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ final class ReaderControllerProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$readerControllerHash() => r'25b13bbbbd961a5c3dbae3cc0ea58017d7bb5ce8';
|
||||
String _$readerControllerHash() => r'23eece0ca4e7b6cbf425488636ef942fe0d4c2bc';
|
||||
|
||||
final class ReaderControllerFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ final class DoBackUpProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$doBackUpHash() => r'd16d5b6e5ed2c20988fa2d49842524d70ac0ed0d';
|
||||
String _$doBackUpHash() => r'e0d28adf6b592e34f26fd6b566151f3691f1946a';
|
||||
|
||||
final class DoBackUpFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
591
lib/modules/novel/widgets/novel_reader_settings_sheet.dart
Normal file
591
lib/modules/novel/widgets/novel_reader_settings_sheet.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ final class FetchItemSourcesListProvider
|
|||
}
|
||||
|
||||
String _$fetchItemSourcesListHash() =>
|
||||
r'16238be20517fddacf52a2694fbd50cafbfa7496';
|
||||
r'219aed67d2329f03101f2270e2f344bf70eff128';
|
||||
|
||||
final class FetchItemSourcesListFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ final class GetDetailProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getDetailHash() => r'6b758b79281cb00a7df2fe1903d4a67068052bca';
|
||||
String _$getDetailHash() => r'7eab7d00e6ad61a9bafaee855eae1f49b127af9f';
|
||||
|
||||
final class GetDetailFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ final class GetLatestUpdatesProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getLatestUpdatesHash() => r'7a3c06c469c77ec933cf2f4dd7d39780d993f0ea';
|
||||
String _$getLatestUpdatesHash() => r'6f99dfe1d4aa950b6852110ec23f92b5c73c413c';
|
||||
|
||||
final class GetLatestUpdatesFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ final class GetPopularProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getPopularHash() => r'f169b6a9ba76d9dd9237ba9c21805151a1419843';
|
||||
String _$getPopularHash() => r'7e1139bc0f6a3a495fa0dc59d450bc7fd70f36a8';
|
||||
|
||||
final class GetPopularFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ final class GetVideoListProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getVideoListHash() => r'c54f7294e15eeede933a6e04cd9b761d82b5f74c';
|
||||
String _$getVideoListHash() => r'd374dccf3edc478c883cb79b9f1be724342b9ac7';
|
||||
|
||||
final class GetVideoListFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
183
lib/services/isolate_service.dart
Normal file
183
lib/services/isolate_service.dart
Normal 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();
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ final class SearchProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$searchHash() => r'b6bac0a3af58547bb93356f3c00a04135cd5a891';
|
||||
String _$searchHash() => r'03bfee6172b386c53aee05fe2429a10ce5915b18';
|
||||
|
||||
final class SearchFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ final class SearchProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$searchHash() => r'e23c6dd56549ffdfc89b5fcfa43719d90ca1760e';
|
||||
String _$searchHash() => r'0fa9d882436b1b58b3420dae5a757e7622273eb5';
|
||||
|
||||
final class SearchFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
336
pubspec.lock
336
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue