import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:isekai_wiki/api/response/parse.dart'; import 'package:isekai_wiki/components/gesture_detector.dart'; import 'package:isekai_wiki/components/safearea_builder.dart'; import 'package:isekai_wiki/styles.dart'; class TOCStyles { static const double listPadding = 2; static const activeColor = Color.fromRGBO(51, 102, 204, 1); } class TOCList extends StatefulWidget { final String activedItem; final bool hasFirstSection; final List sections; final void Function(String anchor)? onSwitchTitle; const TOCList({ super.key, required this.sections, this.activedItem = "", this.hasFirstSection = false, this.onSwitchTitle, }); @override State createState() => _TOCListState(); } class _TOCListState extends State { String _activedItem = ""; String _lastSelectedItem = ""; double _listPaddingTop = 0; double _listPaddingBottom = 0; final ScrollController _scrollController = ScrollController(); @override void didUpdateWidget(covariant TOCList oldWidget) { super.didUpdateWidget(oldWidget); if (_activedItem != widget.activedItem) { _activedItem = widget.activedItem; // Scroll to current item int activedIndex; if (_activedItem == "_firstSection") { activedIndex = 0; } else { activedIndex = widget.sections.indexWhere((element) => element.anchor == _activedItem); if (widget.hasFirstSection) { activedIndex += 1; } if (activedIndex == -1) { activedIndex = 0; } } scrollToTOCItem(activedIndex); } } void scrollToTOCItem(int index) { var listLength = widget.sections.length + (widget.hasFirstSection ? 1 : 0); var scrollInnerOffsetHeight = _scrollController.position.maxScrollExtent + _scrollController.position.viewportDimension; var scrollInnerHeight = scrollInnerOffsetHeight - _listPaddingTop - _listPaddingBottom; var itemHeight = scrollInnerHeight / listLength; var target = itemHeight * index; var scrollTop = _scrollController.position.pixels; var scrollOffsetBottom = scrollTop + _scrollController.position.viewportDimension - _listPaddingTop - _listPaddingBottom - itemHeight; if (target < scrollTop || target > scrollOffsetBottom) { _scrollController.jumpTo(clampDouble(target, _scrollController.position.minScrollExtent, _scrollController.position.maxScrollExtent)); } } void handleTOCItemPressed(String anchor) { _lastSelectedItem = anchor; widget.onSwitchTitle?.call(_lastSelectedItem); } @override Widget build(BuildContext context) { final itemCount = widget.hasFirstSection ? widget.sections.length + 1 : widget.sections.length; List tocItemList = []; if (widget.hasFirstSection) { // 创建首项 tocItemList.add( TOCItem( text: "简介", anchor: "_firstSection", active: widget.activedItem == "_firstSection", onPressed: handleTOCItemPressed, ), ); } for (var index = 0; index < widget.sections.length; index++) { var section = widget.sections[index]; var isTopItem = !widget.hasFirstSection && index == 0; tocItemList.add( Container( decoration: BoxDecoration( border: Border( top: isTopItem ? BorderSide.none : const BorderSide(color: Color.fromRGBO(234, 236, 240, 1))), ), child: TOCItem( text: section.line, number: section.number, anchor: section.anchor, active: widget.activedItem == section.anchor, onPressed: handleTOCItemPressed, ), ), ); } return DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: Container( color: Styles.themePageBackgroundColor, child: SafeAreaBuilder(builder: (context, padding) { _listPaddingTop = padding.top + TOCStyles.listPadding; _listPaddingBottom = padding.bottom + TOCStyles.listPadding; return ListView( controller: _scrollController, padding: EdgeInsets.only( top: _listPaddingTop, bottom: _listPaddingBottom, right: padding.right, ), children: tocItemList, ); }), ), ); } } class TOCItem extends StatelessWidget { final void Function(String anchor)? onPressed; final bool active; final String? number; final String anchor; final String text; const TOCItem( {super.key, this.onPressed, this.active = false, this.number, required this.text, required this.anchor}); void handleTap() { onPressed?.call(anchor); } @override Widget build(BuildContext context) { return Stack( children: [ ClickableBuilder( builder: (context, mode, child) => GestureDetector( onTap: handleTap, child: Container( color: mode == PointerActiveMode.active ? Styles.themeNormalActionActiveColor : Styles.themeNormalActionColor, padding: const EdgeInsets.only(top: 12, right: 10, bottom: 12, left: 15), child: child, ), ), child: DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: Row( children: [ if (number != null) Text(number!), if (number != null) const SizedBox(width: 10), Flexible( child: Text( text, overflow: TextOverflow.fade, maxLines: 1, softWrap: false, ), ), ], ), ), ), if (active) Positioned( top: 0, left: 0, bottom: 0, width: 4, child: Container( color: TOCStyles.activeColor, ), ), ], ); } }