You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
6.2 KiB
Dart
216 lines
6.2 KiB
Dart
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<MWParseSectionInfo> sections;
|
|
final void Function(String anchor)? onSwitchTitle;
|
|
|
|
const TOCList({
|
|
super.key,
|
|
required this.sections,
|
|
this.activedItem = "",
|
|
this.hasFirstSection = false,
|
|
this.onSwitchTitle,
|
|
});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _TOCListState();
|
|
}
|
|
|
|
class _TOCListState extends State<TOCList> {
|
|
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<Widget> 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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|