mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
comments
This commit is contained in:
@@ -103,7 +103,9 @@ class Button extends StatefulWidget {
|
|||||||
required VoidCallback onPressed,
|
required VoidCallback onPressed,
|
||||||
double? size,
|
double? size,
|
||||||
Color? color,
|
Color? color,
|
||||||
String? tooltip}) {
|
String? tooltip,
|
||||||
|
bool isLoading = false,
|
||||||
|
HitTestBehavior behavior = HitTestBehavior.deferToChild}) {
|
||||||
return _IconButton(
|
return _IconButton(
|
||||||
key: key,
|
key: key,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
@@ -111,6 +113,8 @@ class Button extends StatefulWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
|
behavior: behavior,
|
||||||
|
isLoading: isLoading,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,13 +266,16 @@ class _ButtonState extends State<Button> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _IconButton extends StatefulWidget {
|
class _IconButton extends StatefulWidget {
|
||||||
const _IconButton(
|
const _IconButton({
|
||||||
{super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.size,
|
this.size,
|
||||||
this.color,
|
this.color,
|
||||||
this.tooltip});
|
this.tooltip,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.behavior = HitTestBehavior.deferToChild,
|
||||||
|
});
|
||||||
|
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
|
|
||||||
@@ -280,6 +287,10 @@ class _IconButton extends StatefulWidget {
|
|||||||
|
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
||||||
|
final HitTestBehavior behavior;
|
||||||
|
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_IconButton> createState() => _IconButtonState();
|
State<_IconButton> createState() => _IconButtonState();
|
||||||
}
|
}
|
||||||
@@ -289,24 +300,43 @@ class _IconButtonState extends State<_IconButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
var iconSize = widget.size ?? 24;
|
||||||
onTap: widget.onPressed,
|
Widget icon = IconTheme(
|
||||||
mouseCursor: SystemMouseCursors.click,
|
data: IconThemeData(
|
||||||
customBorder: const CircleBorder(),
|
size: iconSize,
|
||||||
|
color: widget.color ?? context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
child: widget.icon,
|
||||||
|
);
|
||||||
|
if (widget.isLoading) {
|
||||||
|
icon = const CircularProgressIndicator(
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
).paddingAll(2).fixWidth(iconSize).fixHeight(iconSize);
|
||||||
|
}
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => isHover = true),
|
||||||
|
onExit: (_) => setState(() => isHover = false),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: widget.behavior,
|
||||||
|
onTap: () {
|
||||||
|
if (widget.isLoading) return;
|
||||||
|
widget.onPressed();
|
||||||
|
},
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: widget.tooltip ?? "",
|
message: widget.tooltip ?? "",
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color: isHover
|
||||||
isHover ? Theme.of(context).colorScheme.surfaceContainer : null,
|
? Theme.of(context)
|
||||||
borderRadius: BorderRadius.circular(16),
|
.colorScheme
|
||||||
|
.outlineVariant
|
||||||
|
.withOpacity(0.4)
|
||||||
|
: null,
|
||||||
|
borderRadius: BorderRadius.circular((iconSize + 12) / 2),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: IconTheme(
|
child: icon,
|
||||||
data: IconThemeData(
|
|
||||||
size: widget.size ?? 24,
|
|
||||||
color: widget.color ?? context.colorScheme.primary),
|
|
||||||
child: widget.icon,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -17,10 +17,9 @@ import '../js_engine.dart';
|
|||||||
import '../log.dart';
|
import '../log.dart';
|
||||||
|
|
||||||
part 'category.dart';
|
part 'category.dart';
|
||||||
|
|
||||||
part 'favorites.dart';
|
part 'favorites.dart';
|
||||||
|
|
||||||
part 'parser.dart';
|
part 'parser.dart';
|
||||||
|
part 'models.dart';
|
||||||
|
|
||||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||||
@@ -43,9 +42,19 @@ typedef GetImageLoadingConfigFunc = Map<String, dynamic> Function(
|
|||||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||||
String imageKey)?;
|
String imageKey)?;
|
||||||
|
|
||||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(String comicId, String? next);
|
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
||||||
|
String comicId, String? next);
|
||||||
|
|
||||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(String comicId, bool isLiking);
|
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
||||||
|
String comicId, bool isLiking);
|
||||||
|
|
||||||
|
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||||
|
/// return the new likes count or null.
|
||||||
|
typedef LikeCommentFunc = Future<Res<int?>> Function(String comicId, String? subId, String commentId, bool isLiking);
|
||||||
|
|
||||||
|
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||||
|
/// return the new vote count or null.
|
||||||
|
typedef VoteCommentFunc = Future<Res<int?>> Function(String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||||
|
|
||||||
class ComicSource {
|
class ComicSource {
|
||||||
static final List<ComicSource> _sources = [];
|
static final List<ComicSource> _sources = [];
|
||||||
@@ -155,8 +164,6 @@ class ComicSource {
|
|||||||
final Map<String, dynamic> Function(String imageKey)?
|
final Map<String, dynamic> Function(String imageKey)?
|
||||||
getThumbnailLoadingConfig;
|
getThumbnailLoadingConfig;
|
||||||
|
|
||||||
final String? matchBriefIdReg;
|
|
||||||
|
|
||||||
var data = <String, dynamic>{};
|
var data = <String, dynamic>{};
|
||||||
|
|
||||||
bool get isLogged => data["account"] != null;
|
bool get isLogged => data["account"] != null;
|
||||||
@@ -175,6 +182,10 @@ class ComicSource {
|
|||||||
|
|
||||||
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
||||||
|
|
||||||
|
final VoteCommentFunc? voteCommentFunc;
|
||||||
|
|
||||||
|
final LikeCommentFunc? likeCommentFunc;
|
||||||
|
|
||||||
Future<void> loadData() async {
|
Future<void> loadData() async {
|
||||||
var file = File("${App.dataPath}/comic_source/$key.data");
|
var file = File("${App.dataPath}/comic_source/$key.data");
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
@@ -228,37 +239,15 @@ class ComicSource {
|
|||||||
this.loadComicPages,
|
this.loadComicPages,
|
||||||
this.getImageLoadingConfig,
|
this.getImageLoadingConfig,
|
||||||
this.getThumbnailLoadingConfig,
|
this.getThumbnailLoadingConfig,
|
||||||
this.matchBriefIdReg,
|
|
||||||
this.filePath,
|
this.filePath,
|
||||||
this.url,
|
this.url,
|
||||||
this.version,
|
this.version,
|
||||||
this.commentsLoader,
|
this.commentsLoader,
|
||||||
this.sendCommentFunc,
|
this.sendCommentFunc,
|
||||||
this.likeOrUnlikeComic)
|
this.likeOrUnlikeComic,
|
||||||
|
this.voteCommentFunc,
|
||||||
|
this.likeCommentFunc,)
|
||||||
: idMatcher = null;
|
: idMatcher = null;
|
||||||
|
|
||||||
ComicSource.unknown(this.key)
|
|
||||||
: name = "Unknown",
|
|
||||||
account = null,
|
|
||||||
categoryData = null,
|
|
||||||
categoryComicsData = null,
|
|
||||||
favoriteData = null,
|
|
||||||
explorePages = [],
|
|
||||||
searchPageData = null,
|
|
||||||
settings = [],
|
|
||||||
loadComicInfo = null,
|
|
||||||
loadComicThumbnail = null,
|
|
||||||
loadComicPages = null,
|
|
||||||
getImageLoadingConfig = null,
|
|
||||||
getThumbnailLoadingConfig = null,
|
|
||||||
matchBriefIdReg = null,
|
|
||||||
filePath = "",
|
|
||||||
url = "",
|
|
||||||
version = "",
|
|
||||||
commentsLoader = null,
|
|
||||||
sendCommentFunc = null,
|
|
||||||
idMatcher = null,
|
|
||||||
likeOrUnlikeComic = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountConfig {
|
class AccountConfig {
|
||||||
@@ -394,131 +383,6 @@ enum SettingType {
|
|||||||
input,
|
input,
|
||||||
}
|
}
|
||||||
|
|
||||||
class Comic {
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
final String cover;
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
final String? subtitle;
|
|
||||||
|
|
||||||
final List<String>? tags;
|
|
||||||
|
|
||||||
final String description;
|
|
||||||
|
|
||||||
final String sourceKey;
|
|
||||||
|
|
||||||
final int? maxPage;
|
|
||||||
|
|
||||||
const Comic(this.title, this.cover, this.id, this.subtitle, this.tags,
|
|
||||||
this.description, this.sourceKey, this.maxPage);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"title": title,
|
|
||||||
"cover": cover,
|
|
||||||
"id": id,
|
|
||||||
"subTitle": subtitle,
|
|
||||||
"tags": tags,
|
|
||||||
"description": description,
|
|
||||||
"sourceKey": sourceKey,
|
|
||||||
"maxPage": maxPage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Comic.fromJson(Map<String, dynamic> json, this.sourceKey)
|
|
||||||
: title = json["title"],
|
|
||||||
subtitle = json["subTitle"] ?? "",
|
|
||||||
cover = json["cover"],
|
|
||||||
id = json["id"],
|
|
||||||
tags = List<String>.from(json["tags"] ?? []),
|
|
||||||
description = json["description"] ?? "",
|
|
||||||
maxPage = json["maxPage"];
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComicDetails with HistoryMixin {
|
|
||||||
@override
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String? subTitle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String cover;
|
|
||||||
|
|
||||||
final String? description;
|
|
||||||
|
|
||||||
final Map<String, List<String>> tags;
|
|
||||||
|
|
||||||
/// id-name
|
|
||||||
final Map<String, String>? chapters;
|
|
||||||
|
|
||||||
final List<String>? thumbnails;
|
|
||||||
|
|
||||||
final List<Comic>? recommend;
|
|
||||||
|
|
||||||
final String sourceKey;
|
|
||||||
|
|
||||||
final String comicId;
|
|
||||||
|
|
||||||
final bool? isFavorite;
|
|
||||||
|
|
||||||
final String? subId;
|
|
||||||
|
|
||||||
final bool? isLiked;
|
|
||||||
|
|
||||||
final int? likesCount;
|
|
||||||
|
|
||||||
final int? commentsCount;
|
|
||||||
|
|
||||||
final String? uploader;
|
|
||||||
|
|
||||||
final String? uploadTime;
|
|
||||||
|
|
||||||
final String? updateTime;
|
|
||||||
|
|
||||||
static Map<String, List<String>> _generateMap(Map<String, dynamic> map) {
|
|
||||||
var res = <String, List<String>>{};
|
|
||||||
map.forEach((key, value) {
|
|
||||||
res[key] = List<String>.from(value);
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
ComicDetails.fromJson(Map<String, dynamic> json)
|
|
||||||
: title = json["title"],
|
|
||||||
subTitle = json["subTitle"],
|
|
||||||
cover = json["cover"],
|
|
||||||
description = json["description"],
|
|
||||||
tags = _generateMap(json["tags"]),
|
|
||||||
chapters = json["chapters"] == null
|
|
||||||
? null
|
|
||||||
: Map<String, String>.from(json["chapters"]),
|
|
||||||
sourceKey = json["sourceKey"],
|
|
||||||
comicId = json["comicId"],
|
|
||||||
thumbnails = ListOrNull.from(json["thumbnails"]),
|
|
||||||
recommend = (json["recommend"] as List?)
|
|
||||||
?.map((e) => Comic.fromJson(e, json["sourceKey"]))
|
|
||||||
.toList(),
|
|
||||||
isFavorite = json["isFavorite"],
|
|
||||||
subId = json["subId"],
|
|
||||||
likesCount = json["likesCount"],
|
|
||||||
isLiked = json["isLiked"],
|
|
||||||
commentsCount = json["commentsCount"],
|
|
||||||
uploader = json["uploader"],
|
|
||||||
uploadTime = json["uploadTime"],
|
|
||||||
updateTime = json["updateTime"];
|
|
||||||
|
|
||||||
@override
|
|
||||||
HistoryType get historyType => HistoryType(sourceKey.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id => comicId;
|
|
||||||
|
|
||||||
ComicType get comicType => ComicType(sourceKey.hashCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
String category, String? param, List<String> options, int page);
|
String category, String? param, List<String> options, int page);
|
||||||
|
|
||||||
@@ -560,14 +424,3 @@ class CategoryComicsOptions {
|
|||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Comment {
|
|
||||||
final String userName;
|
|
||||||
final String? avatar;
|
|
||||||
final String content;
|
|
||||||
final String? time;
|
|
||||||
final int? replyCount;
|
|
||||||
final String? id;
|
|
||||||
|
|
||||||
const Comment(this.userName, this.avatar, this.content, this.time,
|
|
||||||
this.replyCount, this.id);
|
|
||||||
}
|
|
||||||
|
149
lib/foundation/comic_source/models.dart
Normal file
149
lib/foundation/comic_source/models.dart
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
part of 'comic_source.dart';
|
||||||
|
|
||||||
|
class Comment {
|
||||||
|
final String userName;
|
||||||
|
final String? avatar;
|
||||||
|
final String content;
|
||||||
|
final String? time;
|
||||||
|
final int? replyCount;
|
||||||
|
final String? id;
|
||||||
|
final int? score;
|
||||||
|
final bool? isLiked;
|
||||||
|
final int? voteStatus; // 1: upvote, -1: downvote, 0: none
|
||||||
|
|
||||||
|
Comment.fromJson(Map<String, dynamic> json)
|
||||||
|
: userName = json["userName"],
|
||||||
|
avatar = json["avatar"],
|
||||||
|
content = json["content"],
|
||||||
|
time = json["time"],
|
||||||
|
replyCount = json["replyCount"],
|
||||||
|
id = json["id"].toString(),
|
||||||
|
score = json["score"],
|
||||||
|
isLiked = json["isLiked"],
|
||||||
|
voteStatus = json["voteStatus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Comic {
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String cover;
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
final List<String>? tags;
|
||||||
|
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
final int? maxPage;
|
||||||
|
|
||||||
|
const Comic(this.title, this.cover, this.id, this.subtitle, this.tags,
|
||||||
|
this.description, this.sourceKey, this.maxPage);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"cover": cover,
|
||||||
|
"id": id,
|
||||||
|
"subTitle": subtitle,
|
||||||
|
"tags": tags,
|
||||||
|
"description": description,
|
||||||
|
"sourceKey": sourceKey,
|
||||||
|
"maxPage": maxPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Comic.fromJson(Map<String, dynamic> json, this.sourceKey)
|
||||||
|
: title = json["title"],
|
||||||
|
subtitle = json["subTitle"] ?? "",
|
||||||
|
cover = json["cover"],
|
||||||
|
id = json["id"],
|
||||||
|
tags = List<String>.from(json["tags"] ?? []),
|
||||||
|
description = json["description"] ?? "",
|
||||||
|
maxPage = json["maxPage"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComicDetails with HistoryMixin {
|
||||||
|
@override
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String? subTitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String cover;
|
||||||
|
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
final Map<String, List<String>> tags;
|
||||||
|
|
||||||
|
/// id-name
|
||||||
|
final Map<String, String>? chapters;
|
||||||
|
|
||||||
|
final List<String>? thumbnails;
|
||||||
|
|
||||||
|
final List<Comic>? recommend;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
final String comicId;
|
||||||
|
|
||||||
|
final bool? isFavorite;
|
||||||
|
|
||||||
|
final String? subId;
|
||||||
|
|
||||||
|
final bool? isLiked;
|
||||||
|
|
||||||
|
final int? likesCount;
|
||||||
|
|
||||||
|
final int? commentsCount;
|
||||||
|
|
||||||
|
final String? uploader;
|
||||||
|
|
||||||
|
final String? uploadTime;
|
||||||
|
|
||||||
|
final String? updateTime;
|
||||||
|
|
||||||
|
static Map<String, List<String>> _generateMap(Map<String, dynamic> map) {
|
||||||
|
var res = <String, List<String>>{};
|
||||||
|
map.forEach((key, value) {
|
||||||
|
res[key] = List<String>.from(value);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComicDetails.fromJson(Map<String, dynamic> json)
|
||||||
|
: title = json["title"],
|
||||||
|
subTitle = json["subTitle"],
|
||||||
|
cover = json["cover"],
|
||||||
|
description = json["description"],
|
||||||
|
tags = _generateMap(json["tags"]),
|
||||||
|
chapters = json["chapters"] == null
|
||||||
|
? null
|
||||||
|
: Map<String, String>.from(json["chapters"]),
|
||||||
|
sourceKey = json["sourceKey"],
|
||||||
|
comicId = json["comicId"],
|
||||||
|
thumbnails = ListOrNull.from(json["thumbnails"]),
|
||||||
|
recommend = (json["recommend"] as List?)
|
||||||
|
?.map((e) => Comic.fromJson(e, json["sourceKey"]))
|
||||||
|
.toList(),
|
||||||
|
isFavorite = json["isFavorite"],
|
||||||
|
subId = json["subId"],
|
||||||
|
likesCount = json["likesCount"],
|
||||||
|
isLiked = json["isLiked"],
|
||||||
|
commentsCount = json["commentsCount"],
|
||||||
|
uploader = json["uploader"],
|
||||||
|
uploadTime = json["uploadTime"],
|
||||||
|
updateTime = json["updateTime"];
|
||||||
|
|
||||||
|
@override
|
||||||
|
HistoryType get historyType => HistoryType(sourceKey.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => comicId;
|
||||||
|
|
||||||
|
ComicType get comicType => ComicType(sourceKey.hashCode);
|
||||||
|
}
|
@@ -103,8 +103,6 @@ class ComicSourceParser {
|
|||||||
(throw ComicSourceParseException('version is required'));
|
(throw ComicSourceParseException('version is required'));
|
||||||
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
||||||
var url = JsEngine().runCode("this['temp'].url");
|
var url = JsEngine().runCode("this['temp'].url");
|
||||||
var matchBriefIdRegex =
|
|
||||||
JsEngine().runCode("this['temp'].comic.matchBriefIdRegex");
|
|
||||||
if (minAppVersion != null) {
|
if (minAppVersion != null) {
|
||||||
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
||||||
throw ComicSourceParseException(
|
throw ComicSourceParseException(
|
||||||
@@ -123,43 +121,29 @@ class ComicSourceParser {
|
|||||||
ComicSource.sources.$_key = this['temp'];
|
ComicSource.sources.$_key = this['temp'];
|
||||||
""");
|
""");
|
||||||
|
|
||||||
final account = _loadAccountConfig();
|
|
||||||
final explorePageData = _loadExploreData();
|
|
||||||
final categoryPageData = _loadCategoryData();
|
|
||||||
final categoryComicsData = _loadCategoryComicsData();
|
|
||||||
final searchData = _loadSearchData();
|
|
||||||
final loadComicFunc = _parseLoadComicFunc();
|
|
||||||
final loadComicThumbnailFunc = _parseThumbnailLoader();
|
|
||||||
final loadComicPagesFunc = _parseLoadComicPagesFunc();
|
|
||||||
final getImageLoadingConfigFunc = _parseImageLoadingConfigFunc();
|
|
||||||
final getThumbnailLoadingConfigFunc = _parseThumbnailLoadingConfigFunc();
|
|
||||||
final favoriteData = _loadFavoriteData();
|
|
||||||
final commentsLoader = _parseCommentsLoader();
|
|
||||||
final sendCommentFunc = _parseSendCommentFunc();
|
|
||||||
final likeFunc = _parseLikeFunc();
|
|
||||||
|
|
||||||
var source = ComicSource(
|
var source = ComicSource(
|
||||||
_name!,
|
_name!,
|
||||||
key,
|
key,
|
||||||
account,
|
_loadAccountConfig(),
|
||||||
categoryPageData,
|
_loadCategoryData(),
|
||||||
categoryComicsData,
|
_loadCategoryComicsData(),
|
||||||
favoriteData,
|
_loadFavoriteData(),
|
||||||
explorePageData,
|
_loadExploreData(),
|
||||||
searchData,
|
_loadSearchData(),
|
||||||
[],
|
[],
|
||||||
loadComicFunc,
|
_parseLoadComicFunc(),
|
||||||
loadComicThumbnailFunc,
|
_parseThumbnailLoader(),
|
||||||
loadComicPagesFunc,
|
_parseLoadComicPagesFunc(),
|
||||||
getImageLoadingConfigFunc,
|
_parseImageLoadingConfigFunc(),
|
||||||
getThumbnailLoadingConfigFunc,
|
_parseThumbnailLoadingConfigFunc(),
|
||||||
matchBriefIdRegex,
|
|
||||||
filePath,
|
filePath,
|
||||||
url ?? "",
|
url ?? "",
|
||||||
version ?? "1.0.0",
|
version ?? "1.0.0",
|
||||||
commentsLoader,
|
_parseCommentsLoader(),
|
||||||
sendCommentFunc,
|
_parseSendCommentFunc(),
|
||||||
likeFunc,
|
_parseLikeFunc(),
|
||||||
|
_parseVoteCommentFunc(),
|
||||||
|
_parseLikeCommentFunc(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await source.loadData();
|
await source.loadData();
|
||||||
@@ -571,10 +555,7 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
(res["comments"] as List)
|
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
.map((e) => Comment(e["userName"], e["avatar"], e["content"],
|
|
||||||
e["time"], e["replyCount"], e["id"].toString()))
|
|
||||||
.toList(),
|
|
||||||
subData: res["maxPage"]);
|
subData: res["maxPage"]);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
@@ -673,4 +654,38 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VoteCommentFunc? _parseVoteCommentFunc() {
|
||||||
|
if (!_checkExists("comic.voteComment")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (id, subId, commentId, isUp, isCancel) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.voteComment(${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(commentId)}, ${jsonEncode(isUp)}, ${jsonEncode(isCancel)})
|
||||||
|
""");
|
||||||
|
return Res(res is num ? res.toInt() : 0);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
LikeCommentFunc? _parseLikeCommentFunc() {
|
||||||
|
if (!_checkExists("comic.likeComment")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (id, subId, commentId, isLiking) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.likeComment(${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(commentId)}, ${jsonEncode(isLiking)})
|
||||||
|
""");
|
||||||
|
return Res(res is num ? res.toInt() : 0);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,8 @@ import 'package:venera/pages/favorites/favorite_actions.dart';
|
|||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'comments_page.dart';
|
||||||
|
|
||||||
class ComicPage extends StatefulWidget {
|
class ComicPage extends StatefulWidget {
|
||||||
const ComicPage({super.key, required this.id, required this.sourceKey});
|
const ComicPage({super.key, required this.id, required this.sourceKey});
|
||||||
|
|
||||||
@@ -466,7 +468,15 @@ abstract mixin class _ComicPageActions {
|
|||||||
|
|
||||||
void showMoreActions() {}
|
void showMoreActions() {}
|
||||||
|
|
||||||
void showComments() {}
|
void showComments() {
|
||||||
|
showSideBar(
|
||||||
|
App.rootContext,
|
||||||
|
CommentsPage(
|
||||||
|
data: comic,
|
||||||
|
source: comicSource,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionButton extends StatelessWidget {
|
class _ActionButton extends StatelessWidget {
|
||||||
@@ -1107,3 +1117,4 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
470
lib/pages/comments_page.dart
Normal file
470
lib/pages/comments_page.dart
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
class CommentsPage extends StatefulWidget {
|
||||||
|
const CommentsPage(
|
||||||
|
{super.key, required this.data, required this.source, this.replyId});
|
||||||
|
|
||||||
|
final ComicDetails data;
|
||||||
|
|
||||||
|
final ComicSource source;
|
||||||
|
|
||||||
|
final String? replyId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CommentsPage> createState() => _CommentsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentsPageState extends State<CommentsPage> {
|
||||||
|
bool _loading = true;
|
||||||
|
List<Comment>? _comments;
|
||||||
|
String? _error;
|
||||||
|
int _page = 1;
|
||||||
|
int? maxPage;
|
||||||
|
var controller = TextEditingController();
|
||||||
|
bool sending = false;
|
||||||
|
|
||||||
|
void firstLoad() async {
|
||||||
|
var res = await widget.source.commentsLoader!(
|
||||||
|
widget.data.comicId, widget.data.subId, 1, widget.replyId);
|
||||||
|
if (res.error) {
|
||||||
|
setState(() {
|
||||||
|
_error = res.errorMessage;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_comments = res.data;
|
||||||
|
_loading = false;
|
||||||
|
maxPage = res.subData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadMore() async {
|
||||||
|
var res = await widget.source.commentsLoader!(
|
||||||
|
widget.data.comicId, widget.data.subId, _page + 1, widget.replyId);
|
||||||
|
if (res.error) {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Unknown Error");
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_comments!.addAll(res.data);
|
||||||
|
_page++;
|
||||||
|
if (maxPage == null && res.data.isEmpty) {
|
||||||
|
maxPage = _page;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: Appbar(
|
||||||
|
title: Text("Comments".tl),
|
||||||
|
),
|
||||||
|
body: buildBody(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody(BuildContext context) {
|
||||||
|
if (_loading) {
|
||||||
|
firstLoad();
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
} else if (_error != null) {
|
||||||
|
return NetworkError(
|
||||||
|
message: _error!,
|
||||||
|
retry: () {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
withAppbar: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
primary: false,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _comments!.length + 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == _comments!.length) {
|
||||||
|
if (_page < (maxPage ?? _page + 1)) {
|
||||||
|
loadMore();
|
||||||
|
return const ListLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _CommentTile(
|
||||||
|
comment: _comments![index],
|
||||||
|
source: widget.source,
|
||||||
|
comic: widget.data,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
buildBottom(context)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottom(BuildContext context) {
|
||||||
|
if (widget.source.sendCommentFunc == null) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
isCollapsed: true,
|
||||||
|
hintText: "Comment".tl),
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (sending)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(8.5),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (controller.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
sending = true;
|
||||||
|
});
|
||||||
|
var b = await widget.source.sendCommentFunc!(
|
||||||
|
widget.data.comicId,
|
||||||
|
widget.data.subId,
|
||||||
|
controller.text,
|
||||||
|
widget.replyId);
|
||||||
|
if (!b.error) {
|
||||||
|
controller.text = "";
|
||||||
|
setState(() {
|
||||||
|
sending = false;
|
||||||
|
_loading = true;
|
||||||
|
_comments?.clear();
|
||||||
|
_page = 1;
|
||||||
|
maxPage = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: b.errorMessage ?? "Error");
|
||||||
|
setState(() {
|
||||||
|
sending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingVertical(2).paddingLeft(16).paddingRight(4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentTile extends StatefulWidget {
|
||||||
|
const _CommentTile({
|
||||||
|
required this.comment,
|
||||||
|
required this.source,
|
||||||
|
required this.comic,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Comment comment;
|
||||||
|
|
||||||
|
final ComicSource source;
|
||||||
|
|
||||||
|
final ComicDetails comic;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CommentTile> createState() => _CommentTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentTileState extends State<_CommentTile> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
likes = widget.comment.score ?? 0;
|
||||||
|
isLiked = widget.comment.isLiked ?? false;
|
||||||
|
voteStatus = widget.comment.voteStatus;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (widget.comment.avatar != null)
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(
|
||||||
|
widget.comment.avatar!,
|
||||||
|
sourceKey: widget.source.key,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingRight(12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.comment.userName),
|
||||||
|
if (widget.comment.time != null)
|
||||||
|
Text(widget.comment.time!, style: ts.s12),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(widget.comment.content),
|
||||||
|
buildActions(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingAll(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildActions() {
|
||||||
|
if (widget.comment.score == null && widget.comment.replyCount == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (widget.comment.score != null &&
|
||||||
|
widget.source.voteCommentFunc != null)
|
||||||
|
buildVote(),
|
||||||
|
if (widget.comment.score != null &&
|
||||||
|
widget.source.likeCommentFunc != null)
|
||||||
|
buildLike(),
|
||||||
|
if (widget.comment.replyCount != null) buildReply(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildReply() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () {
|
||||||
|
showSideBar(
|
||||||
|
context,
|
||||||
|
CommentsPage(
|
||||||
|
data: widget.comic,
|
||||||
|
source: widget.source,
|
||||||
|
replyId: widget.comment.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.insert_comment_outlined, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(widget.comment.replyCount.toString()),
|
||||||
|
],
|
||||||
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLiking = false;
|
||||||
|
|
||||||
|
bool isLiked = false;
|
||||||
|
|
||||||
|
var likes = 0;
|
||||||
|
|
||||||
|
Widget buildLike() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () async {
|
||||||
|
if (isLiking) return;
|
||||||
|
setState(() {
|
||||||
|
isLiking = true;
|
||||||
|
});
|
||||||
|
var res = await widget.source.likeCommentFunc!(
|
||||||
|
widget.comic.comicId,
|
||||||
|
widget.comic.subId,
|
||||||
|
widget.comment.id!,
|
||||||
|
!isLiked,
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
isLiked = !isLiked;
|
||||||
|
likes += isLiked ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Error");
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLiking = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isLiking)
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
else if (isLiked)
|
||||||
|
const Icon(Icons.favorite, size: 16)
|
||||||
|
else
|
||||||
|
const Icon(Icons.favorite_border, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(likes.toString()),
|
||||||
|
],
|
||||||
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? voteStatus;
|
||||||
|
|
||||||
|
bool isVoteUp = false;
|
||||||
|
|
||||||
|
bool isVoteDown = false;
|
||||||
|
|
||||||
|
void vote(bool isUp) async {
|
||||||
|
if (isVoteUp || isVoteDown) return;
|
||||||
|
setState(() {
|
||||||
|
if (isUp) {
|
||||||
|
isVoteUp = true;
|
||||||
|
} else {
|
||||||
|
isVoteDown = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var res = await widget.source.voteCommentFunc!(
|
||||||
|
widget.comic.comicId,
|
||||||
|
widget.comic.subId,
|
||||||
|
widget.comment.id!,
|
||||||
|
isUp,
|
||||||
|
(isUp && voteStatus == 1) || (!isUp && voteStatus == -1),
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
if (isUp) {
|
||||||
|
voteStatus = 1;
|
||||||
|
} else {
|
||||||
|
voteStatus = -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Error");
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isVoteUp = false;
|
||||||
|
isVoteDown = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildVote() {
|
||||||
|
var upColor = context.colorScheme.outline;
|
||||||
|
if (voteStatus == 1) {
|
||||||
|
upColor = context.useTextColor(Colors.red);
|
||||||
|
}
|
||||||
|
var downColor = context.colorScheme.outline;
|
||||||
|
if (voteStatus == -1) {
|
||||||
|
downColor = context.useTextColor(Colors.blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Button.icon(
|
||||||
|
isLoading: isVoteUp,
|
||||||
|
icon: const Icon(Icons.arrow_upward),
|
||||||
|
size: 18,
|
||||||
|
color: upColor,
|
||||||
|
onPressed: () => vote(true),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(widget.comment.score.toString()),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Button.icon(
|
||||||
|
isLoading: isVoteDown,
|
||||||
|
icon: const Icon(Icons.arrow_downward),
|
||||||
|
size: 18,
|
||||||
|
color: downColor,
|
||||||
|
onPressed: () => vote(false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user