22 Commits

Author SHA1 Message Date
wgh19
eee1141970 update readme.md 2024-06-14 13:09:37 +08:00
wgh19
50a69f77b6 improve ui 2024-06-14 12:50:27 +08:00
wgh19
790ed54d5b Emphasize artworks from following artists 2024-06-14 12:38:50 +08:00
wgh19
21fe14f88b improve ui; update version code 2024-06-14 12:22:37 +08:00
wgh19
a03ad12837 check update 2024-06-13 23:01:01 +08:00
wgh19
426257716f fix save_to_gallery 2024-06-13 22:48:52 +08:00
wgh19
67e01ea69f catch error 2024-06-13 22:24:21 +08:00
wgh19
6cf9ce9c96 improve ui 2024-06-13 20:54:31 +08:00
wgh19
593899af8c disable acrylic effect for windows 10 2024-06-13 12:40:34 +08:00
wgh19
908c26d764 update readme.md 2024-06-12 22:34:53 +08:00
wgh19
eef1af3ad1 update build script 2024-06-12 22:09:29 +08:00
nyne
b97c7cde25 add linux workflow 2024-06-12 21:58:26 +08:00
nyne
6118fc30f8 update version code; add linux build script 2024-06-12 21:51:20 +08:00
nyne
4476ad7f90 linux support 2024-06-12 21:30:23 +08:00
wgh19
7c8fabf52c add shortcuts 2024-06-12 19:17:32 +08:00
wgh19
70da478044 improve login 2024-06-12 17:03:45 +08:00
wgh19
b14c2682a7 save to gallery 2024-06-12 16:45:39 +08:00
wgh19
54b64fb19b improve ui 2024-06-12 16:05:50 +08:00
wgh19
d247455c19 local history 2024-06-12 15:43:06 +08:00
wgh19
759d6959b5 improve login and logout 2024-06-11 17:27:48 +08:00
wgh19
488be5fb1a add retry 2024-06-05 22:54:42 +08:00
wgh19
62b50c466e update readme 2024-06-01 22:47:28 +08:00
44 changed files with 1244 additions and 238 deletions

23
.github/workflows/linux.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Build Linux
run-name: Build Linux
on:
workflow_dispatch: {}
jobs:
Build_Deb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
architecture: x64
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
dart pub global activate flutter_to_debian
- run: python3 debian/build.py
- uses: actions/upload-artifact@v3
with:
name: deb_build
path: build/linux/x64/release/debian

View File

