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.
179 lines
5.3 KiB
Dart
179 lines
5.3 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.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';
|
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
|
|
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 = "";
|
|
final ItemScrollController _itemScrollController = ItemScrollController();
|
|
|
|
@override
|
|
void didUpdateWidget(covariant TOCList oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
if (_activedItem != widget.activedItem && _activedItem != _lastSelectedItem) {
|
|
_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;
|
|
}
|
|
}
|
|
_itemScrollController.jumpTo(index: activedIndex);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
return DefaultTextStyle(
|
|
style: CupertinoTheme.of(context).textTheme.textStyle,
|
|
child: Container(
|
|
color: Styles.themePageBackgroundColor,
|
|
child: SafeAreaBuilder(
|
|
builder: (context, padding) => ScrollablePositionedList.builder(
|
|
itemScrollController: _itemScrollController,
|
|
padding: EdgeInsets.only(
|
|
top: padding.top + 2,
|
|
bottom: padding.bottom + 2,
|
|
right: padding.right,
|
|
),
|
|
itemCount: itemCount,
|
|
itemBuilder: (context, index) {
|
|
if (widget.hasFirstSection && index == 0) {
|
|
// 创建首项
|
|
return TOCItem(
|
|
text: "简介",
|
|
anchor: "_firstSection",
|
|
active: widget.activedItem == "_firstSection",
|
|
onPressed: handleTOCItemPressed,
|
|
);
|
|
}
|
|
|
|
var section =
|
|
widget.hasFirstSection ? widget.sections[index - 1] : widget.sections[index];
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
top: index == 0
|
|
? 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,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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: const Color.fromRGBO(51, 102, 204, 1),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|