修改路由配置,增加收藏相关的API

main
落雨楓 2 years ago
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);
}
}

@ -3,7 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'csrf_token.freezed.dart';
part 'csrf_token.g.dart';
@freezed
@Freezed(copyWith: false)
class CSRFTokenResponse with _$CSRFTokenResponse {
const factory CSRFTokenResponse({required Map<String, String> tokens}) = _CSRFTokenInfoResponse;

@ -3,7 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'mugenapp.freezed.dart';
part 'mugenapp.g.dart';
@freezed
@Freezed(copyWith: false)
class MugenAppStartAuthInfo with _$MugenAppStartAuthInfo {
const factory MugenAppStartAuthInfo({
required String loginUrl,
@ -15,16 +15,16 @@ class MugenAppStartAuthInfo with _$MugenAppStartAuthInfo {
_$MugenAppStartAuthInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MugenAppStartAuthResponse with _$MugenAppStartAuthResponse {
const factory MugenAppStartAuthResponse(
{required MugenAppStartAuthInfo startauth}) = _MugenAppStartAuthResponse;
const factory MugenAppStartAuthResponse({required MugenAppStartAuthInfo startauth}) =
_MugenAppStartAuthResponse;
factory MugenAppStartAuthResponse.fromJson(Map<String, dynamic> json) =>
_$MugenAppStartAuthResponseFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MugenAppAttemptAuthInfo with _$MugenAppAttemptAuthInfo {
const factory MugenAppAttemptAuthInfo({
required String status,
@ -36,7 +36,7 @@ class MugenAppAttemptAuthInfo with _$MugenAppAttemptAuthInfo {
_$MugenAppAttemptAuthInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MugenAppAttemptAuthResponse with _$MugenAppAttemptAuthResponse {
const factory MugenAppAttemptAuthResponse({
required MugenAppAttemptAuthInfo attemptauth,

@ -83,14 +83,14 @@ class PageInfo {
}
}
@freezed
@Freezed(copyWith: false)
class PagesResponse with _$PagesResponse {
factory PagesResponse({required List<PageInfo> pages}) = _PageResponse;
factory PagesResponse.fromJson(Map<String, dynamic> json) => _$PagesResponseFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class PageImageInfo with _$PageImageInfo {
factory PageImageInfo({required String source, int? width, int? height}) = _PageImageInfo;

@ -3,7 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'parse.freezed.dart';
part 'parse.g.dart';
@freezed
@Freezed(copyWith: false)
class MWParseCategoryInfo with _$MWParseCategoryInfo {
factory MWParseCategoryInfo({
required String category,
@ -14,7 +14,7 @@ class MWParseCategoryInfo with _$MWParseCategoryInfo {
_$MWParseCategoryInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MWParseLangLinkInfo with _$MWParseLangLinkInfo {
factory MWParseLangLinkInfo({
required String lang,
@ -28,7 +28,7 @@ class MWParseLangLinkInfo with _$MWParseLangLinkInfo {
_$MWParseLangLinkInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MWParsePageLinkInfo with _$MWParsePageLinkInfo {
factory MWParsePageLinkInfo({
required int ns,
@ -40,7 +40,7 @@ class MWParsePageLinkInfo with _$MWParsePageLinkInfo {
_$MWParsePageLinkInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MWParseSectionInfo with _$MWParseSectionInfo {
factory MWParseSectionInfo({
required int toclevel,
@ -57,7 +57,7 @@ class MWParseSectionInfo with _$MWParseSectionInfo {
_$MWParseSectionInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MWParseInfo with _$MWParseInfo {
factory MWParseInfo({
required String title,

@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'recent_changes.freezed.dart';
part 'recent_changes.g.dart';
@freezed
@Freezed(copyWith: false)
class RecentChangesItem with _$RecentChangesItem {
factory RecentChangesItem({
String? type,
@ -22,7 +22,7 @@ class RecentChangesItem with _$RecentChangesItem {
_$RecentChangesItemFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class RecentChangesResponse with _$RecentChangesResponse {
factory RecentChangesResponse({required List<RecentChangesItem> recentchanges}) =
_RecentChangesResponse;

@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'userinfo.freezed.dart';
part 'userinfo.g.dart';
@freezed
@Freezed(copyWith: false)
class UserGroupMembership with _$UserGroupMembership {
factory UserGroupMembership({
required String group,
@ -16,7 +16,7 @@ class UserGroupMembership with _$UserGroupMembership {
_$UserGroupMembershipFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class UserAcceptLang with _$UserAcceptLang {
factory UserAcceptLang({
required double q,
@ -26,7 +26,7 @@ class UserAcceptLang with _$UserAcceptLang {
factory UserAcceptLang.fromJson(Map<String, dynamic> json) => _$UserAcceptLangFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MetaUserInfo with _$MetaUserInfo {
factory MetaUserInfo({
required int id,
@ -52,7 +52,7 @@ class MetaUserInfo with _$MetaUserInfo {
factory MetaUserInfo.fromJson(Map<String, dynamic> json) => _$MetaUserInfoFromJson(json);
}
@freezed
@Freezed(copyWith: false)
class MetaUserInfoResponse with _$MetaUserInfoResponse {
factory MetaUserInfoResponse({
required MetaUserInfo userinfo,

@ -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

@ -5,7 +5,6 @@ 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';
@ -24,53 +23,59 @@ class PageCardStyles {
static const double footerButtonInnerSize = 26;
}
class PageCardController extends GetxController {
var isLoading = false.obs;
var pageInfo = Rx<PageInfo?>(null);
var isFavorite = false.obs;
class PageCard extends StatelessWidget {
final bool isLoading;
final PageInfo? pageInfo;
final bool isFavorite;
final AddFavoriteCallback? onSetFavorite;
final PageInfoCallback? onShare;
AddFavoriteCallback? onSetFavorite;
PageInfoCallback? onShare;
const PageCard({
super.key,
this.isLoading = false,
this.pageInfo,
this.isFavorite = false,
this.onSetFavorite,
this.onShare,
});
Future<bool> handleFavoriteClick(bool localIsFavorite) async {
if (pageInfo.value != null && onSetFavorite != null) {
return await onSetFavorite!.call(pageInfo.value!, localIsFavorite, false);
if (pageInfo != null && onSetFavorite != null) {
return await onSetFavorite!.call(pageInfo!, localIsFavorite, false);
} else {
return false;
}
}
Future<void> handleAddFavoriteMenuItemClick() async {
if (pageInfo.value != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo.value!, true, true);
if (pageInfo != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo!, true, true);
}
}
Future<void> handleRemoveFavoriteMenuItemClick() async {
if (pageInfo.value != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo.value!, false, true);
if (pageInfo != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo!, false, true);
}
}
handleShareClick() async {
if (pageInfo.value != null && onShare != null) {
await onShare!.call(pageInfo.value!);
if (pageInfo != null && onShare != null) {
await onShare!.call(pageInfo!);
}
}
void handlePageInfoClick() {
if (pageInfo.value != null) {}
if (pageInfo != null) {}
}
Future<void> handleCardClick() async {
if (isLoading.value) {
if (isLoading) {
return;
}
if (pageInfo.value != null) {
var cPageInfo = pageInfo.value!;
if (pageInfo != null) {
var cPageInfo = pageInfo!;
await Navigator.of(Get.context!).push(
CupertinoPageRoute(
builder: (_) => ArticlePage(
@ -86,45 +91,6 @@ class PageCardController extends GetxController {
);
}
}
}
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(
@ -138,20 +104,24 @@ class _PageCardState extends ReactiveState<PageCard> {
}
Widget _buildCardHeader(BuildContext context) {
var textScale = MediaQuery.of(context).textScaleFactor;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Skeleton(
isLoading: c.isLoading.value,
isLoading: isLoading,
skeleton: SkeletonLine(
style: SkeletonLineStyle(
height: (Styles.pageCardTitle.fontSize! + 4) * textScale, randomLength: true),
),
child: Text(c.pageInfo.value?.mainTitle ?? "页面信息丢失", style: Styles.pageCardTitle),
child: Text(pageInfo?.mainTitle ?? "页面信息丢失", style: Styles.pageCardTitle),
),
);
}
Widget _buildCardBody(BuildContext context) {
var textScale = MediaQuery.of(context).textScaleFactor;
return Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
@ -161,7 +131,7 @@ class _PageCardState extends ReactiveState<PageCard> {
children: [
Expanded(
flex: 1,
child: c.isLoading.value
child: isLoading
? ClipRect(
child: SkeletonParagraph(
style: SkeletonParagraphStyle(
@ -173,12 +143,12 @@ class _PageCardState extends ReactiveState<PageCard> {
),
),
)
: Text(c.pageInfo.value?.description ?? "没有简介",
: Text(pageInfo?.description ?? "没有简介",
overflow: TextOverflow.fade, style: Styles.pageCardDescription),
),
const SizedBox(width: 10),
Skeleton(
isLoading: c.isLoading.value,
isLoading: isLoading,
skeleton: const SkeletonAvatar(
style: SkeletonAvatarStyle(width: 114, height: 114),
),
@ -197,49 +167,45 @@ class _PageCardState extends ReactiveState<PageCard> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
c.pageInfo.value?.mainCategory != null
pageInfo?.mainCategory != null
? Chip(
backgroundColor: const Color.fromARGB(1, 238, 238, 238),
label: Text(c.pageInfo.value!.mainCategory!,
style: const TextStyle(color: Colors.black54)),
label:
Text(pageInfo!.mainCategory!, style: const TextStyle(color: Colors.black54)),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap)
: const SizedBox(),
c.pageInfo.value?.mainCategory != null ? const SizedBox(width: 10) : const SizedBox(),
pageInfo?.mainCategory != null ? const SizedBox(width: 10) : const SizedBox(),
//
Skeleton(
isLoading: c.isLoading.value,
isLoading: isLoading,
skeleton: const SkeletonLine(
style: SkeletonLineStyle(width: 100),
),
child: Text(
c.pageInfo.value?.updatedTime != null
? Utils.getFriendDate(c.pageInfo.value!.updatedTime!)
: "",
pageInfo?.updatedTime != null ? Utils.getFriendDate(pageInfo!.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,
);
},
),
_actionButtonSkeleton(
isLoading,
LikeButton(
size: PageCardStyles.footerButtonInnerSize,
isLiked: isFavorite,
onTap: 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,
isLoading,
SizedBox(
height: PageCardStyles.footerButtonSize,
width: PageCardStyles.footerButtonSize,
@ -252,39 +218,7 @@ class _PageCardState extends ReactiveState<PageCard> {
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,
),
],
itemBuilder: _buildMenuItem,
position: PullDownMenuPosition.under,
buttonBuilder: (context, showMenu) => IconButton(
onPressed: showMenu,
@ -304,7 +238,45 @@ class _PageCardState extends ReactiveState<PageCard> {
);
}
List<PullDownMenuEntry> _buildMenuItem(BuildContext context) {
return [
isFavorite
? PullDownMenuItem(
title: '取消收藏',
icon: CupertinoIcons.heart_fill,
onTap: handleRemoveFavoriteMenuItemClick,
)
: PullDownMenuItem(
title: '收藏',
icon: CupertinoIcons.heart,
onTap: handleRemoveFavoriteMenuItemClick,
),
const PullDownMenuDivider(),
PullDownMenuItem(
title: '分享',
icon: CupertinoIcons.share,
onTap: handleShareClick,
),
const PullDownMenuDivider.large(),
/*
PullDownMenuItem(
title: '相似推荐',
onTap: () {},
icon: CupertinoIcons.ellipsis_circle,
),
const PullDownMenuDivider.large(),
*/
PullDownMenuItem(
title: '页面详情',
onTap: handlePageInfoClick,
icon: CupertinoIcons.info_circle,
),
];
}
Widget _buildCard(BuildContext context) {
var textScale = MediaQuery.of(context).textScaleFactor;
return Card(
elevation: 4.0,
//
@ -335,20 +307,13 @@ class _PageCardState extends ReactiveState<PageCard> {
}
@override
Widget render(BuildContext context) {
textScale = MediaQuery.of(context).textScaleFactor;
Widget build(BuildContext context) {
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),
),
),
child: ScaleTap(
enableFeedback: isLoading,
onPressed: handleCardClick,
child: _buildCard(context),
),
);
}

@ -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),
),
),
),
);
}
}

@ -93,7 +93,7 @@ class RecentPageListController extends GetxController {
}
hasNextPage.value = rcListRes.continueInfo != null;
continueInfo.value = rcListRes.continueInfo ?? {};
} catch (err, stack) {
} catch (err) {
hasNextPage.value = false;
if (shouldRefresh) {
pageList.clear();

@ -19,8 +19,7 @@ class FavoriteListController extends GetxController {
}
}
var newPageIds =
pageIds.where((pageId) => !removeList.contains(pageId)).toList();
var newPageIds = pageIds.where((pageId) => !removeList.contains(pageId)).toList();
for (var pageId in addList) {
if (!newPageIds.contains(pageId)) {
newPageIds.add(pageId);
@ -47,16 +46,19 @@ class FavoriteListController extends GetxController {
return pageIds.contains(pageInfo.pageid);
}
Future<bool> setFavorite(
PageInfo pageInfo, bool isFavorite, bool showToast) async {
Future<bool> setFavorite(PageInfo pageInfo, bool isFavorite, bool showToast) async {
//
var uc = Get.find<UserController>();
if (!uc.isLoggedIn) {
var result = await confirm(Get.overlayContext!, "使用收藏功能需要登录",
title: "提示", positiveText: "登录");
return false;
alert(Get.overlayContext!, "使用收藏功能需要登录", title: "提示");
return isFavorite;
}
return false;
if (isFavorite) {
//
} else {
//
}
return !isFavorite;
}
}

@ -25,7 +25,6 @@ class AboutPage extends StatelessWidget {
return IsekaiPageScaffold(
navigationBar: const IsekaiNavigationBar(
middle: Text('关于'),
previousPageTitle: "我的",
),
child: ListView(
children: [

@ -1,16 +1,19 @@
import 'package:flutter/cupertino.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
class DiscoverTab extends StatelessWidget {
const DiscoverTab({super.key});
@override
Widget build(BuildContext context) {
return const CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('发现'),
),
],
return const IsekaiPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('发现'),
),
],
),
);
}
}

@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_nav_bar.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/components/recent_page_list.dart';
import 'package:isekai_wiki/models/user.dart';
import 'package:isekai_wiki/pages/tab_page.dart';
@ -127,106 +128,108 @@ class HomeTab extends StatelessWidget {
Widget build(BuildContext context) {
final c = Get.put(HomeController());
return WebSmoothScroll(
controller: c.scrollController,
child: CustomScrollView(
return IsekaiPageScaffold(
child: WebSmoothScroll(
controller: c.scrollController,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
slivers: <Widget>[
IsekaiSliverNavigationBar(
leading: _buildSearchIconButton(),
backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark,
largeTitle: const Text('首页', style: TextStyle(color: Styles.themeNavTitleColor)),
border: Border.all(style: BorderStyle.none),
trailing: _buildNotificationIconButton(),
child: CustomScrollView(
controller: c.scrollController,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
minHeight: 48,
maxHeight: 48,
child: Container(
decoration: const BoxDecoration(
color: Styles.themeMainColor,
boxShadow: [
BoxShadow(
color: Styles.themeMainColor,
blurRadius: 0.0,
spreadRadius: 0.0,
offset: Offset(0, -2),
),
],
),
padding: const EdgeInsets.only(bottom: 10, left: 12, right: 12),
child: CupertinoButton(
color: Colors.white,
padding: const EdgeInsets.all(0),
onPressed: () {
onSearchClick?.call();
},
child: Wrap(
spacing: 14,
alignment: WrapAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search, color: Colors.black54),
slivers: <Widget>[
IsekaiSliverNavigationBar(
leading: _buildSearchIconButton(),
backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark,
largeTitle: const Text('首页', style: TextStyle(color: Styles.themeNavTitleColor)),
border: Border.all(style: BorderStyle.none),
trailing: _buildNotificationIconButton(),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
minHeight: 48,
maxHeight: 48,
child: Container(
decoration: const BoxDecoration(
color: Styles.themeMainColor,
boxShadow: [
BoxShadow(
color: Styles.themeMainColor,
blurRadius: 0.0,
spreadRadius: 0.0,
offset: Offset(0, -2),
),
const Text("搜索页面...",
textAlign: TextAlign.center, style: TextStyle(color: Colors.black54))
],
),
padding: const EdgeInsets.only(bottom: 10, left: 12, right: 12),
child: CupertinoButton(
color: Colors.white,
padding: const EdgeInsets.all(0),
onPressed: () {
onSearchClick?.call();
},
child: Wrap(
spacing: 14,
alignment: WrapAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search, color: Colors.black54),
),
const Text("搜索页面...",
textAlign: TextAlign.center, style: TextStyle(color: Colors.black54))
],
),
),
),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 40.0,
maxHeight: 40.0,
child: Container(
decoration: BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
)
]),
child: Row(
children: [
SizedBox(
child: TabBar(
isScrollable: true,
controller: c.tabController,
indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45,
tabs: const [CollapsedTabText('最新'), CollapsedTabText('关注')],
onTap: (int selected) {},
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 40.0,
maxHeight: 40.0,
child: Container(
decoration: BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
)
]),
child: Row(
children: [
SizedBox(
child: TabBar(
isScrollable: true,
controller: c.tabController,
indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45,
tabs: const [CollapsedTabText('最新'), CollapsedTabText('关注')],
onTap: (int selected) {},
),
),
),
const Expanded(child: Text('')),
],
const Expanded(child: Text('')),
],
),
),
),
),
),
CupertinoSliverRefreshControl(
onRefresh: c.handleRefresh,
),
SliverSafeArea(
top: false,
bottom: false,
minimum: const EdgeInsets.only(top: 12, bottom: 12),
sliver: RecentPageList(
scrollController: c.scrollController,
CupertinoSliverRefreshControl(
onRefresh: c.handleRefresh,
),
),
],
SliverSafeArea(
top: false,
bottom: false,
minimum: const EdgeInsets.only(top: 12, bottom: 12),
sliver: RecentPageList(
scrollController: c.scrollController,
),
),
],
),
),
);
}

@ -2,11 +2,12 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:cupertino_lists/cupertino_lists.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_nav_bar.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/models/user.dart';
import 'package:isekai_wiki/pages/about.dart';
import 'package:isekai_wiki/pages/settings/list.dart';
import 'package:isekai_wiki/styles.dart';
import 'package:isekai_wiki/utils/dialog.dart';
@ -52,6 +53,24 @@ class OwnProfileController extends GetxController {
Future<void> handleLogout() async {
await uc.logout();
}
Future<void> handleSettingsClick(BuildContext context) async {
await Navigator.of(context, rootNavigator: false).push(
CupertinoPageRoute(
title: "设置",
builder: (_) => const SettingsListPage(),
),
);
}
Future<void> handleAboutClick(BuildContext context) async {
await Navigator.of(context, rootNavigator: false).push(
CupertinoPageRoute(
title: "关于",
builder: (_) => const AboutPage(),
),
);
}
}
class OwnProfileTab extends StatelessWidget {
@ -83,8 +102,7 @@ class OwnProfileTab extends StatelessWidget {
});
}
Widget _buildUserSection(BuildContext context) {
var c = Get.find<OwnProfileController>();
Widget _buildUserSection(BuildContext context, OwnProfileController c) {
var uc = Get.find<UserController>();
return FollowTextScale(
@ -138,7 +156,7 @@ class OwnProfileTab extends StatelessWidget {
);
}
Widget _buildArticleListsSection(BuildContext context) {
Widget _buildArticleListsSection(BuildContext context, OwnProfileController c) {
return FollowTextScale(
child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor,
@ -174,7 +192,7 @@ class OwnProfileTab extends StatelessWidget {
));
}
Widget _buildSettingsSection(BuildContext context) {
Widget _buildSettingsSection(BuildContext context, OwnProfileController c) {
return FollowTextScale(
child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor,
@ -186,7 +204,9 @@ class OwnProfileTab extends StatelessWidget {
icon: CupertinoIcons.settings,
),
trailing: const CupertinoListTileChevron(),
onTap: () {},
onTap: () async {
await c.handleSettingsClick(context);
},
),
CupertinoListTile.notched(
title: const Text('关于'),
@ -196,11 +216,7 @@ class OwnProfileTab extends StatelessWidget {
),
trailing: const CupertinoListTileChevron(),
onTap: () async {
await Navigator.of(context, rootNavigator: false).push(
CupertinoPageRoute(
builder: (_) => const AboutPage(),
),
);
await c.handleAboutClick(context);
},
),
if (kDebugMode)
@ -217,41 +233,34 @@ class OwnProfileTab extends StatelessWidget {
));
}
SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(BuildContext context) {
return SliverChildBuilderDelegate(
(context, index) {
switch (index) {
case 0:
return _buildUserSection(context);
case 1:
return _buildArticleListsSection(context);
case 2:
return _buildSettingsSection(context);
default:
// Do nothing. For now.
}
return null;
},
);
SliverChildListDelegate _buildSliverChildBuilderDelegate(
BuildContext context, OwnProfileController c) {
return SliverChildListDelegate([
_buildUserSection(context, c),
_buildArticleListsSection(context, c),
_buildSettingsSection(context, c),
]);
}
@override
Widget build(BuildContext context) {
Get.put(OwnProfileController());
var c = Get.put(OwnProfileController());
return CustomScrollView(
slivers: <Widget>[
const IsekaiSliverNavigationBar(
largeTitle: Text('我的'),
),
SliverSafeArea(
top: false,
minimum: const EdgeInsets.only(top: 4),
sliver: SliverList(
delegate: _buildSliverChildBuilderDelegate(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, c),
),
)
],
),
);
}
}

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/components/state_test.dart';
import 'package:isekai_wiki/styles.dart';
@ -15,7 +16,7 @@ class SearchTab extends StatelessWidget {
Widget build(BuildContext context) {
var c = Get.put(SearchController());
return CupertinoPageScaffold(
return IsekaiPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: 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),
),
)
],
),
);
}
}

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/pages/discover.dart';
import 'package:isekai_wiki/pages/home.dart';
import 'package:isekai_wiki/pages/search.dart';
@ -60,8 +61,7 @@ class IsekaiWikiTabsPage extends StatelessWidget {
tabBar: CupertinoTabBar(
backgroundColor: Styles.themeBottomColor,
activeColor: Styles.themeMainColor,
border: const Border(
top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)),
border: const Border(top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)),
height: 56,
onTap: c.handleTapTab,
items: const <BottomNavigationBarItem>[
@ -86,37 +86,35 @@ class IsekaiWikiTabsPage extends StatelessWidget {
tabBuilder: (context, index) {
switch (index) {
case 0:
return CupertinoTabView(builder: (context) {
return CupertinoPageScaffold(
child: HomeTab(
onSearchClick: c.toSearchPage,
),
);
});
return CupertinoTabView(
defaultTitle: "首页",
builder: (context) => HomeTab(
onSearchClick: c.toSearchPage,
),
);
case 1:
return CupertinoTabView(builder: (context) {
return const CupertinoPageScaffold(
child: DiscoverTab(),
);
});
return CupertinoTabView(
defaultTitle: "发现",
builder: (context) => const DiscoverTab(),
);
case 2:
return CupertinoTabView(builder: (context) {
return const CupertinoPageScaffold(
child: SearchTab(),
);
});
return CupertinoTabView(
defaultTitle: "搜索",
builder: (context) => const SearchTab(),
);
case 3:
return CupertinoTabView(builder: (context) {
return const CupertinoPageScaffold(
child: OwnProfileTab(),
);
});
return CupertinoTabView(
defaultTitle: "我的",
builder: (context) => const OwnProfileTab(),
);
}
return CupertinoTabView(builder: (context) {
return const CupertinoPageScaffold(
child: HomeTab(),
);
});
return CupertinoTabView(
defaultTitle: "页面不存在",
builder: (context) => const IsekaiPageScaffold(
child: SizedBox(),
),
);
},
),
);

Loading…
Cancel
Save