@@ -1,9 +1,52 @@
# pixes
非官方 Pixiv app, 支持 Windows, Android, iOS, macOS
[![flutter](https://img.shields.io/badge/flutter-3.22.1-blue)](https://flutter.dev/)
[![License](https://img.shields.io/github/license/wgh136/pixes)](https://github.com/wgh136/pixes/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/wgh136/pixes)](https://github.com/wgh136/pixes)
[![stars](https://img.shields.io/github/stars/wgh136/pixes)](https://github.com/wgh136/pixes/stargazers)
[![Telegram Discussion](https://img.shields.io/static/v1?label=Discussion&message=Telegram&color=blue&logo=telegram)](https://t.me/pica_group)
非官方 Pixiv app, 支持 Windows, Android, iOS, macOS, linux
主要功能均已实现
## 下载
在 [Release](https://github.com/wgh136/pixes/releases) 页面下载
## 从源代码构建
### 准备工作
安装Stable版的Flutter
### 构建Android
将你的证书文件(`key.jks`, `key.properties`)放在`android`目录下
执行`flutter build apk`构建apk
### 构建iOS
执行`flutter build ios`构建iOS程序
### 构建Windows
执行`python windows/build_windows.py`构建Windows程序
### 构建macOS
执行`flutter build macos`构建macOS程序
### 构建Linux
执行`python3 debian/build.py`构建deb包
如果你使用其他发行版, 请注意`.desktop`文件中需要注册 URI Scheme `pixiv`
## 屏幕截图
<img src="screenshots/1.png" style="width: 400px">
<img src="screenshots/2.png" style="width: 400px">
<img src="screenshots/3.png" style="width: 400px">
<img src="screenshots/4.png" style="width: 400px">

View File

@@ -5,6 +5,7 @@
<application
android:label="pixes"
android:name="${applicationName}"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"

View File

@@ -10,7 +10,7 @@
"History": "历史",
"Ranking": "排行",
"Settings": "设置",
"Artworks": "插画",
"Artworks": "作品",
"Mangas": "漫画",
"Users": "用户",
"Search artwork": "搜索作品",
@@ -112,7 +112,6 @@
"Multiple path separators will be automatically replaced with a single": "多个路径分隔符将被自动替换为单个",
"Login": "登录",
"You need to complete the login operation in the browser window that will open.": "您需要在打开的浏览器窗口中完成登录操作",
"I have read and agree to the Terms of Use": "我已阅读并同意使用条款",
"Waiting..." : "等待中...",
"Waiting for authentication. Please finished in the browser." : "等待验证. 请在浏览器中完成.",
"Back" : "返回",
@@ -162,7 +161,28 @@
"Current quantity": "当前数量",
"Display the original image on the details page": "在详情页显示原图",
"Open link": "打开链接",
"Read": "阅读"
"Read": "阅读",
"Error": "错误",
"Failed to register URL scheme.": "注册URL协议失败",
"Retry": "重试",
"Network": "网络",
"Save to gallery": "保存到相册",
"Choose a way to login": "选择登录方式",
"Use Webview: you cannot sign in with Google.": "使用Webview: 无法使用Google登录",
"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application": "使用外部浏览器: 可以使用Google登录. 但是, 一些浏览器可能与应用程序不兼容",
"External browser": "外部浏览器",
"Show comments": "显示评论",
"Show original image": "显示原图",
"Illustrations": "插画",
"New version available": "新版本可用",
"A new version of Pixes is available. Do you want to update now?" : "Pixes有新版本可用. 您要立即更新吗?",
"Update": "更新",
"Check for updates": "检查更新",
"Check for updates on startup": "启动时检查更新",
"I understand pixes is a free unofficial application.": "我了解Pixes是一个免费的非官方应用程序",
"Related Artworks": "相关作品",
"Emphasize artworks from following artists": "强调关注画师的作品",
"The border of the artworks will be darker": "作品的边框将被加深"
},
"zh_TW": {
"Search": "搜索",
@@ -277,7 +297,6 @@
"Multiple path separators will be automatically replaced with a single": "多個路徑分隔符號將自動替換為單一",
"Login": "登錄",
"You need to complete the login operation in the browser window that will open.": "您需要在打開的瀏覽器窗口中完成登錄操作",
"I have read and agree to the Terms of Use": "我已閱讀並同意使用條款",
"Waiting..." : "等待中...",
"Waiting for authentication. Please finished in the browser." : "等待驗證. 請在瀏覽器中完成.",
"Back" : "返回",
@@ -327,6 +346,27 @@
"Current quantity": "當前數量",
"Display the original image on the details page": "在詳情頁顯示原圖",
"Open link": "打開鏈接",
"Read": "閱讀"
"Read": "閱讀",
"Error": "錯誤",
"Failed to register URL scheme.": "註冊URL協議失敗",
"Retry": "重試",
"Network": "網絡",
"Save to gallery": "保存到相冊",
"Choose a way to login": "選擇登錄方式",
"Use Webview: you cannot sign in with Google.": "使用Webview: 無法使用Google登錄",
"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application": "使用外部瀏覽器: 可以使用Google登錄. 但是, 一些瀏覽器可能與應用程序不兼容",
"External browser": "外部瀏覽器",
"Show comments": "顯示評論",
"Show original image": "顯示原圖",
"Illustrations": "插畫",
"New version available": "新版本可用",
"A new version of Pixes is available. Do you want to update now?" : "Pixes有新版本可用. 您要立即更新嗎?",
"Update": "更新",
"Check for updates": "檢查更新",
"Check for updates on startup": "啟動時檢查更新",
"I understand pixes is a free unofficial application.": "我了解Pixes是一個免費的非官方應用程序",
"Related Artworks": "相關作品",
"Emphasize artworks from following artists": "強調關注畫師的作品",
"The border of the artworks will be darker": "作品的邊框將被加深"
}
}

27
debian/build.py vendored Normal file
View File

@@ -0,0 +1,27 @@
import subprocess
import os
debianContent = ''
desktopContent = ''
version = ''
with open('debian/debian.yaml', 'r') as f:
debianContent = f.read()
with open('debian/gui/pixes.desktop', 'r') as f:
desktopContent = f.read()
with open('pubspec.yaml', 'r') as f:
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
with open('debian/debian.yaml', 'w') as f:
f.write(debianContent.replace('{{Version}}', version))
with open('debian/gui/pixes.desktop', 'w') as f:
f.write(desktopContent.replace('{{Version}}', version))
subprocess.run(["flutter", "build", "linux"])
subprocess.run(["$HOME/.pub-cache/bin/flutter_to_debian"], shell=True)
with open('debian/debian.yaml', 'w') as f:
f.write(debianContent)
with open('debian/gui/pixes.desktop', 'w') as f:
f.write(desktopContent)

18
debian/debian.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
flutter_app:
command: pixes
arch: x64
parent: /usr/local/lib
nonInteractive: true
execFieldCodes: u
control:
Package: pixes
Version: {{Version}}
Architecture: amd64
Priority: optional
Depends:
Maintainer: nyne
Description: Unofficial pixiv application
#options:
# exec_out_dir: debian/packages

10
debian/gui/pixes.desktop vendored Normal file
View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Version={{Version}}
Name=Pixes
GenericName=Pixes
Comment=Unofficial pixiv application
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;share;images;
MimeType=x-scheme-handler/pixiv;

BIN
debian/gui/pixes.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -56,5 +56,9 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</dict>
</array>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>photo</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>photo</string>
</dict>
</plist>

View File

@@ -32,8 +32,12 @@ class _Appdata {
LogicalKeyboardKey.enter.keyId,
LogicalKeyboardKey.keyD.keyId,
LogicalKeyboardKey.keyF.keyId,
LogicalKeyboardKey.keyC.keyId,
LogicalKeyboardKey.keyG.keyId,
],
"showOriginalImage": false,
"checkUpdate": true,
"emphasizeArtworksFromFollowingArtists": true,
};
bool lock = false;
@@ -63,14 +67,25 @@ class _Appdata {
Future<void> readData() async {
final file = File("${App.dataPath}/account.json");
if (file.existsSync()) {
account = Account.fromJson(jsonDecode(await file.readAsString()));
var json = jsonDecode(await file.readAsString());
if (json != null) {
account = Account.fromJson(json);
}
}
final settingsFile = File("${App.dataPath}/settings.json");
if (settingsFile.existsSync()) {
var json = jsonDecode(await settingsFile.readAsString());
for (var key in json.keys) {
if(json[key] != null) {
settings[key] = json[key];
if (json[key] != null) {
if (json[key] is List && settings[key] is List) {
for (int i = 0;
i < json[key].length && i < settings[key].length;
i++) {
settings[key][i] = json[key][i];
}
} else {
settings[key] = json[key];
}
}
}
}

100
lib/components/button.dart Normal file
View File

@@ -0,0 +1,100 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/foundation/app.dart';
abstract class BaseButton extends StatelessWidget {
const BaseButton({this.enabled = true, this.isLoading = false, super.key});
final bool enabled;
final bool isLoading;
Widget buildNormal(BuildContext context);
Widget buildLoading(BuildContext context);
Widget buildDisabled(BuildContext context);
@override
Widget build(BuildContext context) {
if (isLoading) {
return buildLoading(context);
} else if (enabled) {
return buildNormal(context);
} else {
return buildDisabled(context);
}
}
}
class FluentButton extends BaseButton {
const FluentButton({
required this.onPressed,
required this.child,
this.width,
super.enabled,
super.isLoading,
super.key,
});
final void Function() onPressed;
final Widget child;
final double? width;
static const _kFluentButtonPadding = 12.0;
@override
Widget buildNormal(BuildContext context) {
Widget child = this.child;
if (width != null) {
child = child.fixWidth(width! - _kFluentButtonPadding * 2);
}
return FilledButton(
onPressed: onPressed,
child: child,
);
}
@override
Widget buildLoading(BuildContext context) {
Widget child = Center(
widthFactor: 1,
heightFactor: 1,
child: const ProgressRing(
strokeWidth: 1.6,
).fixWidth(14).fixHeight(14),
);
if (width != null) {
child = child.fixWidth(width! - _kFluentButtonPadding * 2);
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: _kFluentButtonPadding, vertical: 6.5),
decoration: BoxDecoration(
color: FluentTheme.of(context).inactiveBackgroundColor,
borderRadius: BorderRadius.circular(4)),
child: child,
);
}
@override
Widget buildDisabled(BuildContext context) {
Widget child = Center(
widthFactor: 1,
heightFactor: 1,
child: this.child,
);
if (width != null) {
child = child.fixWidth(width! - _kFluentButtonPadding * 2);
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: _kFluentButtonPadding, vertical: 6.5),
decoration: BoxDecoration(
color: FluentTheme.of(context).inactiveBackgroundColor,
borderRadius: BorderRadius.circular(4)),
child: child,
);
}
}

View File

@@ -1,8 +1,11 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/appdata.dart';
import 'package:pixes/components/animated_image.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/history.dart';
import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/network/download.dart';
import 'package:pixes/pages/related_page.dart';
import 'package:pixes/utils/translation.dart';
import '../network/network.dart';
@@ -65,8 +68,22 @@ class _IllustWidgetState extends State<IllustWidget> {
height: height,
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Card(
child: Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
border: () {
var emphasis = widget.illust.author.isFollowed &&
appdata.settings[
'emphasizeArtworksFromFollowingArtists'];
var color = emphasis
? ColorScheme.of(context).primary
: ColorScheme.of(context)
.outlineVariant
.withOpacity(0.64);
var width = emphasis ? 1.6 : 1.0;
return Border.all(color: color, width: width);
}()),
margin: EdgeInsets.zero,
child: GestureDetector(
onTap: widget.onTap ??
@@ -74,6 +91,7 @@ class _IllustWidgetState extends State<IllustWidget> {
context.to(() => IllustPage(widget.illust));
},
onSecondaryTapUp: showMenu,
onLongPress: showMenu,
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: AnimatedImage(
@@ -210,13 +228,13 @@ class _IllustWidgetState extends State<IllustWidget> {
});
}
void showMenu(TapUpDetails details) {
void showMenu([TapUpDetails? details]) {
// This calculates the position of the flyout according to the parent navigator
final targetContext = contextAttachKey.currentContext;
if (targetContext == null) return;
final box = targetContext.findRenderObject() as RenderBox;
final position = box.localToGlobal(
details.localPosition,
Offset? position = box.localToGlobal(
details?.localPosition ?? box.size.center(Offset.zero),
ancestor: Navigator.of(context).context.findRenderObject(),
);
@@ -242,6 +260,12 @@ class _IllustWidgetState extends State<IllustWidget> {
context.showToast(message: "Added");
DownloadManager().addDownloadingTask(widget.illust);
}),
MenuFlyoutItem(
text: Text("Related Artworks".tl),
onPressed: () {
context.to(
() => RelatedIllustsPage(widget.illust.id.toString()));
}),
],
);
},
@@ -307,3 +331,161 @@ class _IllustWidgetState extends State<IllustWidget> {
);
}
}
class IllustHistoryWidget extends StatelessWidget {
const IllustHistoryWidget(this.illust, {super.key});
final IllustHistory illust;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
final width = constrains.maxWidth;
final height = illust.height * width / illust.width;
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
Positioned.fill(
child: Container(
width: width,
height: height,
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Card(
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
child: GestureDetector(
onTap: () {
context.to(() => IllustPageWithId(illust.id.toString()));
},
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: AnimatedImage(
image: CachedImageProvider(illust.imgPath),
fit: BoxFit.cover,
width: width - 16.0,
height: height - 16.0,
),
),
),
),
)),
if (illust.imageCount > 1)
Positioned(
top: 12,
left: 12,
child: Container(
width: 28,
height: 20,
decoration: BoxDecoration(
color: FluentTheme.of(context)
.micaBackgroundColor
.withOpacity(0.72),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
),
child: Center(
child: Text(
"${illust.imageCount}P",
style: const TextStyle(fontSize: 12),
),
)),
),
if (illust.isAi)
Positioned(
bottom: 12,
left: 12,
child: Container(
width: 28,
height: 20,
decoration: BoxDecoration(
color: ColorScheme.of(context)
.errorContainer
.withOpacity(0.8),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
),
child: const Center(
child: Text(
"AI",
style: TextStyle(fontSize: 12),
),
)),
),
if (illust.isGif)
Positioned(
bottom: 12,
left: 12,
child: Container(
width: 28,
height: 20,
decoration: BoxDecoration(
color: ColorScheme.of(context)
.primaryContainer
.withOpacity(0.8),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
),
child: const Center(
child: Text(
"GIF",
style: TextStyle(fontSize: 12),
),
)),
),
if (illust.isR18)
Positioned(
bottom: 12,
right: 12,
child: Container(
width: 28,
height: 20,
decoration: BoxDecoration(
color: ColorScheme.of(context).errorContainer,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
),
child: const Center(
child: Text(
"R18",
style: TextStyle(fontSize: 12),
),
)),
),
if (illust.isR18G)
Positioned(
bottom: 12,
right: 12,
child: Container(
width: 28,
height: 20,
decoration: BoxDecoration(
color: ColorScheme.of(context).errorContainer,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
),
child: const Center(
child: Text(
"R18G",
style: TextStyle(fontSize: 12),
),
)),
),
],
),
);
});
}
}

