修改路由配置,增加收藏相关的API
parent
da38eff56d
commit
4a0076d97d
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:isekai_wiki/api/response/watch.dart';
|
||||||
|
|
||||||
|
import 'mw_api.dart';
|
||||||
|
|
||||||
|
class MWApiWatch {
|
||||||
|
static Future<MWResponse<List<WatchActionResponseList>>> watchPage(List<String> titles,
|
||||||
|
{bool unwatch = false}) async {
|
||||||
|
var query = {"titles": titles.join("|")};
|
||||||
|
if (unwatch) {
|
||||||
|
query["unwatch"] = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
var mwRes = await MWApi.post("watch", params: query, withToken: "watch");
|
||||||
|
|
||||||
|
var data = WatchActionResponse.fromJson(mwRes.data);
|
||||||
|
|
||||||
|
return mwRes.replaceData(data.watch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<MWResponse<List<WatchActionResponseList>>> unwatchPage(List<String> titles) {
|
||||||
|
return watchPage(titles, unwatch: true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// ignore_for_file: unused_element
|
||||||
|
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'watch.freezed.dart';
|
||||||
|
part 'watch.g.dart';
|
||||||
|
|
||||||
|
@Freezed(copyWith: false)
|
||||||
|
class WatchActionResponseList with _$WatchActionResponseList {
|
||||||
|
WatchActionResponseList._();
|
||||||
|
|
||||||
|
factory WatchActionResponseList(
|
||||||
|
{required int ns,
|
||||||
|
required String title,
|
||||||
|
String? watch,
|
||||||
|
String? unwatch}) = _WatchActionResponseList;
|
||||||
|
|
||||||
|
bool get isWatch {
|
||||||
|
return watch != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory WatchActionResponseList.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$WatchActionResponseListFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(copyWith: false)
|
||||||
|
class WatchActionResponse with _$WatchActionResponse {
|
||||||
|
factory WatchActionResponse({required List<WatchActionResponseList> watch}) =
|
||||||
|
_WatchActionResponse;
|
||||||
|
|
||||||
|
factory WatchActionResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$WatchActionResponseFromJson(json);
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,355 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:isekai_wiki/api/response/page_info.dart';
|
||||||
|
import 'package:isekai_wiki/components/flutter_scale_tap/flutter_scale_tap.dart';
|
||||||
|
import 'package:isekai_wiki/components/utils.dart';
|
||||||
|
import 'package:isekai_wiki/pages/article.dart';
|
||||||
|
import 'package:isekai_wiki/reactive/reactive.dart';
|
||||||
|
import 'package:like_button/like_button.dart';
|
||||||
|
import 'package:pull_down_button/pull_down_button.dart';
|
||||||
|
import 'package:skeletons/skeletons.dart';
|
||||||
|
|
||||||
|
import '../styles.dart';
|
||||||
|
|
||||||
|
typedef AddFavoriteCallback = Future<bool> Function(
|
||||||
|
PageInfo pageInfo, bool localIsFavorite, bool showToast);
|
||||||
|
|
||||||
|
typedef PageInfoCallback = Future<void> Function(PageInfo pageInfo);
|
||||||
|
|
||||||
|
class PageCardStyles {
|
||||||
|
static const double cardInnerHeight = 150;
|
||||||
|
static const cardInnerPadding = EdgeInsets.only(top: 16, left: 20, right: 20, bottom: 12);
|
||||||
|
static const double footerButtonSize = 30;
|
||||||
|
static const double footerButtonInnerSize = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageCardController extends GetxController {
|
||||||
|
var isLoading = false.obs;
|
||||||
|
|
||||||
|
var pageInfo = Rx<PageInfo?>(null);
|
||||||
|
|
||||||
|
var isFavorite = false.obs;
|
||||||
|
|
||||||
|
AddFavoriteCallback? onSetFavorite;
|
||||||
|
PageInfoCallback? onShare;
|
||||||
|
|
||||||
|
Future<bool> handleFavoriteClick(bool localIsFavorite) async {
|
||||||
|
if (pageInfo.value != null && onSetFavorite != null) {
|
||||||
|
return await onSetFavorite!.call(pageInfo.value!, localIsFavorite, false);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleAddFavoriteMenuItemClick() async {
|
||||||
|
if (pageInfo.value != null && onSetFavorite != null) {
|
||||||
|
await onSetFavorite!.call(pageInfo.value!, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleRemoveFavoriteMenuItemClick() async {
|
||||||
|
if (pageInfo.value != null && onSetFavorite != null) {
|
||||||
|
await onSetFavorite!.call(pageInfo.value!, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShareClick() async {
|
||||||
|
if (pageInfo.value != null && onShare != null) {
|
||||||
|
await onShare!.call(pageInfo.value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePageInfoClick() {
|
||||||
|
if (pageInfo.value != null) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleCardClick() async {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageInfo.value != null) {
|
||||||
|
var cPageInfo = pageInfo.value!;
|
||||||
|
await Navigator.of(Get.context!).push(
|
||||||
|
CupertinoPageRoute(
|
||||||
|
builder: (_) => ArticlePage(
|
||||||
|
targetPage: cPageInfo.title,
|
||||||
|
initialArticleData: MinimumArticleData(
|
||||||
|
title: cPageInfo.mainTitle,
|
||||||
|
description: cPageInfo.description,
|
||||||
|
mainCategory: cPageInfo.mainCategory,
|
||||||
|
updateTime: cPageInfo.updatedTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageCard extends StatefulWidget {
|
||||||
|
final bool isLoading;
|
||||||
|
final PageInfo? pageInfo;
|
||||||
|
final bool isFavorite;
|
||||||
|
final AddFavoriteCallback? onSetFavorite;
|
||||||
|
final PageInfoCallback? onShare;
|
||||||
|
|
||||||
|
const PageCard({
|
||||||
|
super.key,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.pageInfo,
|
||||||
|
this.isFavorite = false,
|
||||||
|
this.onSetFavorite,
|
||||||
|
this.onShare,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _PageCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PageCardState extends ReactiveState<PageCard> {
|
||||||
|
var c = PageCardController();
|
||||||
|
double textScale = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void receiveProps() {
|
||||||
|
c.isLoading.value = widget.isLoading;
|
||||||
|
c.pageInfo.value = widget.pageInfo;
|
||||||
|
c.isFavorite.value = widget.isFavorite;
|
||||||
|
c.onSetFavorite = widget.onSetFavorite;
|
||||||
|
c.onShare = widget.onShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _actionButtonSkeleton(bool isLoading, Widget child) {
|
||||||
|
return Skeleton(
|
||||||
|
isLoading: isLoading,
|
||||||
|
skeleton: const SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(
|
||||||
|
width: PageCardStyles.footerButtonSize, height: PageCardStyles.footerButtonSize),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCardHeader(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Skeleton(
|
||||||
|
isLoading: c.isLoading.value,
|
||||||
|
skeleton: SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(
|
||||||
|
height: (Styles.pageCardTitle.fontSize! + 4) * textScale, randomLength: true),
|
||||||
|
),
|
||||||
|
child: Text(c.pageInfo.value?.mainTitle ?? "页面信息丢失", style: Styles.pageCardTitle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCardBody(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: c.isLoading.value
|
||||||
|
? ClipRect(
|
||||||
|
child: SkeletonParagraph(
|
||||||
|
style: SkeletonParagraphStyle(
|
||||||
|
lines: 3,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||||
|
lineStyle: SkeletonLineStyle(
|
||||||
|
randomLength: true,
|
||||||
|
height: Styles.pageCardDescription.fontSize! * textScale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(c.pageInfo.value?.description ?? "没有简介",
|
||||||
|
overflow: TextOverflow.fade, style: Styles.pageCardDescription),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Skeleton(
|
||||||
|
isLoading: c.isLoading.value,
|
||||||
|
skeleton: const SkeletonAvatar(
|
||||||
|
style: SkeletonAvatarStyle(width: 114, height: 114),
|
||||||
|
),
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCardFooter(BuildContext context) {
|
||||||
|
return ScaleTapIgnore(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 分类信息
|
||||||
|
c.pageInfo.value?.mainCategory != null
|
||||||
|
? Chip(
|
||||||
|
backgroundColor: const Color.fromARGB(1, 238, 238, 238),
|
||||||
|
label: Text(c.pageInfo.value!.mainCategory!,
|
||||||
|
style: const TextStyle(color: Colors.black54)),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap)
|
||||||
|
: const SizedBox(),
|
||||||
|
c.pageInfo.value?.mainCategory != null ? const SizedBox(width: 10) : const SizedBox(),
|
||||||
|
// 发布日期
|
||||||
|
Skeleton(
|
||||||
|
isLoading: c.isLoading.value,
|
||||||
|
skeleton: const SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(width: 100),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
c.pageInfo.value?.updatedTime != null
|
||||||
|
? Utils.getFriendDate(c.pageInfo.value!.updatedTime!)
|
||||||
|
: "",
|
||||||
|
style: Styles.pageCardDescription),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// 收藏键
|
||||||
|
Obx(
|
||||||
|
() => _actionButtonSkeleton(
|
||||||
|
c.isLoading.value,
|
||||||
|
LikeButton(
|
||||||
|
size: PageCardStyles.footerButtonInnerSize,
|
||||||
|
isLiked: c.isFavorite.value,
|
||||||
|
onTap: c.handleFavoriteClick,
|
||||||
|
likeBuilder: (bool isLiked) {
|
||||||
|
return Icon(
|
||||||
|
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isLiked ? Colors.red : Colors.grey,
|
||||||
|
size: PageCardStyles.footerButtonInnerSize,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 18),
|
||||||
|
// 菜单键
|
||||||
|
_actionButtonSkeleton(
|
||||||
|
c.isLoading.value,
|
||||||
|
SizedBox(
|
||||||
|
height: PageCardStyles.footerButtonSize,
|
||||||
|
width: PageCardStyles.footerButtonSize,
|
||||||
|
child: PullDownButton(
|
||||||
|
routeTheme: PullDownMenuRouteTheme(
|
||||||
|
endShadow: BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.6),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
c.isFavorite.value
|
||||||
|
? PullDownMenuItem(
|
||||||
|
title: '取消收藏',
|
||||||
|
icon: CupertinoIcons.heart_fill,
|
||||||
|
onTap: c.handleRemoveFavoriteMenuItemClick,
|
||||||
|
)
|
||||||
|
: PullDownMenuItem(
|
||||||
|
title: '收藏',
|
||||||
|
icon: CupertinoIcons.heart,
|
||||||
|
onTap: c.handleRemoveFavoriteMenuItemClick,
|
||||||
|
),
|
||||||
|
const PullDownMenuDivider(),
|
||||||
|
PullDownMenuItem(
|
||||||
|
title: '分享',
|
||||||
|
icon: CupertinoIcons.share,
|
||||||
|
onTap: c.handleShareClick,
|
||||||
|
),
|
||||||
|
const PullDownMenuDivider.large(),
|
||||||
|
/*
|
||||||
|
PullDownMenuItem(
|
||||||
|
title: '相似推荐',
|
||||||
|
onTap: () {},
|
||||||
|
icon: CupertinoIcons.ellipsis_circle,
|
||||||
|
),
|
||||||
|
const PullDownMenuDivider.large(),
|
||||||
|
*/
|
||||||
|
PullDownMenuItem(
|
||||||
|
title: '页面详情',
|
||||||
|
onTap: c.handlePageInfoClick,
|
||||||
|
icon: CupertinoIcons.info_circle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
position: PullDownMenuPosition.under,
|
||||||
|
buttonBuilder: (context, showMenu) => IconButton(
|
||||||
|
onPressed: showMenu,
|
||||||
|
padding: const EdgeInsets.all(0.0),
|
||||||
|
splashRadius: PageCardStyles.footerButtonInnerSize - 4,
|
||||||
|
iconSize: PageCardStyles.footerButtonInnerSize,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.more_horiz,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 4.0,
|
||||||
|
// 圆角
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(Styles.isXs ? 0 : 14.0)),
|
||||||
|
),
|
||||||
|
// 抗锯齿
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
semanticContainer: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: PageCardStyles.cardInnerPadding,
|
||||||
|
child: SizedBox(
|
||||||
|
height: PageCardStyles.cardInnerHeight * textScale,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 标题
|
||||||
|
_buildCardHeader(context),
|
||||||
|
// 简介、图片
|
||||||
|
_buildCardBody(context),
|
||||||
|
// Footer
|
||||||
|
_buildCardFooter(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget render(BuildContext context) {
|
||||||
|
textScale = MediaQuery.of(context).textScaleFactor;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Obx(
|
||||||
|
() => ScaleTap(
|
||||||
|
enableFeedback: c.isLoading.value,
|
||||||
|
onPressed: c.handleCardClick,
|
||||||
|
child: GetBuilder<PageCardController>(
|
||||||
|
init: c,
|
||||||
|
builder: (GetxController c) => _buildCard(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,19 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
|
||||||
|
|
||||||
class DiscoverTab extends StatelessWidget {
|
class DiscoverTab extends StatelessWidget {
|
||||||
const DiscoverTab({super.key});
|
const DiscoverTab({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const CustomScrollView(
|
return const IsekaiPageScaffold(
|
||||||
slivers: <Widget>[
|
child: CustomScrollView(
|
||||||
CupertinoSliverNavigationBar(
|
slivers: <Widget>[
|
||||||
largeTitle: Text('发现'),
|
CupertinoSliverNavigationBar(
|
||||||
),
|
largeTitle: Text('发现'),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:cupertino_lists/cupertino_lists.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:isekai_wiki/components/dummy_icon.dart';
|
||||||
|
import 'package:isekai_wiki/components/follow_scale.dart';
|
||||||
|
import 'package:isekai_wiki/components/isekai_nav_bar.dart';
|
||||||
|
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
|
||||||
|
import 'package:isekai_wiki/global.dart';
|
||||||
|
import 'package:isekai_wiki/reactive/reactive.dart';
|
||||||
|
import 'package:isekai_wiki/styles.dart';
|
||||||
|
|
||||||
|
class SettingsListController extends GetxController {
|
||||||
|
VoidFutureCallback? onSelectionChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsList extends StatefulWidget {
|
||||||
|
final String selected;
|
||||||
|
final VoidFutureCallback? onSelectionChange;
|
||||||
|
|
||||||
|
const SettingsList({super.key, this.selected = "", this.onSelectionChange});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _SettingsListState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsListState extends ReactiveState<SettingsList> {
|
||||||
|
var c = SettingsListController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void receiveProps() {
|
||||||
|
c.onSelectionChange = widget.onSelectionChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FollowTextScale(
|
||||||
|
child: CupertinoListSection.insetGrouped(
|
||||||
|
backgroundColor: Styles.themePageBackgroundColor,
|
||||||
|
children: <CupertinoListTile>[
|
||||||
|
CupertinoListTile.notched(
|
||||||
|
title: const Text('阅读设置'),
|
||||||
|
leading: const DummyIcon(
|
||||||
|
color: CupertinoColors.systemGrey,
|
||||||
|
icon: CupertinoIcons.textformat,
|
||||||
|
),
|
||||||
|
trailing: const CupertinoListTileChevron(),
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsListPage extends StatelessWidget {
|
||||||
|
const SettingsListPage({super.key});
|
||||||
|
|
||||||
|
SliverChildListDelegate _buildSliverChildBuilderDelegate(BuildContext context) {
|
||||||
|
return SliverChildListDelegate([
|
||||||
|
const SettingsList(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IsekaiPageScaffold(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
const IsekaiSliverNavigationBar(
|
||||||
|
largeTitle: Text('设置'),
|
||||||
|
),
|
||||||
|
SliverSafeArea(
|
||||||
|
top: false,
|
||||||
|
minimum: const EdgeInsets.only(top: 4),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: _buildSliverChildBuilderDelegate(context),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue