mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-28 13:27:25 +00:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5ae73bd7c8 | ||
![]() |
013e509ebf | ||
![]() |
974e2f0cc6 | ||
![]() |
1d649ebde2 | ||
![]() |
dd1ed690e1 | ||
![]() |
f33df47cd6 | ||
![]() |
c51df1efde | ||
![]() |
93ce4eb94b | ||
![]() |
a3868b1969 | ||
![]() |
2a1a668c25 | ||
![]() |
b0d740a174 | ||
![]() |
811b7b4ed8 | ||
![]() |
1fecb8d55d | ||
![]() |
67ebe4e50b | ||
![]() |
a9bddd7def | ||
![]() |
4b8acfc3ff | ||
![]() |
38f57584b6 | ||
![]() |
8ff269c8a8 | ||
![]() |
dde518ab6b | ||
![]() |
bfad0dc176 | ||
![]() |
ed9213b12e |
@@ -1,5 +1,9 @@
|
|||||||
# pixes
|
# pixes
|
||||||
|
|
||||||
Unofficial pixiv app
|
非官方 Pixiv app, 支持 Windows, Android, iOS, macOS
|
||||||
|
|
||||||
This project is under development.
|
主要功能均已实现
|
||||||
|
|
||||||
|
## 屏幕截图
|
||||||
|
|
||||||
|
<img src="screenshots/1.png" style="width: 400px">
|
@@ -121,7 +121,24 @@
|
|||||||
"Proxy": "代理",
|
"Proxy": "代理",
|
||||||
"Appearance": "外观",
|
"Appearance": "外观",
|
||||||
"Language": "语言",
|
"Language": "语言",
|
||||||
"Theme": "主题"
|
"Theme": "主题",
|
||||||
|
"Pause": "暂停",
|
||||||
|
"Resume": "继续",
|
||||||
|
"Paused": "已暂停",
|
||||||
|
"Delete all": "删除全部",
|
||||||
|
"Related": "相关",
|
||||||
|
"Related artworks": "相关作品",
|
||||||
|
"Related users": "相关用户",
|
||||||
|
"Replace with '-p${index}' if the work have more than one images, otherwise replace with blank.": "替换为'-p${index}'如果作品有多张图片, 否则替换为空白",
|
||||||
|
"Recommendation": "推荐",
|
||||||
|
"Novel": "小说",
|
||||||
|
"Novels": "小说",
|
||||||
|
"Reading Settings": "阅读设置",
|
||||||
|
"Font Size": "字体大小",
|
||||||
|
"Line Height": "行高",
|
||||||
|
"Paragraph Spacing": "段间距",
|
||||||
|
"light": "浅色",
|
||||||
|
"dark": "深色"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Search": "搜索",
|
"Search": "搜索",
|
||||||
@@ -245,6 +262,23 @@
|
|||||||
"Proxy": "代理",
|
"Proxy": "代理",
|
||||||
"Appearance": "外觀",
|
"Appearance": "外觀",
|
||||||
"Language": "語言",
|
"Language": "語言",
|
||||||
"Theme": "主題"
|
"Theme": "主題",
|
||||||
|
"Pause": "暫停",
|
||||||
|
"Resume": "繼續",
|
||||||
|
"Paused": "已暫停",
|
||||||
|
"Delete all": "刪除全部",
|
||||||
|
"Related": "相關",
|
||||||
|
"Related artworks": "相關作品",
|
||||||
|
"Related users": "相關用戶",
|
||||||
|
"Replace with '-p${index}' if the work have more than one images, otherwise replace with blank.": "替換為'-p${index}'如果作品有多張圖片, 否則替換為空白",
|
||||||
|
"Recommendation": "推薦",
|
||||||
|
"Novel": "小說",
|
||||||
|
"Novels": "小說",
|
||||||
|
"Reading Settings": "閱讀設置",
|
||||||
|
"Font Size": "字體大小",
|
||||||
|
"Line Height": "行高",
|
||||||
|
"Paragraph Spacing": "段間距",
|
||||||
|
"light": "淺色",
|
||||||
|
"dark": "深色"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -17,7 +17,7 @@ import Flutter
|
|||||||
let proxyConfig = "\(host):\(port)"
|
let proxyConfig = "\(host):\(port)"
|
||||||
result(proxyConfig)
|
result(proxyConfig)
|
||||||
} else {
|
} else {
|
||||||
result("")
|
result("no proxy")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,6 +20,9 @@ class _Appdata {
|
|||||||
"proxy": "",
|
"proxy": "",
|
||||||
"darkMode": "System",
|
"darkMode": "System",
|
||||||
"language": "System",
|
"language": "System",
|
||||||
|
"readingFontSize": 16.0,
|
||||||
|
"readingLineHeight": 1.5,
|
||||||
|
"readingParagraphSpacing": 8.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool lock = false;
|
bool lock = false;
|
||||||
|
@@ -1,48 +1,43 @@
|
|||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
|
||||||
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
||||||
const SliverGridViewWithFixedItemHeight(
|
const SliverGridViewWithFixedItemHeight(
|
||||||
{required this.delegate,
|
{required this.delegate,
|
||||||
required this.maxCrossAxisExtent,
|
this.maxCrossAxisExtent = double.infinity,
|
||||||
required this.itemHeight,
|
this.minCrossAxisExtent = 0,
|
||||||
super.key});
|
required this.itemHeight,
|
||||||
|
super.key});
|
||||||
|
|
||||||
final SliverChildDelegate delegate;
|
final SliverChildDelegate delegate;
|
||||||
|
|
||||||
final double maxCrossAxisExtent;
|
final double maxCrossAxisExtent;
|
||||||
|
|
||||||
|
final double minCrossAxisExtent;
|
||||||
|
|
||||||
final double itemHeight;
|
final double itemHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverLayoutBuilder(
|
return SliverGrid(
|
||||||
builder: ((context, constraints) => SliverGrid(
|
delegate: delegate,
|
||||||
delegate: delegate,
|
gridDelegate: SliverGridDelegateWithFixedHeight(
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
itemHeight: itemHeight,
|
||||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
childAspectRatio:
|
minCrossAxisExtent: minCrossAxisExtent),
|
||||||
calcChildAspectRatio(constraints.crossAxisExtent)),
|
).sliverPadding(EdgeInsets.only(bottom: context.padding.bottom));
|
||||||
).sliverPadding(EdgeInsets.only(bottom: context.padding.bottom))));
|
|
||||||
}
|
|
||||||
|
|
||||||
double calcChildAspectRatio(double width) {
|
|
||||||
var crossItems = width ~/ maxCrossAxisExtent;
|
|
||||||
if (width % maxCrossAxisExtent != 0) {
|
|
||||||
crossItems += 1;
|
|
||||||
}
|
|
||||||
final itemWidth = width / crossItems;
|
|
||||||
return itemWidth / itemHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridViewWithFixedItemHeight extends StatelessWidget {
|
class GridViewWithFixedItemHeight extends StatelessWidget {
|
||||||
const GridViewWithFixedItemHeight(
|
const GridViewWithFixedItemHeight(
|
||||||
{ required this.builder,
|
{required this.builder,
|
||||||
required this.itemCount,
|
required this.itemCount,
|
||||||
required this.maxCrossAxisExtent,
|
this.maxCrossAxisExtent = double.infinity,
|
||||||
required this.itemHeight,
|
this.minCrossAxisExtent = 0,
|
||||||
super.key});
|
required this.itemHeight,
|
||||||
|
super.key});
|
||||||
|
|
||||||
final Widget Function(BuildContext, int) builder;
|
final Widget Function(BuildContext, int) builder;
|
||||||
|
|
||||||
@@ -50,28 +45,69 @@ class GridViewWithFixedItemHeight extends StatelessWidget {
|
|||||||
|
|
||||||
final double maxCrossAxisExtent;
|
final double maxCrossAxisExtent;
|
||||||
|
|
||||||
|
final double minCrossAxisExtent;
|
||||||
|
|
||||||
final double itemHeight;
|
final double itemHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: ((context, constraints) => GridView.builder(
|
builder: ((context, constraints) => GridView.builder(
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithFixedHeight(
|
||||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
itemHeight: itemHeight,
|
||||||
childAspectRatio:
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
calcChildAspectRatio(constraints.maxWidth)),
|
minCrossAxisExtent: minCrossAxisExtent),
|
||||||
itemBuilder: builder,
|
itemBuilder: builder,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
)));
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
|
||||||
|
const SliverGridDelegateWithFixedHeight({
|
||||||
|
this.maxCrossAxisExtent = double.infinity,
|
||||||
|
this.minCrossAxisExtent = 0,
|
||||||
|
required this.itemHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double maxCrossAxisExtent;
|
||||||
|
|
||||||
|
final double minCrossAxisExtent;
|
||||||
|
|
||||||
|
final double itemHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SliverGridLayout getLayout(SliverConstraints constraints) {
|
||||||
|
var crossItemsCount = calcCrossItemsCount(constraints.crossAxisExtent);
|
||||||
|
return SliverGridRegularTileLayout(
|
||||||
|
crossAxisCount: crossItemsCount,
|
||||||
|
mainAxisStride: itemHeight,
|
||||||
|
childMainAxisExtent: itemHeight,
|
||||||
|
crossAxisStride: constraints.crossAxisExtent / crossItemsCount,
|
||||||
|
childCrossAxisExtent: constraints.crossAxisExtent / crossItemsCount,
|
||||||
|
reverseCrossAxis: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
double calcChildAspectRatio(double width) {
|
int calcCrossItemsCount(double width) {
|
||||||
var crossItems = width ~/ maxCrossAxisExtent;
|
int count = 20;
|
||||||
if (width % maxCrossAxisExtent != 0) {
|
var itemWidth = width / 20;
|
||||||
crossItems += 1;
|
while (
|
||||||
|
!(itemWidth > minCrossAxisExtent && itemWidth < maxCrossAxisExtent)) {
|
||||||
|
count--;
|
||||||
|
itemWidth = width / count;
|
||||||
|
if (count == 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final itemWidth = width / crossItems;
|
return count;
|
||||||
return itemWidth / itemHeight;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
||||||
|
return oldDelegate is! SliverGridDelegateWithFixedHeight ||
|
||||||
|
oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
|
||||||
|
oldDelegate.minCrossAxisExtent != minCrossAxisExtent ||
|
||||||
|
oldDelegate.itemHeight != itemHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -10,10 +10,12 @@ import '../pages/illust_page.dart';
|
|||||||
import 'md.dart';
|
import 'md.dart';
|
||||||
|
|
||||||
class IllustWidget extends StatefulWidget {
|
class IllustWidget extends StatefulWidget {
|
||||||
const IllustWidget(this.illust, {super.key});
|
const IllustWidget(this.illust, {this.onTap, super.key});
|
||||||
|
|
||||||
final Illust illust;
|
final Illust illust;
|
||||||
|
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<IllustWidget> createState() => _IllustWidgetState();
|
State<IllustWidget> createState() => _IllustWidgetState();
|
||||||
}
|
}
|
||||||
@@ -45,7 +47,7 @@ class _IllustWidgetState extends State<IllustWidget> {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: (){
|
onTap: widget.onTap ?? (){
|
||||||
context.to(() => IllustPage(widget.illust, favoriteCallback: (v) {
|
context.to(() => IllustPage(widget.illust, favoriteCallback: (v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.illust.isBookmarked = v;
|
widget.illust.isBookmarked = v;
|
||||||
|
@@ -13,6 +13,34 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
|
|||||||
|
|
||||||
Widget buildContent(BuildContext context, S data);
|
Widget buildContent(BuildContext context, S data);
|
||||||
|
|
||||||
|
Widget? buildFrame(BuildContext context, Widget child) => null;
|
||||||
|
|
||||||
|
Widget buildLoading() {
|
||||||
|
return const Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void retry() {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
loadData().then((value) {
|
||||||
|
if(value.success) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
data = value.data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
error = value.errorMessage!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildError() {
|
Widget buildError() {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -21,25 +49,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
|
|||||||
Text(error!),
|
Text(error!),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Button(
|
Button(
|
||||||
onPressed: () {
|
onPressed: retry,
|
||||||
setState(() {
|
|
||||||
isLoading = true;
|
|
||||||
error = null;
|
|
||||||
});
|
|
||||||
loadData().then((value) {
|
|
||||||
if(value.success) {
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
data = value.data;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
error = value.errorMessage!;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Text("Retry"),
|
child: const Text("Retry"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -69,15 +79,17 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget child;
|
||||||
|
|
||||||
if(isLoading){
|
if(isLoading){
|
||||||
return const Center(
|
child = buildLoading();
|
||||||
child: ProgressRing(),
|
|
||||||
);
|
|
||||||
} else if (error != null){
|
} else if (error != null){
|
||||||
return buildError();
|
child = buildError();
|
||||||
} else {
|
} else {
|
||||||
return buildContent(context, data!);
|
child = buildContent(context, data!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buildFrame(context, child) ?? child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +106,14 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
|
|
||||||
Future<Res<List<S>>> loadData(int page);
|
Future<Res<List<S>>> loadData(int page);
|
||||||
|
|
||||||
|
Widget? buildFrame(BuildContext context, Widget child) => null;
|
||||||
|
|
||||||
Widget buildContent(BuildContext context, final List<S> data);
|
Widget buildContent(BuildContext context, final List<S> data);
|
||||||
|
|
||||||
bool get isLoading => _isLoading || _isFirstLoading;
|
bool get isLoading => _isLoading || _isFirstLoading;
|
||||||
|
|
||||||
|
bool get isFirstLoading => _isFirstLoading;
|
||||||
|
|
||||||
void nextPage() {
|
void nextPage() {
|
||||||
if(_isLoading) return;
|
if(_isLoading) return;
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
@@ -181,12 +197,16 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget child;
|
||||||
|
|
||||||
if(_isFirstLoading){
|
if(_isFirstLoading){
|
||||||
return buildLoading(context);
|
child = buildLoading(context);
|
||||||
} else if (_error != null){
|
} else if (_error != null){
|
||||||
return buildError(context, _error!);
|
child = buildError(context, _error!);
|
||||||
} else {
|
} else {
|
||||||
return buildContent(context, _data!);
|
child = buildContent(context, _data!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buildFrame(context, child) ?? child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import 'package:flutter/material.dart' as md;
|
import 'package:flutter/material.dart' as md;
|
||||||
|
|
||||||
typedef MdIcons = md.Icons;
|
typedef MdIcons = md.Icons;
|
||||||
|
typedef MdTheme = md.Theme;
|
||||||
|
typedef MdThemeData = md.ThemeData;
|
||||||
|
typedef MdColorScheme = md.ColorScheme;
|
||||||
|
|
||||||
class ColorScheme {
|
class ColorScheme {
|
||||||
static md.ColorScheme of(md.BuildContext context) {
|
static md.ColorScheme of(md.BuildContext context) {
|
||||||
|
84
lib/components/novel.dart
Normal file
84
lib/components/novel.dart
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/components/md.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/novel_page.dart';
|
||||||
|
|
||||||
|
class NovelWidget extends StatefulWidget {
|
||||||
|
const NovelWidget(this.novel, {super.key});
|
||||||
|
|
||||||
|
final Novel novel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelWidget> createState() => _NovelWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelWidgetState extends State<NovelWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => NovelPage(widget.novel));
|
||||||
|
},
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 96,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: CachedImageProvider(widget.novel.image),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.novel.title,
|
||||||
|
maxLines: 2,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.novel.caption.trim().replaceAll('<br />', '\n'),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.novel.author.name,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:pixes/components/animated_image.dart';
|
import 'package:pixes/components/animated_image.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
@@ -20,15 +22,15 @@ class UserPreviewWidget extends StatefulWidget {
|
|||||||
class _UserPreviewWidgetState extends State<UserPreviewWidget> {
|
class _UserPreviewWidgetState extends State<UserPreviewWidget> {
|
||||||
bool isFollowing = false;
|
bool isFollowing = false;
|
||||||
|
|
||||||
void follow() async{
|
void follow() async {
|
||||||
if(isFollowing) return;
|
if (isFollowing) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
isFollowing = true;
|
isFollowing = true;
|
||||||
});
|
});
|
||||||
var method = widget.user.isFollowed ? "delete" : "add";
|
var method = widget.user.isFollowed ? "delete" : "add";
|
||||||
var res = await Network().follow(widget.user.id.toString(), method);
|
var res = await Network().follow(widget.user.id.toString(), method);
|
||||||
if(res.error) {
|
if (res.error) {
|
||||||
if(mounted) {
|
if (mounted) {
|
||||||
context.showToast(message: "Network Error");
|
context.showToast(message: "Network Error");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -43,65 +45,120 @@ class _UserPreviewWidgetState extends State<UserPreviewWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
child: Row(
|
child: GestureDetector(
|
||||||
children: [
|
onTap: () => context.to(() => UserInfoPage(widget.user.id.toString())),
|
||||||
SizedBox(
|
behavior: HitTestBehavior.translucent,
|
||||||
width: 64,
|
child: SizedBox.expand(
|
||||||
height: 64,
|
child: Row(
|
||||||
child: ClipRRect(
|
children: [
|
||||||
borderRadius: BorderRadius.circular(64),
|
SizedBox(
|
||||||
child: ColoredBox(
|
width: 64,
|
||||||
color: ColorScheme.of(context).secondaryContainer,
|
height: 64,
|
||||||
child: AnimatedImage(
|
child: ClipRRect(
|
||||||
image: CachedImageProvider(widget.user.avatar),
|
borderRadius: BorderRadius.circular(64),
|
||||||
fit: BoxFit.cover,
|
child: ColoredBox(
|
||||||
filterQuality: FilterQuality.medium,
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(widget.user.avatar),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 96,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Text(widget.user.name,
|
||||||
|
maxLines: 1,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (isFollowing)
|
||||||
|
Button(
|
||||||
|
onPressed: follow,
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 24,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 18,
|
||||||
|
child: ProgressRing(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
else if (!widget.user.isFollowed)
|
||||||
|
Button(onPressed: follow, child: Text("Follow".tl))
|
||||||
|
else
|
||||||
|
Button(
|
||||||
|
onPressed: follow,
|
||||||
|
child: Text(
|
||||||
|
"Unfollow".tl,
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorScheme.of(context).error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
var count = constraints.maxWidth.toInt() ~/ 96;
|
||||||
|
var images = List.generate(
|
||||||
|
min(count, widget.user.artworks.length),
|
||||||
|
(index) => buildIllust(widget.user.artworks[index]));
|
||||||
|
return Row(
|
||||||
|
children: images,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.chevron_right,
|
||||||
|
size: 14,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildIllust(Illust illust) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 96,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
child: AnimatedImage(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
image: CachedImageProvider(illust.images.first.medium),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12,),
|
),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
const Spacer(),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Button(
|
|
||||||
onPressed: () => context.to(() => UserInfoPage(widget.user.id.toString(), followCallback: (v){
|
|
||||||
setState(() {
|
|
||||||
widget.user.isFollowed = v;
|
|
||||||
});
|
|
||||||
},)),
|
|
||||||
child: Text("View".tl,),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8,),
|
|
||||||
if(isFollowing)
|
|
||||||
Button(onPressed: follow, child: const SizedBox(
|
|
||||||
width: 42,
|
|
||||||
height: 24,
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 18,
|
|
||||||
child: ProgressRing(strokeWidth: 2,),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
else if (!widget.user.isFollowed)
|
|
||||||
Button(onPressed: follow, child: Text("Follow".tl))
|
|
||||||
else
|
|
||||||
Button(
|
|
||||||
onPressed: follow,
|
|
||||||
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
).paddingVertical(8),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ export "state_controller.dart";
|
|||||||
export "navigation.dart";
|
export "navigation.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.0.2";
|
final version = "1.0.4";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
bool get isIOS => Platform.isIOS;
|
bool get isIOS => Platform.isIOS;
|
||||||
@@ -26,8 +26,8 @@ class _App {
|
|||||||
if(appdata.settings["language"] != "System"){
|
if(appdata.settings["language"] != "System"){
|
||||||
return switch(appdata.settings["language"]){
|
return switch(appdata.settings["language"]){
|
||||||
"English" => const Locale("en"),
|
"English" => const Locale("en"),
|
||||||
"简体中文" => const Locale("zh"),
|
"简体中文" => const Locale("zh", "CN"),
|
||||||
"繁體中文" => const Locale("zh", "Hant"),
|
"繁體中文" => const Locale("zh", "TW"),
|
||||||
_ => const Locale("en"),
|
_ => const Locale("en"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,8 @@ class _App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
GlobalKey<NavigatorState>? mainNavigatorKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
|
@@ -45,10 +45,10 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ui.Codec> _loadBufferAsync(
|
Future<ui.Codec> _loadBufferAsync(
|
||||||
T key,
|
T key,
|
||||||
StreamController<ImageChunkEvent> chunkEvents,
|
StreamController<ImageChunkEvent> chunkEvents,
|
||||||
ImageDecoderCallback decode,
|
ImageDecoderCallback decode,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
int retryTime = 1;
|
int retryTime = 1;
|
||||||
|
|
||||||
@@ -83,11 +83,11 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stop) {
|
if (stop) {
|
||||||
throw Exception("Image loading is stopped");
|
throw Exception("Image loading is stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data!.isEmpty) {
|
if (data!.isEmpty) {
|
||||||
throw Exception("Empty image data");
|
throw Exception("Empty image data");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,13 +147,13 @@ class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
|||||||
String get key => url;
|
String get key => url;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async{
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
chunkEvents.add(const ImageChunkEvent(
|
chunkEvents.add(const ImageChunkEvent(
|
||||||
cumulativeBytesLoaded: 0,
|
cumulativeBytesLoaded: 0,
|
||||||
expectedTotalBytes: 1,
|
expectedTotalBytes: 1,
|
||||||
));
|
));
|
||||||
var cached = await CacheManager().findCache(key);
|
var cached = await CacheManager().findCache(key);
|
||||||
if(cached != null) {
|
if (cached != null) {
|
||||||
chunkEvents.add(const ImageChunkEvent(
|
chunkEvents.add(const ImageChunkEvent(
|
||||||
cumulativeBytesLoaded: 1,
|
cumulativeBytesLoaded: 1,
|
||||||
expectedTotalBytes: 1,
|
expectedTotalBytes: 1,
|
||||||
@@ -161,30 +161,28 @@ class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
|||||||
return await File(cached).readAsBytes();
|
return await File(cached).readAsBytes();
|
||||||
}
|
}
|
||||||
var dio = AppDio();
|
var dio = AppDio();
|
||||||
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
final time =
|
||||||
|
DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
||||||
var res = await dio.get<ResponseBody>(
|
var res = await dio.get<ResponseBody>(url,
|
||||||
url,
|
options: Options(
|
||||||
options: Options(
|
responseType: ResponseType.stream,
|
||||||
responseType: ResponseType.stream,
|
validateStatus: (status) => status != null && status < 500,
|
||||||
validateStatus: (status) => status != null && status < 500,
|
headers: {
|
||||||
headers: {
|
"referer": "https://app-api.pixiv.net/",
|
||||||
"referer": "https://app-api.pixiv.net/",
|
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
|
||||||
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
|
"x-client-time": time,
|
||||||
"x-client-time": time,
|
"x-client-hash": hash,
|
||||||
"x-client-hash": hash,
|
"accept-enconding": "gzip",
|
||||||
"accept-enconding": "gzip",
|
}));
|
||||||
}
|
if (res.statusCode != 200) {
|
||||||
)
|
|
||||||
);
|
|
||||||
if(res.statusCode != 200) {
|
|
||||||
throw BadRequestException("Failed to load image: ${res.statusCode}");
|
throw BadRequestException("Failed to load image: ${res.statusCode}");
|
||||||
}
|
}
|
||||||
var data = <int>[];
|
var data = <int>[];
|
||||||
var cachingFile = await CacheManager().openWrite(key);
|
var cachingFile = await CacheManager().openWrite(key);
|
||||||
await for (var chunk in res.data!.stream) {
|
await for (var chunk in res.data!.stream) {
|
||||||
var length = res.data!.contentLength+1;
|
var length = res.data!.contentLength + 1;
|
||||||
if(length < data.length) {
|
if (length < data.length) {
|
||||||
length = data.length + 1;
|
length = data.length + 1;
|
||||||
}
|
}
|
||||||
data.addAll(chunk);
|
data.addAll(chunk);
|
||||||
@@ -203,3 +201,71 @@ class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
|||||||
return SynchronousFuture<CachedImageProvider>(this);
|
return SynchronousFuture<CachedImageProvider>(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CachedNovelImageProvider
|
||||||
|
extends BaseImageProvider<CachedNovelImageProvider> {
|
||||||
|
final String novelId;
|
||||||
|
final String imageId;
|
||||||
|
|
||||||
|
CachedNovelImageProvider(this.novelId, this.imageId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => "$novelId/$imageId";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
chunkEvents.add(const ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: 0,
|
||||||
|
expectedTotalBytes: 1,
|
||||||
|
));
|
||||||
|
var cached = await CacheManager().findCache(key);
|
||||||
|
if (cached != null) {
|
||||||
|
chunkEvents.add(const ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: 1,
|
||||||
|
expectedTotalBytes: 1,
|
||||||
|
));
|
||||||
|
return await File(cached).readAsBytes();
|
||||||
|
}
|
||||||
|
var urlRes = await Network().getNovelImage(novelId, imageId);
|
||||||
|
var url = urlRes.data;
|
||||||
|
var dio = AppDio();
|
||||||
|
final time =
|
||||||
|
DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
|
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
||||||
|
var res = await dio.get<ResponseBody>(url,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
validateStatus: (status) => status != null && status < 500,
|
||||||
|
headers: {
|
||||||
|
"referer": "https://app-api.pixiv.net/",
|
||||||
|
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
|
||||||
|
"x-client-time": time,
|
||||||
|
"x-client-hash": hash,
|
||||||
|
"accept-enconding": "gzip",
|
||||||
|
}));
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw BadRequestException("Failed to load image: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
var data = <int>[];
|
||||||
|
var cachingFile = await CacheManager().openWrite(key);
|
||||||
|
await for (var chunk in res.data!.stream) {
|
||||||
|
var length = res.data!.contentLength + 1;
|
||||||
|
if (length < data.length) {
|
||||||
|
length = data.length + 1;
|
||||||
|
}
|
||||||
|
data.addAll(chunk);
|
||||||
|
await cachingFile.writeBytes(chunk);
|
||||||
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: data.length,
|
||||||
|
expectedTotalBytes: length,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
await cachingFile.close();
|
||||||
|
return Uint8List.fromList(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CachedNovelImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<CachedNovelImageProvider>(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -20,4 +20,6 @@ extension Navigation on BuildContext {
|
|||||||
Size get size => MediaQuery.of(this).size;
|
Size get size => MediaQuery.of(this).size;
|
||||||
|
|
||||||
EdgeInsets get padding => MediaQuery.of(this).padding;
|
EdgeInsets get padding => MediaQuery.of(this).padding;
|
||||||
|
|
||||||
|
EdgeInsets get viewInsets => MediaQuery.of(this).viewInsets;
|
||||||
}
|
}
|
||||||
|
117
lib/main.dart
117
lib/main.dart
@@ -1,8 +1,11 @@
|
|||||||
import "dart:ui";
|
import "dart:ui";
|
||||||
|
|
||||||
|
import "package:dynamic_color/dynamic_color.dart";
|
||||||
import "package:fluent_ui/fluent_ui.dart";
|
import "package:fluent_ui/fluent_ui.dart";
|
||||||
|
import "package:flutter/material.dart" as md;
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:pixes/appdata.dart";
|
import "package:pixes/appdata.dart";
|
||||||
|
import "package:pixes/components/md.dart";
|
||||||
import "package:pixes/components/message.dart";
|
import "package:pixes/components/message.dart";
|
||||||
import "package:pixes/foundation/app.dart";
|
import "package:pixes/foundation/app.dart";
|
||||||
import "package:pixes/foundation/log.dart";
|
import "package:pixes/foundation/log.dart";
|
||||||
@@ -11,7 +14,6 @@ import "package:pixes/pages/main_page.dart";
|
|||||||
import "package:pixes/utils/app_links.dart";
|
import "package:pixes/utils/app_links.dart";
|
||||||
import "package:pixes/utils/translation.dart";
|
import "package:pixes/utils/translation.dart";
|
||||||
import "package:window_manager/window_manager.dart";
|
import "package:window_manager/window_manager.dart";
|
||||||
import 'package:system_theme/system_theme.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -19,15 +21,10 @@ void main() async {
|
|||||||
Log.error("Unhandled", "${details.exception}\n${details.stack}");
|
Log.error("Unhandled", "${details.exception}\n${details.stack}");
|
||||||
};
|
};
|
||||||
setSystemProxy();
|
setSystemProxy();
|
||||||
SystemTheme.fallbackColor = Colors.blue;
|
|
||||||
await SystemTheme.accentColor.load();
|
|
||||||
await App.init();
|
await App.init();
|
||||||
await appdata.readData();
|
await appdata.readData();
|
||||||
await Translation.init();
|
await Translation.init();
|
||||||
handleLinks();
|
handleLinks();
|
||||||
SystemTheme.onChange.listen((event) {
|
|
||||||
StateController.findOrNull(tag: "MyApp")?.update();
|
|
||||||
});
|
|
||||||
if (App.isDesktop) {
|
if (App.isDesktop) {
|
||||||
await WindowManager.instance.ensureInitialized();
|
await WindowManager.instance.ensureInitialized();
|
||||||
windowManager.waitUntilReadyToShow().then((_) async {
|
windowManager.waitUntilReadyToShow().then((_) async {
|
||||||
@@ -53,11 +50,12 @@ class MyApp extends StatelessWidget {
|
|||||||
init: SimpleController(),
|
init: SimpleController(),
|
||||||
tag: "MyApp",
|
tag: "MyApp",
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
|
Brightness brightness =
|
||||||
|
PlatformDispatcher.instance.platformBrightness;
|
||||||
|
|
||||||
if(appdata.settings["theme"] == "Dark") {
|
if (appdata.settings["theme"] == "Dark") {
|
||||||
brightness = Brightness.dark;
|
brightness = Brightness.dark;
|
||||||
} else if(appdata.settings["theme"] == "Light") {
|
} else if (appdata.settings["theme"] == "Light") {
|
||||||
brightness = Brightness.light;
|
brightness = Brightness.light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,40 +66,77 @@ class MyApp extends StatelessWidget {
|
|||||||
statusBarIconBrightness: brightness.opposite,
|
statusBarIconBrightness: brightness.opposite,
|
||||||
systemNavigationBarIconBrightness: brightness.opposite,
|
systemNavigationBarIconBrightness: brightness.opposite,
|
||||||
),
|
),
|
||||||
child: FluentApp(
|
child: DynamicColorBuilder(
|
||||||
navigatorKey: App.rootNavigatorKey,
|
builder: (light, dark) {
|
||||||
debugShowCheckedModeBanner: false,
|
final colorScheme =
|
||||||
title: 'pixes',
|
(brightness == Brightness.light ? light : dark) ??
|
||||||
theme: FluentThemeData(
|
md.ColorScheme.fromSeed(
|
||||||
brightness: brightness,
|
seedColor: Colors.blue, brightness: brightness);
|
||||||
fontFamily: App.isWindows ? 'font' : null,
|
return FluentApp(
|
||||||
accentColor: AccentColor.swatch({
|
navigatorKey: App.rootNavigatorKey,
|
||||||
'darkest': SystemTheme.accentColor.darkest,
|
debugShowCheckedModeBanner: false,
|
||||||
'darker': SystemTheme.accentColor.darker,
|
title: 'pixes',
|
||||||
'dark': SystemTheme.accentColor.dark,
|
theme: FluentThemeData(
|
||||||
'normal': SystemTheme.accentColor.accent,
|
brightness: brightness,
|
||||||
'light': SystemTheme.accentColor.light,
|
fontFamily: App.isWindows ? 'font' : null,
|
||||||
'lighter': SystemTheme.accentColor.lighter,
|
accentColor: AccentColor.swatch({
|
||||||
'lightest': SystemTheme.accentColor.lightest,
|
'darkest': darken(colorScheme.primary, 30),
|
||||||
})),
|
'darker': darken(colorScheme.primary, 20),
|
||||||
home: const MainPage(),
|
'dark': darken(colorScheme.primary, 10),
|
||||||
builder: (context, child) {
|
'normal': colorScheme.primary,
|
||||||
ErrorWidget.builder = (details) {
|
'light': lighten(colorScheme.primary, 10),
|
||||||
if (details.exception
|
'lighter': lighten(colorScheme.primary, 20),
|
||||||
.toString()
|
'lightest': lighten(colorScheme.primary, 30)
|
||||||
.contains("RenderFlex overflowed")) {
|
})),
|
||||||
return const SizedBox.shrink();
|
home: const MainPage(),
|
||||||
}
|
builder: (context, child) {
|
||||||
Log.error("UI", "${details.exception}\n${details.stack}");
|
ErrorWidget.builder = (details) {
|
||||||
return Text(details.exception.toString());
|
if (details.exception
|
||||||
};
|
.toString()
|
||||||
if (child == null) {
|
.contains("RenderFlex overflowed")) {
|
||||||
throw "widget is null";
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
Log.error(
|
||||||
|
"UI", "${details.exception}\n${details.stack}");
|
||||||
|
return Text(details.exception.toString());
|
||||||
|
};
|
||||||
|
if (child == null) {
|
||||||
|
throw "widget is null";
|
||||||
|
}
|
||||||
|
|
||||||
return OverlayWidget(child);
|
return MdTheme(
|
||||||
}),
|
data: MdThemeData.from(
|
||||||
|
colorScheme: colorScheme, useMaterial3: true),
|
||||||
|
child: DefaultTextStyle.merge(
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: App.isWindows ? 'font' : null,
|
||||||
|
),
|
||||||
|
child: OverlayWidget(child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// from https://stackoverflow.com/a/60191441
|
||||||
|
Color darken(Color c, [int percent = 10]) {
|
||||||
|
assert(1 <= percent && percent <= 100);
|
||||||
|
var f = 1 - percent / 100;
|
||||||
|
return Color.fromARGB(c.alpha, (c.red * f).round(), (c.green * f).round(),
|
||||||
|
(c.blue * f).round());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// from https://stackoverflow.com/a/60191441
|
||||||
|
Color lighten(Color c, [int percent = 10]) {
|
||||||
|
assert(1 <= percent && percent <= 100);
|
||||||
|
var p = percent / 100;
|
||||||
|
return Color.fromARGB(
|
||||||
|
c.alpha,
|
||||||
|
c.red + ((255 - c.red) * p).round(),
|
||||||
|
c.green + ((255 - c.green) * p).round(),
|
||||||
|
c.blue + ((255 - c.blue) * p).round());
|
||||||
|
}
|
||||||
|
@@ -125,23 +125,22 @@ class AppDio extends DioForNative {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setSystemProxy() {
|
void setSystemProxy() {
|
||||||
HttpOverrides.global = _ProxyHttpOverrides()
|
HttpOverrides.global = _ProxyHttpOverrides()..findProxy(Uri());
|
||||||
..findProxy(Uri());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProxyHttpOverrides extends HttpOverrides {
|
class _ProxyHttpOverrides extends HttpOverrides {
|
||||||
String proxy = "DIRECT";
|
String proxy = "DIRECT";
|
||||||
|
|
||||||
String findProxy(Uri uri) {
|
String findProxy(Uri uri) {
|
||||||
var haveUserProxy = appdata.settings["proxy"] != null
|
var haveUserProxy = appdata.settings["proxy"] != null &&
|
||||||
&& appdata.settings["proxy"].toString().isNotEmpty;
|
appdata.settings["proxy"].toString().removeAllBlank.isNotEmpty;
|
||||||
if(!App.isLinux && !haveUserProxy){
|
if (!App.isLinux && !haveUserProxy) {
|
||||||
var channel = const MethodChannel("pixes/proxy");
|
var channel = const MethodChannel("pixes/proxy");
|
||||||
channel.invokeMethod("getProxy").then((value) {
|
channel.invokeMethod("getProxy").then((value) {
|
||||||
if(value.toString().toLowerCase() == "no proxy"){
|
if (value.toString().toLowerCase() == "no proxy") {
|
||||||
proxy = "DIRECT";
|
proxy = "DIRECT";
|
||||||
} else {
|
} else {
|
||||||
if(proxy.contains("https")){
|
if (proxy.contains("https")) {
|
||||||
var proxies = value.split(";");
|
var proxies = value.split(";");
|
||||||
for (String proxy in proxies) {
|
for (String proxy in proxies) {
|
||||||
proxy = proxy.removeAllBlank;
|
proxy = proxy.removeAllBlank;
|
||||||
@@ -154,10 +153,20 @@ class _ProxyHttpOverrides extends HttpOverrides {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if(haveUserProxy){
|
if (haveUserProxy) {
|
||||||
proxy = "PROXY ${appdata.settings["proxy"]}";
|
proxy = "PROXY ${appdata.settings["proxy"]}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// check validation
|
||||||
|
if (proxy.startsWith("PROXY")) {
|
||||||
|
var uri = proxy.replaceFirst("PROXY", "").removeAllBlank;
|
||||||
|
if (!uri.startsWith("http")) {
|
||||||
|
uri += "http://";
|
||||||
|
}
|
||||||
|
if (!uri.isURL) {
|
||||||
|
return "DIRECT";
|
||||||
|
}
|
||||||
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -131,6 +131,8 @@ class DownloadingTask {
|
|||||||
subPathPatten = subPathPatten.replaceAll(r"${title}", illust.title);
|
subPathPatten = subPathPatten.replaceAll(r"${title}", illust.title);
|
||||||
subPathPatten = subPathPatten.replaceAll(r"${author}", illust.author.name);
|
subPathPatten = subPathPatten.replaceAll(r"${author}", illust.author.name);
|
||||||
subPathPatten = subPathPatten.replaceAll(r"${index}", index.toString());
|
subPathPatten = subPathPatten.replaceAll(r"${index}", index.toString());
|
||||||
|
subPathPatten = subPathPatten.replaceAll(r"${page}",
|
||||||
|
illust.images.length == 1 ? "" : "-p$index");
|
||||||
subPathPatten = subPathPatten.replaceAll(r"${ext}", ext);
|
subPathPatten = subPathPatten.replaceAll(r"${ext}", ext);
|
||||||
subPathPatten = subPathPatten.replaceAll(r"${AI}", illust.isAi ? "AI" : "");
|
subPathPatten = subPathPatten.replaceAll(r"${AI}", illust.isAi ? "AI" : "");
|
||||||
List<String> extractTags(String input) {
|
List<String> extractTags(String input) {
|
||||||
@@ -163,6 +165,10 @@ class DownloadingTask {
|
|||||||
_stop = false;
|
_stop = false;
|
||||||
_download();
|
_download();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
_stop = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
@@ -276,8 +282,20 @@ class DownloadManager {
|
|||||||
|
|
||||||
int get maxConcurrentTasks => appdata.settings["maxParallels"];
|
int get maxConcurrentTasks => appdata.settings["maxParallels"];
|
||||||
|
|
||||||
|
bool _paused = false;
|
||||||
|
|
||||||
|
bool get paused => _paused;
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
_paused = true;
|
||||||
|
for(var task in tasks) {
|
||||||
|
task.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void run() {
|
void run() {
|
||||||
_loop ??= Timer.periodic(const Duration(seconds: 1), (timer) {
|
_loop ??= Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if(_paused) return;
|
||||||
_bytesPerSecond = _currentBytes;
|
_bytesPerSecond = _currentBytes;
|
||||||
_currentBytes = 0;
|
_currentBytes = 0;
|
||||||
uiUpdateCallback?.call();
|
uiUpdateCallback?.call();
|
||||||
@@ -349,4 +367,8 @@ class DownloadManager {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resume() {
|
||||||
|
_paused = false;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -121,15 +121,14 @@ class UserDetails {
|
|||||||
pawooUrl = json['profile']['pawoo_url'];
|
pawooUrl = json['profile']['pawoo_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
class IllustAuthor {
|
class Author {
|
||||||
final int id;
|
final int id;
|
||||||
final String name;
|
final String name;
|
||||||
final String account;
|
final String account;
|
||||||
final String avatar;
|
final String avatar;
|
||||||
bool isFollowed;
|
bool isFollowed;
|
||||||
|
|
||||||
IllustAuthor(
|
Author(this.id, this.name, this.account, this.avatar, this.isFollowed);
|
||||||
this.id, this.name, this.account, this.avatar, this.isFollowed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tag {
|
class Tag {
|
||||||
@@ -171,9 +170,9 @@ class Illust {
|
|||||||
final List<IllustImage> images;
|
final List<IllustImage> images;
|
||||||
final String caption;
|
final String caption;
|
||||||
final int restrict;
|
final int restrict;
|
||||||
final IllustAuthor author;
|
final Author author;
|
||||||
final List<Tag> tags;
|
final List<Tag> tags;
|
||||||
final String createDate;
|
final DateTime createDate;
|
||||||
final int pageCount;
|
final int pageCount;
|
||||||
final int width;
|
final int width;
|
||||||
final int height;
|
final int height;
|
||||||
@@ -211,7 +210,7 @@ class Illust {
|
|||||||
}()),
|
}()),
|
||||||
caption = json['caption'],
|
caption = json['caption'],
|
||||||
restrict = json['restrict'],
|
restrict = json['restrict'],
|
||||||
author = IllustAuthor(
|
author = Author(
|
||||||
json['user']['id'],
|
json['user']['id'],
|
||||||
json['user']['name'],
|
json['user']['name'],
|
||||||
json['user']['account'],
|
json['user']['account'],
|
||||||
@@ -220,7 +219,7 @@ class Illust {
|
|||||||
tags = (json['tags'] as List)
|
tags = (json['tags'] as List)
|
||||||
.map((e) => Tag(e['name'], e['translated_name']))
|
.map((e) => Tag(e['name'], e['translated_name']))
|
||||||
.toList(),
|
.toList(),
|
||||||
createDate = json['create_date'],
|
createDate = DateTime.parse(json['create_date']),
|
||||||
pageCount = json['page_count'],
|
pageCount = json['page_count'],
|
||||||
width = json['width'],
|
width = json['width'],
|
||||||
height = json['height'],
|
height = json['height'],
|
||||||
@@ -250,11 +249,11 @@ enum KeywordMatchType {
|
|||||||
@override
|
@override
|
||||||
toString() => text;
|
toString() => text;
|
||||||
|
|
||||||
String toParam() => switch(this) {
|
String toParam() => switch (this) {
|
||||||
KeywordMatchType.tagsPartialMatches => "partial_match_for_tags",
|
KeywordMatchType.tagsPartialMatches => "partial_match_for_tags",
|
||||||
KeywordMatchType.tagsExactMatch => "exact_match_for_tags",
|
KeywordMatchType.tagsExactMatch => "exact_match_for_tags",
|
||||||
KeywordMatchType.titleOrDescriptionSearch => "title_and_caption"
|
KeywordMatchType.titleOrDescriptionSearch => "title_and_caption"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FavoriteNumber {
|
enum FavoriteNumber {
|
||||||
@@ -273,9 +272,11 @@ enum FavoriteNumber {
|
|||||||
const FavoriteNumber(this.number);
|
const FavoriteNumber(this.number);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() => this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks";
|
toString() =>
|
||||||
|
this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks";
|
||||||
|
|
||||||
String toParam() => this == FavoriteNumber.unlimited ? "" : " ${number}users入り";
|
String toParam() =>
|
||||||
|
this == FavoriteNumber.unlimited ? "" : " ${number}users入り";
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SearchSort {
|
enum SearchSort {
|
||||||
@@ -288,37 +289,35 @@ enum SearchSort {
|
|||||||
bool get isPremium => appdata.account?.user.isPremium == true;
|
bool get isPremium => appdata.account?.user.isPremium == true;
|
||||||
|
|
||||||
static List<SearchSort> get availableValues => [
|
static List<SearchSort> get availableValues => [
|
||||||
SearchSort.newToOld,
|
SearchSort.newToOld,
|
||||||
SearchSort.oldToNew,
|
SearchSort.oldToNew,
|
||||||
SearchSort.popular,
|
SearchSort.popular,
|
||||||
if(appdata.account?.user.isPremium == true)
|
if (appdata.account?.user.isPremium == true) SearchSort.popularMale,
|
||||||
SearchSort.popularMale,
|
if (appdata.account?.user.isPremium == true) SearchSort.popularFemale
|
||||||
if(appdata.account?.user.isPremium == true)
|
];
|
||||||
SearchSort.popularFemale
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
if(this == SearchSort.popular) {
|
if (this == SearchSort.popular) {
|
||||||
return isPremium ? "Popular" : "Popular(limited)";
|
return isPremium ? "Popular" : "Popular(limited)";
|
||||||
} else if(this == SearchSort.newToOld) {
|
} else if (this == SearchSort.newToOld) {
|
||||||
return "New to old";
|
return "New to old";
|
||||||
} else if(this == SearchSort.oldToNew){
|
} else if (this == SearchSort.oldToNew) {
|
||||||
return "Old to new";
|
return "Old to new";
|
||||||
} else if(this == SearchSort.popularMale){
|
} else if (this == SearchSort.popularMale) {
|
||||||
return "Popular(Male)";
|
return "Popular(Male)";
|
||||||
} else {
|
} else {
|
||||||
return "Popular(Female)";
|
return "Popular(Female)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String toParam() => switch(this) {
|
String toParam() => switch (this) {
|
||||||
SearchSort.newToOld => "date_desc",
|
SearchSort.newToOld => "date_desc",
|
||||||
SearchSort.oldToNew => "date_asc",
|
SearchSort.oldToNew => "date_asc",
|
||||||
SearchSort.popular => "popular_desc",
|
SearchSort.popular => "popular_desc",
|
||||||
SearchSort.popularMale => "popular_male_desc",
|
SearchSort.popularMale => "popular_male_desc",
|
||||||
SearchSort.popularFemale => "popular_female_desc",
|
SearchSort.popularFemale => "popular_female_desc",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AgeLimit {
|
enum AgeLimit {
|
||||||
@@ -333,11 +332,11 @@ enum AgeLimit {
|
|||||||
@override
|
@override
|
||||||
toString() => text;
|
toString() => text;
|
||||||
|
|
||||||
String toParam() => switch(this) {
|
String toParam() => switch (this) {
|
||||||
AgeLimit.unlimited => "",
|
AgeLimit.unlimited => "",
|
||||||
AgeLimit.allAges => " -R-18",
|
AgeLimit.allAges => " -R-18",
|
||||||
AgeLimit.r18 => "R-18",
|
AgeLimit.r18 => "R-18",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchOptions {
|
class SearchOptions {
|
||||||
@@ -369,17 +368,20 @@ class UserPreview {
|
|||||||
final String avatar;
|
final String avatar;
|
||||||
bool isFollowed;
|
bool isFollowed;
|
||||||
final bool isBlocking;
|
final bool isBlocking;
|
||||||
|
final List<Illust> artworks;
|
||||||
|
|
||||||
UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed,
|
UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed,
|
||||||
this.isBlocking);
|
this.isBlocking, this.artworks);
|
||||||
|
|
||||||
UserPreview.fromJson(Map<String, dynamic> json)
|
UserPreview.fromJson(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['user']['id'],
|
||||||
name = json['name'],
|
name = json['user']['name'],
|
||||||
account = json['account'],
|
account = json['user']['account'],
|
||||||
avatar = json['profile_image_urls']['medium'],
|
avatar = json['user']['profile_image_urls']['medium'],
|
||||||
isFollowed = json['is_followed'],
|
isFollowed = json['user']['is_followed'],
|
||||||
isBlocking = json['is_access_blocking_user'] ?? false;
|
isBlocking = json['user']['is_access_blocking_user'] ?? false,
|
||||||
|
artworks =
|
||||||
|
(json['illusts'] as List).map((e) => Illust.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -402,7 +404,7 @@ class UserPreview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
class Comment{
|
class Comment {
|
||||||
final String id;
|
final String id;
|
||||||
final String comment;
|
final String comment;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
@@ -419,6 +421,107 @@ class Comment{
|
|||||||
uid = json['user']['id'].toString(),
|
uid = json['user']['id'].toString(),
|
||||||
name = json['user']['name'],
|
name = json['user']['name'],
|
||||||
avatar = json['user']['profile_image_urls']['medium'],
|
avatar = json['user']['profile_image_urls']['medium'],
|
||||||
hasReplies = json['has_replies'],
|
hasReplies = json['has_replies'] ?? false,
|
||||||
stampUrl = json['stamp']?['stamp_url'];
|
stampUrl = json['stamp']?['stamp_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"id": 20741342,
|
||||||
|
"title": "中身が一般人のやつがれくん",
|
||||||
|
"caption": "なんか思いついたので書いてみた。<br />よくある芥川成り代わり。<br />3年くらい前の書きかけのやつをサルベージ。<br />じっくりは書いてないので抜け抜け。<br /><br />デイリー1位ありがとうございます✨<br /><br />※※※※※※※※<br />※※※※※※※※<br /><br />以下読了後推奨の蛇足<br /><br />「芥川くん」<br />「なんですかボス」<br />「君は将来的にどんな地位につきたいとかある?」<br />「僕はしがない一構成員ゆえ」<br />「ほら幹部とか隊長とか人事部とかさ。君あれこれオールマイティにできるから希望を聞いておこうと思って」<br />「ございます」<br />「なにかな?」<br />「僕は将来的にポートマフィア直営のいちじく農家になりたいと思います」<br />「なんて?」<br />「さらに、ゆくゆくはいちじく農家兼、いちじくの素晴らしさを世に知らしめるポートマフィア直営いちじくレストランを開きたいと」<br />「なんて???」",
|
||||||
|
"restrict": 0,
|
||||||
|
"x_restrict": 0,
|
||||||
|
"is_original": false,
|
||||||
|
"image_urls": {
|
||||||
|
"square_medium": "https://i.pximg.net/c/128x128/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_square1200.jpg",
|
||||||
|
"medium": "https://i.pximg.net/c/176x352/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_master1200.jpg",
|
||||||
|
"large": "https://i.pximg.net/c/240x480_80/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_master1200.jpg"
|
||||||
|
},
|
||||||
|
"create_date": "2023-09-27T16:14:45+09:00",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "文スト夢",
|
||||||
|
"translated_name": "Bungo Stray Dogs original/self-insert",
|
||||||
|
"added_by_uploaded_user": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "成り代わり",
|
||||||
|
"translated_name": "取代即有角色",
|
||||||
|
"added_by_uploaded_user": true
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"page_count": 6,
|
||||||
|
"text_length": 12550,
|
||||||
|
"user": {
|
||||||
|
"id": 9275134,
|
||||||
|
"name": "もろろ",
|
||||||
|
"account": "sleepinglife",
|
||||||
|
"profile_image_urls": {
|
||||||
|
"medium": "https://s.pximg.net/common/images/no_profile.png"
|
||||||
|
},
|
||||||
|
"is_followed": false
|
||||||
|
},
|
||||||
|
"series": {
|
||||||
|
"id": 11897059,
|
||||||
|
"title": "文スト夢"
|
||||||
|
},
|
||||||
|
"is_bookmarked": false,
|
||||||
|
"total_bookmarks": 8099,
|
||||||
|
"total_view": 76112,
|
||||||
|
"visible": true,
|
||||||
|
"total_comments": 146,
|
||||||
|
"is_muted": false,
|
||||||
|
"is_mypixiv_only": false,
|
||||||
|
"is_x_restricted": false,
|
||||||
|
"novel_ai_type": 1
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
class Novel {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String caption;
|
||||||
|
final bool isOriginal;
|
||||||
|
final String image;
|
||||||
|
final DateTime createDate;
|
||||||
|
final List<Tag> tags;
|
||||||
|
final int pages;
|
||||||
|
final int length;
|
||||||
|
final Author author;
|
||||||
|
final int? seriesId;
|
||||||
|
final String? seriesTitle;
|
||||||
|
bool isBookmarked;
|
||||||
|
final int totalBookmarks;
|
||||||
|
final int totalViews;
|
||||||
|
final int commentsCount;
|
||||||
|
final bool isAi;
|
||||||
|
|
||||||
|
Novel.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json["id"],
|
||||||
|
title = json["title"],
|
||||||
|
caption = json["caption"],
|
||||||
|
isOriginal = json["is_original"],
|
||||||
|
image = json["image_urls"]["large"] ??
|
||||||
|
json["image_urls"]["medium"] ??
|
||||||
|
json["image_urls"]["square_medium"] ??
|
||||||
|
"",
|
||||||
|
createDate = DateTime.parse(json["create_date"]),
|
||||||
|
tags = (json['tags'] as List)
|
||||||
|
.map((e) => Tag(e['name'], e['translated_name']))
|
||||||
|
.toList(),
|
||||||
|
pages = json["page_count"],
|
||||||
|
length = json["text_length"],
|
||||||
|
author = Author(
|
||||||
|
json['user']['id'],
|
||||||
|
json['user']['name'],
|
||||||
|
json['user']['account'],
|
||||||
|
json['user']['profile_image_urls']['medium'],
|
||||||
|
json['user']['is_followed'] ?? false),
|
||||||
|
seriesId = json["series"]?["id"],
|
||||||
|
seriesTitle = json["series"]?["title"],
|
||||||
|
isBookmarked = json["is_bookmarked"],
|
||||||
|
totalBookmarks = json["total_bookmarks"],
|
||||||
|
totalViews = json["total_view"],
|
||||||
|
commentsCount = json["total_comments"],
|
||||||
|
isAi = json["novel_ai_type"] == 2;
|
||||||
|
}
|
||||||
|
@@ -14,6 +14,8 @@ import 'models.dart';
|
|||||||
export 'models.dart';
|
export 'models.dart';
|
||||||
export 'res.dart';
|
export 'res.dart';
|
||||||
|
|
||||||
|
part 'novel.dart';
|
||||||
|
|
||||||
class Network {
|
class Network {
|
||||||
static const hashSalt =
|
static const hashSalt =
|
||||||
"28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
|
"28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
|
||||||
@@ -108,9 +110,9 @@ class Network {
|
|||||||
contentType: Headers.formUrlEncodedContentType,
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
validateStatus: (i) => true,
|
validateStatus: (i) => true,
|
||||||
headers: headers));
|
headers: headers));
|
||||||
if(res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var data = res.data ?? "";
|
var data = res.data ?? "";
|
||||||
if(data.contains("Invalid refresh token")) {
|
if (data.contains("Invalid refresh token")) {
|
||||||
throw "Failed to refresh token. Please log out.";
|
throw "Failed to refresh token. Please log out.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,8 +134,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
final res = await dio.get<Map<String, dynamic>>(path,
|
final res = await dio.get<Map<String, dynamic>>(path,
|
||||||
queryParameters: query,
|
queryParameters: query,
|
||||||
options:
|
options: Options(headers: headers, validateStatus: (status) => true));
|
||||||
Options(headers: headers, validateStatus: (status) => true));
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
return Res(res.data!);
|
return Res(res.data!);
|
||||||
} else if (res.statusCode == 400) {
|
} else if (res.statusCode == 400) {
|
||||||
@@ -159,6 +160,37 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Res<String>> apiGetPlain(String path,
|
||||||
|
{Map<String, dynamic>? query}) async {
|
||||||
|
try {
|
||||||
|
if (!path.startsWith("http")) {
|
||||||
|
path = "$baseUrl$path";
|
||||||
|
}
|
||||||
|
final res = await dio.get<String>(path,
|
||||||
|
queryParameters: query,
|
||||||
|
options: Options(headers: headers, validateStatus: (status) => true));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return Res(res.data!);
|
||||||
|
} else if (res.statusCode == 400) {
|
||||||
|
if (res.data.toString().contains("Access Token")) {
|
||||||
|
var refresh = await refreshToken();
|
||||||
|
if (refresh.success) {
|
||||||
|
return apiGetPlain(path, query: query);
|
||||||
|
} else {
|
||||||
|
return Res.error(refresh.errorMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Res.error("Invalid Status Code: ${res.statusCode}");
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Res<Map<String, dynamic>>> apiPost(String path,
|
Future<Res<Map<String, dynamic>>> apiPost(String path,
|
||||||
{Map<String, dynamic>? query, Map<String, dynamic>? data}) async {
|
{Map<String, dynamic>? query, Map<String, dynamic>? data}) async {
|
||||||
try {
|
try {
|
||||||
@@ -208,13 +240,15 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const recommendationUrl =
|
||||||
|
"/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true";
|
||||||
|
|
||||||
Future<Res<List<Illust>>> getRecommendedIllusts() async {
|
Future<Res<List<Illust>>> getRecommendedIllusts() async {
|
||||||
var res = await apiGet(
|
var res = await apiGet(recommendationUrl);
|
||||||
"/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true");
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res((res.data["illusts"] as List)
|
return Res(
|
||||||
.map((e) => Illust.fromJson(e))
|
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
||||||
.toList());
|
subData: recommendationUrl);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
}
|
}
|
||||||
@@ -233,6 +267,19 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Illust>>> getUserBookmarks(String uid,
|
||||||
|
[String? nextUrl]) async {
|
||||||
|
var res = await apiGet(
|
||||||
|
nextUrl ?? "/v1/user/bookmarks/illust?user_id=$uid&restrict=public");
|
||||||
|
if (res.success) {
|
||||||
|
return Res(
|
||||||
|
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
||||||
|
subData: res.data["next_url"]);
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Res<bool>> addBookmark(String id, String method,
|
Future<Res<bool>> addBookmark(String id, String method,
|
||||||
[String type = "public"]) async {
|
[String type = "public"]) async {
|
||||||
var res = method == "add"
|
var res = method == "add"
|
||||||
@@ -298,7 +345,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<Illust>>> getIllustsWithNextUrl(String nextUrl) async{
|
Future<Res<List<Illust>>> getIllustsWithNextUrl(String nextUrl) async {
|
||||||
var res = await apiGet(nextUrl);
|
var res = await apiGet(nextUrl);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
@@ -309,12 +356,16 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<UserPreview>>> searchUsers(String keyword, [String? nextUrl]) async{
|
Future<Res<List<UserPreview>>> searchUsers(String keyword,
|
||||||
var path = nextUrl ?? "/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}";
|
[String? nextUrl]) async {
|
||||||
|
var path = nextUrl ??
|
||||||
|
"/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}";
|
||||||
var res = await apiGet(path);
|
var res = await apiGet(path);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(),
|
(res.data["user_previews"] as List)
|
||||||
|
.map((e) => UserPreview.fromJson(e))
|
||||||
|
.toList(),
|
||||||
subData: res.data["next_url"]);
|
subData: res.data["next_url"]);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
@@ -322,7 +373,8 @@ class Network {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<Illust>>> getUserIllusts(String uid) async {
|
Future<Res<List<Illust>>> getUserIllusts(String uid) async {
|
||||||
var res = await apiGet("/v1/user/illusts?filter=for_android&user_id=$uid&type=illust");
|
var res = await apiGet(
|
||||||
|
"/v1/user/illusts?filter=for_android&user_id=$uid&type=illust");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
||||||
@@ -332,19 +384,24 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<UserPreview>>> getFollowing(String uid, String type, [String? nextUrl]) async {
|
Future<Res<List<UserPreview>>> getFollowing(String uid, String type,
|
||||||
var path = nextUrl ?? "/v1/user/following?filter=for_android&user_id=$uid&restrict=$type";
|
[String? nextUrl]) async {
|
||||||
|
var path = nextUrl ??
|
||||||
|
"/v1/user/following?filter=for_android&user_id=$uid&restrict=$type";
|
||||||
var res = await apiGet(path);
|
var res = await apiGet(path);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(),
|
(res.data["user_previews"] as List)
|
||||||
|
.map((e) => UserPreview.fromJson(e))
|
||||||
|
.toList(),
|
||||||
subData: res.data["next_url"]);
|
subData: res.data["next_url"]);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<Illust>>> getFollowingArtworks(String restrict, [String? nextUrl]) async {
|
Future<Res<List<Illust>>> getFollowingArtworks(String restrict,
|
||||||
|
[String? nextUrl]) async {
|
||||||
var res = await apiGet(nextUrl ?? "/v2/illust/follow?restrict=$restrict");
|
var res = await apiGet(nextUrl ?? "/v2/illust/follow?restrict=$restrict");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
@@ -359,7 +416,9 @@ class Network {
|
|||||||
var res = await apiGet("/v1/user/recommended?filter=for_android");
|
var res = await apiGet("/v1/user/recommended?filter=for_android");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(),
|
(res.data["user_previews"] as List)
|
||||||
|
.map((e) => UserPreview.fromJson(e))
|
||||||
|
.toList(),
|
||||||
subData: res.data["next_url"]);
|
subData: res.data["next_url"]);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
@@ -368,7 +427,8 @@ class Network {
|
|||||||
|
|
||||||
/// mode: day, week, month, day_male, day_female, week_original, week_rookie, day_manga, week_manga, month_manga, day_r18_manga, day_r18
|
/// mode: day, week, month, day_male, day_female, week_original, week_rookie, day_manga, week_manga, month_manga, day_r18_manga, day_r18
|
||||||
Future<Res<List<Illust>>> getRanking(String mode, [String? nextUrl]) async {
|
Future<Res<List<Illust>>> getRanking(String mode, [String? nextUrl]) async {
|
||||||
var res = await apiGet(nextUrl ?? "/v1/illust/ranking?filter=for_android&mode=$mode");
|
var res = await apiGet(
|
||||||
|
nextUrl ?? "/v1/illust/ranking?filter=for_android&mode=$mode");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
||||||
@@ -382,7 +442,9 @@ class Network {
|
|||||||
var res = await apiGet(nextUrl ?? "/v3/illust/comments?illust_id=$id");
|
var res = await apiGet(nextUrl ?? "/v3/illust/comments?illust_id=$id");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
(res.data["comments"] as List)
|
||||||
|
.map((e) => Comment.fromJson(e))
|
||||||
|
.toList(),
|
||||||
subData: res.data["next_url"]);
|
subData: res.data["next_url"]);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
@@ -409,7 +471,8 @@ class Network {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<Illust>>> getRecommendedMangas() async {
|
Future<Res<List<Illust>>> getRecommendedMangas() async {
|
||||||
var res = await apiGet("/v1/manga/recommended?filter=for_android&include_ranking_illusts=true&include_privacy_policy=true");
|
var res = await apiGet(
|
||||||
|
"/v1/manga/recommended?filter=for_android&include_ranking_illusts=true&include_privacy_policy=true");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res(
|
return Res(
|
||||||
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
|
||||||
@@ -421,13 +484,14 @@ class Network {
|
|||||||
|
|
||||||
Future<Res<List<Illust>>> getHistory(int page) async {
|
Future<Res<List<Illust>>> getHistory(int page) async {
|
||||||
String param = "";
|
String param = "";
|
||||||
if(page > 1) {
|
if (page > 1) {
|
||||||
param = "?offset=${30*(page-1)}";
|
param = "?offset=${30 * (page - 1)}";
|
||||||
}
|
}
|
||||||
var res = await apiGet("/v1/user/browsing-history/illusts$param");
|
var res = await apiGet("/v1/user/browsing-history/illusts$param");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Res((res.data["illusts"] as List)
|
return Res((res.data["illusts"] as List)
|
||||||
.map((e) => Illust.fromJson(e)).toList());
|
.map((e) => Illust.fromJson(e))
|
||||||
|
.toList());
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
}
|
}
|
||||||
@@ -436,23 +500,59 @@ class Network {
|
|||||||
Future<List<Tag>> getMutedTags() async {
|
Future<List<Tag>> getMutedTags() async {
|
||||||
var res = await apiGet("/v1/mute/list");
|
var res = await apiGet("/v1/mute/list");
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return res.data["mute_tags"].map<Tag>((e) =>
|
return res.data["mute_tags"]
|
||||||
Tag(e["tag"]["name"], e["tag"]["translated_name"]))
|
.map<Tag>((e) => Tag(e["tag"]["name"], e["tag"]["translated_name"]))
|
||||||
.toList();
|
.toList();
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<bool>> muteTags(List<String> muteTags, List<String> unmuteTags) async {
|
Future<Res<bool>> muteTags(
|
||||||
var res = await apiPost("/v1/mute/edit", data: {
|
List<String> muteTags, List<String> unmuteTags) async {
|
||||||
"add_tags": muteTags,
|
var res = await apiPost("/v1/mute/edit",
|
||||||
"delete_tags": unmuteTags
|
data: {"add_tags": muteTags, "delete_tags": unmuteTags});
|
||||||
});
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return const Res(true);
|
return const Res(true);
|
||||||
} else {
|
} else {
|
||||||
return Res.fromErrorRes(res);
|
return Res.fromErrorRes(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Res<List<UserPreview>>> relatedUsers(String id) async {
|
||||||
|
var res =
|
||||||
|
await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id");
|
||||||
|
if (res.success) {
|
||||||
|
return Res((res.data["user_previews"] as List)
|
||||||
|
.map((e) => UserPreview.fromJson(e))
|
||||||
|
.toList());
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Illust>>> relatedIllusts(String id) async {
|
||||||
|
var res =
|
||||||
|
await apiGet("/v2/illust/related?filter=for_android&illust_id=$id");
|
||||||
|
if (res.success) {
|
||||||
|
return Res((res.data["illusts"] as List)
|
||||||
|
.map((e) => Illust.fromJson(e))
|
||||||
|
.toList());
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<String>> getNovelImage(String novelId, String imageId) async {
|
||||||
|
var res = await apiGetPlain(
|
||||||
|
"/web/v1/novel/image?novel_id=$novelId&uploaded_image_id=$imageId");
|
||||||
|
if (res.success) {
|
||||||
|
var html = res.data;
|
||||||
|
int start = html.indexOf('<img src="') + 10;
|
||||||
|
int end = html.indexOf('"', start);
|
||||||
|
return Res(html.substring(start, end));
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
152
lib/network/novel.dart
Normal file
152
lib/network/novel.dart
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
part of "network.dart";
|
||||||
|
|
||||||
|
extension NovelExt on Network {
|
||||||
|
Future<Res<List<Novel>>> getRecommendNovels() {
|
||||||
|
return getNovelsWithNextUrl("/v1/novel/recommended");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> getNovelsWithNextUrl(String nextUrl) async {
|
||||||
|
var res = await apiGet(nextUrl);
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
(res.data["novels"] as List).map((e) => Novel.fromJson(e)).toList(),
|
||||||
|
subData: res.data["next_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> searchNovels(String keyword, SearchOptions options) {
|
||||||
|
var url = "/v1/search/novel?"
|
||||||
|
"include_translated_tag_results=true&"
|
||||||
|
"merge_plain_keyword_results=true&"
|
||||||
|
"word=${Uri.encodeComponent(keyword)}&"
|
||||||
|
"sort=${options.sort.toParam()}&"
|
||||||
|
"search_target=${options.matchType.toParam()}&"
|
||||||
|
"search_ai_type=0";
|
||||||
|
return getNovelsWithNextUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// mode: day, day_male, day_female, week_rookie, week, week_ai
|
||||||
|
Future<Res<List<Novel>>> getNovelRanking(String mode, DateTime? date) {
|
||||||
|
var url = "/v1/novel/ranking?mode=$mode";
|
||||||
|
if (date != null) {
|
||||||
|
url += "&date=${date.year}-${date.month}-${date.day}";
|
||||||
|
}
|
||||||
|
return getNovelsWithNextUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> getBookmarkedNovels(String uid) {
|
||||||
|
return getNovelsWithNextUrl(
|
||||||
|
"/v1/user/bookmarks/novel?user_id=$uid&restrict=public");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> favoriteNovel(String id) async {
|
||||||
|
var res = await apiPost("/v2/novel/bookmark/add", data: {
|
||||||
|
"novel_id": id,
|
||||||
|
"restrict": "public",
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return const Res(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> deleteFavoriteNovel(String id) async {
|
||||||
|
var res = await apiPost("/v1/novel/bookmark/delete", data: {
|
||||||
|
"novel_id": id,
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return const Res(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<String>> getNovelContent(String id) async {
|
||||||
|
var res = await apiGetPlain(
|
||||||
|
"/webview/v2/novel?id=$id&font=default&font_size=16.0px&line_height=1.75&color=%23101010&background_color=%23EFEFEF&margin_top=56px&margin_bottom=48px&theme=light&use_block=true&viewer_version=20221031_ai");
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var html = res.data;
|
||||||
|
int start = html.indexOf("novel:");
|
||||||
|
while (html[start] != '{') {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
int leftCount = 0;
|
||||||
|
int end = start;
|
||||||
|
for (end = start; end < html.length; end++) {
|
||||||
|
if (html[end] == '{') {
|
||||||
|
leftCount++;
|
||||||
|
} else if (html[end] == '}') {
|
||||||
|
leftCount--;
|
||||||
|
}
|
||||||
|
if (leftCount == 0) {
|
||||||
|
end++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var json = jsonDecode(html.substring(start, end));
|
||||||
|
return Res(json['text']);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error(
|
||||||
|
"Data Convert", "Failed to analyze html novel content: \n$e\n$s");
|
||||||
|
return Res.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> relatedNovels(String id) async {
|
||||||
|
var res = await apiPost("/v1/novel/related", data: {
|
||||||
|
"novel_id": id,
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
(res.data["novels"] as List).map((e) => Novel.fromJson(e)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> getUserNovels(String uid) {
|
||||||
|
return getNovelsWithNextUrl("/v1/user/novels?user_id=$uid");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Novel>>> getNovelSeries(String id, [String? nextUrl]) async {
|
||||||
|
var res = await apiGet(nextUrl ?? "/v2/novel/series?series_id=$id");
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
(res.data["novels"] as List).map((e) => Novel.fromJson(e)).toList(),
|
||||||
|
subData: res.data["next_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Comment>>> getNovelComments(String id,
|
||||||
|
[String? nextUrl]) async {
|
||||||
|
var res = await apiGet(nextUrl ?? "/v1/novel/comments?novel_id=$id");
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
(res.data["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
|
subData: res.data["next_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> commentNovel(String id, String content) async {
|
||||||
|
var res = await apiPost("/v1/novel/comment/add", data: {
|
||||||
|
"novel_id": id,
|
||||||
|
"content": content,
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return const Res(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<Novel>> getNovelDetail(String id) async {
|
||||||
|
var res = await apiGet("/v2/novel/detail?novel_id=$id");
|
||||||
|
if (res.error) {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
return Res(Novel.fromJson(res.data["novel"]));
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import 'package:pixes/components/segmented_button.dart';
|
|||||||
import 'package:pixes/components/title_bar.dart';
|
import 'package:pixes/components/title_bar.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/illust_page.dart';
|
||||||
import 'package:pixes/utils/translation.dart';
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
import '../components/illust_widget.dart';
|
import '../components/illust_widget.dart';
|
||||||
@@ -83,7 +84,13 @@ class _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage,
|
|||||||
if(index == data.length - 1){
|
if(index == data.length - 1){
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(data[index], onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data,
|
||||||
|
initialPage: index,
|
||||||
|
nextUrl: nextUrl
|
||||||
|
));
|
||||||
|
},);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
210
lib/pages/comments_page.dart
Normal file
210
lib/pages/comments_page.dart
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/page_route.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../components/md.dart';
|
||||||
|
import '../components/message.dart';
|
||||||
|
|
||||||
|
class CommentsPage extends StatefulWidget {
|
||||||
|
const CommentsPage(this.id, {this.isNovel = false, super.key});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final bool isNovel;
|
||||||
|
|
||||||
|
static void show(BuildContext context, String id, {bool isNovel = false}) {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(SideBarRoute(CommentsPage(id, isNovel: isNovel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CommentsPage> createState() => _CommentsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentsPageState extends MultiPageLoadingState<CommentsPage, Comment> {
|
||||||
|
bool isCommenting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Comment> data) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: buildBody(context, data)),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildBottom(context),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody(BuildContext context, List<Comment> data) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: data.length + 2,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Text("Comments".tl, style: const TextStyle(fontSize: 20))
|
||||||
|
.paddingVertical(16)
|
||||||
|
.paddingHorizontal(12);
|
||||||
|
} else if (index == data.length + 1) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
var date = data[index].date;
|
||||||
|
var dateText = "${date.year}/${date.month}/${date.day}";
|
||||||
|
return Card(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 38,
|
||||||
|
width: 38,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(38),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => context.to(
|
||||||
|
() => UserInfoPage(data[index].id.toString())),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(data[index].avatar),
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
data[index].name,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
dateText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ColorScheme.of(context).outline),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
if (data[index].comment.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
data[index].comment,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
if (data[index].stampUrl != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 64,
|
||||||
|
width: 64,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(data[index].stampUrl!),
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottom(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
backgroundColor:
|
||||||
|
FluentTheme.of(context).micaBackgroundColor.withOpacity(0.96),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: TextBox(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
placeholder: "Comment".tl,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
onSubmitted: (s) {
|
||||||
|
showToast(context, message: "Sending".tl);
|
||||||
|
if (isCommenting) return;
|
||||||
|
setState(() {
|
||||||
|
isCommenting = true;
|
||||||
|
});
|
||||||
|
if (widget.isNovel) {
|
||||||
|
Network().commentNovel(widget.id, s).then((value) {
|
||||||
|
if (value.error) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
setState(() {
|
||||||
|
isCommenting = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isCommenting = false;
|
||||||
|
nextUrl = null;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Network().comment(widget.id, s).then((value) {
|
||||||
|
if (value.error) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
setState(() {
|
||||||
|
isCommenting = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isCommenting = false;
|
||||||
|
nextUrl = null;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).paddingVertical(8).paddingHorizontal(12),
|
||||||
|
).paddingBottom(context.padding.bottom + context.viewInsets.bottom),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Comment>>> loadData(int page) async {
|
||||||
|
if (nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = widget.isNovel
|
||||||
|
? await Network().getNovelComments(widget.id, nextUrl)
|
||||||
|
: await Network().getComments(widget.id, nextUrl);
|
||||||
|
if (!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:photo_view/photo_view_gallery.dart';
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
import 'package:pixes/components/grid.dart';
|
import 'package:pixes/components/grid.dart';
|
||||||
import 'package:pixes/components/md.dart';
|
import 'package:pixes/components/md.dart';
|
||||||
import 'package:pixes/components/message.dart';
|
import 'package:pixes/components/message.dart';
|
||||||
@@ -73,9 +74,11 @@ class _DownloadedPageState extends State<DownloadedPage> {
|
|||||||
color: ColorScheme.of(context).secondaryContainer
|
color: ColorScheme.of(context).secondaryContainer
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: image == null ? null : Image(
|
child: image == null ? null : AnimatedImage(
|
||||||
image: FileImage(image),
|
image: FileImage(image),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
width: 96,
|
||||||
|
height: double.infinity,
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -47,7 +47,38 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
|||||||
Widget buildTop() {
|
Widget buildTop() {
|
||||||
int bytesPerSecond = DownloadManager().bytesPerSecond;
|
int bytesPerSecond = DownloadManager().bytesPerSecond;
|
||||||
|
|
||||||
return SliverTitleBar(title: "${"Speed".tl}: ${bytesToText(bytesPerSecond)}/s");
|
bool paused = DownloadManager().paused;
|
||||||
|
|
||||||
|
return SliverTitleBar(
|
||||||
|
title: paused
|
||||||
|
? "Paused".tl
|
||||||
|
:"${"Speed".tl}: ${bytesToText(bytesPerSecond)}/s",
|
||||||
|
action: SplitButton(
|
||||||
|
onInvoked: (){
|
||||||
|
if(!paused) {
|
||||||
|
DownloadManager().pause();
|
||||||
|
setState(() {});
|
||||||
|
} else {
|
||||||
|
DownloadManager().resume();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flyout: MenuFlyout(
|
||||||
|
items: [
|
||||||
|
MenuFlyoutItem(text: Text("Cancel All".tl), onPressed: (){
|
||||||
|
var tasks = List.from(DownloadManager().tasks);
|
||||||
|
DownloadManager().tasks.clear();
|
||||||
|
for(var task in tasks) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(paused ? "Resume".tl : "Pause".tl)
|
||||||
|
.toCenter().fixWidth(56).fixHeight(32),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildContent() {
|
Widget buildContent() {
|
||||||
|
@@ -9,6 +9,7 @@ import '../components/illust_widget.dart';
|
|||||||
import '../components/loading.dart';
|
import '../components/loading.dart';
|
||||||
import '../components/segmented_button.dart';
|
import '../components/segmented_button.dart';
|
||||||
import '../network/network.dart';
|
import '../network/network.dart';
|
||||||
|
import 'illust_page.dart';
|
||||||
|
|
||||||
class FollowingArtworksPage extends StatefulWidget {
|
class FollowingArtworksPage extends StatefulWidget {
|
||||||
const FollowingArtworksPage({super.key});
|
const FollowingArtworksPage({super.key});
|
||||||
@@ -84,7 +85,13 @@ class _OneFollowingPageState extends MultiPageLoadingState<_OneFollowingPage, Il
|
|||||||
if(index == data.length - 1){
|
if(index == data.length - 1){
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(data[index], onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data,
|
||||||
|
initialPage: index,
|
||||||
|
nextUrl: nextUrl
|
||||||
|
));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -18,7 +18,8 @@ class FollowingUsersPage extends StatefulWidget {
|
|||||||
State<FollowingUsersPage> createState() => _FollowingUsersPageState();
|
State<FollowingUsersPage> createState() => _FollowingUsersPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage, UserPreview> {
|
class _FollowingUsersPageState
|
||||||
|
extends MultiPageLoadingState<FollowingUsersPage, UserPreview> {
|
||||||
String type = "public";
|
String type = "public";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,11 +29,13 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Following".tl,
|
Text(
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),)
|
"Following".tl,
|
||||||
.paddingVertical(12).paddingLeft(16),
|
style:
|
||||||
|
const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
).paddingVertical(12).paddingLeft(16),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if(widget.uid == appdata.account?.user.id)
|
if (widget.uid == appdata.account?.user.id)
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
value: type,
|
value: type,
|
||||||
options: [
|
options: [
|
||||||
@@ -44,22 +47,21 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
|
|||||||
reset();
|
reset();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16,)
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverGridViewWithFixedItemHeight(
|
SliverGridViewWithFixedItemHeight(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
(context, index) {
|
if (index == data.length - 1) {
|
||||||
if(index == data.length - 1){
|
nextPage();
|
||||||
nextPage();
|
}
|
||||||
}
|
return UserPreviewWidget(data[index]);
|
||||||
return UserPreviewWidget(data[index]);
|
}, childCount: data.length),
|
||||||
},
|
minCrossAxisExtent: 440,
|
||||||
childCount: data.length
|
itemHeight: 136,
|
||||||
),
|
|
||||||
maxCrossAxisExtent: 520,
|
|
||||||
itemHeight: 114,
|
|
||||||
).sliverPaddingHorizontal(8)
|
).sliverPaddingHorizontal(8)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -68,12 +70,12 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
|
|||||||
String? nextUrl;
|
String? nextUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Res<List<UserPreview>>> loadData(page) async{
|
Future<Res<List<UserPreview>>> loadData(page) async {
|
||||||
if(nextUrl == "end") {
|
if (nextUrl == "end") {
|
||||||
return Res.error("No more data");
|
return Res.error("No more data");
|
||||||
}
|
}
|
||||||
var res = await Network().getFollowing(widget.uid, type, nextUrl);
|
var res = await Network().getFollowing(widget.uid, type, nextUrl);
|
||||||
if(!res.error) {
|
if (!res.error) {
|
||||||
nextUrl = res.subData;
|
nextUrl = res.subData;
|
||||||
nextUrl ??= "end";
|
nextUrl ??= "end";
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,11 @@ import 'package:pixes/appdata.dart';
|
|||||||
import 'package:pixes/components/loading.dart';
|
import 'package:pixes/components/loading.dart';
|
||||||
import 'package:pixes/components/title_bar.dart';
|
import 'package:pixes/components/title_bar.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/models.dart';
|
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
import 'package:pixes/utils/translation.dart';
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
import '../components/illust_widget.dart';
|
import '../components/illust_widget.dart';
|
||||||
|
import 'illust_page.dart';
|
||||||
|
|
||||||
class HistoryPage extends StatefulWidget {
|
class HistoryPage extends StatefulWidget {
|
||||||
const HistoryPage({super.key});
|
const HistoryPage({super.key});
|
||||||
@@ -36,7 +36,12 @@ class _HistoryPageState extends MultiPageLoadingState<HistoryPage, Illust> {
|
|||||||
if(index == data.length - 1){
|
if(index == data.length - 1){
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(data[index], onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data,
|
||||||
|
initialPage: index,
|
||||||
|
));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -16,15 +16,14 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class ImagePage extends StatefulWidget {
|
class ImagePage extends StatefulWidget {
|
||||||
const ImagePage(this.urls, {this.initialPage = 1, super.key});
|
const ImagePage(this.urls, {this.initialPage = 0, super.key});
|
||||||
|
|
||||||
final List<String> urls;
|
final List<String> urls;
|
||||||
|
|
||||||
final int initialPage;
|
final int initialPage;
|
||||||
|
|
||||||
static show(List<String> urls, {int initialPage = 1}) {
|
static show(List<String> urls, {int initialPage = 0}) {
|
||||||
App.rootNavigatorKey.currentState
|
App.rootNavigatorKey.currentState?.push(AppPageRoute(
|
||||||
?.push(AppPageRoute(
|
|
||||||
builder: (context) => ImagePage(urls, initialPage: initialPage)));
|
builder: (context) => ImagePage(urls, initialPage: initialPage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,61 +68,67 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
|
|
||||||
Future<File?> getFile() async {
|
Future<File?> getFile() async {
|
||||||
var image = widget.urls[currentPage];
|
var image = widget.urls[currentPage];
|
||||||
if(image.startsWith("file://")){
|
if (image.startsWith("file://")) {
|
||||||
return File(image.replaceFirst("file://", ""));
|
return File(image.replaceFirst("file://", ""));
|
||||||
}
|
}
|
||||||
var file = await CacheManager().findCache(image);
|
var key = image;
|
||||||
return file == null
|
if (key.startsWith("novel:")) {
|
||||||
? null
|
key = key.split(':').last;
|
||||||
: File(file);
|
}
|
||||||
|
var file = await CacheManager().findCache(key);
|
||||||
|
return file == null ? null : File(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getExtensionName() {
|
String getExtensionName() {
|
||||||
var fileName = widget.urls[currentPage].split('/').last;
|
var fileName = widget.urls[currentPage].split('/').last;
|
||||||
if(fileName.contains('.')){
|
if (fileName.contains('.')) {
|
||||||
return '.${fileName.split('.').last}';
|
return '.${fileName.split('.').last}';
|
||||||
}
|
}
|
||||||
return '.jpg';
|
return '.jpg';
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMenu() {
|
void showMenu() {
|
||||||
menuController.showFlyout(builder: (context) => MenuFlyout(
|
menuController.showFlyout(
|
||||||
items: [
|
builder: (context) => MenuFlyout(
|
||||||
MenuFlyoutItem(text: Text("Save to".tl), onPressed: () async{
|
items: [
|
||||||
var file = await getFile();
|
MenuFlyoutItem(
|
||||||
if(file != null){
|
text: Text("Save to".tl),
|
||||||
var fileName = file.path.split('/').last;
|
onPressed: () async {
|
||||||
if(!fileName.contains('.')){
|
var file = await getFile();
|
||||||
fileName += getExtensionName();
|
if (file != null) {
|
||||||
}
|
var fileName = file.path.split('/').last;
|
||||||
saveFile(file, fileName);
|
if (!fileName.contains('.')) {
|
||||||
}
|
fileName += getExtensionName();
|
||||||
}),
|
}
|
||||||
MenuFlyoutItem(text: Text("Share".tl), onPressed: () async{
|
saveFile(file, fileName);
|
||||||
var file = await getFile();
|
}
|
||||||
if(file != null){
|
}),
|
||||||
var ext = getExtensionName();
|
MenuFlyoutItem(
|
||||||
var fileName = file.path.split('/').last;
|
text: Text("Share".tl),
|
||||||
if(!fileName.contains('.')){
|
onPressed: () async {
|
||||||
fileName += ext;
|
var file = await getFile();
|
||||||
}
|
if (file != null) {
|
||||||
var mediaType = switch(ext){
|
var ext = getExtensionName();
|
||||||
'.jpg' => 'image/jpeg',
|
var fileName = file.path.split('/').last;
|
||||||
'.jpeg' => 'image/jpeg',
|
if (!fileName.contains('.')) {
|
||||||
'.png' => 'image/png',
|
fileName += ext;
|
||||||
'.gif' => 'image/gif',
|
}
|
||||||
'.webp' => 'image/webp',
|
var mediaType = switch (ext) {
|
||||||
_ => 'application/octet-stream'
|
'.jpg' => 'image/jpeg',
|
||||||
};
|
'.jpeg' => 'image/jpeg',
|
||||||
Share.shareXFiles([XFile.fromData(
|
'.png' => 'image/png',
|
||||||
await file.readAsBytes(),
|
'.gif' => 'image/gif',
|
||||||
mimeType: mediaType,
|
'.webp' => 'image/webp',
|
||||||
name: fileName)]
|
_ => 'application/octet-stream'
|
||||||
);
|
};
|
||||||
}
|
Share.shareXFiles([
|
||||||
}),
|
XFile.fromData(await file.readAsBytes(),
|
||||||
],
|
mimeType: mediaType, name: fileName)
|
||||||
));
|
]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -133,12 +138,13 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
color: FluentTheme.of(context).micaBackgroundColor,
|
color: FluentTheme.of(context).micaBackgroundColor,
|
||||||
child: Listener(
|
child: Listener(
|
||||||
onPointerSignal: (event) {
|
onPointerSignal: (event) {
|
||||||
if(event is PointerScrollEvent &&
|
if (event is PointerScrollEvent &&
|
||||||
!HardwareKeyboard.instance.isControlPressed) {
|
!HardwareKeyboard.instance.isControlPressed) {
|
||||||
if(event.scrollDelta.dy > 0
|
if (event.scrollDelta.dy > 0 &&
|
||||||
&& controller.page!.toInt() < widget.urls.length - 1) {
|
controller.page!.toInt() < widget.urls.length - 1) {
|
||||||
controller.jumpToPage(controller.page!.toInt() + 1);
|
controller.jumpToPage(controller.page!.toInt() + 1);
|
||||||
} else if(event.scrollDelta.dy < 0 && controller.page!.toInt() > 0){
|
} else if (event.scrollDelta.dy < 0 &&
|
||||||
|
controller.page!.toInt() > 0) {
|
||||||
controller.jumpToPage(controller.page!.toInt() - 1);
|
controller.jumpToPage(controller.page!.toInt() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,19 +154,17 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
var height = constrains.maxHeight;
|
var height = constrains.maxHeight;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(child: PhotoViewGallery.builder(
|
Positioned.fill(
|
||||||
|
child: PhotoViewGallery.builder(
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
backgroundDecoration: const BoxDecoration(
|
backgroundDecoration:
|
||||||
color: Colors.transparent
|
const BoxDecoration(color: Colors.transparent),
|
||||||
),
|
|
||||||
itemCount: widget.urls.length,
|
itemCount: widget.urls.length,
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
var image = widget.urls[index];
|
var image = widget.urls[index];
|
||||||
|
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: image.startsWith("file://")
|
imageProvider: getImageProvider(image),
|
||||||
? FileImage(File(image.replaceFirst("file://", "")))
|
|
||||||
: CachedImageProvider(image) as ImageProvider,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onPageChanged: (index) {
|
onPageChanged: (index) {
|
||||||
@@ -177,17 +181,22 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
height: 36,
|
height: 36,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 6,),
|
const SizedBox(
|
||||||
|
width: 6,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(FluentIcons.back).paddingAll(2),
|
icon: const Icon(FluentIcons.back).paddingAll(2),
|
||||||
onPressed: () => context.pop()
|
onPressed: () => context.pop()),
|
||||||
),
|
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: DragToMoveArea(child: SizedBox.expand(),),
|
child: DragToMoveArea(
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
buildActions(),
|
buildActions(),
|
||||||
if(App.isDesktop)
|
if (App.isDesktop)
|
||||||
WindowButtons(key: ValueKey(windowButtonKey),),
|
WindowButtons(
|
||||||
|
key: ValueKey(windowButtonKey),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -196,7 +205,10 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
left: 0,
|
left: 0,
|
||||||
top: height / 2 - 9,
|
top: height / 2 - 9,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(FluentIcons.chevron_left, size: 18,),
|
icon: const Icon(
|
||||||
|
FluentIcons.chevron_left,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.previousPage(
|
controller.previousPage(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -239,25 +251,35 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
controller: menuController,
|
controller: menuController,
|
||||||
child: width > 600
|
child: width > 600
|
||||||
? Button(
|
? Button(
|
||||||
onPressed: showMenu,
|
onPressed: showMenu,
|
||||||
child: const Row(
|
child: const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
MdIcons.menu,
|
MdIcons.menu,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
),
|
),
|
||||||
Text('Actions'),
|
Text('Actions'),
|
||||||
],
|
],
|
||||||
))
|
))
|
||||||
: IconButton(
|
: IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
MdIcons.more_horiz,
|
MdIcons.more_horiz,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: showMenu),
|
onPressed: showMenu),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageProvider getImageProvider(String url) {
|
||||||
|
if (url.startsWith("file://")) {
|
||||||
|
return FileImage(File(url.replaceFirst("file://", "")));
|
||||||
|
} else if (url.startsWith("novel:")) {
|
||||||
|
var ids = url.split(':').last.split('/');
|
||||||
|
return CachedNovelImageProvider(ids[0], ids[1]);
|
||||||
|
}
|
||||||
|
return CachedImageProvider(url) as ImageProvider;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import "dart:async";
|
|||||||
|
|
||||||
import "package:fluent_ui/fluent_ui.dart";
|
import "package:fluent_ui/fluent_ui.dart";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:flutter/material.dart" as md;
|
|
||||||
import "package:pixes/appdata.dart";
|
import "package:pixes/appdata.dart";
|
||||||
import "package:pixes/components/md.dart";
|
import "package:pixes/components/md.dart";
|
||||||
import "package:pixes/foundation/app.dart";
|
import "package:pixes/foundation/app.dart";
|
||||||
@@ -12,6 +11,9 @@ import "package:pixes/pages/bookmarks.dart";
|
|||||||
import "package:pixes/pages/downloaded_page.dart";
|
import "package:pixes/pages/downloaded_page.dart";
|
||||||
import "package:pixes/pages/following_artworks.dart";
|
import "package:pixes/pages/following_artworks.dart";
|
||||||
import "package:pixes/pages/history.dart";
|
import "package:pixes/pages/history.dart";
|
||||||
|
import "package:pixes/pages/novel_bookmarks_page.dart";
|
||||||
|
import "package:pixes/pages/novel_ranking_page.dart";
|
||||||
|
import "package:pixes/pages/novel_recommendation_page.dart";
|
||||||
import "package:pixes/pages/ranking.dart";
|
import "package:pixes/pages/ranking.dart";
|
||||||
import "package:pixes/pages/recommendation_page.dart";
|
import "package:pixes/pages/recommendation_page.dart";
|
||||||
import "package:pixes/pages/login_page.dart";
|
import "package:pixes/pages/login_page.dart";
|
||||||
@@ -28,6 +30,32 @@ import "downloading_page.dart";
|
|||||||
|
|
||||||
double get _appBarHeight => App.isDesktop ? 36.0 : 48.0;
|
double get _appBarHeight => App.isDesktop ? 36.0 : 48.0;
|
||||||
|
|
||||||
|
class TitleBarAction {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final void Function() onPressed;
|
||||||
|
|
||||||
|
TitleBarAction(this.icon, this.title, this.onPressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TitleBarController extends StateController {
|
||||||
|
TitleBarController();
|
||||||
|
|
||||||
|
final List<TitleBarAction> actions = [
|
||||||
|
if (kDebugMode) TitleBarAction(MdIcons.bug_report, "Debug", debug)
|
||||||
|
];
|
||||||
|
|
||||||
|
void addAction(TitleBarAction action) {
|
||||||
|
actions.add(action);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAction(TitleBarAction action) {
|
||||||
|
actions.remove(action);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
class MainPage extends StatefulWidget {
|
||||||
const MainPage({super.key});
|
const MainPage({super.key});
|
||||||
|
|
||||||
@@ -44,13 +72,16 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
StateController.put<TitleBarController>(TitleBarController());
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
listenMouseSideButtonToBack(navigatorKey);
|
listenMouseSideButtonToBack(navigatorKey);
|
||||||
|
App.mainNavigatorKey = navigatorKey;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
StateController.remove<TitleBarController>();
|
||||||
windowManager.removeListener(this);
|
windowManager.removeListener(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -79,91 +110,115 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
content: LoginPage(() => setState(() {})),
|
content: LoginPage(() => setState(() {})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return md.Theme(
|
return DefaultSelectionStyle.merge(
|
||||||
data: md.ThemeData.from(
|
selectionColor: FluentTheme.of(context).selectionColor.withOpacity(0.4),
|
||||||
useMaterial3: true,
|
child: NavigationView(
|
||||||
colorScheme: md.ColorScheme.fromSeed(
|
appBar: buildAppBar(context, navigatorKey),
|
||||||
seedColor: FluentTheme.of(context).accentColor.withOpacity(1),
|
pane: NavigationPane(
|
||||||
brightness: FluentTheme.of(context).brightness,
|
selected: index,
|
||||||
)),
|
onChanged: (value) {
|
||||||
child: DefaultSelectionStyle.merge(
|
setState(() {
|
||||||
selectionColor: FluentTheme.of(context).selectionColor.withOpacity(0.4),
|
index = value;
|
||||||
child: NavigationView(
|
});
|
||||||
appBar: buildAppBar(context, navigatorKey),
|
navigate(value);
|
||||||
pane: NavigationPane(
|
},
|
||||||
selected: index,
|
items: [
|
||||||
onChanged: (value) {
|
UserPane(),
|
||||||
setState(() {
|
PaneItem(
|
||||||
index = value;
|
icon: const Icon(
|
||||||
});
|
MdIcons.search,
|
||||||
navigate(value);
|
size: 20,
|
||||||
},
|
),
|
||||||
items: [
|
title: Text('Search'.tl),
|
||||||
UserPane(),
|
body: const SizedBox.shrink(),
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.search, size: 20,),
|
|
||||||
title: Text('Search'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.downloading, size: 20,),
|
|
||||||
title: Text('Downloading'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.download, size: 20,),
|
|
||||||
title: Text('Downloaded'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItemSeparator(),
|
|
||||||
PaneItemHeader(header: Text("Artwork".tl).paddingBottom(4).paddingLeft(8)),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.explore_outlined, size: 20,),
|
|
||||||
title: Text('Explore'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.bookmark_outline, size: 20),
|
|
||||||
title: Text('Bookmarks'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.interests_outlined, size: 20),
|
|
||||||
title: Text('Following'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.history, size: 20),
|
|
||||||
title: Text('History'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.leaderboard_outlined, size: 20),
|
|
||||||
title: Text('Ranking'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
footerItems: [
|
|
||||||
PaneItem(
|
|
||||||
icon: const Icon(MdIcons.settings_outlined, size: 20),
|
|
||||||
title: Text('Settings'.tl),
|
|
||||||
body: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
paneBodyBuilder: (pane, child) => NavigatorPopHandler(
|
PaneItem(
|
||||||
key: const Key("navigator"),
|
icon: const Icon(
|
||||||
onPop: () => navigatorKey.currentState?.pop(),
|
MdIcons.downloading,
|
||||||
child: MediaQuery.removePadding(
|
size: 20,
|
||||||
context: context,
|
),
|
||||||
removeTop: true,
|
title: Text('Downloading'.tl),
|
||||||
child: Navigator(
|
body: const SizedBox.shrink(),
|
||||||
key: navigatorKey,
|
),
|
||||||
onGenerateRoute: (settings) => AppPageRoute(
|
PaneItem(
|
||||||
builder: (context) => const RecommendationPage()),
|
icon: const Icon(
|
||||||
),
|
MdIcons.download,
|
||||||
))),
|
size: 20,
|
||||||
));
|
),
|
||||||
|
title: Text('Downloaded'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItemSeparator(),
|
||||||
|
PaneItemHeader(
|
||||||
|
header: Text("Artwork".tl).paddingBottom(4).paddingLeft(8)),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(
|
||||||
|
MdIcons.explore_outlined,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
title: Text('Explore'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.bookmark_outline, size: 20),
|
||||||
|
title: Text('Bookmarks'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.interests_outlined, size: 20),
|
||||||
|
title: Text('Following'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.history, size: 20),
|
||||||
|
title: Text('History'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.leaderboard_outlined, size: 20),
|
||||||
|
title: Text('Ranking'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItemSeparator(),
|
||||||
|
PaneItemHeader(
|
||||||
|
header: Text("Novel".tl).paddingBottom(4).paddingLeft(8)),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.featured_play_list_outlined, size: 20),
|
||||||
|
title: Text('Recommendation'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon:
|
||||||
|
const Icon(MdIcons.collections_bookmark_outlined, size: 20),
|
||||||
|
title: Text('Bookmarks'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.leaderboard_outlined, size: 20),
|
||||||
|
title: Text('Ranking'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
PaneItemSeparator(),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.settings_outlined, size: 20),
|
||||||
|
title: Text('Settings'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
paneBodyBuilder: (pane, child) => NavigatorPopHandler(
|
||||||
|
key: const Key("navigator"),
|
||||||
|
onPop: () => navigatorKey.currentState?.pop(),
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: Navigator(
|
||||||
|
key: navigatorKey,
|
||||||
|
onGenerateRoute: (settings) => AppPageRoute(
|
||||||
|
builder: (context) => const RecommendationPage()),
|
||||||
|
),
|
||||||
|
))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final pageBuilders = <Widget Function()>[
|
static final pageBuilders = <Widget Function()>[
|
||||||
@@ -176,6 +231,9 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
() => const FollowingArtworksPage(),
|
() => const FollowingArtworksPage(),
|
||||||
() => const HistoryPage(),
|
() => const HistoryPage(),
|
||||||
() => const RankingPage(),
|
() => const RankingPage(),
|
||||||
|
() => const NovelRecommendationPage(),
|
||||||
|
() => const NovelBookmarksPage(),
|
||||||
|
() => const NovelRankingPage(),
|
||||||
() => const SettingsPage(),
|
() => const SettingsPage(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -194,39 +252,58 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
height: _appBarHeight,
|
height: _appBarHeight,
|
||||||
title: () {
|
title: () {
|
||||||
if (!App.isDesktop) {
|
return StateBuilder<TitleBarController>(
|
||||||
return const Align(
|
builder: (controller) {
|
||||||
alignment: AlignmentDirectional.centerStart,
|
Widget content = Padding(
|
||||||
child: Text("pixes"),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
);
|
child: Align(
|
||||||
}
|
alignment: AlignmentDirectional.centerStart,
|
||||||
return const DragToMoveArea(
|
child: Row(
|
||||||
child: Padding(
|
children: [
|
||||||
padding: EdgeInsets.only(bottom: 4),
|
if (!App.isDesktop)
|
||||||
child: Align(
|
const Text(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
"Pixes",
|
||||||
child: Row(
|
style: TextStyle(fontSize: 13),
|
||||||
children: [
|
),
|
||||||
Text(
|
if (!App.isDesktop) const Spacer(),
|
||||||
"Pixes",
|
if (App.isDesktop)
|
||||||
style: TextStyle(fontSize: 13),
|
const Expanded(
|
||||||
),
|
child: DragToMoveArea(
|
||||||
Spacer(),
|
child: Text(
|
||||||
if(kDebugMode)
|
"Pixes",
|
||||||
Padding(
|
style: TextStyle(fontSize: 13),
|
||||||
padding: EdgeInsets.only(right: 138),
|
)),
|
||||||
child: Button(onPressed: debug, child: Text("Debug")),
|
),
|
||||||
)
|
for (var action in controller.actions)
|
||||||
],
|
Button(
|
||||||
|
onPressed: action.onPressed,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
action.icon,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(action.title),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(4).paddingLeft(4),
|
||||||
|
if (App.isDesktop) const SizedBox(width: 128),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}(),
|
}(),
|
||||||
leading: _BackButton(navigatorKey),
|
leading: _BackButton(navigatorKey),
|
||||||
actions: App.isDesktop ? WindowButtons(
|
actions: App.isDesktop
|
||||||
key: ValueKey(windowButtonKey),
|
? WindowButtons(
|
||||||
) : null,
|
key: ValueKey(windowButtonKey),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,11 +333,11 @@ class _BackButtonState extends State<_BackButton> {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
if(!mounted) {
|
if (!mounted) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
} else {
|
} else {
|
||||||
bool enabled = navigatorKey.currentState?.canPop() == true;
|
bool enabled = navigatorKey.currentState?.canPop() == true;
|
||||||
if(enabled != this.enabled) {
|
if (enabled != this.enabled) {
|
||||||
setState(() {
|
setState(() {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
});
|
});
|
||||||
@@ -301,18 +378,19 @@ class _BackButtonState extends State<_BackButton> {
|
|||||||
title: const Text("Back"),
|
title: const Text("Back"),
|
||||||
body: const SizedBox.shrink(),
|
body: const SizedBox.shrink(),
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
).build(
|
)
|
||||||
context,
|
.build(
|
||||||
false,
|
context,
|
||||||
onPressed,
|
false,
|
||||||
displayMode: PaneDisplayMode.compact,
|
onPressed,
|
||||||
).paddingTop(2),
|
displayMode: PaneDisplayMode.compact,
|
||||||
|
)
|
||||||
|
.paddingTop(2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WindowButtons extends StatelessWidget {
|
class WindowButtons extends StatelessWidget {
|
||||||
const WindowButtons({super.key});
|
const WindowButtons({super.key});
|
||||||
|
|
||||||
@@ -466,7 +544,8 @@ class UserPane extends PaneItem {
|
|||||||
child: Image(
|
child: Image(
|
||||||
height: 48,
|
height: 48,
|
||||||
width: 48,
|
width: 48,
|
||||||
image: CachedImageProvider(appdata.account!.user.profile),
|
image:
|
||||||
|
CachedImageProvider(appdata.account!.user.profile),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -489,7 +568,9 @@ class UserPane extends PaneItem {
|
|||||||
fontSize: 16, fontWeight: FontWeight.w500),
|
fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
kDebugMode ? "<hide due to debug>" : appdata.account!.user.email,
|
kDebugMode
|
||||||
|
? "<hide due to debug>"
|
||||||
|
: appdata.account!.user.email,
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
53
lib/pages/novel_bookmarks_page.dart
Normal file
53
lib/pages/novel_bookmarks_page.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/appdata.dart';
|
||||||
|
import 'package:pixes/components/grid.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
|
import 'package:pixes/components/title_bar.dart';
|
||||||
|
import 'package:pixes/foundation/widget_utils.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
class NovelBookmarksPage extends StatefulWidget {
|
||||||
|
const NovelBookmarksPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelBookmarksPage> createState() => _NovelBookmarksPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelBookmarksPageState
|
||||||
|
extends MultiPageLoadingState<NovelBookmarksPage, Novel> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Novel> data) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TitleBar(title: "Bookmarks".tl),
|
||||||
|
Expanded(
|
||||||
|
child: GridViewWithFixedItemHeight(
|
||||||
|
itemCount: data.length,
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
builder: (context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(int page) async {
|
||||||
|
if (nextUrl == "end") return Res.error("No more data");
|
||||||
|
var res = nextUrl == null
|
||||||
|
? await Network().getBookmarkedNovels(appdata.account!.user.id)
|
||||||
|
: await Network().getNovelsWithNextUrl(nextUrl!);
|
||||||
|
nextUrl = res.subData ?? "end";
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
681
lib/pages/novel_page.dart
Normal file
681
lib/pages/novel_page.dart
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/components/grid.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/md.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
|
import 'package:pixes/components/title_bar.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/comments_page.dart';
|
||||||
|
import 'package:pixes/pages/novel_reading_page.dart';
|
||||||
|
import 'package:pixes/pages/search_page.dart';
|
||||||
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
|
import 'package:pixes/utils/app_links.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
const kFluentButtonPadding = 28.0;
|
||||||
|
|
||||||
|
class NovelPage extends StatefulWidget {
|
||||||
|
const NovelPage(this.novel, {super.key});
|
||||||
|
|
||||||
|
final Novel novel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelPage> createState() => _NovelPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelPageState extends State<NovelPage> {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: buildTop(),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: buildActions(),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: buildDescription(),
|
||||||
|
),
|
||||||
|
if (widget.novel.seriesId != null)
|
||||||
|
NovelSeriesWidget(
|
||||||
|
widget.novel.seriesId!, widget.novel.seriesTitle!),
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 16 + MediaQuery.of(context).padding.bottom)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(const EdgeInsets.symmetric(horizontal: 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTop() {
|
||||||
|
return Card(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 128,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 96,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: CachedImageProvider(widget.novel.image)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.novel.title,
|
||||||
|
maxLines: 3,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Spacer(),
|
||||||
|
if (widget.novel.seriesId != null)
|
||||||
|
Text(
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
"${"Series".tl}: ${widget.novel.seriesTitle!}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorScheme.of(context).primary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).paddingVertical(4)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)).paddingTop(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildStats() {
|
||||||
|
return Container(
|
||||||
|
height: 74,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 68,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context).outlineVariant,
|
||||||
|
width: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.view,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Views".tl,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.novel.totalViews.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorScheme.of(context).primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 18),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 68,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context).outlineVariant, width: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.six_point_star,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Favorites".tl,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.novel.totalBookmarks.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorScheme.of(context).primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 18),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAuthor() {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.only(left: 2, right: 2, bottom: 12),
|
||||||
|
borderColor: ColorScheme.of(context).outlineVariant.withOpacity(0.52),
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => UserInfoPage(widget.novel.author.id.toString()));
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
height: 38,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(36),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
image: CachedImageProvider(widget.novel.author.avatar),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(widget.novel.author.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)),
|
||||||
|
Text(
|
||||||
|
widget.novel.createDate.toString().substring(0, 10),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ColorScheme.of(context).outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(MdIcons.chevron_right)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAddingFavorite = false;
|
||||||
|
|
||||||
|
Widget buildActions() {
|
||||||
|
void favorite() async {
|
||||||
|
if (isAddingFavorite) return;
|
||||||
|
setState(() {
|
||||||
|
isAddingFavorite = true;
|
||||||
|
});
|
||||||
|
var res = widget.novel.isBookmarked
|
||||||
|
? await Network().deleteFavoriteNovel(widget.novel.id.toString())
|
||||||
|
: await Network().favoriteNovel(widget.novel.id.toString());
|
||||||
|
if (res.error) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showToast(message: res.errorMessage ?? "Network Error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widget.novel.isBookmarked = !widget.novel.isBookmarked;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isAddingFavorite = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
final width = constraints.maxWidth;
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (width < 560) buildAuthor().toAlign(Alignment.centerLeft),
|
||||||
|
if (width < 560) buildStats().toAlign(Alignment.centerLeft),
|
||||||
|
if (width >= 560)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 1132),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: buildAuthor()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: buildStats()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).toAlign(Alignment.centerLeft),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
var width = constrains.maxWidth;
|
||||||
|
bool shouldFillSpace = width < 500;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
FilledButton(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(MdIcons.menu_book_outlined, size: 18),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text("Read".tl),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(MdIcons.chevron_right, size: 18)
|
||||||
|
.paddingTop(2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.fixWidth(shouldFillSpace
|
||||||
|
? width / 2 - 4 - kFluentButtonPadding
|
||||||
|
: 220)
|
||||||
|
.fixHeight(32),
|
||||||
|
onPressed: () {
|
||||||
|
context.to(() => NovelReadingPage(widget.novel));
|
||||||
|
}),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Button(
|
||||||
|
onPressed: favorite,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: constrains.maxWidth > 420
|
||||||
|
? MainAxisAlignment.start
|
||||||
|
: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (isAddingFavorite)
|
||||||
|
const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: ProgressRing(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (widget.novel.isBookmarked)
|
||||||
|
Icon(
|
||||||
|
MdIcons.favorite,
|
||||||
|
size: 18,
|
||||||
|
color: ColorScheme.of(context).error,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(MdIcons.favorite_outline, size: 18),
|
||||||
|
if (constrains.maxWidth > 420)
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
if (constrains.maxWidth > 420) Text("Favorite".tl)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.fixWidth(shouldFillSpace
|
||||||
|
? width / 4 - 4 - kFluentButtonPadding
|
||||||
|
: 64)
|
||||||
|
.fixHeight(32),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Button(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: constrains.maxWidth > 420
|
||||||
|
? MainAxisAlignment.start
|
||||||
|
: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(MdIcons.comment, size: 18),
|
||||||
|
if (constrains.maxWidth > 420)
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
if (constrains.maxWidth > 420) Text("Comments".tl)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.fixWidth(shouldFillSpace
|
||||||
|
? width / 4 - 4 - kFluentButtonPadding
|
||||||
|
: 64)
|
||||||
|
.fixHeight(32),
|
||||||
|
onPressed: () {
|
||||||
|
CommentsPage.show(context, widget.novel.id.toString(),
|
||||||
|
isNovel: true);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(2),
|
||||||
|
SelectableText(
|
||||||
|
"ID: ${widget.novel.id}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: ColorScheme.of(context).outline),
|
||||||
|
).paddingTop(8).paddingLeft(2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDescription() {
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Description".tl,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText.rich(
|
||||||
|
TextSpan(children: buildDescriptionText().toList())),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final tag in widget.novel.tags)
|
||||||
|
MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => SearchNovelResultPage(tag.name));
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 8, bottom: 6),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorScheme.of(context).primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
tag.name,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Button(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(MdIcons.bookmark_outline, size: 18),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text("Related".tl)
|
||||||
|
],
|
||||||
|
).fixWidth(64).fixHeight(32),
|
||||||
|
onPressed: () {
|
||||||
|
context
|
||||||
|
.to(() => _RelatedNovelsPage(widget.novel.id.toString()));
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<TextSpan> buildDescriptionText() sync* {
|
||||||
|
var text = widget.novel.caption;
|
||||||
|
text = text.replaceAll("<br />", "\n");
|
||||||
|
text = text.replaceAll('\n\n', '\n');
|
||||||
|
var labels = Queue<String>();
|
||||||
|
var buffer = StringBuffer();
|
||||||
|
var style = const TextStyle();
|
||||||
|
String? link;
|
||||||
|
Map<String, String> attributes = {};
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
if (text[i] == '<' && text[i + 1] != '/') {
|
||||||
|
var label =
|
||||||
|
text.substring(i + 1, text.indexOf('>', i)).split(' ').first;
|
||||||
|
labels.addLast(label);
|
||||||
|
for (var part
|
||||||
|
in text.substring(i + 1, text.indexOf('>', i)).split(' ')) {
|
||||||
|
var kv = part.split('=');
|
||||||
|
if (kv.length >= 2) {
|
||||||
|
attributes[kv[0]] =
|
||||||
|
kv.join('=').substring(kv[0].length + 2).replaceAll('"', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = text.indexOf('>', i);
|
||||||
|
} else if (text[i] == '<' && text[i + 1] == '/') {
|
||||||
|
var label = text.substring(i + 2, text.indexOf('>', i));
|
||||||
|
if (label == labels.last) {
|
||||||
|
switch (label) {
|
||||||
|
case "strong":
|
||||||
|
style = style.copyWith(fontWeight: FontWeight.bold);
|
||||||
|
case "a":
|
||||||
|
style = style.copyWith(color: ColorScheme.of(context).primary);
|
||||||
|
link = attributes["href"];
|
||||||
|
}
|
||||||
|
labels.removeLast();
|
||||||
|
}
|
||||||
|
i = text.indexOf('>', i);
|
||||||
|
} else {
|
||||||
|
buffer.write(text[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i + 1 >= text.length ||
|
||||||
|
(labels.isEmpty &&
|
||||||
|
(text[i + 1] == '<' || (i != 0 && text[i - 1] == '>')))) {
|
||||||
|
var content = buffer.toString();
|
||||||
|
var url = link;
|
||||||
|
yield TextSpan(
|
||||||
|
text: content,
|
||||||
|
style: style,
|
||||||
|
recognizer: url != null
|
||||||
|
? (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (!handleLink(Uri.parse(url))) {
|
||||||
|
launchUrlString(url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null);
|
||||||
|
buffer.clear();
|
||||||
|
link = null;
|
||||||
|
attributes.clear();
|
||||||
|
style = const TextStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NovelSeriesWidget extends StatefulWidget {
|
||||||
|
const NovelSeriesWidget(this.seriesId, this.title, {super.key});
|
||||||
|
|
||||||
|
final int seriesId;
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelSeriesWidget> createState() => _NovelSeriesWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelSeriesWidgetState
|
||||||
|
extends MultiPageLoadingState<NovelSeriesWidget, Novel> {
|
||||||
|
@override
|
||||||
|
Widget? buildFrame(BuildContext context, Widget child) {
|
||||||
|
return DecoratedSliver(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context).outlineVariant.withOpacity(0.6),
|
||||||
|
width: 0.5,
|
||||||
|
)),
|
||||||
|
sliver: SliverMainAxisGroup(slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(widget.title.trim(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)).paddingTop(16).paddingLeft(12).paddingRight(12),
|
||||||
|
),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||||
|
child
|
||||||
|
]),
|
||||||
|
).sliverPadding(const EdgeInsets.only(top: 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildLoading(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: const Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
).fixHeight(124),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildError(BuildContext context, String error) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Text(error),
|
||||||
|
).fixHeight(124),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, final List<Novel> data) {
|
||||||
|
return SliverGridViewWithFixedItemHeight(
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
childCount: data.length,
|
||||||
|
),
|
||||||
|
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(page) async {
|
||||||
|
if (nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res =
|
||||||
|
await Network().getNovelSeries(widget.seriesId.toString(), nextUrl);
|
||||||
|
if (!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NovelPageWithId extends StatefulWidget {
|
||||||
|
const NovelPageWithId(this.id, {super.key});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelPageWithId> createState() => _NovelPageWithIdState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelPageWithIdState extends LoadingState<NovelPageWithId, Novel> {
|
||||||
|
@override
|
||||||
|
Future<Res<Novel>> loadData() async {
|
||||||
|
return Network().getNovelDetail(widget.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, Novel data) {
|
||||||
|
return NovelPage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RelatedNovelsPage extends StatefulWidget {
|
||||||
|
const _RelatedNovelsPage(this.id, {super.key});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RelatedNovelsPage> createState() => __RelatedNovelsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __RelatedNovelsPageState
|
||||||
|
extends LoadingState<_RelatedNovelsPage, List<Novel>> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Novel> data) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TitleBar(title: "Related Novels".tl),
|
||||||
|
Expanded(
|
||||||
|
child: GridViewWithFixedItemHeight(
|
||||||
|
itemHeight: 164,
|
||||||
|
itemCount: data.length,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
builder: (context, index) {
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData() async {
|
||||||
|
return Network().relatedNovels(widget.id);
|
||||||
|
}
|
||||||
|
}
|
102
lib/pages/novel_ranking_page.dart
Normal file
102
lib/pages/novel_ranking_page.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
|
import 'package:pixes/components/title_bar.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../components/grid.dart';
|
||||||
|
|
||||||
|
class NovelRankingPage extends StatefulWidget {
|
||||||
|
const NovelRankingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelRankingPage> createState() => _NovelRankingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelRankingPageState extends State<NovelRankingPage> {
|
||||||
|
String type = "day";
|
||||||
|
|
||||||
|
/// mode: day, day_male, day_female, week_rookie, week, week_ai
|
||||||
|
static const types = {
|
||||||
|
"day": "Daily",
|
||||||
|
"week": "Weekly",
|
||||||
|
"day_male": "For male",
|
||||||
|
"day_female": "For female",
|
||||||
|
"week_rookie": "Rookies",
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ScaffoldPage(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
buildHeader(),
|
||||||
|
Expanded(
|
||||||
|
child: _OneRankingPage(type, key: Key(type),),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildHeader() {
|
||||||
|
return TitleBar(
|
||||||
|
title: "Ranking".tl,
|
||||||
|
action: DropDownButton(
|
||||||
|
title: Text(types[type]!.tl),
|
||||||
|
items: types.entries.map((e) => MenuFlyoutItem(
|
||||||
|
text: Text(e.value.tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
type = e.key;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneRankingPage extends StatefulWidget {
|
||||||
|
const _OneRankingPage(this.type, {super.key});
|
||||||
|
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OneRankingPage> createState() => _OneRankingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneRankingPageState extends MultiPageLoadingState<_OneRankingPage, Novel> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, final List<Novel> data) {
|
||||||
|
return GridViewWithFixedItemHeight(
|
||||||
|
itemCount: data.length,
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
builder: (context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(page) async{
|
||||||
|
if(nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = await Network().getNovelRanking(widget.type, null);
|
||||||
|
if(!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
262
lib/pages/novel_reading_page.dart
Normal file
262
lib/pages/novel_reading_page.dart
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/appdata.dart';
|
||||||
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/md.dart';
|
||||||
|
import 'package:pixes/components/page_route.dart';
|
||||||
|
import 'package:pixes/components/title_bar.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/image_page.dart';
|
||||||
|
import 'package:pixes/pages/main_page.dart';
|
||||||
|
import 'package:pixes/utils/ext.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
class NovelReadingPage extends StatefulWidget {
|
||||||
|
const NovelReadingPage(this.novel, {super.key});
|
||||||
|
|
||||||
|
final Novel novel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelReadingPage> createState() => _NovelReadingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {
|
||||||
|
TitleBarAction? action;
|
||||||
|
|
||||||
|
bool isShowingSettings = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
action = TitleBarAction(MdIcons.tune, "Settings", () {
|
||||||
|
if (!isShowingSettings) {
|
||||||
|
_NovelReadingSettings.show(context, () {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
isShowingSettings = true;
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
isShowingSettings = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
StateController.find<TitleBarController>().addAction(action!);
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
StateController.find<TitleBarController>().removeAction(action!);
|
||||||
|
});
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, String data) {
|
||||||
|
var content = buildList(context).toList();
|
||||||
|
return ScaffoldPage(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
content: SelectionArea(
|
||||||
|
child: DefaultTextStyle.merge(
|
||||||
|
style: const TextStyle(fontSize: 16.0, height: 1.6),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return content[index];
|
||||||
|
},
|
||||||
|
itemCount: content.length,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<String>> loadData() {
|
||||||
|
return Network().getNovelContent(widget.novel.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Widget> buildList(BuildContext context) sync* {
|
||||||
|
double fontSizeAdd = appdata.settings["readingFontSize"] - 16.0;
|
||||||
|
double fontHeight = appdata.settings["readingLineHeight"];
|
||||||
|
|
||||||
|
yield Text(widget.novel.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24.0 + fontSizeAdd, fontWeight: FontWeight.bold));
|
||||||
|
yield const SizedBox(height: 12.0);
|
||||||
|
yield const Divider(
|
||||||
|
style: DividerThemeData(horizontalMargin: EdgeInsets.all(0)),
|
||||||
|
);
|
||||||
|
yield const SizedBox(height: 12.0);
|
||||||
|
|
||||||
|
var novelContent = data!.split('\n');
|
||||||
|
for (var content in novelContent) {
|
||||||
|
if (content.isEmpty) continue;
|
||||||
|
if (content.startsWith('[uploadedimage:')) {
|
||||||
|
var imageId = content.nums;
|
||||||
|
yield GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ImagePage.show(["novel:${widget.novel.id.toString()}/$imageId"]);
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
height: 300,
|
||||||
|
width: double.infinity,
|
||||||
|
child: AnimatedImage(
|
||||||
|
image:
|
||||||
|
CachedNovelImageProvider(widget.novel.id.toString(), imageId),
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
height: 300,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (content.startsWith('[chapter:')) {
|
||||||
|
var title = content.replaceLast(']', '').split(':')[1];
|
||||||
|
yield Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.0 + fontSizeAdd,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: fontHeight))
|
||||||
|
.paddingBottom(8);
|
||||||
|
} else {
|
||||||
|
yield Text(content,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 16.0 + fontSizeAdd, height: fontHeight))
|
||||||
|
.paddingBottom(appdata.settings["readingParagraphSpacing"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelReadingSettings extends StatefulWidget {
|
||||||
|
const _NovelReadingSettings(this.callback);
|
||||||
|
|
||||||
|
final void Function() callback;
|
||||||
|
|
||||||
|
static void show(BuildContext context, void Function() callback) {
|
||||||
|
Navigator.of(context).push(SideBarRoute(_NovelReadingSettings(callback)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NovelReadingSettings> createState() => __NovelReadingSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __NovelReadingSettingsState extends State<_NovelReadingSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TitleBar(title: "Reading Settings".tl),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("Font Size".tl),
|
||||||
|
subtitle: Slider(
|
||||||
|
value: appdata.settings["readingFontSize"],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["readingFontSize"] = value;
|
||||||
|
});
|
||||||
|
appdata.writeSettings();
|
||||||
|
widget.callback();
|
||||||
|
},
|
||||||
|
min: 12.0,
|
||||||
|
max: 24.0,
|
||||||
|
divisions: 12,
|
||||||
|
label: appdata.settings["readingFontSize"].toString(),
|
||||||
|
),
|
||||||
|
trailing: Text(appdata.settings["readingFontSize"].toString()),
|
||||||
|
),
|
||||||
|
).paddingHorizontal(8).paddingBottom(8),
|
||||||
|
Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("Line Height".tl),
|
||||||
|
subtitle: Slider(
|
||||||
|
value: appdata.settings["readingLineHeight"],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["readingLineHeight"] = value;
|
||||||
|
});
|
||||||
|
appdata.writeSettings();
|
||||||
|
widget.callback();
|
||||||
|
},
|
||||||
|
min: 1.0,
|
||||||
|
max: 2.0,
|
||||||
|
divisions: 10,
|
||||||
|
label: appdata.settings["readingLineHeight"].toString(),
|
||||||
|
),
|
||||||
|
trailing: Text(appdata.settings["readingLineHeight"].toString()),
|
||||||
|
),
|
||||||
|
).paddingHorizontal(8).paddingBottom(8),
|
||||||
|
Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("Paragraph Spacing".tl),
|
||||||
|
subtitle: Slider(
|
||||||
|
value: appdata.settings["readingParagraphSpacing"],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["readingParagraphSpacing"] = value;
|
||||||
|
});
|
||||||
|
appdata.writeSettings();
|
||||||
|
widget.callback();
|
||||||
|
},
|
||||||
|
min: 0.0,
|
||||||
|
max: 16.0,
|
||||||
|
divisions: 8,
|
||||||
|
label: appdata.settings["readingParagraphSpacing"].toString(),
|
||||||
|
),
|
||||||
|
trailing:
|
||||||
|
Text(appdata.settings["readingParagraphSpacing"].toString()),
|
||||||
|
),
|
||||||
|
).paddingHorizontal(8).paddingBottom(8),
|
||||||
|
// 深色模式
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("Theme".tl),
|
||||||
|
trailing: DropDownButton(
|
||||||
|
title: Text(appdata.settings["theme"] ?? "System".tl),
|
||||||
|
items: [
|
||||||
|
MenuFlyoutItem(
|
||||||
|
text: Text("System".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["theme"] = "System";
|
||||||
|
});
|
||||||
|
appdata.writeData();
|
||||||
|
StateController.findOrNull(tag: "MyApp")?.update();
|
||||||
|
}),
|
||||||
|
MenuFlyoutItem(
|
||||||
|
text: Text("light".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["theme"] = "Light";
|
||||||
|
});
|
||||||
|
appdata.writeData();
|
||||||
|
StateController.findOrNull(tag: "MyApp")?.update();
|
||||||
|
}),
|
||||||
|
MenuFlyoutItem(
|
||||||
|
text: Text("dark".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["theme"] = "Dark";
|
||||||
|
});
|
||||||
|
appdata.writeData();
|
||||||
|
StateController.findOrNull(tag: "MyApp")?.update();
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
lib/pages/novel_recommendation_page.dart
Normal file
46
lib/pages/novel_recommendation_page.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/grid.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
|
import 'package:pixes/components/title_bar.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
class NovelRecommendationPage extends StatefulWidget {
|
||||||
|
const NovelRecommendationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NovelRecommendationPage> createState() =>
|
||||||
|
_NovelRecommendationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NovelRecommendationPageState
|
||||||
|
extends MultiPageLoadingState<NovelRecommendationPage, Novel> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Novel> data) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TitleBar(title: "Recommendation".tl),
|
||||||
|
Expanded(
|
||||||
|
child: GridViewWithFixedItemHeight(
|
||||||
|
itemCount: data.length,
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
builder: (context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(int page) {
|
||||||
|
return Network().getRecommendNovels();
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,7 @@ import '../components/illust_widget.dart';
|
|||||||
import '../components/loading.dart';
|
import '../components/loading.dart';
|
||||||
import '../components/title_bar.dart';
|
import '../components/title_bar.dart';
|
||||||
import '../network/network.dart';
|
import '../network/network.dart';
|
||||||
|
import 'illust_page.dart';
|
||||||
|
|
||||||
class RankingPage extends StatefulWidget {
|
class RankingPage extends StatefulWidget {
|
||||||
const RankingPage({super.key});
|
const RankingPage({super.key});
|
||||||
@@ -97,7 +98,13 @@ class _OneRankingPageState extends MultiPageLoadingState<_OneRankingPage, Illust
|
|||||||
if(index == data.length - 1){
|
if(index == data.length - 1){
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(data[index], onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data,
|
||||||
|
initialPage: index,
|
||||||
|
nextUrl: nextUrl
|
||||||
|
));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:pixes/components/loading.dart';
|
|||||||
import 'package:pixes/components/title_bar.dart';
|
import 'package:pixes/components/title_bar.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/pages/illust_page.dart';
|
||||||
import 'package:pixes/utils/translation.dart';
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
import '../components/grid.dart';
|
import '../components/grid.dart';
|
||||||
@@ -28,8 +29,11 @@ class _RecommendationPageState extends State<RecommendationPage> {
|
|||||||
buildTab(),
|
buildTab(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: type != 2
|
child: type != 2
|
||||||
? _RecommendationArtworksPage(type, key: Key(type.toString()),)
|
? _RecommendationArtworksPage(
|
||||||
: const _RecommendationUsersPage(),
|
type,
|
||||||
|
key: Key(type.toString()),
|
||||||
|
)
|
||||||
|
: const _RecommendationUsersPage(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -45,7 +49,7 @@ class _RecommendationPageState extends State<RecommendationPage> {
|
|||||||
SegmentedButtonOption(2, "Users".tl),
|
SegmentedButtonOption(2, "Users".tl),
|
||||||
],
|
],
|
||||||
onPressed: (key) {
|
onPressed: (key) {
|
||||||
if(key != type) {
|
if (key != type) {
|
||||||
setState(() {
|
setState(() {
|
||||||
type = key;
|
type = key;
|
||||||
});
|
});
|
||||||
@@ -57,32 +61,42 @@ class _RecommendationPageState extends State<RecommendationPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _RecommendationArtworksPage extends StatefulWidget {
|
class _RecommendationArtworksPage extends StatefulWidget {
|
||||||
const _RecommendationArtworksPage(this.type, {super.key});
|
const _RecommendationArtworksPage(this.type, {super.key});
|
||||||
|
|
||||||
final int type;
|
final int type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_RecommendationArtworksPage> createState() => _RecommendationArtworksPageState();
|
State<_RecommendationArtworksPage> createState() =>
|
||||||
|
_RecommendationArtworksPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecommendationArtworksPageState extends MultiPageLoadingState<_RecommendationArtworksPage, Illust> {
|
class _RecommendationArtworksPageState
|
||||||
|
extends MultiPageLoadingState<_RecommendationArtworksPage, Illust> {
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, final List<Illust> data) {
|
Widget buildContent(BuildContext context, final List<Illust> data) {
|
||||||
return LayoutBuilder(builder: (context, constrains){
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
return MasonryGridView.builder(
|
return MasonryGridView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8)
|
padding: const EdgeInsets.symmetric(horizontal: 8) +
|
||||||
+ EdgeInsets.only(bottom: context.padding.bottom),
|
EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 240,
|
maxCrossAxisExtent: 240,
|
||||||
),
|
),
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if(index == data.length - 1){
|
if (index == data.length - 1) {
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(
|
||||||
|
data[index],
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data,
|
||||||
|
initialPage: index,
|
||||||
|
nextUrl: Network.recommendationUrl,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -100,33 +114,32 @@ class _RecommendationUsersPage extends StatefulWidget {
|
|||||||
const _RecommendationUsersPage();
|
const _RecommendationUsersPage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_RecommendationUsersPage> createState() => _RecommendationUsersPageState();
|
State<_RecommendationUsersPage> createState() =>
|
||||||
|
_RecommendationUsersPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecommendationUsersPageState extends MultiPageLoadingState<_RecommendationUsersPage, UserPreview> {
|
class _RecommendationUsersPageState
|
||||||
|
extends MultiPageLoadingState<_RecommendationUsersPage, UserPreview> {
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, List<UserPreview> data) {
|
Widget buildContent(BuildContext context, List<UserPreview> data) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGridViewWithFixedItemHeight(
|
SliverGridViewWithFixedItemHeight(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
(context, index) {
|
if (index == data.length - 1) {
|
||||||
if(index == data.length - 1){
|
nextPage();
|
||||||
nextPage();
|
}
|
||||||
}
|
return UserPreviewWidget(data[index]);
|
||||||
return UserPreviewWidget(data[index]);
|
}, childCount: data.length),
|
||||||
},
|
minCrossAxisExtent: 440,
|
||||||
childCount: data.length
|
itemHeight: 136,
|
||||||
),
|
|
||||||
maxCrossAxisExtent: 520,
|
|
||||||
itemHeight: 114,
|
|
||||||
).sliverPaddingHorizontal(8)
|
).sliverPaddingHorizontal(8)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Res<List<UserPreview>>> loadData(page) async{
|
Future<Res<List<UserPreview>>> loadData(page) async {
|
||||||
var res = await Network().getRecommendationUsers();
|
var res = await Network().getRecommendationUsers();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,13 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|||||||
import 'package:pixes/appdata.dart';
|
import 'package:pixes/appdata.dart';
|
||||||
import 'package:pixes/components/loading.dart';
|
import 'package:pixes/components/loading.dart';
|
||||||
import 'package:pixes/components/message.dart';
|
import 'package:pixes/components/message.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
import 'package:pixes/components/page_route.dart';
|
import 'package:pixes/components/page_route.dart';
|
||||||
import 'package:pixes/components/user_preview.dart';
|
import 'package:pixes/components/user_preview.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
import 'package:pixes/pages/illust_page.dart';
|
import 'package:pixes/pages/illust_page.dart';
|
||||||
|
import 'package:pixes/pages/novel_page.dart';
|
||||||
import 'package:pixes/pages/user_info_page.dart';
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
import 'package:pixes/utils/translation.dart';
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
@@ -39,11 +41,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
void search() {
|
void search() {
|
||||||
switch(searchType) {
|
switch (searchType) {
|
||||||
case 0:
|
case 0:
|
||||||
context.to(() => SearchResultPage(text));
|
context.to(() => SearchResultPage(text));
|
||||||
case 1:
|
case 1:
|
||||||
showToast(context, message: "Not implemented");
|
context.to(() => SearchNovelResultPage(text));
|
||||||
case 2:
|
case 2:
|
||||||
context.to(() => SearchUserResultPage(text));
|
context.to(() => SearchUserResultPage(text));
|
||||||
case 3:
|
case 3:
|
||||||
@@ -51,7 +53,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
case 4:
|
case 4:
|
||||||
context.to(() => UserInfoPage(text));
|
context.to(() => UserInfoPage(text));
|
||||||
case 5:
|
case 5:
|
||||||
showToast(context, message: "Not implemented");
|
context.to(() => NovelPageWithId(text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +64,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
buildSearchBar(),
|
buildSearchBar(),
|
||||||
const SizedBox(height: 8,),
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: _TrendingTagsView(),
|
child: _TrendingTagsView(),
|
||||||
)
|
)
|
||||||
@@ -130,7 +134,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4,),
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
Button(
|
Button(
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
height: 42,
|
height: 42,
|
||||||
@@ -139,7 +145,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(SideBarRoute(const SearchSettings()));
|
Navigator.of(context).push(SideBarRoute(SearchSettings(
|
||||||
|
isNovel: searchType == 1,
|
||||||
|
)));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -169,12 +177,13 @@ class _TrendingTagsView extends StatefulWidget {
|
|||||||
State<_TrendingTagsView> createState() => _TrendingTagsViewState();
|
State<_TrendingTagsView> createState() => _TrendingTagsViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<TrendingTag>> {
|
class _TrendingTagsViewState
|
||||||
|
extends LoadingState<_TrendingTagsView, List<TrendingTag>> {
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, List<TrendingTag> data) {
|
Widget buildContent(BuildContext context, List<TrendingTag> data) {
|
||||||
return MasonryGridView.builder(
|
return MasonryGridView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0)
|
padding: const EdgeInsets.symmetric(horizontal: 8.0) +
|
||||||
+ EdgeInsets.only(bottom: context.padding.bottom),
|
EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 240,
|
maxCrossAxisExtent: 240,
|
||||||
),
|
),
|
||||||
@@ -189,7 +198,7 @@ class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<Trendi
|
|||||||
final illust = tag.illust;
|
final illust = tag.illust;
|
||||||
|
|
||||||
var text = tag.tag.name;
|
var text = tag.tag.name;
|
||||||
if(tag.tag.translatedName != null) {
|
if (tag.tag.translatedName != null) {
|
||||||
text += "/${tag.tag.translatedName}";
|
text += "/${tag.tag.translatedName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,18 +215,19 @@ class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<Trendi
|
|||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: (){
|
onTap: () {
|
||||||
context.to(() => SearchResultPage(tag.tag.name));
|
context.to(() => SearchResultPage(tag.tag.name));
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(child: ClipRRect(
|
Positioned.fill(
|
||||||
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
child: AnimatedImage(
|
child: AnimatedImage(
|
||||||
image: CachedImageProvider(illust.images.first.medium),
|
image: CachedImageProvider(illust.images.first.medium),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: width-16.0,
|
width: width - 16.0,
|
||||||
height: height-16.0,
|
height: height - 16.0,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -226,10 +236,14 @@ class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<Trendi
|
|||||||
right: 0,
|
right: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.84),
|
color: FluentTheme.of(context)
|
||||||
borderRadius: BorderRadius.circular(4)
|
.micaBackgroundColor
|
||||||
),
|
.withOpacity(0.84),
|
||||||
child: Text(text).paddingHorizontal(4).paddingVertical(6).paddingBottom(2),
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
child: Text(text)
|
||||||
|
.paddingHorizontal(4)
|
||||||
|
.paddingVertical(6)
|
||||||
|
.paddingBottom(2),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -248,10 +262,12 @@ class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List<Trendi
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SearchSettings extends StatefulWidget {
|
class SearchSettings extends StatefulWidget {
|
||||||
const SearchSettings({this.onChanged, super.key});
|
const SearchSettings({this.onChanged, this.isNovel = false, super.key});
|
||||||
|
|
||||||
final void Function()? onChanged;
|
final void Function()? onChanged;
|
||||||
|
|
||||||
|
final bool isNovel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SearchSettings> createState() => _SearchSettingsState();
|
State<SearchSettings> createState() => _SearchSettingsState();
|
||||||
}
|
}
|
||||||
@@ -264,113 +280,139 @@ class _SearchSettingsState extends State<SearchSettings> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||||
child: Text("Search Settings".tl, style: const TextStyle(fontSize: 18),),
|
child: Text(
|
||||||
|
"Search Settings".tl,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
).toAlign(Alignment.centerLeft),
|
).toAlign(Alignment.centerLeft),
|
||||||
buildItem(title: "Match".tl, child: DropDownButton(
|
buildItem(
|
||||||
title: Text(appdata.searchOptions.matchType.toString().tl),
|
title: "Match".tl,
|
||||||
items: KeywordMatchType.values.map((e) =>
|
child: DropDownButton(
|
||||||
MenuFlyoutItem(
|
title: Text(appdata.searchOptions.matchType.toString().tl),
|
||||||
text: Text(e.toString().tl),
|
items: KeywordMatchType.values
|
||||||
onPressed: () {
|
.map((e) => MenuFlyoutItem(
|
||||||
if(appdata.searchOptions.matchType != e) {
|
text: Text(e.toString().tl),
|
||||||
setState(() => appdata.searchOptions.matchType = e);
|
onPressed: () {
|
||||||
widget.onChanged?.call();
|
if (appdata.searchOptions.matchType != e) {
|
||||||
}
|
setState(() => appdata.searchOptions.matchType = e);
|
||||||
}
|
widget.onChanged?.call();
|
||||||
)
|
}
|
||||||
).toList(),
|
}))
|
||||||
)),
|
.toList(),
|
||||||
buildItem(title: "Favorite number".tl, child: DropDownButton(
|
|
||||||
title: Text(appdata.searchOptions.favoriteNumber.toString().tl),
|
|
||||||
items: FavoriteNumber.values.map((e) =>
|
|
||||||
MenuFlyoutItem(
|
|
||||||
text: Text(e.toString().tl),
|
|
||||||
onPressed: () {
|
|
||||||
if(appdata.searchOptions.favoriteNumber != e) {
|
|
||||||
setState(() => appdata.searchOptions.favoriteNumber = e);
|
|
||||||
widget.onChanged?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toList(),
|
|
||||||
)),
|
|
||||||
buildItem(title: "Sort".tl, child: DropDownButton(
|
|
||||||
title: Text(appdata.searchOptions.sort.toString().tl),
|
|
||||||
items: SearchSort.values.map((e) =>
|
|
||||||
MenuFlyoutItem(
|
|
||||||
text: Text(e.toString().tl),
|
|
||||||
onPressed: () {
|
|
||||||
if(appdata.searchOptions.sort != e) {
|
|
||||||
setState(() => appdata.searchOptions.sort = e);
|
|
||||||
widget.onChanged?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toList(),
|
|
||||||
)),
|
|
||||||
Card(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text("Start Time".tl, style: const TextStyle(fontSize: 16),)
|
|
||||||
.paddingVertical(8)
|
|
||||||
.toAlign(Alignment.centerLeft)
|
|
||||||
.paddingLeft(16),
|
|
||||||
DatePicker(
|
|
||||||
selected: appdata.searchOptions.startTime,
|
|
||||||
onChanged: (t) {
|
|
||||||
if(appdata.searchOptions.startTime != t) {
|
|
||||||
setState(() => appdata.searchOptions.startTime = t);
|
|
||||||
widget.onChanged?.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
Card(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text("End Time".tl, style: const TextStyle(fontSize: 16),)
|
|
||||||
.paddingVertical(8)
|
|
||||||
.toAlign(Alignment.centerLeft)
|
|
||||||
.paddingLeft(16),
|
|
||||||
DatePicker(
|
|
||||||
selected: appdata.searchOptions.endTime,
|
|
||||||
onChanged: (t) {
|
|
||||||
if(appdata.searchOptions.endTime != t) {
|
|
||||||
setState(() => appdata.searchOptions.endTime = t);
|
|
||||||
widget.onChanged?.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
buildItem(title: "Age limit".tl, child: DropDownButton(
|
if (!widget.isNovel)
|
||||||
title: Text(appdata.searchOptions.ageLimit.toString().tl),
|
buildItem(
|
||||||
items: AgeLimit.values.map((e) =>
|
title: "Favorite number".tl,
|
||||||
MenuFlyoutItem(
|
child: DropDownButton(
|
||||||
text: Text(e.toString().tl),
|
title:
|
||||||
onPressed: () {
|
Text(appdata.searchOptions.favoriteNumber.toString().tl),
|
||||||
if(appdata.searchOptions.ageLimit != e) {
|
items: FavoriteNumber.values
|
||||||
setState(() => appdata.searchOptions.ageLimit = e);
|
.map((e) => MenuFlyoutItem(
|
||||||
widget.onChanged?.call();
|
text: Text(e.toString().tl),
|
||||||
}
|
onPressed: () {
|
||||||
}
|
if (appdata.searchOptions.favoriteNumber != e) {
|
||||||
)
|
setState(() =>
|
||||||
).toList(),
|
appdata.searchOptions.favoriteNumber = e);
|
||||||
)),
|
widget.onChanged?.call();
|
||||||
SizedBox(height: context.padding.bottom,)
|
}
|
||||||
|
}))
|
||||||
|
.toList(),
|
||||||
|
)),
|
||||||
|
buildItem(
|
||||||
|
title: "Sort".tl,
|
||||||
|
child: DropDownButton(
|
||||||
|
title: Text(appdata.searchOptions.sort.toString().tl),
|
||||||
|
items: SearchSort.values
|
||||||
|
.map((e) => MenuFlyoutItem(
|
||||||
|
text: Text(e.toString().tl),
|
||||||
|
onPressed: () {
|
||||||
|
if (appdata.searchOptions.sort != e) {
|
||||||
|
setState(() => appdata.searchOptions.sort = e);
|
||||||
|
widget.onChanged?.call();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.toList(),
|
||||||
|
)),
|
||||||
|
if (!widget.isNovel)
|
||||||
|
Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Start Time".tl,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
)
|
||||||
|
.paddingVertical(8)
|
||||||
|
.toAlign(Alignment.centerLeft)
|
||||||
|
.paddingLeft(16),
|
||||||
|
DatePicker(
|
||||||
|
selected: appdata.searchOptions.startTime,
|
||||||
|
onChanged: (t) {
|
||||||
|
if (appdata.searchOptions.startTime != t) {
|
||||||
|
setState(() => appdata.searchOptions.startTime = t);
|
||||||
|
widget.onChanged?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (!widget.isNovel)
|
||||||
|
Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"End Time".tl,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
)
|
||||||
|
.paddingVertical(8)
|
||||||
|
.toAlign(Alignment.centerLeft)
|
||||||
|
.paddingLeft(16),
|
||||||
|
DatePicker(
|
||||||
|
selected: appdata.searchOptions.endTime,
|
||||||
|
onChanged: (t) {
|
||||||
|
if (appdata.searchOptions.endTime != t) {
|
||||||
|
setState(() => appdata.searchOptions.endTime = t);
|
||||||
|
widget.onChanged?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (!widget.isNovel)
|
||||||
|
buildItem(
|
||||||
|
title: "Age limit".tl,
|
||||||
|
child: DropDownButton(
|
||||||
|
title: Text(appdata.searchOptions.ageLimit.toString().tl),
|
||||||
|
items: AgeLimit.values
|
||||||
|
.map((e) => MenuFlyoutItem(
|
||||||
|
text: Text(e.toString().tl),
|
||||||
|
onPressed: () {
|
||||||
|
if (appdata.searchOptions.ageLimit != e) {
|
||||||
|
setState(
|
||||||
|
() => appdata.searchOptions.ageLimit = e);
|
||||||
|
widget.onChanged?.call();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.toList(),
|
||||||
|
)),
|
||||||
|
SizedBox(
|
||||||
|
height: context.padding.bottom,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -388,7 +430,6 @@ class _SearchSettingsState extends State<SearchSettings> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchResultPage extends StatefulWidget {
|
class SearchResultPage extends StatefulWidget {
|
||||||
const SearchResultPage(this.keyword, {super.key});
|
const SearchResultPage(this.keyword, {super.key});
|
||||||
|
|
||||||
@@ -398,7 +439,8 @@ class SearchResultPage extends StatefulWidget {
|
|||||||
State<SearchResultPage> createState() => _SearchResultPageState();
|
State<SearchResultPage> createState() => _SearchResultPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Illust> {
|
class _SearchResultPageState
|
||||||
|
extends MultiPageLoadingState<SearchResultPage, Illust> {
|
||||||
late String keyword = widget.keyword;
|
late String keyword = widget.keyword;
|
||||||
|
|
||||||
late String oldKeyword = widget.keyword;
|
late String oldKeyword = widget.keyword;
|
||||||
@@ -406,7 +448,7 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
|||||||
late final controller = TextEditingController(text: widget.keyword);
|
late final controller = TextEditingController(text: widget.keyword);
|
||||||
|
|
||||||
void search() {
|
void search() {
|
||||||
if(keyword != oldKeyword) {
|
if (keyword != oldKeyword) {
|
||||||
oldKeyword = keyword;
|
oldKeyword = keyword;
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
@@ -423,15 +465,23 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
|||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) {
|
(context, index) {
|
||||||
if(index == data.length - 1){
|
if (index == data.length - 1) {
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(
|
||||||
|
data[index],
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data, initialPage: index, nextUrl: nextUrl));
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
childCount: data.length,
|
childCount: data.length,
|
||||||
),
|
),
|
||||||
).sliverPaddingHorizontal(8),
|
).sliverPaddingHorizontal(8),
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom),)
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -475,7 +525,9 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4,),
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
Button(
|
Button(
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
height: 42,
|
height: 42,
|
||||||
@@ -483,12 +535,13 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
|||||||
child: Icon(FluentIcons.settings),
|
child: Icon(FluentIcons.settings),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async{
|
onPressed: () async {
|
||||||
bool isChanged = false;
|
bool isChanged = false;
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context)
|
||||||
SideBarRoute(SearchSettings(
|
.push(SideBarRoute(SearchSettings(
|
||||||
onChanged: () => isChanged = true,)));
|
onChanged: () => isChanged = true,
|
||||||
if(isChanged) {
|
)));
|
||||||
|
if (isChanged) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -507,14 +560,14 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
|||||||
String? nextUrl;
|
String? nextUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Res<List<Illust>>> loadData(page) async{
|
Future<Res<List<Illust>>> loadData(page) async {
|
||||||
if(nextUrl == "end") {
|
if (nextUrl == "end") {
|
||||||
return Res.error("No more data");
|
return Res.error("No more data");
|
||||||
}
|
}
|
||||||
var res = nextUrl == null
|
var res = nextUrl == null
|
||||||
? await Network().search(keyword, appdata.searchOptions)
|
? await Network().search(keyword, appdata.searchOptions)
|
||||||
: await Network().getIllustsWithNextUrl(nextUrl!);
|
: await Network().getIllustsWithNextUrl(nextUrl!);
|
||||||
if(!res.error) {
|
if (!res.error) {
|
||||||
nextUrl = res.subData;
|
nextUrl = res.subData;
|
||||||
nextUrl ??= "end";
|
nextUrl ??= "end";
|
||||||
}
|
}
|
||||||
@@ -531,30 +584,31 @@ class SearchUserResultPage extends StatefulWidget {
|
|||||||
State<SearchUserResultPage> createState() => _SearchUserResultPageState();
|
State<SearchUserResultPage> createState() => _SearchUserResultPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchUserResultPageState extends MultiPageLoadingState<SearchUserResultPage, UserPreview> {
|
class _SearchUserResultPageState
|
||||||
|
extends MultiPageLoadingState<SearchUserResultPage, UserPreview> {
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, final List<UserPreview> data) {
|
Widget buildContent(BuildContext context, final List<UserPreview> data) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Text("${"Search".tl}: ${widget.keyword}",
|
child: Text(
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),)
|
"${"Search".tl}: ${widget.keyword}",
|
||||||
.paddingVertical(12).paddingHorizontal(16),
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
).paddingVertical(12).paddingHorizontal(16),
|
||||||
),
|
),
|
||||||
SliverGridViewWithFixedItemHeight(
|
SliverGridViewWithFixedItemHeight(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
(context, index) {
|
if (index == data.length - 1) {
|
||||||
if(index == data.length - 1){
|
nextPage();
|
||||||
nextPage();
|
}
|
||||||
}
|
return UserPreviewWidget(data[index]);
|
||||||
return UserPreviewWidget(data[index]);
|
}, childCount: data.length),
|
||||||
},
|
minCrossAxisExtent: 440,
|
||||||
childCount: data.length
|
itemHeight: 136,
|
||||||
),
|
|
||||||
maxCrossAxisExtent: 520,
|
|
||||||
itemHeight: 114,
|
|
||||||
).sliverPaddingHorizontal(8),
|
).sliverPaddingHorizontal(8),
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom),)
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -562,12 +616,12 @@ class _SearchUserResultPageState extends MultiPageLoadingState<SearchUserResultP
|
|||||||
String? nextUrl;
|
String? nextUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Res<List<UserPreview>>> loadData(page) async{
|
Future<Res<List<UserPreview>>> loadData(page) async {
|
||||||
if(nextUrl == "end") {
|
if (nextUrl == "end") {
|
||||||
return Res.error("No more data");
|
return Res.error("No more data");
|
||||||
}
|
}
|
||||||
var res = await Network().searchUsers(widget.keyword, nextUrl);
|
var res = await Network().searchUsers(widget.keyword, nextUrl);
|
||||||
if(!res.error) {
|
if (!res.error) {
|
||||||
nextUrl = res.subData;
|
nextUrl = res.subData;
|
||||||
nextUrl ??= "end";
|
nextUrl ??= "end";
|
||||||
}
|
}
|
||||||
@@ -575,3 +629,141 @@ class _SearchUserResultPageState extends MultiPageLoadingState<SearchUserResultP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchNovelResultPage extends StatefulWidget {
|
||||||
|
const SearchNovelResultPage(this.keyword, {super.key});
|
||||||
|
|
||||||
|
final String keyword;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchNovelResultPage> createState() => _SearchNovelResultPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchNovelResultPageState
|
||||||
|
extends MultiPageLoadingState<SearchNovelResultPage, Novel> {
|
||||||
|
late String keyword = widget.keyword;
|
||||||
|
|
||||||
|
late String oldKeyword = widget.keyword;
|
||||||
|
|
||||||
|
late final controller = TextEditingController(text: widget.keyword);
|
||||||
|
|
||||||
|
void search() {
|
||||||
|
if (keyword != oldKeyword) {
|
||||||
|
oldKeyword = keyword;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, final List<Novel> data) {
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
buildSearchBar(),
|
||||||
|
SliverGridViewWithFixedItemHeight(
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
childCount: data.length,
|
||||||
|
),
|
||||||
|
).sliverPaddingHorizontal(8),
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSearchBar() {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: constrains.maxWidth,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextBox(
|
||||||
|
controller: controller,
|
||||||
|
placeholder: "Search artworks".tl,
|
||||||
|
onChanged: (s) => keyword = s,
|
||||||
|
onSubmitted: (s) => search(),
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context)
|
||||||
|
.outlineVariant
|
||||||
|
.withOpacity(0.6)),
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
suffix: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: search,
|
||||||
|
child: const Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 16,
|
||||||
|
).paddingHorizontal(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(FluentIcons.settings),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
bool isChanged = false;
|
||||||
|
await Navigator.of(context)
|
||||||
|
.push(SideBarRoute(SearchSettings(
|
||||||
|
onChanged: () => isChanged = true,
|
||||||
|
isNovel: true,
|
||||||
|
)));
|
||||||
|
if (isChanged) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).sliverPadding(const EdgeInsets.only(top: 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(page) async {
|
||||||
|
if (nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = nextUrl == null
|
||||||
|
? await Network().searchNovels(keyword, appdata.searchOptions)
|
||||||
|
: await Network().getNovelsWithNextUrl(nextUrl!);
|
||||||
|
if (!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -408,9 +408,10 @@ ${"Some keywords will be replaced by the following rule:".tl}
|
|||||||
\${author} -> ${"Name of the author".tl}
|
\${author} -> ${"Name of the author".tl}
|
||||||
\${id} -> ${"Artwork ID".tl}
|
\${id} -> ${"Artwork ID".tl}
|
||||||
\${index} -> ${"Index of the image in the artwork".tl}
|
\${index} -> ${"Index of the image in the artwork".tl}
|
||||||
|
\${page} -> ${"Replace with '-p\${index}' if the work have more than one images, otherwise replace with blank.".tl}
|
||||||
\${ext} -> ${"File extension".tl}
|
\${ext} -> ${"File extension".tl}
|
||||||
\${AI} -> ${"Replace with 'AI' if the work was generated by AI, otherwise replace with blank".tl}
|
\${AI} -> ${"Replace with 'AI' if the work was generated by AI, otherwise replace with blank".tl}
|
||||||
\${tag{*}} -> ${"Replace with * if the work have tag *, otherwise replace with blank.".tl}
|
\${tag(*)} -> ${"Replace with * if the work have tag *, otherwise replace with blank.".tl}
|
||||||
|
|
||||||
${"Multiple path separators will be automatically replaced with a single".tl}
|
${"Multiple path separators will be automatically replaced with a single".tl}
|
||||||
""";
|
""";
|
||||||
|
@@ -3,8 +3,12 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:pixes/appdata.dart';
|
import 'package:pixes/appdata.dart';
|
||||||
import 'package:pixes/components/batch_download.dart';
|
import 'package:pixes/components/batch_download.dart';
|
||||||
|
import 'package:pixes/components/grid.dart';
|
||||||
import 'package:pixes/components/loading.dart';
|
import 'package:pixes/components/loading.dart';
|
||||||
import 'package:pixes/components/md.dart';
|
import 'package:pixes/components/md.dart';
|
||||||
|
import 'package:pixes/components/novel.dart';
|
||||||
|
import 'package:pixes/components/segmented_button.dart';
|
||||||
|
import 'package:pixes/components/user_preview.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/foundation/image_provider.dart';
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
@@ -13,6 +17,7 @@ import 'package:pixes/utils/translation.dart';
|
|||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../components/illust_widget.dart';
|
import '../components/illust_widget.dart';
|
||||||
|
import 'illust_page.dart';
|
||||||
|
|
||||||
class UserInfoPage extends StatefulWidget {
|
class UserInfoPage extends StatefulWidget {
|
||||||
const UserInfoPage(this.id, {this.followCallback, super.key});
|
const UserInfoPage(this.id, {this.followCallback, super.key});
|
||||||
@@ -26,21 +31,30 @@ class UserInfoPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
||||||
|
int page = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, UserDetails data) {
|
Widget buildContent(BuildContext context, UserDetails data) {
|
||||||
return ScaffoldPage(
|
return ScaffoldPage(
|
||||||
content: CustomScrollView(
|
content: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
buildUser(),
|
buildUser(),
|
||||||
buildInformation(),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: buildHeader(
|
child: buildHeader("Related users".tl),
|
||||||
"Artworks",
|
),
|
||||||
action: BatchDownloadButton(
|
_RelatedUsers(widget.id),
|
||||||
request: () => Network().getUserIllusts(widget.id))
|
buildInformation(),
|
||||||
),),
|
buildArtworkHeader(),
|
||||||
_UserArtworks(data.id.toString(), key: ValueKey(data.id),),
|
if (page == 2)
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
_UserNovels(widget.id)
|
||||||
|
else
|
||||||
|
_UserArtworks(
|
||||||
|
data.id.toString(),
|
||||||
|
page,
|
||||||
|
key: ValueKey(data.id + page),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -48,23 +62,24 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
|
|
||||||
bool isFollowing = false;
|
bool isFollowing = false;
|
||||||
|
|
||||||
void follow() async{
|
void follow() async {
|
||||||
if(isFollowing) return;
|
if (isFollowing) return;
|
||||||
String type = "";
|
String type = "";
|
||||||
if(!data!.isFollowed) {
|
if (!data!.isFollowed) {
|
||||||
await flyoutController.showFlyout(
|
await flyoutController.showFlyout(
|
||||||
navigatorKey: App.rootNavigatorKey.currentState,
|
navigatorKey: App.rootNavigatorKey.currentState,
|
||||||
builder: (context) =>
|
builder: (context) => MenuFlyout(
|
||||||
MenuFlyout(
|
|
||||||
items: [
|
items: [
|
||||||
MenuFlyoutItem(text: Text("Public".tl),
|
MenuFlyoutItem(
|
||||||
|
text: Text("Public".tl),
|
||||||
onPressed: () => type = "public"),
|
onPressed: () => type = "public"),
|
||||||
MenuFlyoutItem(text: Text("Private".tl),
|
MenuFlyoutItem(
|
||||||
|
text: Text("Private".tl),
|
||||||
onPressed: () => type = "private"),
|
onPressed: () => type = "private"),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if(type.isEmpty && !data!.isFollowed) {
|
if (type.isEmpty && !data!.isFollowed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -72,8 +87,8 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
});
|
});
|
||||||
var method = data!.isFollowed ? "delete" : "add";
|
var method = data!.isFollowed ? "delete" : "add";
|
||||||
var res = await Network().follow(data!.id.toString(), method, type);
|
var res = await Network().follow(data!.id.toString(), method, type);
|
||||||
if(res.error) {
|
if (res.error) {
|
||||||
if(mounted) {
|
if (mounted) {
|
||||||
context.showToast(message: "Network Error");
|
context.showToast(message: "Network Error");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -96,7 +111,8 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
height: 64,
|
height: 64,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(64),
|
borderRadius: BorderRadius.circular(64),
|
||||||
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6)),
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context).outlineVariant, width: 0.6)),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(64),
|
borderRadius: BorderRadius.circular(64),
|
||||||
child: Image(
|
child: Image(
|
||||||
@@ -105,47 +121,60 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
height: 64,
|
height: 64,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(data!.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
Text(data!.name,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(text: 'Follows: '.tl),
|
TextSpan(text: 'Follows: '.tl),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${data!.totalFollowUsers}',
|
text: '${data!.totalFollowUsers}',
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = (() => context.to(() => FollowingUsersPage(widget.id))),
|
..onTap = (() =>
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: FluentTheme.of(context).accentColor)
|
context.to(() => FollowingUsersPage(widget.id))),
|
||||||
),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FluentTheme.of(context).accentColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
if(widget.id != appdata.account?.user.id)
|
if (widget.id != appdata.account?.user.id)
|
||||||
const SizedBox(height: 8,),
|
const SizedBox(
|
||||||
if(widget.id != appdata.account?.user.id)
|
height: 8,
|
||||||
if(isFollowing)
|
),
|
||||||
Button(onPressed: follow, child: const SizedBox(
|
if (widget.id != appdata.account?.user.id)
|
||||||
width: 42,
|
if (isFollowing)
|
||||||
height: 24,
|
Button(
|
||||||
child: Center(
|
onPressed: follow,
|
||||||
child: SizedBox.square(
|
child: const SizedBox(
|
||||||
dimension: 18,
|
width: 42,
|
||||||
child: ProgressRing(strokeWidth: 2,),
|
height: 24,
|
||||||
),
|
child: Center(
|
||||||
),
|
child: SizedBox.square(
|
||||||
))
|
dimension: 18,
|
||||||
|
child: ProgressRing(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
else if (!data!.isFollowed)
|
else if (!data!.isFollowed)
|
||||||
FlyoutTarget(
|
FlyoutTarget(
|
||||||
controller: flyoutController,
|
controller: flyoutController,
|
||||||
child: Button(onPressed: follow, child: Text("Follow".tl))
|
child: Button(onPressed: follow, child: Text("Follow".tl)))
|
||||||
)
|
|
||||||
else
|
else
|
||||||
Button(
|
Button(
|
||||||
onPressed: follow,
|
onPressed: follow,
|
||||||
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
|
child: Text(
|
||||||
|
"Unfollow".tl,
|
||||||
|
style: TextStyle(color: ColorScheme.of(context).error),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -154,31 +183,78 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
|
|
||||||
Widget buildHeader(String title, {Widget? action}) {
|
Widget buildHeader(String title, {Widget? action}) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 38,
|
height: 38,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
).toAlign(Alignment.centerLeft),
|
).toAlign(Alignment.centerLeft),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if(action != null)
|
if (action != null) action.toAlign(Alignment.centerRight)
|
||||||
action.toAlign(Alignment.centerRight)
|
],
|
||||||
],
|
).paddingHorizontal(16))
|
||||||
).paddingHorizontal(16)).paddingTop(8);
|
.paddingTop(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildArtworkHeader() {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 38,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SegmentedButton<int>(
|
||||||
|
options: [
|
||||||
|
SegmentedButtonOption(0, "Artworks".tl),
|
||||||
|
SegmentedButtonOption(1, "Bookmarks".tl),
|
||||||
|
SegmentedButtonOption(2, "Novels".tl),
|
||||||
|
],
|
||||||
|
value: page,
|
||||||
|
onPressed: (value) {
|
||||||
|
setState(() {
|
||||||
|
page = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (page != 2)
|
||||||
|
BatchDownloadButton(
|
||||||
|
request: () {
|
||||||
|
if (page == 0) {
|
||||||
|
return Network().getUserIllusts(data!.id.toString());
|
||||||
|
} else {
|
||||||
|
return Network()
|
||||||
|
.getUserBookmarks(data!.id.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16))
|
||||||
|
.paddingTop(12),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildInformation() {
|
Widget buildInformation() {
|
||||||
Widget buildItem({IconData? icon, required String title, required String? content, Widget? trailing}) {
|
Widget buildItem(
|
||||||
if(content == null || content.isEmpty) {
|
{IconData? icon,
|
||||||
|
required String title,
|
||||||
|
required String? content,
|
||||||
|
Widget? trailing}) {
|
||||||
|
if (content == null || content.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: icon == null ? null : Icon(icon, size: 20,),
|
leading: icon == null
|
||||||
|
? null
|
||||||
|
: Icon(
|
||||||
|
icon,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
subtitle: SelectableText(content),
|
subtitle: SelectableText(content),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
@@ -190,30 +266,46 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
buildHeader("Information".tl),
|
buildHeader("Information".tl),
|
||||||
buildItem(icon: MdIcons.comment_outlined, title: "Introduction".tl, content: data!.comment),
|
buildItem(
|
||||||
buildItem(icon: MdIcons.cake_outlined, title: "Birthday".tl, content: data!.birth),
|
icon: MdIcons.comment_outlined,
|
||||||
buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data!.region),
|
title: "Introduction".tl,
|
||||||
buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data!.job),
|
content: data!.comment),
|
||||||
buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data!.gender),
|
buildItem(
|
||||||
|
icon: MdIcons.cake_outlined,
|
||||||
|
title: "Birthday".tl,
|
||||||
|
content: data!.birth),
|
||||||
|
buildItem(
|
||||||
|
icon: MdIcons.location_city_outlined,
|
||||||
|
title: "Region",
|
||||||
|
content: data!.region),
|
||||||
|
buildItem(
|
||||||
|
icon: MdIcons.work_outline, title: "Job".tl, content: data!.job),
|
||||||
|
buildItem(
|
||||||
|
icon: MdIcons.person_2_outlined,
|
||||||
|
title: "Gender".tl,
|
||||||
|
content: data!.gender),
|
||||||
buildHeader("Social Network".tl),
|
buildHeader("Social Network".tl),
|
||||||
buildItem(title: "Webpage",
|
buildItem(
|
||||||
|
title: "Webpage",
|
||||||
content: data!.webpage,
|
content: data!.webpage,
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(MdIcons.open_in_new, size: 18),
|
icon: const Icon(MdIcons.open_in_new, size: 18),
|
||||||
onPressed: () => launchUrlString(data!.twitterUrl!)
|
onPressed: () => launchUrlString(data!.twitterUrl!))),
|
||||||
)),
|
buildItem(
|
||||||
buildItem(title: "Twitter",
|
title: "Twitter",
|
||||||
content: data!.twitterUrl,
|
content: data!.twitterUrl,
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(MdIcons.open_in_new, size: 18),
|
icon: const Icon(MdIcons.open_in_new, size: 18),
|
||||||
onPressed: () => launchUrlString(data!.twitterUrl!)
|
onPressed: () => launchUrlString(data!.twitterUrl!))),
|
||||||
)),
|
buildItem(
|
||||||
buildItem(title: "pawoo",
|
title: "pawoo",
|
||||||
content: data!.pawooUrl,
|
content: data!.pawooUrl,
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(MdIcons.open_in_new, size: 18,),
|
icon: const Icon(
|
||||||
onPressed: () => launchUrlString(data!.pawooUrl!)
|
MdIcons.open_in_new,
|
||||||
)),
|
size: 18,
|
||||||
|
),
|
||||||
|
onPressed: () => launchUrlString(data!.pawooUrl!))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -226,10 +318,12 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UserArtworks extends StatefulWidget {
|
class _UserArtworks extends StatefulWidget {
|
||||||
const _UserArtworks(this.uid, {super.key});
|
const _UserArtworks(this.uid, this.type, {super.key});
|
||||||
|
|
||||||
final String uid;
|
final String uid;
|
||||||
|
|
||||||
|
final int type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_UserArtworks> createState() => _UserArtworksState();
|
State<_UserArtworks> createState() => _UserArtworksState();
|
||||||
}
|
}
|
||||||
@@ -254,7 +348,9 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(FluentIcons.info),
|
const Icon(FluentIcons.info),
|
||||||
const SizedBox(width: 4,),
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
Text(error)
|
Text(error)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -270,11 +366,14 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
|
|||||||
maxCrossAxisExtent: 240,
|
maxCrossAxisExtent: 240,
|
||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) {
|
(context, index) {
|
||||||
if(index == data.length - 1){
|
if (index == data.length - 1) {
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
return IllustWidget(data[index]);
|
return IllustWidget(data[index], onTap: () {
|
||||||
|
context.to(() => IllustGalleryPage(
|
||||||
|
illusts: data, initialPage: index, nextUrl: nextUrl));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
childCount: data.length,
|
childCount: data.length,
|
||||||
),
|
),
|
||||||
@@ -284,14 +383,16 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
|
|||||||
String? nextUrl;
|
String? nextUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Res<List<Illust>>> loadData(page) async{
|
Future<Res<List<Illust>>> loadData(page) async {
|
||||||
if(nextUrl == "end") {
|
if (nextUrl == "end") {
|
||||||
return Res.error("No more data");
|
return Res.error("No more data");
|
||||||
}
|
}
|
||||||
var res = nextUrl == null
|
var res = nextUrl == null
|
||||||
? await Network().getUserIllusts(widget.uid)
|
? (widget.type == 0
|
||||||
|
? await Network().getUserIllusts(widget.uid)
|
||||||
|
: await Network().getUserBookmarks(widget.uid))
|
||||||
: await Network().getIllustsWithNextUrl(nextUrl!);
|
: await Network().getIllustsWithNextUrl(nextUrl!);
|
||||||
if(!res.error) {
|
if (!res.error) {
|
||||||
nextUrl = res.subData;
|
nextUrl = res.subData;
|
||||||
nextUrl ??= "end";
|
nextUrl ??= "end";
|
||||||
}
|
}
|
||||||
@@ -299,3 +400,135 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _UserNovels extends StatefulWidget {
|
||||||
|
const _UserNovels(this.uid, {super.key});
|
||||||
|
|
||||||
|
final String uid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_UserNovels> createState() => _UserNovelsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserNovelsState extends MultiPageLoadingState<_UserNovels, Novel> {
|
||||||
|
@override
|
||||||
|
Widget buildLoading(BuildContext context) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: ProgressRing(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildError(context, error) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(FluentIcons.info),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
Text(error)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Novel> data) {
|
||||||
|
return SliverGridViewWithFixedItemHeight(
|
||||||
|
itemHeight: 164,
|
||||||
|
minCrossAxisExtent: 400,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (index == data.length - 1) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
return NovelWidget(data[index]);
|
||||||
|
},
|
||||||
|
childCount: data.length,
|
||||||
|
),
|
||||||
|
).sliverPaddingHorizontal(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Novel>>> loadData(page) async {
|
||||||
|
if (nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = nextUrl == null
|
||||||
|
? await Network().getUserNovels(widget.uid)
|
||||||
|
: await Network().getNovelsWithNextUrl(nextUrl!);
|
||||||
|
if (!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RelatedUsers extends StatefulWidget {
|
||||||
|
const _RelatedUsers(this.uid);
|
||||||
|
|
||||||
|
final String uid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RelatedUsers> createState() => _RelatedUsersState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RelatedUsersState
|
||||||
|
extends LoadingState<_RelatedUsers, List<UserPreview>> {
|
||||||
|
@override
|
||||||
|
Widget buildFrame(BuildContext context, Widget child) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 146,
|
||||||
|
width: double.infinity,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ScrollController _controller = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<UserPreview> data) {
|
||||||
|
Widget content = Scrollbar(
|
||||||
|
controller: _controller,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
padding: const EdgeInsets.only(bottom: 8, left: 8),
|
||||||
|
primary: false,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return UserPreviewWidget(data[index]).fixWidth(342);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if (MediaQuery.of(context).size.width > 500) {
|
||||||
|
content = ScrollbarTheme.merge(
|
||||||
|
data: const ScrollbarThemeData(
|
||||||
|
thickness: 6,
|
||||||
|
hoveringThickness: 6,
|
||||||
|
mainAxisMargin: 4,
|
||||||
|
hoveringPadding: EdgeInsets.zero,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
hoveringMainAxisMargin: 4),
|
||||||
|
child: content);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<UserPreview>>> loadData() {
|
||||||
|
return Network().relatedUsers(widget.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,9 @@ import 'dart:io';
|
|||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/foundation/log.dart';
|
import 'package:pixes/foundation/log.dart';
|
||||||
|
import 'package:pixes/pages/illust_page.dart';
|
||||||
|
import 'package:pixes/pages/novel_page.dart';
|
||||||
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
import 'package:win32_registry/win32_registry.dart';
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
Future<void> _register(String scheme) async {
|
Future<void> _register(String scheme) async {
|
||||||
@@ -37,5 +40,36 @@ void handleLinks() async {
|
|||||||
if (onLink?.call(uri) == true) {
|
if (onLink?.call(uri) == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
handleLink(uri);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool handleLink(Uri uri) {
|
||||||
|
if (uri.scheme == "pixiv") {
|
||||||
|
var path = uri.toString().split("/").sublist(2);
|
||||||
|
if (path.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (path[0]) {
|
||||||
|
case "users":
|
||||||
|
if (path.length == 2) {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(() => UserInfoPage(path[1]));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "novels":
|
||||||
|
if (path.length == 2) {
|
||||||
|
App.mainNavigatorKey?.currentContext
|
||||||
|
?.to(() => NovelPageWithId(path[1]));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "illusts":
|
||||||
|
if (path.length == 2) {
|
||||||
|
App.mainNavigatorKey?.currentContext
|
||||||
|
?.to(() => IllustPageWithId(path[1]));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<false/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
|
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.2+3
|
version: 1.0.4+104
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.4 <4.0.0'
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
@@ -36,7 +36,7 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
window_manager: ^0.3.8
|
window_manager: ^0.3.8
|
||||||
fluent_ui: ^4.8.7
|
fluent_ui: ^4.8.7
|
||||||
system_theme: ^2.3.1
|
dynamic_color: ^1.7.0
|
||||||
dio: ^5.4.3
|
dio: ^5.4.3
|
||||||
crypto:
|
crypto:
|
||||||
intl:
|
intl:
|
||||||
|
BIN
screenshots/1.png
Normal file
BIN
screenshots/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 702 KiB |
Reference in New Issue
Block a user