mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-10 19:20:12 +08:00
feat: article list
Closes #841 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -829,4 +829,6 @@ class Api {
|
|||||||
static const String topicFeed = '/x/polymer/web-dynamic/v1/feed/topic';
|
static const String topicFeed = '/x/polymer/web-dynamic/v1/feed/topic';
|
||||||
|
|
||||||
static const String spaceOpus = '/x/polymer/web-dynamic/v1/opus/feed/space';
|
static const String spaceOpus = '/x/polymer/web-dynamic/v1/opus/feed/space';
|
||||||
|
|
||||||
|
static const String articleList = '/x/article/list/web/articles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:PiliPlus/http/constants.dart';
|
|||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
|
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/data.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_card_list.dart';
|
import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_card_list.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/dyn_topic_top/top_details.dart';
|
import 'package:PiliPlus/models/dynamics/dyn_topic_top/top_details.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||||
@@ -304,4 +305,21 @@ class DynamicsHttp {
|
|||||||
return LoadingState.error(res.data['message']);
|
return LoadingState.error(res.data['message']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<ArticleListData>> articleList({
|
||||||
|
required id,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().get(
|
||||||
|
Api.articleList,
|
||||||
|
queryParameters: {
|
||||||
|
'id': id,
|
||||||
|
'web_location': 333.1400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return LoadingState.success(ArticleListData.fromJson(res.data['data']));
|
||||||
|
} else {
|
||||||
|
return LoadingState.error(res.data['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ class Pic {
|
|||||||
num? size;
|
num? size;
|
||||||
String? liveUrl;
|
String? liveUrl;
|
||||||
|
|
||||||
double? calHeight;
|
|
||||||
|
|
||||||
Pic.fromJson(Map<String, dynamic> json) {
|
Pic.fromJson(Map<String, dynamic> json) {
|
||||||
url = json['url'];
|
url = json['url'];
|
||||||
width = json['width'];
|
width = json['width'];
|
||||||
@@ -45,12 +43,6 @@ class Pic {
|
|||||||
style = json['style'];
|
style = json['style'];
|
||||||
liveUrl = json['live_url'];
|
liveUrl = json['live_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCalHeight(double maxWidth) {
|
|
||||||
if (calHeight == null && height != null && width != null) {
|
|
||||||
calHeight = maxWidth * height! / width!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Line {
|
class Line {
|
||||||
|
|||||||
84
lib/models/dynamics/article_list/article.dart
Normal file
84
lib/models/dynamics/article_list/article.dart
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:PiliPlus/models/dynamics/article_list/category.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/stats.dart';
|
||||||
|
|
||||||
|
class Article {
|
||||||
|
int? id;
|
||||||
|
String? title;
|
||||||
|
int? state;
|
||||||
|
int? publishTime;
|
||||||
|
int? words;
|
||||||
|
List? imageUrls;
|
||||||
|
Category? category;
|
||||||
|
List<Category>? categories;
|
||||||
|
String? summary;
|
||||||
|
int? type;
|
||||||
|
String? dynIdStr;
|
||||||
|
int? attributes;
|
||||||
|
int? authorUid;
|
||||||
|
int? onlyFans;
|
||||||
|
Stats? stats;
|
||||||
|
int? likeState;
|
||||||
|
|
||||||
|
Article({
|
||||||
|
this.id,
|
||||||
|
this.title,
|
||||||
|
this.state,
|
||||||
|
this.publishTime,
|
||||||
|
this.words,
|
||||||
|
this.imageUrls,
|
||||||
|
this.category,
|
||||||
|
this.categories,
|
||||||
|
this.summary,
|
||||||
|
this.type,
|
||||||
|
this.dynIdStr,
|
||||||
|
this.attributes,
|
||||||
|
this.authorUid,
|
||||||
|
this.onlyFans,
|
||||||
|
this.stats,
|
||||||
|
this.likeState,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Article.fromJson(Map<String, dynamic> json) => Article(
|
||||||
|
id: json['id'] as int?,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
state: json['state'] as int?,
|
||||||
|
publishTime: json['publish_time'] as int?,
|
||||||
|
words: json['words'] as int?,
|
||||||
|
imageUrls: json['image_urls'],
|
||||||
|
category: json['category'] == null
|
||||||
|
? null
|
||||||
|
: Category.fromJson(json['category'] as Map<String, dynamic>),
|
||||||
|
categories: (json['categories'] as List<dynamic>?)
|
||||||
|
?.map((e) => Category.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
summary: json['summary'] as String?,
|
||||||
|
type: json['type'] as int?,
|
||||||
|
dynIdStr: json['dyn_id_str'] as String?,
|
||||||
|
attributes: json['attributes'] as int?,
|
||||||
|
authorUid: json['author_uid'] as int?,
|
||||||
|
onlyFans: json['only_fans'] as int?,
|
||||||
|
stats: json['stats'] == null
|
||||||
|
? null
|
||||||
|
: Stats.fromJson(json['stats'] as Map<String, dynamic>),
|
||||||
|
likeState: json['like_state'] as int?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'state': state,
|
||||||
|
'publish_time': publishTime,
|
||||||
|
'words': words,
|
||||||
|
'image_urls': imageUrls,
|
||||||
|
'category': category?.toJson(),
|
||||||
|
'categories': categories?.map((e) => e.toJson()).toList(),
|
||||||
|
'summary': summary,
|
||||||
|
'type': type,
|
||||||
|
'dyn_id_str': dynIdStr,
|
||||||
|
'attributes': attributes,
|
||||||
|
'author_uid': authorUid,
|
||||||
|
'only_fans': onlyFans,
|
||||||
|
'stats': stats?.toJson(),
|
||||||
|
'like_state': likeState,
|
||||||
|
};
|
||||||
|
}
|
||||||
23
lib/models/dynamics/article_list/author.dart
Normal file
23
lib/models/dynamics/article_list/author.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class Author {
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? face;
|
||||||
|
|
||||||
|
Author({
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.face,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Author.fromJson(Map<String, dynamic> json) => Author(
|
||||||
|
mid: json['mid'] as int?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
face: json['face'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'mid': mid,
|
||||||
|
'name': name,
|
||||||
|
'face': face,
|
||||||
|
};
|
||||||
|
}
|
||||||
19
lib/models/dynamics/article_list/category.dart
Normal file
19
lib/models/dynamics/article_list/category.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
class Category {
|
||||||
|
int? id;
|
||||||
|
int? parentId;
|
||||||
|
String? name;
|
||||||
|
|
||||||
|
Category({this.id, this.parentId, this.name});
|
||||||
|
|
||||||
|
factory Category.fromJson(Map<String, dynamic> json) => Category(
|
||||||
|
id: json['id'] as int?,
|
||||||
|
parentId: json['parent_id'] as int?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'parent_id': parentId,
|
||||||
|
'name': name,
|
||||||
|
};
|
||||||
|
}
|
||||||
38
lib/models/dynamics/article_list/data.dart
Normal file
38
lib/models/dynamics/article_list/data.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:PiliPlus/models/dynamics/article_list/article.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/author.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/list.dart';
|
||||||
|
|
||||||
|
class ArticleListData {
|
||||||
|
ArticleList? list;
|
||||||
|
List<Article>? articles;
|
||||||
|
Author? author;
|
||||||
|
bool? attention;
|
||||||
|
|
||||||
|
ArticleListData({
|
||||||
|
this.list,
|
||||||
|
this.articles,
|
||||||
|
this.author,
|
||||||
|
this.attention,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ArticleListData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ArticleListData(
|
||||||
|
list: json['list'] == null
|
||||||
|
? null
|
||||||
|
: ArticleList.fromJson(json['list'] as Map<String, dynamic>),
|
||||||
|
articles: (json['articles'] as List<dynamic>?)
|
||||||
|
?.map((e) => Article.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
author: json['author'] == null
|
||||||
|
? null
|
||||||
|
: Author.fromJson(json['author'] as Map<String, dynamic>),
|
||||||
|
attention: json['attention'] as bool?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'list': list?.toJson(),
|
||||||
|
'articles': articles?.map((e) => e.toJson()).toList(),
|
||||||
|
'author': author?.toJson(),
|
||||||
|
'attention': attention,
|
||||||
|
};
|
||||||
|
}
|
||||||
71
lib/models/dynamics/article_list/list.dart
Normal file
71
lib/models/dynamics/article_list/list.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
class ArticleList {
|
||||||
|
int? id;
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? imageUrl;
|
||||||
|
int? updateTime;
|
||||||
|
int? ctime;
|
||||||
|
int? publishTime;
|
||||||
|
String? summary;
|
||||||
|
int? words;
|
||||||
|
int? read;
|
||||||
|
int? articlesCount;
|
||||||
|
int? state;
|
||||||
|
String? reason;
|
||||||
|
String? applyTime;
|
||||||
|
String? checkTime;
|
||||||
|
|
||||||
|
ArticleList({
|
||||||
|
this.id,
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.imageUrl,
|
||||||
|
this.updateTime,
|
||||||
|
this.ctime,
|
||||||
|
this.publishTime,
|
||||||
|
this.summary,
|
||||||
|
this.words,
|
||||||
|
this.read,
|
||||||
|
this.articlesCount,
|
||||||
|
this.state,
|
||||||
|
this.reason,
|
||||||
|
this.applyTime,
|
||||||
|
this.checkTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ArticleList.fromJson(Map<String, dynamic> json) => ArticleList(
|
||||||
|
id: json['id'] as int?,
|
||||||
|
mid: json['mid'] as int?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
imageUrl: json['image_url'] as String?,
|
||||||
|
updateTime: json['update_time'] as int?,
|
||||||
|
ctime: json['ctime'] as int?,
|
||||||
|
publishTime: json['publish_time'] as int?,
|
||||||
|
summary: json['summary'] as String?,
|
||||||
|
words: json['words'] as int?,
|
||||||
|
read: json['read'] as int?,
|
||||||
|
articlesCount: json['articles_count'] as int?,
|
||||||
|
state: json['state'] as int?,
|
||||||
|
reason: json['reason'] as String?,
|
||||||
|
applyTime: json['apply_time'] as String?,
|
||||||
|
checkTime: json['check_time'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'mid': mid,
|
||||||
|
'name': name,
|
||||||
|
'image_url': imageUrl,
|
||||||
|
'update_time': updateTime,
|
||||||
|
'ctime': ctime,
|
||||||
|
'publish_time': publishTime,
|
||||||
|
'summary': summary,
|
||||||
|
'words': words,
|
||||||
|
'read': read,
|
||||||
|
'articles_count': articlesCount,
|
||||||
|
'state': state,
|
||||||
|
'reason': reason,
|
||||||
|
'apply_time': applyTime,
|
||||||
|
'check_time': checkTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
43
lib/models/dynamics/article_list/stats.dart
Normal file
43
lib/models/dynamics/article_list/stats.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
class Stats {
|
||||||
|
int? view;
|
||||||
|
int? favorite;
|
||||||
|
int? like;
|
||||||
|
int? dislike;
|
||||||
|
int? reply;
|
||||||
|
int? share;
|
||||||
|
int? coin;
|
||||||
|
int? dynam1c;
|
||||||
|
|
||||||
|
Stats({
|
||||||
|
this.view,
|
||||||
|
this.favorite,
|
||||||
|
this.like,
|
||||||
|
this.dislike,
|
||||||
|
this.reply,
|
||||||
|
this.share,
|
||||||
|
this.coin,
|
||||||
|
this.dynam1c,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Stats.fromJson(Map<String, dynamic> json) => Stats(
|
||||||
|
view: json['view'] as int?,
|
||||||
|
favorite: json['favorite'] as int?,
|
||||||
|
like: json['like'] as int?,
|
||||||
|
dislike: json['dislike'] as int?,
|
||||||
|
reply: json['reply'] as int?,
|
||||||
|
share: json['share'] as int?,
|
||||||
|
coin: json['coin'] as int?,
|
||||||
|
dynam1c: json['dynamic'] as int?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'view': view,
|
||||||
|
'favorite': favorite,
|
||||||
|
'like': like,
|
||||||
|
'dislike': dislike,
|
||||||
|
'reply': reply,
|
||||||
|
'share': share,
|
||||||
|
'coin': coin,
|
||||||
|
'dynamic': dynam1c,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -98,6 +98,7 @@ class ItemModulesModel {
|
|||||||
|
|
||||||
// 专栏
|
// 专栏
|
||||||
ModuleTop? moduleTop;
|
ModuleTop? moduleTop;
|
||||||
|
ModuleCollection? moduleCollection;
|
||||||
List<ModuleTag>? moduleExtend; // opus的tag
|
List<ModuleTag>? moduleExtend; // opus的tag
|
||||||
List<ArticleContentModel>? moduleContent;
|
List<ArticleContentModel>? moduleContent;
|
||||||
ModuleBlocked? moduleBlocked;
|
ModuleBlocked? moduleBlocked;
|
||||||
@@ -133,6 +134,11 @@ class ItemModulesModel {
|
|||||||
? null
|
? null
|
||||||
: ModuleTag.fromJson(i['module_title']);
|
: ModuleTag.fromJson(i['module_title']);
|
||||||
break;
|
break;
|
||||||
|
case 'MODULE_TYPE_COLLECTION':
|
||||||
|
moduleCollection = i['module_collection'] == null
|
||||||
|
? null
|
||||||
|
: ModuleCollection.fromJson(i['module_collection']);
|
||||||
|
break;
|
||||||
case 'MODULE_TYPE_AUTHOR':
|
case 'MODULE_TYPE_AUTHOR':
|
||||||
moduleAuthor = i['module_author'] == null
|
moduleAuthor = i['module_author'] == null
|
||||||
? null
|
? null
|
||||||
@@ -167,6 +173,20 @@ class ItemModulesModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ModuleCollection {
|
||||||
|
String? count;
|
||||||
|
int? id;
|
||||||
|
String? name;
|
||||||
|
String? title;
|
||||||
|
|
||||||
|
ModuleCollection.fromJson(Map<String, dynamic> json) {
|
||||||
|
count = json['count'];
|
||||||
|
id = json['id'];
|
||||||
|
name = json['name'];
|
||||||
|
title = json['title'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ModuleTop {
|
class ModuleTop {
|
||||||
ModuleTopDisplay? display;
|
ModuleTopDisplay? display;
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,14 @@ class _ArticlePageState extends State<ArticlePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_articleCtr.type != 'read' &&
|
||||||
|
_articleCtr.opusData?.modules.moduleCollection != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: opusCollection(
|
||||||
|
theme,
|
||||||
|
_articleCtr.opusData!.modules.moduleCollection!,
|
||||||
|
),
|
||||||
|
),
|
||||||
content,
|
content,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ class OpusContent extends StatelessWidget {
|
|||||||
return widget;
|
return widget;
|
||||||
case 2 when (element.pic != null):
|
case 2 when (element.pic != null):
|
||||||
if (element.pic!.pics!.length == 1) {
|
if (element.pic!.pics!.length == 1) {
|
||||||
element.pic!.pics!.first.onCalHeight(maxWidth);
|
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: element.pic!.pics!.first.url!,
|
tag: element.pic!.pics!.first.url!,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -142,11 +141,20 @@ class OpusContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: NetworkImgLayer(
|
child: Center(
|
||||||
width: maxWidth,
|
child: ClipRRect(
|
||||||
height: element.pic!.pics!.first.calHeight,
|
borderRadius: StyleString.mdRadius,
|
||||||
src: element.pic!.pics!.first.url!,
|
child: CachedNetworkImage(
|
||||||
quality: 60,
|
imageUrl: Utils.thumbnailImgUrl(
|
||||||
|
element.pic!.pics!.first.url!,
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 120),
|
||||||
|
placeholder: (context, url) =>
|
||||||
|
Image.asset('assets/images/loading.png'),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -670,3 +678,63 @@ Widget moduleBlockedItem(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget opusCollection(ThemeData theme, ModuleCollection item) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Material(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||||
|
color: theme.colorScheme.onInverseSurface,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
'/articleList',
|
||||||
|
parameters: {'id': '${item.id}'},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(item.title!),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Icon(
|
||||||
|
size: 18,
|
||||||
|
Icons.article_outlined,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '${item.name} · ${item.count}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.keyboard_arrow_right,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
33
lib/pages/article_list/controller.dart
Normal file
33
lib/pages/article_list/controller.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:PiliPlus/http/dynamics.dart';
|
||||||
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/article.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/author.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/data.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/list.dart';
|
||||||
|
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class ArticleListController
|
||||||
|
extends CommonListController<ArticleListData, Article> {
|
||||||
|
final id = Get.parameters['id'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
queryData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticleList? list;
|
||||||
|
Author? author;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Article>? getDataList(ArticleListData response) {
|
||||||
|
list = response.list;
|
||||||
|
author = response.author;
|
||||||
|
return response.articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LoadingState<ArticleListData>> customGetData() =>
|
||||||
|
DynamicsHttp.articleList(id: id);
|
||||||
|
}
|
||||||
192
lib/pages/article_list/view.dart
Normal file
192
lib/pages/article_list/view.dart
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||||
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/article.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/list.dart';
|
||||||
|
import 'package:PiliPlus/pages/article_list/controller.dart';
|
||||||
|
import 'package:PiliPlus/pages/article_list/widgets/item.dart';
|
||||||
|
import 'package:PiliPlus/utils/grid.dart';
|
||||||
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class ArticleListPage extends StatefulWidget {
|
||||||
|
const ArticleListPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ArticleListPage> createState() => _ArticleListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArticleListPageState extends State<ArticleListPage> {
|
||||||
|
final _controller =
|
||||||
|
Get.put(ArticleListController(), tag: Utils.generateRandomString(8));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: refreshIndicator(
|
||||||
|
onRefresh: _controller.onRefresh,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 80),
|
||||||
|
sliver: Obx(
|
||||||
|
() => _buildBody(theme, _controller.loadingState.value)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody(
|
||||||
|
ThemeData theme, LoadingState<List<Article>?> loadingState) {
|
||||||
|
return switch (loadingState) {
|
||||||
|
Loading() => SliverPadding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.paddingOf(context).top + kToolbarHeight + 120),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: Grid.videoCardHDelegate(context),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return const VideoCardHSkeleton();
|
||||||
|
},
|
||||||
|
childCount: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Success() => SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
if (_controller.list != null)
|
||||||
|
_buildHeader(theme, _controller.list!),
|
||||||
|
if (loadingState.response?.isNotEmpty == true)
|
||||||
|
SliverGrid(
|
||||||
|
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||||
|
mainAxisSpacing: 2,
|
||||||
|
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||||
|
childAspectRatio: StyleString.aspectRatio * 2.6,
|
||||||
|
minHeight: MediaQuery.textScalerOf(context).scale(90),
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return ArticleListItem(
|
||||||
|
item: loadingState.response![index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: loadingState.response!.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
HttpError(onReload: _controller.onReload),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Error() => HttpError(
|
||||||
|
errMsg: loadingState.errMsg,
|
||||||
|
onReload: _controller.onReload,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(ThemeData theme, ArticleList item) {
|
||||||
|
late final style = TextStyle(color: theme.colorScheme.onSurfaceVariant);
|
||||||
|
late final divider = TextSpan(
|
||||||
|
text: ' | ',
|
||||||
|
style: TextStyle(color: theme.colorScheme.outline.withOpacity(0.7)),
|
||||||
|
);
|
||||||
|
final padding = MediaQuery.paddingOf(context).top + kToolbarHeight;
|
||||||
|
return SliverAppBar.medium(
|
||||||
|
title: Text(item.name!),
|
||||||
|
expandedHeight: kToolbarHeight + 130,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: Container(
|
||||||
|
height: 120,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: padding,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (item.imageUrl?.isNotEmpty == true)
|
||||||
|
NetworkImgLayer(
|
||||||
|
width: 91,
|
||||||
|
height: 120,
|
||||||
|
src: item.imageUrl,
|
||||||
|
radius: 6,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.name!,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_controller.author != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed('/member?mid=${_controller.author!.mid}');
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
NetworkImgLayer(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
src: _controller.author!.face,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(_controller.author!.name!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '${item.articlesCount}篇专栏'),
|
||||||
|
divider,
|
||||||
|
TextSpan(text: '${item.words}个字'),
|
||||||
|
divider,
|
||||||
|
TextSpan(text: '${item.read}次阅读'),
|
||||||
|
],
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'${Utils.dateFormat(item.updateTime, formatType: 'day')}更新'),
|
||||||
|
divider,
|
||||||
|
TextSpan(text: '文集号: ${item.id}'),
|
||||||
|
],
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
lib/pages/article_list/widgets/item.dart
Normal file
113
lib/pages/article_list/widgets/item.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||||
|
import 'package:PiliPlus/models/dynamics/article_list/article.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class ArticleListItem extends StatelessWidget {
|
||||||
|
const ArticleListItem({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Article item;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
final dynIdStr = item.dynIdStr;
|
||||||
|
Get.toNamed(
|
||||||
|
'/articlePage',
|
||||||
|
parameters: {
|
||||||
|
'id': dynIdStr ?? item.id!.toString(),
|
||||||
|
'type': dynIdStr != null ? 'opus' : 'read',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: StyleString.safeSpace,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.42,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
if (item.summary != null)
|
||||||
|
Text(
|
||||||
|
item.summary!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
StatView(
|
||||||
|
context: context,
|
||||||
|
value: item.stats?.view ?? 0,
|
||||||
|
goto: 'picture',
|
||||||
|
textColor: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
StatView(
|
||||||
|
context: context,
|
||||||
|
goto: 'like',
|
||||||
|
value: item.stats?.like ?? 0,
|
||||||
|
textColor: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
StatView(
|
||||||
|
context: context,
|
||||||
|
goto: 'reply',
|
||||||
|
value: item.stats?.reply ?? 0,
|
||||||
|
textColor: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item.imageUrls?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: StyleString.aspectRatio,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, boxConstraints) {
|
||||||
|
return NetworkImgLayer(
|
||||||
|
src: item.imageUrls!.first,
|
||||||
|
width: boxConstraints.maxWidth,
|
||||||
|
height: boxConstraints.maxHeight,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:PiliPlus/pages/about/view.dart';
|
import 'package:PiliPlus/pages/about/view.dart';
|
||||||
import 'package:PiliPlus/pages/article/view.dart';
|
import 'package:PiliPlus/pages/article/view.dart';
|
||||||
|
import 'package:PiliPlus/pages/article_list/view.dart';
|
||||||
import 'package:PiliPlus/pages/blacklist/view.dart';
|
import 'package:PiliPlus/pages/blacklist/view.dart';
|
||||||
import 'package:PiliPlus/pages/danmaku_block/view.dart';
|
import 'package:PiliPlus/pages/danmaku_block/view.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics/view.dart';
|
import 'package:PiliPlus/pages/dynamics/view.dart';
|
||||||
@@ -174,6 +175,7 @@ class Routes {
|
|||||||
CustomGetPage(
|
CustomGetPage(
|
||||||
name: '/searchTrending', page: () => const SearchTrendingPage()),
|
name: '/searchTrending', page: () => const SearchTrendingPage()),
|
||||||
CustomGetPage(name: '/dynTopic', page: () => const DynTopicPage()),
|
CustomGetPage(name: '/dynTopic', page: () => const DynTopicPage()),
|
||||||
|
CustomGetPage(name: '/articleList', page: () => const ArticleListPage()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user