mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Support chapter groups.
This commit is contained in:
348
lib/pages/comic_details_page/chapters.dart
Normal file
348
lib/pages/comic_details_page/chapters.dart
Normal file
@@ -0,0 +1,348 @@
|
||||
part of 'comic_page.dart';
|
||||
|
||||
class _ComicChapters extends StatelessWidget {
|
||||
const _ComicChapters({this.history, required this.groupedMode});
|
||||
|
||||
final History? history;
|
||||
|
||||
final bool groupedMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return groupedMode
|
||||
? _GroupedComicChapters(history)
|
||||
: _NormalComicChapters(history);
|
||||
}
|
||||
}
|
||||
|
||||
class _NormalComicChapters extends StatefulWidget {
|
||||
const _NormalComicChapters(this.history);
|
||||
|
||||
final History? history;
|
||||
|
||||
@override
|
||||
State<_NormalComicChapters> createState() => _NormalComicChaptersState();
|
||||
}
|
||||
|
||||
class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
||||
late _ComicPageState state;
|
||||
|
||||
bool reverse = false;
|
||||
|
||||
bool showAll = false;
|
||||
|
||||
late History? history;
|
||||
|
||||
late Map<String, String> chapters;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
history = widget.history;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
state = context.findAncestorStateOfType<_ComicPageState>()!;
|
||||
chapters = state.comic.chapters!;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _NormalComicChapters oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
setState(() {
|
||||
history = widget.history;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverLayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
int length = chapters.length;
|
||||
bool canShowAll = showAll;
|
||||
if (!showAll) {
|
||||
var width = constrains.crossAxisExtent - 16;
|
||||
var crossItems = width ~/ 200;
|
||||
if (width % 200 != 0) {
|
||||
crossItems += 1;
|
||||
}
|
||||
length = math.min(length, crossItems * 8);
|
||||
if (length == chapters.length) {
|
||||
canShowAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text("Chapters".tl),
|
||||
trailing: Tooltip(
|
||||
message: "Order".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(reverse
|
||||
? Icons.vertical_align_top
|
||||
: Icons.vertical_align_bottom_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse = !reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: length,
|
||||
(context, i) {
|
||||
if (reverse) {
|
||||
i = chapters.length - i - 1;
|
||||
}
|
||||
var key = chapters.keys.elementAt(i);
|
||||
var value = chapters[key]!;
|
||||
bool visited = (history?.readEpisode ?? {}).contains(i + 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => state.read(i + 1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: visited
|
||||
? context.colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||
maxCrossAxisExtent: 200,
|
||||
itemHeight: 48,
|
||||
),
|
||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||
if (chapters.length > 20 && !canShowAll)
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showAll = true;
|
||||
});
|
||||
},
|
||||
label: Text("${"Show all".tl} (${chapters.length})"),
|
||||
).paddingTop(12),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GroupedComicChapters extends StatefulWidget {
|
||||
const _GroupedComicChapters(this.history);
|
||||
|
||||
final History? history;
|
||||
|
||||
@override
|
||||
State<_GroupedComicChapters> createState() => _GroupedComicChaptersState();
|
||||
}
|
||||
|
||||
class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late _ComicPageState state;
|
||||
|
||||
bool reverse = false;
|
||||
|
||||
bool showAll = false;
|
||||
|
||||
late History? history;
|
||||
|
||||
late Map<String, Map<String, String>> chapters;
|
||||
|
||||
late TabController tabController;
|
||||
|
||||
int index = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
history = widget.history;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
state = context.findAncestorStateOfType<_ComicPageState>()!;
|
||||
chapters = state.comic.groupedChapters!;
|
||||
tabController = TabController(
|
||||
length: chapters.keys.length,
|
||||
vsync: this,
|
||||
);
|
||||
tabController.addListener(onTabChange);
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
void onTabChange() {
|
||||
if (index != tabController.index) {
|
||||
setState(() {
|
||||
index = tabController.index;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _GroupedComicChapters oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
setState(() {
|
||||
history = widget.history;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverLayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
var group = chapters.values.elementAt(index);
|
||||
int length = group.length;
|
||||
bool canShowAll = showAll;
|
||||
if (!showAll) {
|
||||
var width = constrains.crossAxisExtent - 16;
|
||||
var crossItems = width ~/ 200;
|
||||
if (width % 200 != 0) {
|
||||
crossItems += 1;
|
||||
}
|
||||
length = math.min(length, crossItems * 8);
|
||||
if (length == group.length) {
|
||||
canShowAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text("Chapters".tl),
|
||||
trailing: Tooltip(
|
||||
message: "Order".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(reverse
|
||||
? Icons.vertical_align_top
|
||||
: Icons.vertical_align_bottom_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse = !reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: AppTabBar(
|
||||
withUnderLine: false,
|
||||
controller: tabController,
|
||||
tabs: chapters.keys.map((e) => Tab(text: e)).toList(),
|
||||
),
|
||||
),
|
||||
SliverPadding(padding: const EdgeInsets.only(top: 8)),
|
||||
SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: length,
|
||||
(context, i) {
|
||||
if (reverse) {
|
||||
i = group.length - i - 1;
|
||||
}
|
||||
var key = group.keys.elementAt(i);
|
||||
var value = group[key]!;
|
||||
var chapterIndex = 0;
|
||||
for (var j = 0; j < chapters.length; j++) {
|
||||
if (j == index) {
|
||||
chapterIndex += i;
|
||||
break;
|
||||
}
|
||||
chapterIndex += chapters.values.elementAt(j).length;
|
||||
}
|
||||
bool visited =
|
||||
(history?.readEpisode ?? {}).contains(chapterIndex + 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => state.read(chapterIndex + 1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: visited
|
||||
? context.colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||
maxCrossAxisExtent: 200,
|
||||
itemHeight: 48,
|
||||
),
|
||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||
if (chapters.length > 20 && !canShowAll)
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showAll = true;
|
||||
});
|
||||
},
|
||||
label: Text("${"Show all".tl} (${group.length})"),
|
||||
).paddingTop(12),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user