import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/data.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; import 'package:yaml/yaml.dart'; part 'reader.dart'; part 'explore_settings.dart'; part 'setting_components.dart'; part 'appearance.dart'; part 'local_favorites.dart'; part 'app.dart'; part 'about.dart'; part 'network.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({this.initialPage = -1, super.key}); final int initialPage; @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State implements PopEntry { int currentPage = -1; ColorScheme get colors => Theme.of(context).colorScheme; bool get enableTwoViews => context.width > 720; final categories = [ "Explore", "Reading", "Appearance", "Local Favorites", "APP", "Network", "About", ]; final icons = [ Icons.explore, Icons.book, Icons.color_lens, Icons.collections_bookmark_rounded, Icons.apps, Icons.public, Icons.info ]; double offset = 0; late final HorizontalDragGestureRecognizer gestureRecognizer; ModalRoute? _route; @override void didChangeDependencies() { super.didChangeDependencies(); final ModalRoute? nextRoute = ModalRoute.of(context); if (nextRoute != _route) { _route?.unregisterPopEntry(this); _route = nextRoute; _route?.registerPopEntry(this); } } @override void initState() { currentPage = widget.initialPage; gestureRecognizer = HorizontalDragGestureRecognizer(debugOwner: this) ..onUpdate = ((details) => setState(() => offset += details.delta.dx)) ..onEnd = (details) async { if (details.velocity.pixelsPerSecond.dx.abs() > 1 && details.velocity.pixelsPerSecond.dx >= 0) { setState(() { Future.delayed(const Duration(milliseconds: 300), () => offset = 0); currentPage = -1; }); } else if (offset > MediaQuery.of(context).size.width / 2) { setState(() { Future.delayed(const Duration(milliseconds: 300), () => offset = 0); currentPage = -1; }); } else { int i = 10; while (offset != 0) { setState(() { offset -= i; i *= 10; if (offset < 0) { offset = 0; } }); await Future.delayed(const Duration(milliseconds: 10)); } } } ..onCancel = () async { int i = 10; while (offset != 0) { setState(() { offset -= i; i *= 10; if (offset < 0) { offset = 0; } }); await Future.delayed(const Duration(milliseconds: 10)); } }; super.initState(); } @override dispose() { super.dispose(); gestureRecognizer.dispose(); _route?.unregisterPopEntry(this); } @override Widget build(BuildContext context) { if (currentPage != -1) { canPop.value = false; } else { canPop.value = true; } return Material( child: buildBody(), ); } Widget buildBody() { if (enableTwoViews) { return Row( children: [ SizedBox( width: 280, height: double.infinity, child: buildLeft(), ), Container( height: double.infinity, decoration: BoxDecoration( border: Border( left: BorderSide( color: context.colorScheme.outlineVariant, width: 0.6, ), ), ), ), Expanded(child: buildRight()) ], ); } else { return Stack( children: [ Positioned.fill(child: buildLeft()), Positioned( left: offset, width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Listener( onPointerDown: handlePointerDown, child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), reverseDuration: const Duration(milliseconds: 300), switchInCurve: Curves.fastOutSlowIn, switchOutCurve: Curves.fastOutSlowIn, transitionBuilder: (child, animation) { var tween = Tween( begin: const Offset(1, 0), end: const Offset(0, 0)); return SlideTransition( position: tween.animate(animation), child: child, ); }, child: currentPage == -1 ? const SizedBox( key: Key("1"), ) : buildRight(), ), ), ) ], ); } } void handlePointerDown(PointerDownEvent event) { if (event.position.dx < 20) { gestureRecognizer.addPointer(event); } } Widget buildLeft() { return Material( child: Column( children: [ SizedBox( height: MediaQuery.of(context).padding.top, ), SizedBox( height: 56, child: Row(children: [ const SizedBox( width: 8, ), Tooltip( message: "Back", child: IconButton( icon: const Icon(Icons.arrow_back), onPressed: context.pop, ), ), const SizedBox( width: 24, ), Text( "Settings".tl, style: ts.s20, ) ]), ), const SizedBox( height: 4, ), Expanded( child: buildCategories(), ) ], ), ); } Widget buildCategories() { Widget buildItem(String name, int id) { final bool selected = id == currentPage; Widget content = AnimatedContainer( key: ValueKey(id), duration: const Duration(milliseconds: 200), width: double.infinity, height: 46, padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), decoration: BoxDecoration( color: selected ? colors.primaryContainer.withOpacity(0.36) : null, border: Border( left: BorderSide( color: selected ? colors.primary : Colors.transparent, width: 2, ), ), ), child: Row(children: [ Icon(icons[id]), const SizedBox(width: 16), Text( name, style: ts.s16, ), const Spacer(), if (selected) const Icon(Icons.arrow_right) ]), ); return Padding( padding: enableTwoViews ? const EdgeInsets.fromLTRB(8, 0, 8, 0) : EdgeInsets.zero, child: InkWell( onTap: () => setState(() => currentPage = id), child: content, ).paddingVertical(4), ); } return ListView.builder( padding: EdgeInsets.zero, itemCount: categories.length, itemBuilder: (context, index) => buildItem(categories[index].tl, index), ); } Widget buildRight() { final Widget body = switch (currentPage) { -1 => const SizedBox(), 0 => const ExploreSettings(), 1 => const ReaderSettings(), 2 => const AppearanceSettings(), 3 => const LocalFavoritesSettings(), 4 => const AppSettings(), 5 => const NetworkSettings(), 6 => const AboutSettings(), _ => throw UnimplementedError() }; return Material( child: body, ); } var canPop = ValueNotifier(true); @override ValueListenable get canPopNotifier => canPop; @override void onPopInvokedWithResult(bool didPop, result) { if (currentPage != -1) { setState(() { currentPage = -1; }); } } @override void onPopInvoked(bool didPop) { if (currentPage != -1) { setState(() { currentPage = -1; }); } } }