33 Commits

Author SHA1 Message Date
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
wgh19
49da2b772b fix display error 2024-06-01 22:13:22 +08:00
wgh19
4dc1ec8784 improve ui 2024-06-01 22:03:31 +08:00
wgh19
1fad3694cf open link 2024-06-01 16:44:25 +08:00
wgh136
39eb5c836e fix macos build 2024-05-31 23:52:14 +08:00
wgh136
9603706fc6 fix macos build 2024-05-31 23:41:15 +08:00
wgh19
343e993627 fix macos build 2024-05-31 20:26:42 +08:00
wgh19
246f96bdbf update macos&windows build 2024-05-31 20:06:28 +08:00
wgh19
61c6ed0e1b fix init 2024-05-31 19:58:06 +08:00
wgh19
fd63b02b60 update version code 2024-05-31 19:40:03 +08:00
wgh19
9275024050 add a setting for original image 2024-05-31 19:32:46 +08:00
wgh19
5e53c57755 improve progress display 2024-05-31 18:39:28 +08:00
wgh19
6a95fb37ed font 2024-05-31 17:46:59 +08:00
wgh19
20829e1ad5 improve text 2024-05-31 17:44:50 +08:00
wgh19
cb356dbf71 shortcuts 2024-05-31 17:38:27 +08:00
wgh19
9ad6207bd5 fix drag issue 2024-05-31 15:28:04 +08:00
wgh19
676e7508c7 update icon 2024-05-31 15:23:26 +08:00
wgh19
1652a93772 window effect for windows 2024-05-31 15:21:58 +08:00
wgh19
e6d015a2bc deep link for android 2024-05-31 11:45:54 +08:00
wgh19
35dd9dee5f fix window placement 2024-05-27 14:48:07 +08:00
wgh19
b34a8342d2 fix ui 2024-05-23 13:55:44 +08:00
nyne
0ed17edd3e Create LICENSE 2024-05-23 13:17:02 +08:00
50 changed files with 1624 additions and 304 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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 nyne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,5 +1,11 @@
# pixes # pixes
[![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/PicaComic/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 非官方 Pixiv app, 支持 Windows, Android, iOS, macOS
主要功能均已实现 主要功能均已实现
@@ -7,3 +13,6 @@
## 屏幕截图 ## 屏幕截图
<img src="screenshots/1.png" style="width: 400px"> <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

@@ -33,6 +33,15 @@
<!-- Accepts URIs that begin with "example://gizmos” --> <!-- Accepts URIs that begin with "example://gizmos” -->
<data android:scheme="pixiv"/> <data android:scheme="pixiv"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="www.pixiv.net" android:pathPrefix="/users"/>
<data android:scheme="https" android:host="www.pixiv.net" android:pathPrefix="/novel"/>
<data android:scheme="https" android:host="www.pixiv.net" android:pathPrefix="/tags"/>
<data android:scheme="https" android:host="www.pixiv.net" android:pathPrefix="/artworks"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@@ -3,14 +3,14 @@
"Search": "搜索", "Search": "搜索",
"Downloading": "下载中", "Downloading": "下载中",
"Downloaded": "已下载", "Downloaded": "已下载",
"Artwork": "作品", "Artwork": "插画",
"Explore": "探索", "Explore": "探索",
"Bookmarks": "收藏", "Bookmarks": "收藏",
"Following": "关注", "Following": "关注",
"History": "历史", "History": "历史",
"Ranking": "排行", "Ranking": "排行",
"Settings": "设置", "Settings": "设置",
"Artworks": "作品", "Artworks": "插画",
"Mangas": "漫画", "Mangas": "漫画",
"Users": "用户", "Users": "用户",
"Search artwork": "搜索作品", "Search artwork": "搜索作品",
@@ -148,7 +148,32 @@
"Local": "本地", "Local": "本地",
"Both": "同时", "Both": "同时",
"This artwork is blocked": "此作品已被屏蔽", "This artwork is blocked": "此作品已被屏蔽",
"Delete Invalid Items": "删除无效项目" "Delete Invalid Items": "删除无效项目",
"Private Favorite": "私人收藏",
"Shortcuts": "快捷键",
"Page down": "向下翻页",
"Page up": "向上翻页",
"Next work": "下一作品",
"Previous work": "上一作品",
"Add to favorites": "添加收藏",
"Follow the artist": "关注画师",
"Manga": "漫画",
"Actions": "操作",
"Current quantity": "当前数量",
"Display the original image on the details page": "在详情页显示原图",
"Open link": "打开链接",
"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": "显示原图"
}, },
"zh_TW": { "zh_TW": {
"Search": "搜索", "Search": "搜索",
@@ -299,6 +324,31 @@
"Local": "本地", "Local": "本地",
"Both": "同時", "Both": "同時",
"This artwork is blocked": "此作品已被屏蔽", "This artwork is blocked": "此作品已被屏蔽",
"Delete Invalid Items": "刪除無效項目" "Delete Invalid Items": "刪除無效項目",
"Private Favorite": "私人收藏",
"Shortcuts": "快捷鍵",
"Page down": "向下翻頁",
"Page up": "向上翻頁",
"Next work": "下一作品",
"Previous work": "上一作品",
"Add to favorites": "添加收藏",
"Follow the artist": "關注畫師",
"Manga": "漫畫",
"Actions": "操作",
"Current quantity": "當前數量",
"Display the original image on the details page": "在詳情頁顯示原圖",
"Open link": "打開鏈接",
"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": "顯示原圖"
} }
} }

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> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</dict> </dict>
</array> </array>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>photo</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>photo</string>
</dict> </dict>
</plist> </plist>

View File