View File

@@ -40,12 +40,11 @@ class KeyEventListenerState extends State<KeyEventListener> {
focusNode: focusNode,
autofocus: true,
onKeyEvent: (node, event) {
if (event is! KeyUpEvent) return KeyEventResult.ignored;
if (event is! KeyUpEvent) return KeyEventResult.handled;
if (event.logicalKey == LogicalKeyboardKey.escape) {
if (App.rootNavigatorKey.currentState?.canPop() ?? false) {
App.rootNavigatorKey.currentState?.pop();
}
if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {
} else if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {
App.mainNavigatorKey?.currentState?.pop();
}
return KeyEventResult.handled;

View File

@@ -4,6 +4,7 @@ typedef MdIcons = md.Icons;
typedef MdTheme = md.Theme;
typedef MdThemeData = md.ThemeData;
typedef MdColorScheme = md.ColorScheme;
typedef TextField = md.TextField;
class ColorScheme {
static md.ColorScheme of(md.BuildContext context) {

View File

@@ -12,7 +12,7 @@ export "state_controller.dart";
export "navigation.dart";
class _App {
final version = "1.0.6";
final version = "1.0.8";
bool get isAndroid => Platform.isAndroid;
bool get isIOS => Platform.isIOS;

102
lib/foundation/history.dart Normal file
View File

@@ -0,0 +1,102 @@
import 'package:pixes/foundation/app.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:pixes/network/models.dart';
class IllustHistory {
final int id;
final String imgPath;
final DateTime time;
final int imageCount;
final bool isR18;
final bool isR18G;
final bool isAi;
final bool isGif;
final int width;
final int height;
IllustHistory(this.id, this.imgPath, this.time, this.imageCount, this.isR18,
this.isR18G, this.isAi, this.isGif, this.width, this.height);
}
class HistoryManager {
static HistoryManager? instance;
factory HistoryManager() => instance ??= HistoryManager._create();
HistoryManager._create();
late Database _db;
init() {
_db = sqlite3.open("${App.dataPath}/history.db");
_db.execute('''
create table if not exists history (
id integer primary key not null,
imgPath text not null,
time integer not null,
imageCount integer not null,
isR18 integer not null,
isR18g integer not null,
isAi integer not null,
isGif integer not null,
width integer not null,
height integer not null
)
''');
}
void addHistory(Illust illust) {
var time = DateTime.now();
_db.execute('''
insert or replace into history (id, imgPath, time, imageCount, isR18, isR18g, isAi, isGif, width, height)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', [
illust.id,
illust.images.first.medium,
time.millisecondsSinceEpoch,
illust.pageCount,
illust.isR18 ? 1 : 0,
illust.isR18G ? 1 : 0,
illust.isAi ? 1 : 0,
illust.isUgoira ? 1 : 0,
illust.width,
illust.height
]);
if(length > 1000) {
_db.execute('''
delete from history where id in (
select id from history order by time asc limit 100
)
''');
}
}
List<IllustHistory> getHistories(int page) {
var rows = _db.select('''
select * from history order by time desc
limit 20 offset ?
''', [(page - 1) * 20]);
List<IllustHistory> res = [];
for (var row in rows) {
res.add(IllustHistory(
row['id'],
row['imgPath'],
DateTime.fromMillisecondsSinceEpoch(row['time']),
row['imageCount'],
row['isR18'] == 1,
row['isR18g'] == 1,
row['isAi'] == 1,
row['isGif'] == 1,
row['width'],
row['height']));
}
return res;
}
int get length {
var rows = _db.select('''
select count(*) from history
''');
return rows.first.values.first! as int;
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:pixes/utils/ext.dart';
@@ -26,6 +28,9 @@ class Log {
static bool ignoreLimitation = false;
/// only for debug
static const String? logFile = null;
static void printWarning(String text) {
print('\x1B[33m$text\x1B[0m');
}
@@ -57,6 +62,9 @@ class Log {
}
_logs.add(newLog);
if(logFile != null) {
File(logFile!).writeAsString(newLog.toString(), mode: FileMode.append);
}
if (_logs.length > maxLogNumber) {
var res = _logs.remove(
_logs.firstWhereOrNull((element) => element.level == LogLevel.info));

View File

@@ -1,3 +1,4 @@
import "dart:async";
import "dart:ui";
import "package:dynamic_color/dynamic_color.dart";
@@ -11,45 +12,58 @@ import "package:pixes/components/keyboard.dart";
import "package:pixes/components/md.dart";
import "package:pixes/components/message.dart";
import "package:pixes/foundation/app.dart";
import "package:pixes/foundation/history.dart";
import "package:pixes/foundation/log.dart";
import "package:pixes/network/app_dio.dart";
import "package:pixes/pages/main_page.dart";
import "package:pixes/utils/app_links.dart";
import "package:pixes/utils/loop.dart";
import "package:pixes/utils/translation.dart";
import "package:pixes/utils/update.dart";
import "package:pixes/utils/window.dart";
import "package:window_manager/window_manager.dart";
void main() async {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
Log.error("Unhandled", "${details.exception}\n${details.stack}");
};
setSystemProxy();
await App.init();
await appdata.readData();
await Translation.init();
handleLinks();
if (App.isDesktop) {
await flutter_acrylic.Window.initialize();
if (App.isWindows) {
await flutter_acrylic.Window.hideWindowControls();
void main() {
runZonedGuarded(() async {
Future.delayed(const Duration(seconds: 3), checkUpdate);
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
Log.error("Unhandled", "${details.exception}\n${details.stack}");
};
setSystemProxy();
await App.init();
await appdata.readData();
await Translation.init();
HistoryManager().init();
handleLinks();
if (App.isDesktop) {
await flutter_acrylic.Window.initialize();
if (App.isWindows) {
await flutter_acrylic.Window.hideWindowControls();
}
await WindowManager.instance.ensureInitialized();
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setTitleBarStyle(
TitleBarStyle.hidden,
windowButtonVisibility: false,
);
if (App.isLinux) {
// https://github.com/leanflutter/window_manager/issues/460
return;
}
await windowManager.setMinimumSize(const Size(500, 600));
var placement = await WindowPlacement.loadFromFile();
await placement.applyToWindow();
await windowManager.show();
Loop.register(WindowPlacement.loop);
});
}
await WindowManager.instance.ensureInitialized();
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setTitleBarStyle(
TitleBarStyle.hidden,
windowButtonVisibility: false,
);
await windowManager.setMinimumSize(const Size(500, 600));
var placement = await WindowPlacement.loadFromFile();
await placement.applyToWindow();
await windowManager.show();
Loop.register(WindowPlacement.loop);
});
}
Loop.start();
runApp(const MyApp());
Loop.start();
Log.info("APP", "Application started");
runApp(const MyApp());
}, (error, stack) {
Log.error("Unhandled Exception", "$error\n$stack");
});
}
class MyApp extends StatelessWidget {
@@ -144,7 +158,7 @@ class MyApp extends StatelessWidget {
),
child: widget,
);
} else if (App.windowsVersion == 10) {
} /* else if (App.windowsVersion == 10) {
flutter_acrylic.Window.setEffect(
effect: flutter_acrylic.WindowEffect.acrylic,
dark: FluentTheme.of(context).brightness ==
@@ -157,7 +171,7 @@ class MyApp extends StatelessWidget {
),
child: widget,
);
}
}*/
}
return KeyEventListener(child: widget);

View File

@@ -387,9 +387,9 @@ class Network {
}
}
Future<Res<List<Illust>>> getUserIllusts(String uid) async {
Future<Res<List<Illust>>> getUserIllusts(String uid, String? type) async {
var res = await apiGet(
"/v1/user/illusts?filter=for_android&user_id=$uid&type=illust");
"/v1/user/illusts?filter=for_android&user_id=$uid${type != null ? "&type=$type" : ""}");
if (res.success) {
return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
@@ -573,4 +573,14 @@ class Network {
return Res.error(res.errorMessage);
}
}
Future<Res<bool>> sendHistory(List<int> ids) async {
var res = await apiPost("/v2/user/browsing-history/illust/add",
data: {"illust_ids": ids});
if (res.success) {
return const Res(true);
} else {
return Res.fromErrorRes(res);
}
}
}

View File

@@ -2,8 +2,10 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/appdata.dart';
import 'package:pixes/components/loading.dart';
import 'package:pixes/components/segmented_button.dart';
import 'package:pixes/components/title_bar.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/history.dart';
import 'package:pixes/network/network.dart';
import 'package:pixes/utils/translation.dart';
@@ -17,42 +19,109 @@ class HistoryPage extends StatefulWidget {
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends MultiPageLoadingState<HistoryPage, Illust> {
class _HistoryPageState extends State<HistoryPage> {
int page = 0;
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
Widget build(BuildContext context) {
return Column(
children: [
TitleBar(title: "History".tl),
TitleBar(
title: "History".tl,
action: SegmentedButton<int>(
options: [
SegmentedButtonOption(0, "Local".tl,),
SegmentedButtonOption(1, "Network".tl,),
],
value: page,
onPressed: (key) {
setState(() {
page = key;
});
},
),
),
Expanded(
child: LayoutBuilder(builder: (context, constrains){
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8)
+ EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if(index == data.length - 1){
nextPage();
}
return IllustWidget(data[index], onTap: () {
context.to(() => IllustGalleryPage(
illusts: data,
initialPage: index,
));
});
},
);
}),
)
child: page == 0
? const LocalHistoryPage()
: const NetworkHistoryPage(),
),
],
);
}
}
class LocalHistoryPage extends StatefulWidget {
const LocalHistoryPage({super.key});
@override
State<LocalHistoryPage> createState() => _LocalHistoryPageState();
}
class _LocalHistoryPageState extends State<LocalHistoryPage> {
int page = 1;
var data = <IllustHistory>[];
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8) +
EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: HistoryManager().length,
itemBuilder: (context, index) {
if (index == data.length) {
data.addAll(HistoryManager().getHistories(page));
page++;
}
return IllustHistoryWidget(data[index]);
},
);
});
}
}
class NetworkHistoryPage extends StatefulWidget {
const NetworkHistoryPage({super.key});
@override
State<NetworkHistoryPage> createState() => _NetworkHistoryPageState();
}
class _NetworkHistoryPageState
extends MultiPageLoadingState<NetworkHistoryPage, Illust> {
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
return LayoutBuilder(builder: (context, constrains) {
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8) +
EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if (index == data.length - 1) {
nextPage();
}
return IllustWidget(data[index], onTap: () {
context.to(() => IllustGalleryPage(
illusts: data,
initialPage: index,
));
});
},
);
});
}
@override
Future<Res<List<Illust>>> loadData(page) {
if(appdata.account?.user.isPremium != true) {
if (appdata.account?.user.isPremium != true) {
return Future.value(Res.error("Premium Required".tl));
}
return Network().getHistory(page);

View File

@@ -4,7 +4,6 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:flutter/services.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/appdata.dart';
import 'package:pixes/components/animated_image.dart';
import 'package:pixes/components/keyboard.dart';
@@ -14,11 +13,13 @@ import 'package:pixes/components/page_route.dart';
import 'package:pixes/components/title_bar.dart';
import 'package:pixes/components/user_preview.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/history.dart';
import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/network/download.dart';
import 'package:pixes/network/network.dart';
import 'package:pixes/pages/comments_page.dart';
import 'package:pixes/pages/image_page.dart';
import 'package:pixes/pages/related_page.dart';
import 'package:pixes/pages/search_page.dart';
import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/block.dart';
@@ -44,6 +45,8 @@ class IllustGalleryPage extends StatefulWidget {
final String? nextUrl;
static var cachedHistoryIds = <int>{};
@override
State<IllustGalleryPage> createState() => _IllustGalleryPageState();
}
@@ -57,6 +60,8 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
bool loading = false;
late int page = widget.initialPage;
@override
void initState() {
illusts = List.from(widget.illusts);
@@ -68,6 +73,16 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
super.initState();
}
@override
void dispose() {
if (IllustGalleryPage.cachedHistoryIds.length > 5) {
Network().sendHistory(
IllustGalleryPage.cachedHistoryIds.toList().reversed.toList());
IllustGalleryPage.cachedHistoryIds.clear();
}
super.dispose();
}
void nextPage() {
var length = illusts.length;
if (controller.page == length - 1) return;
@@ -88,16 +103,51 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
length++;
}
return PageView.builder(
controller: controller,
itemCount: length,
itemBuilder: (context, index) {
if (index == illusts.length) {
return buildLast();
}
return IllustPage(illusts[index],
nextPage: nextPage, previousPage: previousPage);
},
return Stack(
children: [
Positioned.fill(
child: PageView.builder(
controller: controller,
itemCount: length,
itemBuilder: (context, index) {
if (index == illusts.length) {
return buildLast();
}
return IllustPage(illusts[index],
nextPage: nextPage, previousPage: previousPage);
},
onPageChanged: (value) => setState(() {
page = value;
}),
),
),
if (page < length - 1 && length > 1 && App.isDesktop)
Positioned(
right: 0,
top: 0,
bottom: 32,
child: Center(
child: IconButton(
icon: const Icon(FluentIcons.chevron_right),
onPressed: () {
nextPage();
},
)),
),
if (page != 0 && length > 1 && App.isDesktop)
Positioned(
left: 0,
top: 0,
bottom: 32,
child: Center(
child: IconButton(
icon: const Icon(FluentIcons.chevron_left),
onPressed: () {
previousPage();
},
)),
),
],
);
}
@@ -169,6 +219,10 @@ class _IllustPageState extends State<IllustPage> {
widget.illust.author.isFollowed = v;
});
};
HistoryManager().addHistory(widget.illust);
if (appdata.account!.user.isPremium) {
IllustGalleryPage.cachedHistoryIds.add(widget.illust.id);
}
super.initState();
}
@@ -265,6 +319,14 @@ class _IllustPageState extends State<IllustPage> {
_bottomBarController.download();
case 6:
_bottomBarController.follow();
case 7:
if (ModalRoute.of(context)?.isCurrent ?? true) {
CommentsPage.show(context, widget.illust.id.toString());
} else {
context.pop();
}
case 8:
openImage(0);
}
}
@@ -997,7 +1059,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {
Widget buildMoreActions() {
return Wrap(
runSpacing: 4,
runSpacing: 8,
spacing: 8,
children: [
Button(
@@ -1078,7 +1140,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {
).fixWidth(96),
Button(
onPressed: () {
context.to(() => _RelatedIllustsPage(widget.illust.id.toString()));
context.to(() => RelatedIllustsPage(widget.illust.id.toString()));
},
child: SizedBox(
height: 28,
@@ -1295,64 +1357,3 @@ class _IllustPageWithIdState extends LoadingState<IllustPageWithId, Illust> {
return Network().getIllustByID(widget.id);
}
}
class _RelatedIllustsPage extends StatefulWidget {
const _RelatedIllustsPage(this.id);
final String id;
@override
State<_RelatedIllustsPage> createState() => _RelatedIllustsPageState();
}
class _RelatedIllustsPageState
extends MultiPageLoadingState<_RelatedIllustsPage, Illust> {
@override
Widget? buildFrame(BuildContext context, Widget child) {
return Column(
children: [
TitleBar(title: "Related artworks".tl),
Expanded(
child: child,
)
],
);
}
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
return LayoutBuilder(builder: (context, constrains) {
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8) +
EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if (index == data.length - 1) {
nextPage();
}
return IllustWidget(data[index]);
},
);
});
}
String? nextUrl;
@override
Future<Res<List<Illust>>> loadData(page) async {
if (nextUrl == "end") {
return Res.error("No more data");
}
var res = nextUrl == null
? await Network().relatedIllusts(widget.id)
: await Network().getIllustsWithNextUrl(nextUrl!);
if (!res.error) {
nextUrl = res.subData;
nextUrl ??= "end";
}
return res;
}
}

View File

@@ -3,8 +3,10 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:pixes/components/md.dart';
import 'package:pixes/components/message.dart';
import 'package:pixes/components/page_route.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/cache_manager.dart';
@@ -89,6 +91,8 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
void showMenu() {
menuController.showFlyout(
barrierColor: Colors.transparent,
position: App.isMobile ? Offset(context.size!.width, 0) : null,
builder: (context) => MenuFlyout(
items: [
MenuFlyoutItem(
@@ -96,20 +100,40 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
onPressed: () async {
var file = await getFile();
if (file != null) {
var fileName = file.path.split('/').last;
var fileName = widget.urls[currentPage].split('/').last;
if (!fileName.contains('.')) {
fileName += getExtensionName();
}
saveFile(file, fileName);
}
}),
if (App.isMobile)
MenuFlyoutItem(
text: Text("Save to gallery".tl),
onPressed: () async {
var file = await getFile();
if (file != null) {
var fileName =
widget.urls[currentPage].split('/').last;
if (!fileName.contains('.')) {
fileName += getExtensionName();
}
await ImageGallerySaver.saveImage(
await file.readAsBytes(),
quality: 100,
name: fileName);
if (mounted) {
showToast(context, message: "Saved".tl);
}
}
}),
MenuFlyoutItem(
text: Text("Share".tl),
onPressed: () async {
var file = await getFile();
if (file != null) {
var ext = getExtensionName();
var fileName = file.path.split('/').last;
var fileName = widget.urls[currentPage].split('/').last;
if (!fileName.contains('.')) {
fileName += ext;
}
@@ -164,6 +188,7 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
var image = widget.urls[index];
return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium,
imageProvider: getImageProvider(image),
);
},

View File

@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/button.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/network/network.dart';
import 'package:pixes/pages/webview_page.dart';
@@ -24,7 +25,7 @@ class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
if(isLogging) {
if (isLogging) {
return buildLoading(context);
} else if (!waitingForAuth) {
return buildLogin(context);
@@ -56,23 +57,12 @@ class _LoginPageState extends State<LoginPage> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (checked)
FilledButton(
onPressed: onContinue,
child: Text("Continue".tl),
)
else
Container(
height: 28,
width: 78,
decoration: BoxDecoration(
color: FluentTheme.of(context)
.inactiveBackgroundColor,
borderRadius: BorderRadius.circular(4)),
child: Center(
child: Text("Continue".tl),
),
),
FluentButton(
onPressed: onContinue,
enabled: checked,
width: 96,
child: Text("Continue".tl),
),
const SizedBox(
height: 16,
),
@@ -97,7 +87,9 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(
width: 8,
),
Text("I have read and agree to the Terms of Use".tl)
Expanded(
child: Text("I understand pixes is a free unofficial application.".tl),
)
],
)
],
@@ -186,6 +178,39 @@ class _LoginPageState extends State<LoginPage> {
}
void onContinue() async {
bool? useExternal;
if (App.isMobile) {
await showDialog(
context: context,
barrierDismissible: true,
builder: (context) => ContentDialog(
title: Text("Choose a way to login".tl),
content: Text("${"Use Webview: you cannot sign in with Google.".tl}"
"\n\n"
"${"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application".tl}"),
actions: [
Button(
child: Text("Webview".tl),
onPressed: () {
useExternal = false;
App.rootNavigatorKey.currentState!.pop();
},
),
Button(
child: Text("External browser".tl),
onPressed: () {
useExternal = true;
App.rootNavigatorKey.currentState!.pop();
},
)
]),
);
} else {
useExternal = true;
}
if (useExternal == null) {
return;
}
var url = await Network().generateWebviewUrl();
onLink = (uri) {
if (uri.scheme == "pixiv") {
@@ -198,15 +223,18 @@ class _LoginPageState extends State<LoginPage> {
setState(() {
waitingForAuth = true;
});
if(App.isMobile && mounted) {
context.to(() => WebviewPage(url, onNavigation: (req) {
if(req.url.startsWith("pixiv://")) {
App.rootNavigatorKey.currentState!.pop();
onLink?.call(Uri.parse(req.url));
return false;
}
return true;
},));
if (!useExternal! && mounted) {
context.to(() => WebviewPage(
url,
onNavigation: (req) {
if (req.url.startsWith("pixiv://")) {
App.rootNavigatorKey.currentState!.pop();
onLink?.call(Uri.parse(req.url));
return false;
}
return true;
},
));
} else {
launchUrlString(url);
}
@@ -219,7 +247,7 @@ class _LoginPageState extends State<LoginPage> {
});
var res = await Network().loginWithCode(code);
if (res.error) {
if(mounted) {
if (mounted) {
context.showToast(message: res.errorMessage!);
}
setState(() {

View File

@@ -2,6 +2,7 @@ import "dart:async";
import "package:fluent_ui/fluent_ui.dart";
import "package:flutter/foundation.dart";
import "package:flutter/services.dart";
import "package:pixes/appdata.dart";
import "package:pixes/components/md.dart";
import "package:pixes/foundation/app.dart";
@@ -64,13 +65,17 @@ class MainPage extends StatefulWidget {
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with WindowListener {
class _MainPageState extends State<MainPage>
with WindowListener
implements PopEntry {
final navigatorKey = GlobalKey<NavigatorState>();
int index = 4;
int windowButtonKey = 0;
ModalRoute<dynamic>? _route;
@override
void initState() {
StateController.put<TitleBarController>(TitleBarController());
@@ -80,10 +85,22 @@ class _MainPageState extends State<MainPage> with WindowListener {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
}
@override
void dispose() {
StateController.remove<TitleBarController>();
windowManager.removeListener(this);
ModalRoute.of(context)!.unregisterPopEntry(this);
super.dispose();
}
@@ -151,7 +168,7 @@ class _MainPageState extends State<MainPage> with WindowListener {
),
PaneItemSeparator(),
PaneItemHeader(
header: Text('${"Artwork".tl}/${"Manga".tl}')
header: Text('${"Illustrations".tl}/${"Manga".tl}')
.paddingBottom(4)
.paddingLeft(8)),
PaneItem(
@@ -209,10 +226,7 @@ class _MainPageState extends State<MainPage> with WindowListener {
),
],
),
paneBodyBuilder: (pane, child) => NavigatorPopHandler(
key: const Key("navigator"),
onPop: () => navigatorKey.currentState?.pop(),
child: MediaQuery.removePadding(
paneBodyBuilder: (pane, child) => MediaQuery.removePadding(
context: context,
removeTop: true,
child: Navigator(
@@ -220,7 +234,7 @@ class _MainPageState extends State<MainPage> with WindowListener {
onGenerateRoute: (settings) => AppPageRoute(
builder: (context) => const RecommendationPage()),
),
))),
)),
);
}
@@ -315,6 +329,25 @@ class _MainPageState extends State<MainPage> with WindowListener {
: null,
);
}
final popValue = ValueNotifier(false);
@override
ValueListenable<bool> get canPopNotifier => popValue;
@override
PopInvokedCallback? get onPopInvoked => onPop;
void onPop(bool value) {
print("ok");
if (App.rootNavigatorKey.currentState?.canPop() ?? false) {
App.rootNavigatorKey.currentState?.pop();
} else if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {
App.mainNavigatorKey?.currentState?.pop();
} else {
SystemNavigator.pop();
}
}
}
class _BackButton extends StatefulWidget {

View File

@@ -45,7 +45,7 @@ class _RecommendationPageState extends State<RecommendationPage> {
title: "Explore".tl,
action: SegmentedButton<int>(
options: [
SegmentedButtonOption(0, "Artworks".tl),
SegmentedButtonOption(0, "Illustrations".tl),
SegmentedButtonOption(1, "Mangas".tl),
SegmentedButtonOption(2, "Users".tl),
],

View File

@@ -0,0 +1,69 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/components/illust_widget.dart';
import 'package:pixes/components/loading.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 RelatedIllustsPage extends StatefulWidget {
const RelatedIllustsPage(this.id, {super.key});
final String id;
@override
State<RelatedIllustsPage> createState() => _RelatedIllustsPageState();
}
class _RelatedIllustsPageState
extends MultiPageLoadingState<RelatedIllustsPage, Illust> {
@override
Widget? buildFrame(BuildContext context, Widget child) {
return Column(
children: [
TitleBar(title: "Related artworks".tl),
Expanded(
child: child,
)
],
);
}
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
return LayoutBuilder(builder: (context, constrains) {
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8) +
EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if (index == data.length - 1) {
nextPage();
}
return IllustWidget(data[index]);
},
);
});
}
String? nextUrl;
@override
Future<Res<List<Illust>>> loadData(page) async {
if (nextUrl == "end") {
return Res.error("No more data");
}
var res = nextUrl == null
? await Network().relatedIllusts(widget.id)
: await Network().getIllustsWithNextUrl(nextUrl!);
if (!res.error) {
nextUrl = res.subData;
nextUrl ??= "end";
}
return res;
}
}

View File

@@ -33,6 +33,8 @@ class _SearchPageState extends State<SearchPage> {
int searchType = 0;
final focusNode = FocusNode();
static const searchTypes = [
"Search artwork",
"Search novel",
@@ -100,6 +102,9 @@ class _SearchPageState extends State<SearchPage> {
children: [
Expanded(
child: TextBox(
focusNode: focusNode,
autofocus: false,
padding: const EdgeInsets.symmetric(horizontal: 12),
placeholder:
'${searchTypes[searchType].tl} / ${"Open link".tl}',
onChanged: (s) => text = s,

View File

@@ -88,6 +88,7 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text('Continue'.tl),
onPressed: () {
appdata.account = null;
appdata.writeData();
App.rootNavigatorKey.currentState!.pushAndRemoveUntil(
AppPageRoute(
builder: (context) => const MainPage()),
@@ -181,6 +182,16 @@ class _SettingsPageState extends State<SettingsPage> {
child: Column(
children: [
buildItem(title: "Version", subtitle: App.version),
buildItem(
title: "Check for updates on startup".tl,
action: ToggleSwitch(
checked: appdata.settings["checkUpdate"],
onChanged: (value) {
setState(() {
appdata.settings["checkUpdate"] = value;
});
appdata.writeData();
})),
buildItem(
title: "Github",
action: IconButton(
@@ -262,6 +273,19 @@ class _SettingsPageState extends State<SettingsPage> {
});
appdata.writeData();
})),
buildItem(
title: "Emphasize artworks from following artists".tl,
subtitle: "The border of the artworks will be darker".tl,
action: ToggleSwitch(
checked:
appdata.settings['emphasizeArtworksFromFollowingArtists'],
onChanged: (value) {
setState(() {
appdata.settings[
'emphasizeArtworksFromFollowingArtists'] = value;
});
appdata.writeData();
})),
],
),
);
@@ -591,6 +615,8 @@ class _ShortcutsSettingsState extends State<ShortcutsSettings> {
"Add to favorites",
"Download",
"Follow the artist",
"Show comments",
"Show original image"
];
@override

View File

@@ -63,7 +63,7 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
_RelatedUsers(widget.id),
buildInformation(),
buildArtworkHeader(),
if (page == 2)
if (page == 4)
_UserNovels(widget.id)
else
_UserArtworks(
@@ -228,8 +228,10 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
SegmentedButton<int>(
options: [
SegmentedButtonOption(0, "Artworks".tl),
SegmentedButtonOption(1, "Bookmarks".tl),
SegmentedButtonOption(2, "Novels".tl),
SegmentedButtonOption(1, "Illustrations".tl),
SegmentedButtonOption(2, "Mangas".tl),
SegmentedButtonOption(3, "Bookmarks".tl),
SegmentedButtonOption(4, "Novels".tl),
],
value: page,
onPressed: (value) {
@@ -239,15 +241,24 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
},
),
const Spacer(),
if (page != 2)
if (page != 4)
BatchDownloadButton(
request: () {
if (page == 0) {
return Network().getUserIllusts(data!.id.toString());
} else {
return Network()
.getUserBookmarks(data!.id.toString());
switch (page) {
case 0:
return Network()
.getUserIllusts(data!.id.toString(), null);
case 1:
return Network()
.getUserIllusts(data!.id.toString(), "illust");
case 2:
return Network()
.getUserIllusts(data!.id.toString(), "manga");
case 3:
return Network()
.getUserBookmarks(data!.id.toString());
}
throw "Invalid page";
},
),
],
@@ -269,14 +280,14 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: EdgeInsets.zero,
child: ListTile(
leading: icon == null
? null
: Icon(
icon,
size: 20,
),
title: Text(title),
subtitle: SelectableText(content),
title: Row(
children: [
Icon(icon, size: 20),
const SizedBox(width: 8),
Text(title)
],
),
subtitle: SelectableText(content).paddingLeft(icon == null ? 0 : 28),
trailing: trailing,
),
);
@@ -409,8 +420,9 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
return Res.error("No more data");
}
var res = nextUrl == null
? (widget.type == 0
? await Network().getUserIllusts(widget.uid)
? (widget.type != 3
? await Network().getUserIllusts(
widget.uid, [null, "illust", "manga"][widget.type])
: await Network().getUserBookmarks(widget.uid))
: await Network().getIllustsWithNextUrl(nextUrl!);
if (!res.error) {
@@ -422,7 +434,7 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
}
class _UserNovels extends StatefulWidget {
const _UserNovels(this.uid, {super.key});
const _UserNovels(this.uid);
final String uid;
@@ -534,7 +546,7 @@ class _RelatedUsersState
return UserPreviewWidget(data[index]).fixWidth(342);
},
));
if (MediaQuery.of(context).size.width > 500) {
if (App.isDesktop) {
content = ScrollbarTheme.merge(
data: const ScrollbarThemeData(
thickness: 6,
@@ -542,7 +554,21 @@ class _RelatedUsersState
mainAxisMargin: 4,
hoveringPadding: EdgeInsets.zero,
padding: EdgeInsets.zero,
hoveringMainAxisMargin: 4),
hoveringMainAxisMargin: 4,
crossAxisMargin: 0,
hoveringCrossAxisMargin: 0),
child: content);
} else {
content = ScrollbarTheme.merge(
data: const ScrollbarThemeData(
thickness: 4,
hoveringThickness: 4,
mainAxisMargin: 4,
hoveringPadding: EdgeInsets.zero,
padding: EdgeInsets.zero,
hoveringMainAxisMargin: 4,
crossAxisMargin: 0,
hoveringCrossAxisMargin: 0),
child: content);
}
return MediaQuery.removePadding(

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/log.dart';
import 'package:pixes/pages/illust_page.dart';
@@ -8,6 +9,7 @@ import 'package:pixes/pages/novel_page.dart';
import 'package:pixes/pages/search_page.dart';
import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/ext.dart';
import 'package:pixes/utils/translation.dart';
import 'package:win32_registry/win32_registry.dart';
Future<void> _register(String scheme) async {
@@ -31,13 +33,40 @@ Future<void> _register(String scheme) async {
regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue);
}
void _registerPixiv() async {
try {
await _register("pixiv");
} catch (e) {
// 注册失败会导致登录不可用
while (App.mainNavigatorKey == null) {
await Future.delayed(const Duration(milliseconds: 100));
}
Future.delayed(const Duration(seconds: 1), () async {
showDialog(
context: App.rootNavigatorKey.currentContext!,
builder: (context) => ContentDialog(
title: Text("Error".tl),
content: Text("${"Failed to register URL scheme.".tl}\n$e"),
actions: [
FilledButton(
child: Text("Retry".tl),
onPressed: () {
context.pop();
_registerPixiv();
})
],
));
});
}
}
bool Function(Uri uri)? onLink;
bool _firstLink = true;
void handleLinks() async {
if (App.isWindows) {
await _register("pixiv");
_registerPixiv();
}
AppLinks().uriLinkStream.listen((uri) async {
if (_firstLink) {

63
lib/utils/update.dart Normal file
View File

@@ -0,0 +1,63 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/appdata.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/network/app_dio.dart';
import 'package:pixes/utils/translation.dart';
import 'package:url_launcher/url_launcher_string.dart';
Future<String> getLatestVersion() async {
var dio = AppDio();
var res = await dio
.get("https://api.github.com/repos/wgh136/pixes/releases/latest");
return (res.data["tag_name"] as String).replaceFirst("v", "");
}
/// Compare two versions.
/// Return `true` if `a` is greater than `b`.
bool compareVersion(String a, String b) {
var aList = a.split(".").map(int.parse).toList();
var bList = b.split(".").map(int.parse).toList();
for (var i = 0; i < aList.length; i++) {
if (aList[i] > bList[i]) {
return true;
} else if (aList[i] < bList[i]) {
return false;
}
}
return false;
}
Future<void> checkUpdate() async {
if (appdata.account == null) return;
try {
var latestVersion = await getLatestVersion();
if (compareVersion(latestVersion, App.version)) {
showDialog(
context: App.rootNavigatorKey.currentContext!,
builder: (context) => ContentDialog(
title: Text("New version available".tl),
content: Text(
"A new version of Pixes is available. Do you want to update now?"
.tl,
),
actions: [
Button(
child: Text("Cancel".tl),
onPressed: () {
Navigator.of(context).pop();
},
),
FilledButton(
child: Text("Update".tl),
onPressed: () {
Navigator.of(context).pop();
launchUrlString(
"https://github.com/wgh136/pixes/releases/latest");
})
],
));
}
} catch (e) {
// ignore
}
}

View File

@@ -6,14 +6,25 @@
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <system_theme/system_theme_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin");
flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
@@ -23,9 +34,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) system_theme_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin");
system_theme_plugin_register_with_registrar(system_theme_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,10 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
file_selector_linux
flutter_acrylic
gtk
screen_retriever
sqlite3_flutter_libs
system_theme
url_launcher_linux
window_manager
)

View File

@@ -55,7 +55,7 @@ static void my_application_activate(GApplication* application) {
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_realize(GTK_WIDGET(window));
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);

View File

@@ -6,19 +6,27 @@ import FlutterMacOS
import Foundation
import app_links
import device_info_plus
import dynamic_color
import file_selector_macos
import flutter_acrylic
import path_provider_foundation
import screen_retriever
import share_plus
import sqlite3_flutter_libs
import system_theme
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterAcrylicPlugin.register(with: registry.registrar(forPlugin: "FlutterAcrylicPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -218,10 +218,10 @@ packages:
dependency: "direct main"
description:
name: flutter_acrylic
sha256: a9a1fdf91ff1fb47858fd82507f57e255a132a5d355056694fdb9fd303633b18
sha256: "646200d98e8dd2bd4ab931d4ba4f6b4cb899475d6401414017ba5d71b0fac42b"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.0.0+2"
flutter_file_dialog:
dependency: "direct main"
description:
@@ -285,6 +285,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image_gallery_saver:
dependency: "direct main"
description:
path: "."
ref: master
resolved-ref: "3f8c4d2cc41002d3ffdb770cab3b62583326ce01"
url: "https://github.com/wgh136/image_gallery_saver"
source: git
version: "2.0.0"
intl:
dependency: "direct main"
description:
@@ -325,14 +334,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
macos_window_utils:
dependency: transitive
description:
name: macos_window_utils
sha256: "230be594d26f6dee92c5a1544f4242d25138a5bfb9f185b27f14de3949ef0be8"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
matcher:
dependency: transitive
description:

View File

@@ -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
# 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.
version: 1.0.6+106
version: 1.0.8+108
environment:
sdk: '>=3.3.4 <4.0.0'
@@ -58,6 +58,10 @@ dependencies:
webview_flutter: ^4.7.0
flutter_acrylic: 1.0.0+2
device_info_plus: ^10.1.0
image_gallery_saver:
git:
url: https://github.com/wgh136/image_gallery_saver
ref: master
dev_dependencies:
flutter_test:
sdk: flutter

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 520 KiB

BIN
screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

BIN
screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View File

@@ -2,11 +2,11 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "pixes"
#define MyAppVersion "1.0.5"
#define MyAppVersion "{{version}}"
#define MyAppPublisher "Nyne"
#define MyAppURL "https://github.com/wgh136/pixes"
#define MyAppExeName "pixes.exe"
#define RootPath "C:\Users\wgh19\IdeaProjects\pixes"
#define RootPath "{{root_path}}"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.

View File

@@ -7,21 +7,30 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterAcrylicPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
SystemThemePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SystemThemePlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(

View File

@@ -4,9 +4,12 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
dynamic_color
file_selector_windows
flutter_acrylic
screen_retriever
share_plus
sqlite3_flutter_libs
system_theme
url_launcher_windows
window_manager
)