@@ -24,6 +24,18 @@ class _Appdata {
"readingLineHeight": 1.5, "readingLineHeight": 1.5,
"readingParagraphSpacing": 8.0, "readingParagraphSpacing": 8.0,
"blockTags": [], "blockTags": [],
"shortcuts": <int>[
LogicalKeyboardKey.arrowDown.keyId,
LogicalKeyboardKey.arrowUp.keyId,
LogicalKeyboardKey.arrowRight.keyId,
LogicalKeyboardKey.arrowLeft.keyId,
LogicalKeyboardKey.enter.keyId,
LogicalKeyboardKey.keyD.keyId,
LogicalKeyboardKey.keyF.keyId,
LogicalKeyboardKey.keyC.keyId,
LogicalKeyboardKey.keyG.keyId,
],
"showOriginalImage": false,
}; };
bool lock = false; bool lock = false;
@@ -53,14 +65,25 @@ class _Appdata {
Future<void> readData() async { Future<void> readData() async {
final file = File("${App.dataPath}/account.json"); final file = File("${App.dataPath}/account.json");
if (file.existsSync()) { 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"); final settingsFile = File("${App.dataPath}/settings.json");
if (settingsFile.existsSync()) { if (settingsFile.existsSync()) {
var json = jsonDecode(await settingsFile.readAsString()); var json = jsonDecode(await settingsFile.readAsString());
for (var key in json.keys) { for (var key in json.keys) {
if(json[key] != null) { if (json[key] != null) {
settings[key] = json[key]; 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];
}
} }
} }
} }

View File

@@ -15,7 +15,10 @@ class BatchDownloadButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Button( return Button(
child: const Icon(MdIcons.download, size: 20,), child: const Icon(
MdIcons.download,
size: 20,
),
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
@@ -40,6 +43,8 @@ class _DownloadDialog extends StatefulWidget {
class _DownloadDialogState extends State<_DownloadDialog> { class _DownloadDialogState extends State<_DownloadDialog> {
int maxCount = 30; int maxCount = 30;
int currentCount = 0;
bool loading = false; bool loading = false;
bool cancel = false; bool cancel = false;
@@ -53,15 +58,18 @@ class _DownloadDialogState extends State<_DownloadDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('${"Maximum number of downloads".tl}:'), if (!loading) Text('${"Maximum number of downloads".tl}:'),
const SizedBox(height: 16,), if (loading) Text("${"Current quantity".tl}: $currentCount"),
const SizedBox(
height: 16,
),
SizedBox( SizedBox(
height: 42, height: 42,
width: 196, width: 196,
child: NumberBox( child: NumberBox(
value: maxCount, value: maxCount,
onChanged: (value) { onChanged: (value) {
if(!loading) { if (!loading) {
setState(() => maxCount = value ?? maxCount); setState(() => maxCount = value ?? maxCount);
} }
}, },
@@ -71,35 +79,39 @@ class _DownloadDialogState extends State<_DownloadDialog> {
largeChange: 30, largeChange: 30,
clearButton: false, clearButton: false,
), ),
) ),
], ],
).paddingVertical(8), ).paddingVertical(8),
), ),
actions: [ actions: [
Button(child: Text("Cancel".tl), onPressed: () { Button(
cancel = true; child: Text("Cancel".tl),
context.pop(); onPressed: () {
}), cancel = true;
if(!loading) context.pop();
}),
if (!loading)
FilledButton(onPressed: load, child: Text("Continue".tl)) FilledButton(onPressed: load, child: Text("Continue".tl))
else else
FilledButton(onPressed: (){}, child: const SizedBox( FilledButton(
height: 20, onPressed: () {},
width: 64, child: const SizedBox(
child: Center( height: 20,
child: SizedBox.square( width: 64,
dimension: 18, child: Center(
child: ProgressRing( child: SizedBox.square(
strokeWidth: 1.6, dimension: 18,
child: ProgressRing(
strokeWidth: 1.6,
),
),
), ),
), ))
),
))
], ],
); );
} }
void load() async{ void load() async {
setState(() { setState(() {
loading = true; loading = true;
}); });
@@ -109,17 +121,17 @@ class _DownloadDialogState extends State<_DownloadDialog> {
List<Illust> all = []; List<Illust> all = [];
String? nextUrl; String? nextUrl;
int retryCount = 0; int retryCount = 0;
while(nextUrl != "end" && all.length < maxCount) { while (nextUrl != "end" && all.length < maxCount) {
if(nextUrl != null) { if (nextUrl != null) {
request = Network().getIllustsWithNextUrl(nextUrl); request = Network().getIllustsWithNextUrl(nextUrl);
} }
var res = await request; var res = await request;
if(cancel || !mounted) { if (cancel || !mounted) {
return; return;
} }
if(res.error) { if (res.error) {
retryCount++; retryCount++;
if(retryCount > 3) { if (retryCount > 3) {
setState(() { setState(() {
loading = false; loading = false;
}); });
@@ -130,15 +142,17 @@ class _DownloadDialogState extends State<_DownloadDialog> {
continue; continue;
} }
all.addAll(res.data); all.addAll(res.data);
setState(() {
currentCount = all.length;
});
nextUrl = res.subData ?? "end"; nextUrl = res.subData ?? "end";
} }
int i = 0; int i = 0;
for(var illust in all) { for (var illust in all) {
if(i > maxCount) return; if (i > maxCount) break;
DownloadManager().addDownloadingTask(illust); DownloadManager().addDownloadingTask(illust);
i++; i++;
} }
context.pop(); context.pop();
} }
} }

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,6 +1,7 @@
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';
import 'package:pixes/foundation/history.dart';
import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/network/download.dart'; import 'package:pixes/network/download.dart';
import 'package:pixes/utils/translation.dart'; import 'package:pixes/utils/translation.dart';
@@ -59,48 +60,58 @@ class _IllustWidgetState extends State<IllustWidget> {
height: height, height: height,
child: Stack( child: Stack(
children: [ children: [
Positioned.fill(child: Container( Positioned.fill(
child: Container(
width: width, width: width,
height: height, height: height,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Card( child: Card(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: GestureDetector( child: GestureDetector(
onTap: widget.onTap ?? (){ onTap: widget.onTap ??
context.to(() => IllustPage(widget.illust)); () {
}, context.to(() => IllustPage(widget.illust));
},
onSecondaryTapUp: showMenu, onSecondaryTapUp: showMenu,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
child: AnimatedImage( child: AnimatedImage(
image: CachedImageProvider(widget.illust.images.first.medium), image: CachedImageProvider(
widget.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,
), ),
), ),
), ),
), ),
)), )),
if(widget.illust.images.length > 1) if (widget.illust.images.length > 1)
Positioned( Positioned(
top: 12, top: 12,
left: 12, left: 12,
child: Container( child: Container(
width: 28, width: 28,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor, color: FluentTheme.of(context)
borderRadius: BorderRadius.circular(4), .micaBackgroundColor
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6), .withOpacity(0.72),
), borderRadius: BorderRadius.circular(4),
child: Center( border: Border.all(
child: Text("${widget.illust.images.length}P", color: ColorScheme.of(context).outlineVariant,
style: const TextStyle(fontSize: 12),), width: 0.6),
)), ),
child: Center(
child: Text(
"${widget.illust.images.length}P",
style: const TextStyle(fontSize: 12),
),
)),
), ),
if(widget.illust.isAi) if (widget.illust.isAi)
Positioned( Positioned(
bottom: 12, bottom: 12,
left: 12, left: 12,
@@ -108,16 +119,22 @@ class _IllustWidgetState extends State<IllustWidget> {
width: 28, width: 28,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorScheme.of(context).errorContainer.withOpacity(0.8), color: ColorScheme.of(context)
.errorContainer
.withOpacity(0.8),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6), border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
), ),
child: const Center( child: const Center(
child: Text("AI", child: Text(
style: TextStyle(fontSize: 12),), "AI",
style: TextStyle(fontSize: 12),
),
)), )),
), ),
if(widget.illust.isUgoira) if (widget.illust.isUgoira)
Positioned( Positioned(
bottom: 12, bottom: 12,
left: 12, left: 12,
@@ -125,16 +142,22 @@ class _IllustWidgetState extends State<IllustWidget> {
width: 28, width: 28,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorScheme.of(context).primaryContainer.withOpacity(0.8), color: ColorScheme.of(context)
.primaryContainer
.withOpacity(0.8),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6), border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
), ),
child: const Center( child: const Center(
child: Text("GIF", child: Text(
style: TextStyle(fontSize: 12),), "GIF",
style: TextStyle(fontSize: 12),
),
)), )),
), ),
if(widget.illust.isR18) if (widget.illust.isR18)
Positioned( Positioned(
bottom: 12, bottom: 12,
right: 12, right: 12,
@@ -144,14 +167,18 @@ class _IllustWidgetState extends State<IllustWidget> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorScheme.of(context).errorContainer, color: ColorScheme.of(context).errorContainer,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6), border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
), ),
child: const Center( child: const Center(
child: Text("R18", child: Text(
style: TextStyle(fontSize: 12),), "R18",
style: TextStyle(fontSize: 12),
),
)), )),
), ),
if(widget.illust.isR18G) if (widget.illust.isR18G)
Positioned( Positioned(
bottom: 12, bottom: 12,
right: 12, right: 12,
@@ -161,11 +188,15 @@ class _IllustWidgetState extends State<IllustWidget> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorScheme.of(context).errorContainer, color: ColorScheme.of(context).errorContainer,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6), border: Border.all(
color: ColorScheme.of(context).outlineVariant,
width: 0.6),
), ),
child: const Center( child: const Center(
child: Text("R18G", child: Text(
style: TextStyle(fontSize: 12),), "R18G",
style: TextStyle(fontSize: 12),
),
)), )),
), ),
Positioned( Positioned(
@@ -196,31 +227,38 @@ class _IllustWidgetState extends State<IllustWidget> {
builder: (context) { builder: (context) {
return MenuFlyout( return MenuFlyout(
items: [ items: [
MenuFlyoutItem(text: Text("View".tl), onPressed: (){ MenuFlyoutItem(
context.to(() => IllustPage(widget.illust)); text: Text("View".tl),
}), onPressed: () {
MenuFlyoutItem(text: Text("Private Favorite".tl), onPressed: (){ context.to(() => IllustPage(widget.illust));
favorite("private"); }),
}), MenuFlyoutItem(
MenuFlyoutItem(text: Text("Download".tl), onPressed: (){ text: Text("Private Favorite".tl),
context.showToast(message: "Added"); onPressed: () {
DownloadManager().addDownloadingTask(widget.illust); favorite("private");
}), }),
MenuFlyoutItem(
text: Text("Download".tl),
onPressed: () {
context.showToast(message: "Added");
DownloadManager().addDownloadingTask(widget.illust);
}),
], ],
); );
}, },
); );
} }
void favorite([String type = "public"]) async{ void favorite([String type = "public"]) async {
if(isBookmarking) return; if (isBookmarking) return;
setState(() { setState(() {
isBookmarking = true; isBookmarking = true;
}); });
var method = widget.illust.isBookmarked ? "delete" : "add"; var method = widget.illust.isBookmarked ? "delete" : "add";
var res = await Network().addBookmark(widget.illust.id.toString(), method, type); var res =
if(res.error) { await Network().addBookmark(widget.illust.id.toString(), method, type);
if(mounted) { if (res.error) {
if (mounted) {
context.showToast(message: "Network Error"); context.showToast(message: "Network Error");
} }
} else { } else {
@@ -233,16 +271,18 @@ class _IllustWidgetState extends State<IllustWidget> {
Widget buildButton() { Widget buildButton() {
Widget child; Widget child;
if(isBookmarking) { if (isBookmarking) {
child = const SizedBox( child = const SizedBox(
width: 14, width: 14,
height: 14, height: 14,
child: ProgressRing(strokeWidth: 1.6,), child: ProgressRing(
strokeWidth: 1.6,
),
); );
} else if(widget.illust.isBookmarked) { } else if (widget.illust.isBookmarked) {
child = Icon( child = Icon(
MdIcons.favorite, MdIcons.favorite,
color: ColorScheme.of(context).error, color: Colors.red,
size: 22, size: 22,
); );
} else { } else {
@@ -268,3 +308,162 @@ 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

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pixes/foundation/app.dart';
typedef KeyEventHandler = void Function(LogicalKeyboardKey key);
class KeyEventListener extends StatefulWidget {
const KeyEventListener({required this.child, super.key});
final Widget child;
static KeyEventListenerState? of(BuildContext context) {
return context.findAncestorStateOfType<KeyEventListenerState>();
}
@override
State<KeyEventListener> createState() => KeyEventListenerState();
}
class KeyEventListenerState extends State<KeyEventListener> {
final focusNode = FocusNode();
final List<KeyEventHandler> _handlers = [];
void addHandler(KeyEventHandler handler) {
_handlers.add(handler);
}
void removeHandler(KeyEventHandler handler) {
_handlers.remove(handler);
}
void removeAll() {
_handlers.clear();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focusNode,
autofocus: true,
onKeyEvent: (node, event) {
if (event is! KeyUpEvent) return KeyEventResult.handled;
if (event.logicalKey == LogicalKeyboardKey.escape) {
if (App.rootNavigatorKey.currentState?.canPop() ?? false) {
App.rootNavigatorKey.currentState?.pop();
} else if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {
App.mainNavigatorKey?.currentState?.pop();
}
return KeyEventResult.handled;
}
for (var handler in _handlers) {
handler(event.logicalKey);
}
return KeyEventResult.handled;
},
child: widget.child,
);
}
}

View File

@@ -328,11 +328,15 @@ class SideBarRoute<T> extends PopupRoute<T> {
bottom: 0, bottom: 0,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.98), color: FluentTheme.of(context)
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4)) .micaBackgroundColor
), .withOpacity(0.98),
constraints: BoxConstraints(maxWidth: min(_kSideBarWidth, borderRadius: const BorderRadius.only(
MediaQuery.of(context).size.width)), topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4))),
constraints: BoxConstraints(
maxWidth:
min(_kSideBarWidth, MediaQuery.of(context).size.width)),
width: double.infinity, width: double.infinity,
child: child, child: child,
), ),

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -11,11 +12,13 @@ export "state_controller.dart";
export "navigation.dart"; export "navigation.dart";
class _App { class _App {
final version = "1.0.5"; final version = "1.0.7";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;
bool get isIOS => Platform.isIOS; bool get isIOS => Platform.isIOS;
bool get isWindows => Platform.isWindows; bool get isWindows => Platform.isWindows;
int? _windowsVersion;
int get windowsVersion => _windowsVersion!;
bool get isLinux => Platform.isLinux; bool get isLinux => Platform.isLinux;
bool get isMacOS => Platform.isMacOS; bool get isMacOS => Platform.isMacOS;
bool get isDesktop => bool get isDesktop =>
@@ -23,8 +26,8 @@ class _App {
bool get isMobile => Platform.isAndroid || Platform.isIOS; bool get isMobile => Platform.isAndroid || Platform.isIOS;
Locale get locale { Locale get locale {
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", "CN"), "简体中文" => const Locale("zh", "CN"),
"繁體中文" => const Locale("zh", "TW"), "繁體中文" => const Locale("zh", "TW"),
@@ -32,7 +35,8 @@ class _App {
}; };
} }
Locale deviceLocale = PlatformDispatcher.instance.locale; Locale deviceLocale = PlatformDispatcher.instance.locale;
if (deviceLocale.languageCode == "zh" && deviceLocale.scriptCode == "Hant") { if (deviceLocale.languageCode == "zh" &&
deviceLocale.scriptCode == "Hant") {
deviceLocale = const Locale("zh", "TW"); deviceLocale = const Locale("zh", "TW");
} }
return deviceLocale; return deviceLocale;
@@ -41,9 +45,24 @@ class _App {
late String dataPath; late String dataPath;
late String cachePath; late String cachePath;
init() async{ init() async {
cachePath = (await getApplicationCacheDirectory()).path; cachePath = (await getApplicationCacheDirectory()).path;
dataPath = (await getApplicationSupportDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path;
if (App.isWindows) {
final deviceInfoPlugin = DeviceInfoPlugin();
final deviceInfo = await deviceInfoPlugin.windowsInfo;
if (deviceInfo.majorVersion <= 6) {
if (deviceInfo.minorVersion < 2) {
_windowsVersion = 7;
} else {
_windowsVersion = 8;
}
} else if (deviceInfo.buildNumber < 22000) {
_windowsVersion = 10;
} else {
_windowsVersion = 11;
}
}
} }
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();

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

View File

@@ -2,12 +2,16 @@ import "dart:ui";
import "package:dynamic_color/dynamic_color.dart"; import "package:dynamic_color/dynamic_color.dart";
import "package:fluent_ui/fluent_ui.dart"; import "package:fluent_ui/fluent_ui.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart" as md; import "package:flutter/material.dart" as md;
import "package:flutter/services.dart"; import "package:flutter/services.dart";
import "package:flutter_acrylic/flutter_acrylic.dart" as flutter_acrylic;
import "package:pixes/appdata.dart"; import "package:pixes/appdata.dart";
import "package:pixes/components/keyboard.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";
import "package:pixes/foundation/app.dart"; import "package:pixes/foundation/app.dart";
import "package:pixes/foundation/history.dart";
import "package:pixes/foundation/log.dart"; import "package:pixes/foundation/log.dart";
import "package:pixes/network/app_dio.dart"; import "package:pixes/network/app_dio.dart";
import "package:pixes/pages/main_page.dart"; import "package:pixes/pages/main_page.dart";
@@ -26,14 +30,23 @@ void main() async {
await App.init(); await App.init();
await appdata.readData(); await appdata.readData();
await Translation.init(); await Translation.init();
HistoryManager().init();
handleLinks(); handleLinks();
if (App.isDesktop) { if (App.isDesktop) {
await flutter_acrylic.Window.initialize();
if (App.isWindows) {
await flutter_acrylic.Window.hideWindowControls();
}
await WindowManager.instance.ensureInitialized(); await WindowManager.instance.ensureInitialized();
windowManager.waitUntilReadyToShow().then((_) async { windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setTitleBarStyle( await windowManager.setTitleBarStyle(
TitleBarStyle.hidden, TitleBarStyle.hidden,
windowButtonVisibility: false, windowButtonVisibility: false,
); );
if(App.isLinux) {
// https://github.com/leanflutter/window_manager/issues/460
return;
}
await windowManager.setMinimumSize(const Size(500, 600)); await windowManager.setMinimumSize(const Size(500, 600));
var placement = await WindowPlacement.loadFromFile(); var placement = await WindowPlacement.loadFromFile();
await placement.applyToWindow(); await placement.applyToWindow();
@@ -42,6 +55,7 @@ void main() async {
}); });
} }
Loop.start(); Loop.start();
Log.info("APP", "Application started");
runApp(const MyApp()); runApp(const MyApp());
} }
@@ -51,6 +65,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
var windowsFont = kDebugMode ? "微软雅黑" : "font";
return StateBuilder<SimpleController>( return StateBuilder<SimpleController>(
init: SimpleController(), init: SimpleController(),
tag: "MyApp", tag: "MyApp",
@@ -83,7 +98,7 @@ class MyApp extends StatelessWidget {
title: 'pixes', title: 'pixes',
theme: FluentThemeData( theme: FluentThemeData(
brightness: brightness, brightness: brightness,
fontFamily: App.isWindows ? 'font' : null, fontFamily: App.isWindows ? windowsFont : null,
accentColor: AccentColor.swatch({ accentColor: AccentColor.swatch({
'darkest': darken(colorScheme.primary, 30), 'darkest': darken(colorScheme.primary, 30),
'darker': darken(colorScheme.primary, 20), 'darker': darken(colorScheme.primary, 20),
@@ -92,7 +107,11 @@ class MyApp extends StatelessWidget {
'light': lighten(colorScheme.primary, 10), 'light': lighten(colorScheme.primary, 10),
'lighter': lighten(colorScheme.primary, 20), 'lighter': lighten(colorScheme.primary, 20),
'lightest': lighten(colorScheme.primary, 30) 'lightest': lighten(colorScheme.primary, 30)
})), }),
focusTheme: const FocusThemeData(
primaryBorder: BorderSide.none,
secondaryBorder: BorderSide.none,
)),
home: const MainPage(), home: const MainPage(),
builder: (context, child) { builder: (context, child) {
ErrorWidget.builder = (details) { ErrorWidget.builder = (details) {
@@ -109,7 +128,7 @@ class MyApp extends StatelessWidget {
throw "widget is null"; throw "widget is null";
} }
return MdTheme( Widget widget = MdTheme(
data: MdThemeData.from( data: MdThemeData.from(
colorScheme: colorScheme, useMaterial3: true), colorScheme: colorScheme, useMaterial3: true),
child: DefaultTextStyle.merge( child: DefaultTextStyle.merge(
@@ -119,6 +138,36 @@ class MyApp extends StatelessWidget {
child: OverlayWidget(child), child: OverlayWidget(child),
), ),
); );
if (App.isWindows) {
if (App.windowsVersion == 11) {
flutter_acrylic.Window.setEffect(
effect: flutter_acrylic.WindowEffect.mica,
dark: FluentTheme.of(context).brightness ==
Brightness.dark);
widget = NavigationPaneTheme(
data: const NavigationPaneThemeData(
backgroundColor: Colors.transparent,
),
child: widget,
);
} else if (App.windowsVersion == 10) {
flutter_acrylic.Window.setEffect(
effect: flutter_acrylic.WindowEffect.acrylic,
dark: FluentTheme.of(context).brightness ==
Brightness.dark);
widget = NavigationPaneTheme(
data: NavigationPaneThemeData(
backgroundColor: FluentTheme.of(context)
.micaBackgroundColor
.withOpacity(0.72),
),
child: widget,
);
}
}
return KeyEventListener(child: widget);
}); });
}, },
), ),

View File

@@ -573,4 +573,14 @@ class Network {
return Res.error(res.errorMessage); 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: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/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/foundation/history.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';
@@ -17,42 +19,109 @@ class HistoryPage extends StatefulWidget {
State<HistoryPage> createState() => _HistoryPageState(); State<HistoryPage> createState() => _HistoryPageState();
} }
class _HistoryPageState extends MultiPageLoadingState<HistoryPage, Illust> { class _HistoryPageState extends State<HistoryPage> {
int page = 0;
@override @override
Widget buildContent(BuildContext context, final List<Illust> data) { Widget build(BuildContext context) {
return Column( return Column(
children: [ 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( Expanded(
child: LayoutBuilder(builder: (context, constrains){ child: page == 0
return MasonryGridView.builder( ? const LocalHistoryPage()
padding: const EdgeInsets.symmetric(horizontal: 8) : const NetworkHistoryPage(),
+ 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,
));
});
},
);
}),
)
], ],
); );
} }
}
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 @override
Future<Res<List<Illust>>> loadData(page) { 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 Future.value(Res.error("Premium Required".tl));
} }
return Network().getHistory(page); return Network().getHistory(page);

View File

@@ -7,12 +7,14 @@ import 'package:flutter/services.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/animated_image.dart'; import 'package:pixes/components/animated_image.dart';
import 'package:pixes/components/keyboard.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/page_route.dart'; import 'package:pixes/components/page_route.dart';
import 'package:pixes/components/title_bar.dart'; import 'package:pixes/components/title_bar.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/foundation/history.dart';
import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/network/download.dart'; import 'package:pixes/network/download.dart';
import 'package:pixes/network/network.dart'; import 'package:pixes/network/network.dart';
@@ -43,6 +45,8 @@ class IllustGalleryPage extends StatefulWidget {
final String? nextUrl; final String? nextUrl;
static var cachedHistoryIds = <int>{};
@override @override
State<IllustGalleryPage> createState() => _IllustGalleryPageState(); State<IllustGalleryPage> createState() => _IllustGalleryPageState();
} }
@@ -56,6 +60,8 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
bool loading = false; bool loading = false;
late int page = widget.initialPage;
@override @override
void initState() { void initState() {
illusts = List.from(widget.illusts); illusts = List.from(widget.illusts);
@@ -67,6 +73,16 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
super.initState(); super.initState();
} }
@override
void dispose() {
if (IllustGalleryPage.cachedHistoryIds.length > 5) {
Network().sendHistory(
IllustGalleryPage.cachedHistoryIds.toList().reversed.toList());
IllustGalleryPage.cachedHistoryIds.clear();
}
super.dispose();
}
void nextPage() { void nextPage() {
var length = illusts.length; var length = illusts.length;
if (controller.page == length - 1) return; if (controller.page == length - 1) return;
@@ -87,16 +103,51 @@ class _IllustGalleryPageState extends State<IllustGalleryPage> {
length++; length++;
} }
return PageView.builder( return Stack(
controller: controller, children: [
itemCount: length, Positioned.fill(
itemBuilder: (context, index) { child: PageView.builder(
if (index == illusts.length) { controller: controller,
return buildLast(); itemCount: length,
} itemBuilder: (context, index) {
return IllustPage(illusts[index], if (index == illusts.length) {
nextPage: nextPage, previousPage: previousPage); 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();
},
)),
),
],
); );
} }
@@ -154,18 +205,30 @@ class IllustPage extends StatefulWidget {
class _IllustPageState extends State<IllustPage> { class _IllustPageState extends State<IllustPage> {
String get id => "${widget.illust.author.id}#${widget.illust.id}"; String get id => "${widget.illust.author.id}#${widget.illust.id}";
final _bottomBarController = _BottomBarController();
KeyEventListenerState? keyboardListener;
@override @override
void initState() { void initState() {
keyboardListener = KeyEventListener.of(context);
keyboardListener?.removeAll();
keyboardListener?.addHandler(handleKey);
IllustPage.followCallbacks[id] = (v) { IllustPage.followCallbacks[id] = (v) {
setState(() { setState(() {
widget.illust.author.isFollowed = v; widget.illust.author.isFollowed = v;
}); });
}; };
HistoryManager().addHistory(widget.illust);
if (appdata.account!.user.isPremium) {
IllustGalleryPage.cachedHistoryIds.add(widget.illust.id);
}
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
keyboardListener?.removeHandler(handleKey);
IllustPage.followCallbacks.remove(id); IllustPage.followCallbacks.remove(id);
super.dispose(); super.dispose();
} }
@@ -173,7 +236,7 @@ class _IllustPageState extends State<IllustPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var isBlocked = checkIllusts([widget.illust]).isEmpty; var isBlocked = checkIllusts([widget.illust]).isEmpty;
return buildKeyboardListener(ColoredBox( return ColoredBox(
color: FluentTheme.of(context).micaBackgroundColor, color: FluentTheme.of(context).micaBackgroundColor,
child: SizedBox.expand( child: SizedBox.expand(
child: ColoredBox( child: ColoredBox(
@@ -195,6 +258,7 @@ class _IllustPageState extends State<IllustPage> {
constrains.maxHeight, constrains.maxHeight,
constrains.maxWidth, constrains.maxWidth,
updateCallback: () => setState(() {}), updateCallback: () => setState(() {}),
controller: _bottomBarController,
), ),
if (isBlocked) if (isBlocked)
const Positioned.fill( const Positioned.fill(
@@ -209,36 +273,61 @@ class _IllustPageState extends State<IllustPage> {
}), }),
), ),
), ),
)); );
} }
final scrollController = ScrollController(); final scrollController = ScrollController();
Widget buildKeyboardListener(Widget child) { void handleKey(LogicalKeyboardKey key) {
return KeyboardListener( const kShortcutScrollOffset = 200;
focusNode: FocusNode(),
autofocus: true, var shortcuts = appdata.settings["shortcuts"] as List;
onKeyEvent: (event) {
if (event is! KeyUpEvent) return; switch (shortcuts.indexOf(key.keyId)) {
const kShortcutScrollOffset = 200; case 0:
if (event.logicalKey == LogicalKeyboardKey.arrowDown) { if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent) {
_bottomBarController.openOrClose();
} else {
scrollController.animateTo( scrollController.animateTo(
scrollController.offset + kShortcutScrollOffset, scrollController.offset + kShortcutScrollOffset,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.easeOut); curve: Curves.easeOut,
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { );
scrollController.animateTo(
scrollController.offset - kShortcutScrollOffset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut);
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
widget.nextPage?.call();
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
widget.previousPage?.call();
} }
}, break;
child: child, case 1:
); if (_bottomBarController.isOpen()) {
_bottomBarController.openOrClose();
break;
}
scrollController.animateTo(
scrollController.offset - kShortcutScrollOffset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
break;
case 2:
widget.nextPage?.call();
break;
case 3:
widget.previousPage?.call();
break;
case 4:
_bottomBarController.favorite();
case 5:
_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);
}
} }
Widget buildBody(double width, double height) { Widget buildBody(double width, double height) {
@@ -291,6 +380,10 @@ class _IllustPageState extends State<IllustPage> {
} }
Widget image; Widget image;
var imageUrl = appdata.settings["showOriginalImage"]
? widget.illust.images[index].original
: widget.illust.images[index].large;
if (!widget.illust.isUgoira) { if (!widget.illust.isUgoira) {
image = SizedBox( image = SizedBox(
width: imageWidth, width: imageWidth,
@@ -300,8 +393,7 @@ class _IllustPageState extends State<IllustPage> {
child: Image( child: Image(
key: ValueKey(index), key: ValueKey(index),
image: downloadFile == null image: downloadFile == null
? CachedImageProvider(widget.illust.images[index].large) ? CachedImageProvider(imageUrl) as ImageProvider
as ImageProvider
: FileImage(downloadFile) as ImageProvider, : FileImage(downloadFile) as ImageProvider,
width: imageWidth, width: imageWidth,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -344,8 +436,31 @@ class _IllustPageState extends State<IllustPage> {
} }
} }
class _BottomBarController {
VoidCallback? _openOrClose;
VoidCallback get openOrClose => _openOrClose!;
bool Function()? _isOpen;
bool isOpen() => _isOpen!();
VoidCallback? _favorite;
VoidCallback get favorite => _favorite!;
VoidCallback? _download;
VoidCallback get download => _download!;
VoidCallback? _follow;
VoidCallback get follow => _follow!;
}
class _BottomBar extends StatefulWidget { class _BottomBar extends StatefulWidget {
const _BottomBar(this.illust, this.height, this.width, {this.updateCallback}); const _BottomBar(this.illust, this.height, this.width,
{this.updateCallback, this.controller});
final void Function()? updateCallback; final void Function()? updateCallback;
@@ -355,6 +470,8 @@ class _BottomBar extends StatefulWidget {
final double width; final double width;
final _BottomBarController? controller;
@override @override
State<_BottomBar> createState() => _BottomBarState(); State<_BottomBar> createState() => _BottomBarState();
} }
@@ -391,9 +508,32 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {
..onCancel = _handlePointerCancel; ..onCancel = _handlePointerCancel;
animationController = AnimationController( animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 180), value: 1); vsync: this, duration: const Duration(milliseconds: 180), value: 1);
if (widget.controller != null) {
widget.controller!._openOrClose = () {
if (animationController.value == 0) {
animationController.animateTo(1);
} else if (animationController.value == 1) {
animationController.animateTo(0);
}
};
widget.controller!._isOpen = () => animationController.value == 0;
widget.controller!._favorite = favorite;
widget.controller!._download = () {
DownloadManager().addDownloadingTask(widget.illust);
setState(() {});
};
widget.controller!._follow = follow;
}
super.initState(); super.initState();
} }
@override
void dispose() {
animationController.dispose();
_recognizer.dispose();
super.dispose();
}
void _handlePointerDown(DragStartDetails details) {} void _handlePointerDown(DragStartDetails details) {}
void _handlePointerMove(DragUpdateDetails details) { void _handlePointerMove(DragUpdateDetails details) {
var offset = details.primaryDelta ?? 0; var offset = details.primaryDelta ?? 0;
@@ -541,31 +681,31 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {
bool isFollowing = false; bool isFollowing = false;
Widget buildAuthor() { void follow() async {
void follow() async { if (isFollowing) return;
if (isFollowing) return; setState(() {
setState(() { isFollowing = true;
isFollowing = true; });
}); var method = widget.illust.author.isFollowed ? "delete" : "add";
var method = widget.illust.author.isFollowed ? "delete" : "add"; var res =
var res = await Network().follow(widget.illust.author.id.toString(), method);
await Network().follow(widget.illust.author.id.toString(), method); if (res.error) {
if (res.error) { if (mounted) {
if (mounted) { context.showToast(message: "Network Error");
context.showToast(message: "Network Error");
}
} else {
widget.illust.author.isFollowed = !widget.illust.author.isFollowed;
} }
setState(() { } else {
isFollowing = false; widget.illust.author.isFollowed = !widget.illust.author.isFollowed;
});
UserInfoPage.followCallbacks[widget.illust.author.id.toString()]
?.call(widget.illust.author.isFollowed);
UserPreviewWidget.followCallbacks[widget.illust.author.id.toString()]
?.call(widget.illust.author.isFollowed);
} }
setState(() {
isFollowing = false;
});
UserInfoPage.followCallbacks[widget.illust.author.id.toString()]
?.call(widget.illust.author.isFollowed);
UserPreviewWidget.followCallbacks[widget.illust.author.id.toString()]
?.call(widget.illust.author.isFollowed);
}
Widget buildAuthor() {
final bool showUserName = MediaQuery.of(context).size.width > 640; final bool showUserName = MediaQuery.of(context).size.width > 640;
return Card( return Card(
@@ -919,7 +1059,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {
Widget buildMoreActions() { Widget buildMoreActions() {
return Wrap( return Wrap(
runSpacing: 4, runSpacing: 8,
spacing: 8, spacing: 8,
children: [ children: [
Button( Button(

View File

@@ -3,8 +3,10 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; 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:image_gallery_saver/image_gallery_saver.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:pixes/components/md.dart'; import 'package:pixes/components/md.dart';
import 'package:pixes/components/message.dart';
import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/page_route.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/cache_manager.dart'; import 'package:pixes/foundation/cache_manager.dart';
@@ -89,6 +91,8 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
void showMenu() { void showMenu() {
menuController.showFlyout( menuController.showFlyout(
barrierColor: Colors.transparent,
position: Offset(context.size!.width, 0),
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: [ items: [
MenuFlyoutItem( MenuFlyoutItem(
@@ -103,6 +107,23 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
saveFile(file, fileName); saveFile(file, fileName);
} }
}), }),
if (App.isMobile)
MenuFlyoutItem(
text: Text("Save to gallery".tl),
onPressed: () async {
var file = await getFile();
if (file != null) {
var fileName = file.path.split('/').last;
if (!fileName.contains('.')) {
fileName += getExtensionName();
}
await ImageGallerySaver.saveFile(file.path,
name: fileName);
if (mounted) {
showToast(context, message: "Saved".tl);
}
}
}),
MenuFlyoutItem( MenuFlyoutItem(
text: Text("Share".tl), text: Text("Share".tl),
onPressed: () async { onPressed: () async {
@@ -201,35 +222,37 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
), ),
), ),
), ),
Positioned( if (currentPage != 0)
left: 0, Positioned(
top: height / 2 - 9, left: 0,
child: IconButton( top: height / 2 - 9,
icon: const Icon( child: IconButton(
FluentIcons.chevron_left, icon: const Icon(
size: 18, FluentIcons.chevron_left,
), size: 18,
onPressed: () { ),
controller.previousPage( onPressed: () {
duration: const Duration(milliseconds: 300), controller.previousPage(
curve: Curves.easeInOut, duration: const Duration(milliseconds: 300),
); curve: Curves.easeInOut,
}, );
).paddingAll(8), },
), ).paddingAll(8),
Positioned( ),
right: 0, if (currentPage != widget.urls.length - 1)
top: height / 2 - 9, Positioned(
child: IconButton( right: 0,
icon: const Icon(FluentIcons.chevron_right, size: 18), top: height / 2 - 9,
onPressed: () { child: IconButton(
controller.nextPage( icon: const Icon(FluentIcons.chevron_right, size: 18),
duration: const Duration(milliseconds: 300), onPressed: () {
curve: Curves.easeInOut, controller.nextPage(
); duration: const Duration(milliseconds: 300),
}, curve: Curves.easeInOut,
).paddingAll(8), );
), },
).paddingAll(8),
),
Positioned( Positioned(
left: 12, left: 12,
bottom: 8, bottom: 8,
@@ -252,16 +275,16 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
child: width > 600 child: width > 600
? Button( ? Button(
onPressed: showMenu, onPressed: showMenu,
child: const Row( child: Row(
children: [ children: [
Icon( const Icon(
MdIcons.menu, MdIcons.menu,
size: 18, size: 18,
), ),
SizedBox( const SizedBox(
width: 8, width: 8,
), ),
Text('Actions'), Text('Actions'.tl),
], ],
)) ))
: IconButton( : IconButton(

View File

@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/button.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/webview_page.dart'; import 'package:pixes/pages/webview_page.dart';
@@ -24,7 +25,7 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if(isLogging) { if (isLogging) {
return buildLoading(context); return buildLoading(context);
} else if (!waitingForAuth) { } else if (!waitingForAuth) {
return buildLogin(context); return buildLogin(context);
@@ -56,23 +57,12 @@ class _LoginPageState extends State<LoginPage> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (checked) FluentButton(
FilledButton( onPressed: onContinue,
onPressed: onContinue, enabled: checked,
child: Text("Continue".tl), width: 96,
) 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),
),
),
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
@@ -186,6 +176,39 @@ class _LoginPageState extends State<LoginPage> {
} }
void onContinue() async { 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(); var url = await Network().generateWebviewUrl();
onLink = (uri) { onLink = (uri) {
if (uri.scheme == "pixiv") { if (uri.scheme == "pixiv") {
@@ -198,15 +221,18 @@ class _LoginPageState extends State<LoginPage> {
setState(() { setState(() {
waitingForAuth = true; waitingForAuth = true;
}); });
if(App.isMobile && mounted) { if (!useExternal! && mounted) {
context.to(() => WebviewPage(url, onNavigation: (req) { context.to(() => WebviewPage(
if(req.url.startsWith("pixiv://")) { url,
App.rootNavigatorKey.currentState!.pop(); onNavigation: (req) {
onLink?.call(Uri.parse(req.url)); if (req.url.startsWith("pixiv://")) {
return false; App.rootNavigatorKey.currentState!.pop();
} onLink?.call(Uri.parse(req.url));
return true; return false;
},)); }
return true;
},
));
} else { } else {
launchUrlString(url); launchUrlString(url);
} }
@@ -219,7 +245,7 @@ class _LoginPageState extends State<LoginPage> {
}); });
var res = await Network().loginWithCode(code); var res = await Network().loginWithCode(code);
if (res.error) { if (res.error) {
if(mounted) { if (mounted) {
context.showToast(message: res.errorMessage!); context.showToast(message: res.errorMessage!);
} }
setState(() { setState(() {

View File

@@ -151,7 +151,9 @@ class _MainPageState extends State<MainPage> with WindowListener {
), ),
PaneItemSeparator(), PaneItemSeparator(),
PaneItemHeader( PaneItemHeader(
header: Text("Artwork".tl).paddingBottom(4).paddingLeft(8)), header: Text('${"Artwork".tl}/${"Manga".tl}')
.paddingBottom(4)
.paddingLeft(8)),
PaneItem( PaneItem(
icon: const Icon( icon: const Icon(
MdIcons.explore_outlined, MdIcons.explore_outlined,
@@ -269,11 +271,17 @@ class _MainPageState extends State<MainPage> with WindowListener {
if (!App.isDesktop) const Spacer(), if (!App.isDesktop) const Spacer(),
if (App.isDesktop) if (App.isDesktop)
const Expanded( const Expanded(
child: DragToMoveArea( child: SizedBox(
height: double.infinity,
child: DragToMoveArea(
child: Align(
alignment: Alignment.centerLeft,
child: Text( child: Text(
"Pixes", "Pixes",
style: TextStyle(fontSize: 13), style: TextStyle(fontSize: 13),
)), ),
)),
),
), ),
for (var action in controller.actions) for (var action in controller.actions)
Button( Button(

View File

@@ -29,7 +29,7 @@ class _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {
@override @override
void initState() { void initState() {
action = TitleBarAction(MdIcons.tune, "Settings", () { action = TitleBarAction(MdIcons.tune, "Settings".tl, () {
if (!isShowingSettings) { if (!isShowingSettings) {
_NovelReadingSettings.show(context, () { _NovelReadingSettings.show(context, () {
setState(() {}); setState(() {});
@@ -138,7 +138,8 @@ class _NovelReadingSettings extends StatefulWidget {
final void Function() callback; final void Function() callback;
static Future show(BuildContext context, void Function() callback) { static Future show(BuildContext context, void Function() callback) {
return Navigator.of(context).push(SideBarRoute(_NovelReadingSettings(callback))); return Navigator.of(context)
.push(SideBarRoute(_NovelReadingSettings(callback)));
} }
@override @override

View File

@@ -10,7 +10,9 @@ 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/novel_page.dart';
import 'package:pixes/pages/user_info_page.dart'; import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/app_links.dart';
import 'package:pixes/utils/block.dart'; import 'package:pixes/utils/block.dart';
import 'package:pixes/utils/ext.dart';
import 'package:pixes/utils/translation.dart'; import 'package:pixes/utils/translation.dart';
import '../components/animated_image.dart'; import '../components/animated_image.dart';
@@ -41,6 +43,12 @@ class _SearchPageState extends State<SearchPage> {
]; ];
void search() { void search() {
if (text.isURL && handleLink(Uri.parse(text))) {
return;
} else if ("https://$text".isURL &&
handleLink(Uri.parse("https://$text"))) {
return;
}
switch (searchType) { switch (searchType) {
case 0: case 0:
context.to(() => SearchResultPage(text)); context.to(() => SearchResultPage(text));
@@ -92,7 +100,8 @@ class _SearchPageState extends State<SearchPage> {
children: [ children: [
Expanded( Expanded(
child: TextBox( child: TextBox(
placeholder: searchTypes[searchType].tl, placeholder:
'${searchTypes[searchType].tl} / ${"Open link".tl}',
onChanged: (s) => text = s, onChanged: (s) => text = s,
onSubmitted: (s) => search(), onSubmitted: (s) => search(),
foregroundDecoration: BoxDecoration( foregroundDecoration: BoxDecoration(
@@ -127,9 +136,9 @@ class _SearchPageState extends State<SearchPage> {
), ),
onPressed: () { onPressed: () {
optionController.showFlyout( optionController.showFlyout(
placementMode: FlyoutPlacementMode.bottomCenter, placementMode: FlyoutPlacementMode.bottomCenter,
builder: buildSearchOption, builder: buildSearchOption,
); barrierColor: Colors.transparent);
}, },
), ),
), ),

View File

@@ -1,7 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:pixes/appdata.dart'; import 'package:pixes/appdata.dart';
import 'package:pixes/components/keyboard.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';
import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/page_route.dart';
@@ -86,6 +88,7 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text('Continue'.tl), child: Text('Continue'.tl),
onPressed: () { onPressed: () {
appdata.account = null; appdata.account = null;
appdata.writeData();
App.rootNavigatorKey.currentState!.pushAndRemoveUntil( App.rootNavigatorKey.currentState!.pushAndRemoveUntil(
AppPageRoute( AppPageRoute(
builder: (context) => const MainPage()), builder: (context) => const MainPage()),
@@ -242,6 +245,24 @@ class _SettingsPageState extends State<SettingsPage> {
context.to(() => const _BlockTagsPage()); context.to(() => const _BlockTagsPage());
}, },
)), )),
buildItem(
title: "Shortcuts".tl,
action: Button(
child: Text("Edit".tl).fixWidth(64),
onPressed: () {
context.to(() => const ShortcutsSettings());
},
)),
buildItem(
title: "Display the original image on the details page".tl,
action: ToggleSwitch(
checked: appdata.settings['showOriginalImage'],
onChanged: (value) {
setState(() {
appdata.settings['showOriginalImage'] = value;
});
appdata.writeData();
})),
], ],
), ),
); );
@@ -538,3 +559,83 @@ class __BlockTagsPageState extends State<_BlockTagsPage> {
); );
} }
} }
class ShortcutsSettings extends StatefulWidget {
const ShortcutsSettings({super.key});
@override
State<ShortcutsSettings> createState() => _ShortcutsSettingsState();
}
class _ShortcutsSettingsState extends State<ShortcutsSettings> {
int listening = -1;
KeyEventListenerState? listener;
@override
void initState() {
listener = KeyEventListener.of(context);
super.initState();
}
@override
void dispose() {
listener?.removeAll();
super.dispose();
}
final settings = <String>[
"Page down",
"Page up",
"Next work",
"Previous work",
"Add to favorites",
"Download",
"Follow the artist",
"Show comments",
"Show original image"
];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(children: [
TitleBar(title: "Shortcuts".tl),
...settings.map((e) => buildItem(e, settings.indexOf(e)))
]),
);
}
Widget buildItem(String text, int index) {
var keyText = listening == index
? "Waiting..."
: LogicalKeyboardKey(appdata.settings['shortcuts'][index]).keyLabel;
return Card(
padding: EdgeInsets.zero,
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
child: ListTile(
title: Text(text.tl),
trailing: Button(
child: Text(keyText),
onPressed: () {
if (listening != -1) {
listener?.removeAll();
}
setState(() {
listening = index;
});
listener?.addHandler((key) {
if (key == LogicalKeyboardKey.escape) return;
setState(() {
appdata.settings['shortcuts'][index] = key.keyId;
listening = -1;
appdata.writeData();
});
Future.microtask(() => listener?.removeAll());
});
},
),
),
);
}
}

View File

@@ -545,7 +545,8 @@ class _RelatedUsersState
hoveringMainAxisMargin: 4), hoveringMainAxisMargin: 4),
child: content); child: content);
} }
return content; return MediaQuery.removePadding(
context: context, removeBottom: true, child: content);
} }
@override @override

View File

@@ -1,11 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:fluent_ui/fluent_ui.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/illust_page.dart';
import 'package:pixes/pages/novel_page.dart'; 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/pages/user_info_page.dart';
import 'package:pixes/utils/ext.dart';
import 'package:pixes/utils/translation.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 {
@@ -29,13 +33,46 @@ Future<void> _register(String scheme) async {
regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); 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 Function(Uri uri)? onLink;
bool _firstLink = true;
void handleLinks() async { void handleLinks() async {
if (App.isWindows) { if (App.isWindows) {
await _register("pixiv"); _registerPixiv();
} }
AppLinks().uriLinkStream.listen((uri) { AppLinks().uriLinkStream.listen((uri) async {
if (_firstLink) {
await Future.delayed(const Duration(milliseconds: 200));
}
_firstLink = false;
Log.info("App Link", uri.toString()); Log.info("App Link", uri.toString());
if (onLink?.call(uri) == true) { if (onLink?.call(uri) == true) {
return; return;
@@ -70,6 +107,33 @@ bool handleLink(Uri uri) {
} }
} }
return false; return false;
} else if (uri.scheme == "https") {
var path = uri.toString().split("/").sublist(3);
switch (path[0]) {
case "users":
if (path.length >= 2) {
App.mainNavigatorKey?.currentContext?.to(() => UserInfoPage(path[1]));
return true;
}
case "novel":
if (path.length == 2) {
App.mainNavigatorKey?.currentContext
?.to(() => NovelPageWithId(path[1].nums));
return true;
}
case "artworks":
if (path.length == 2) {
App.mainNavigatorKey?.currentContext
?.to(() => IllustPageWithId(path[1]));
return true;
}
case "tags":
if (path.length == 2) {
App.mainNavigatorKey?.currentContext
?.to(() => SearchResultPage(path[1]));
return true;
}
}
} }
return false; return false;
} }

View File

@@ -15,6 +15,10 @@ class WindowPlacement {
Future<void> applyToWindow() async { Future<void> applyToWindow() async {
await windowManager.setBounds(rect); await windowManager.setBounds(rect);
if(!validate(rect)){
await windowManager.center();
}
if (isMaximized) { if (isMaximized) {
await windowManager.maximize(); await windowManager.maximize();
} }
@@ -55,10 +59,17 @@ class WindowPlacement {
static void loop() async { static void loop() async {
var placement = await WindowPlacement.current; var placement = await WindowPlacement.current;
if(!validate(placement.rect)){
return;
}
if (placement.rect != cache.rect || if (placement.rect != cache.rect ||
placement.isMaximized != cache.isMaximized) { placement.isMaximized != cache.isMaximized) {
cache = placement; cache = placement;
await placement.writeToFile(); await placement.writeToFile();
} }
} }
static bool validate(Rect rect){
return rect.topLeft.dx >= 0 && rect.topLeft.dy >= 0;
}
} }

View File

@@ -6,14 +6,25 @@
#include "generated_plugin_registrant.h" #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 <gtk/gtk_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_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 <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { 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 = g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar); gtk_plugin_register_with_registrar(gtk_registrar);
@@ -23,9 +34,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); 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 = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

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

View File

@@ -55,7 +55,7 @@ static void my_application_activate(GApplication* application) {
} }
gtk_window_set_default_size(window, 1280, 720); 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(); g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);

View File

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

46
macos/Podfile Normal file
View File

@@ -0,0 +1,46 @@
platform :osx, '10.14.6'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
# target 'RunnerTests' do
# inherit! :search_paths
# end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14.6'
end
end
end

View File

@@ -461,7 +461,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14.6;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
@@ -543,7 +543,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14.6;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@@ -593,7 +593,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14.6;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -73,6 +73,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91
url: "https://pub.dev"
source: hosted
version: "10.1.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64
url: "https://pub.dev"
source: hosted
version: "7.0.0"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -81,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.3+1" version: "5.4.3+1"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
url: "https://pub.dev"
source: hosted
version: "1.7.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -190,6 +214,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_acrylic:
dependency: "direct main"
description:
name: flutter_acrylic
sha256: "646200d98e8dd2bd4ab931d4ba4f6b4cb899475d6401414017ba5d71b0fac42b"
url: "https://pub.dev"
source: hosted
version: "1.0.0+2"
flutter_file_dialog: flutter_file_dialog:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -253,6 +285,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" 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: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -515,22 +556,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
system_theme:
dependency: "direct main"
description:
name: system_theme
sha256: "1f208db140a3d1e1eac2034b54920d95699c1534df576ced44b3312c5de3975f"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
system_theme_web:
dependency: transitive
description:
name: system_theme_web
sha256: "7566f5a928f6d28d7a60c97bea8a851d1c6bc9b86a4df2366230a97458489219"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: 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 # 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.5+105 version: 1.0.7+107
environment: environment:
sdk: '>=3.3.4 <4.0.0' sdk: '>=3.3.4 <4.0.0'
@@ -56,6 +56,12 @@ dependencies:
flutter_file_dialog: 3.0.1 flutter_file_dialog: 3.0.1
archive: ^3.5.1 archive: ^3.5.1
webview_flutter: ^4.7.0 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter 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! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "pixes" #define MyAppName "pixes"
#define MyAppVersion "1.0.5" #define MyAppVersion "{{version}}"
#define MyAppPublisher "Nyne" #define MyAppPublisher "Nyne"
#define MyAppURL "https://github.com/wgh136/pixes" #define MyAppURL "https://github.com/wgh136/pixes"
#define MyAppExeName "pixes.exe" #define MyAppExeName "pixes.exe"
#define RootPath "C:\Users\wgh19\IdeaProjects\pixes" #define RootPath "{{root_path}}"
[Setup] [Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
@@ -52,6 +52,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{a
Source: "{#RootPath}\build\windows\x64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\flutter_acrylic_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files

View File

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

View File

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

BIN
windows/runner/RCa14464 Normal file

Binary file not shown.

BIN
windows/runner/Runner.aps Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 121 KiB