Initial commit

This commit is contained in:
wgh19
2024-05-13 09:36:23 +08:00
commit b095643cbc
160 changed files with 9956 additions and 0 deletions

41
lib/foundation/app.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'dart:io';
import 'dart:ui';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:path_provider/path_provider.dart';
export "widget_utils.dart";
export "state_controller.dart";
export "navigation.dart";
class _App {
bool get isAndroid => Platform.isAndroid;
bool get isIOS => Platform.isIOS;
bool get isWindows => Platform.isWindows;
bool get isLinux => Platform.isLinux;
bool get isMacOS => Platform.isMacOS;
bool get isDesktop =>
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
bool get isMobile => Platform.isAndroid || Platform.isIOS;
Locale get locale {
Locale deviceLocale = PlatformDispatcher.instance.locale;
if (deviceLocale.languageCode == "zh" && deviceLocale.scriptCode == "Hant") {
deviceLocale = const Locale("zh", "TW");
}
return deviceLocale;
}
late String dataPath;
late String cachePath;
init() async{
cachePath = (await getApplicationCacheDirectory()).path;
dataPath = (await getApplicationSupportDirectory()).path;
}
final rootNavigatorKey = GlobalKey<NavigatorState>();
}
// ignore: non_constant_identifier_names
final App = _App();

View File

@@ -0,0 +1,255 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:pixes/utils/io.dart';
import 'package:sqlite3/sqlite3.dart';
import 'app.dart';
class CacheManager {
static String get cachePath => '${App.cachePath}/cache';
static CacheManager? instance;
late Database _db;
int? _currentSize;
/// size in bytes
int get currentSize => _currentSize ?? 0;
int dir = 0;
int _limitSize = 2 * 1024 * 1024 * 1024;
CacheManager._create(){
Directory(cachePath).createSync(recursive: true);
_db = sqlite3.open('${App.dataPath}/cache.db');
_db.execute('''
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY NOT NULL,
dir TEXT NOT NULL,
name TEXT NOT NULL,
expires INTEGER NOT NULL
)
''');
compute((path) => Directory(path).size, cachePath)
.then((value) => _currentSize = value);
}
factory CacheManager() => instance ??= CacheManager._create();
/// set cache size limit in bytes
void setLimitSize(int size){
_limitSize = size;
}
Future<void> writeCache(String key, Uint8List data, [int duration = 7 * 24 * 60 * 60 * 1000]) async{
this.dir++;
this.dir %= 100;
var dir = this.dir;
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
var file = File('$cachePath/$dir/$name');
while(await file.exists()){
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
file = File('$cachePath/$dir/$name');
}
await file.create(recursive: true);
await file.writeAsBytes(data);
var expires = DateTime.now().millisecondsSinceEpoch + duration;
_db.execute('''
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
''', [key, dir.toString(), name, expires]);
if(_currentSize != null) {
_currentSize = _currentSize! + data.length;
}
if(_currentSize != null && _currentSize! > _limitSize){
await checkCache();
}
}
Future<CachingFile> openWrite(String key) async{
this.dir++;
this.dir %= 100;
var dir = this.dir;
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
var file = File('$cachePath/$dir/$name');
while(await file.exists()){
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
file = File('$cachePath/$dir/$name');
}
await file.create(recursive: true);
return CachingFile._(key, dir.toString(), name, file);
}
Future<String?> findCache(String key) async{
var res = _db.select('''
SELECT * FROM cache
WHERE key = ?
''', [key]);
if(res.isEmpty){
return null;
}
var row = res.first;
var dir = row.values[1] as String;
var name = row.values[2] as String;
var file = File('$cachePath/$dir/$name');
if(await file.exists()){
return file.path;
}
return null;
}
bool _isChecking = false;
Future<void> checkCache() async{
if(_isChecking){
return;
}
_isChecking = true;
var res = _db.select('''
SELECT * FROM cache
WHERE expires < ?
''', [DateTime.now().millisecondsSinceEpoch]);
for(var row in res){
var dir = row.values[1] as int;
var name = row.values[2] as String;
var file = File('$cachePath/$dir/$name');
if(await file.exists()){
await file.delete();
}
}
_db.execute('''
DELETE FROM cache
WHERE expires < ?
''', [DateTime.now().millisecondsSinceEpoch]);
while(_currentSize != null && _currentSize! > _limitSize){
var res = _db.select('''
SELECT * FROM cache
ORDER BY time ASC
limit 10
''');
for(var row in res){
var key = row.values[0] as String;
var dir = row.values[1] as int;
var name = row.values[2] as String;
var file = File('$cachePath/$dir/$name');
if(await file.exists()){
var size = await file.length();
await file.delete();
_db.execute('''
DELETE FROM cache
WHERE key = ?
''', [key]);
_currentSize = _currentSize! - size;
if(_currentSize! <= _limitSize){
break;
}
} else {
_db.execute('''
DELETE FROM cache
WHERE key = ?
''', [key]);
}
}
}
_isChecking = false;
}
Future<void> delete(String key) async{
var res = _db.select('''
SELECT * FROM cache
WHERE key = ?
''', [key]);
if(res.isEmpty){
return;
}
var row = res.first;
var dir = row.values[1] as String;
var name = row.values[2] as String;
var file = File('$cachePath/$dir/$name');
var fileSize = 0;
if(await file.exists()){
fileSize = await file.length();
await file.delete();
}
_db.execute('''
DELETE FROM cache
WHERE key = ?
''', [key]);
if(_currentSize != null) {
_currentSize = _currentSize! - fileSize;
}
}
Future<void> clear() async {
await Directory(cachePath).delete(recursive: true);
Directory(cachePath).createSync(recursive: true);
_db.execute('''
DELETE FROM cache
''');
_currentSize = 0;
}
Future<void> deleteKeyword(String keyword) async{
var res = _db.select('''
SELECT * FROM cache
WHERE key LIKE ?
''', ['%$keyword%']);
for(var row in res){
var key = row.values[0] as String;
var dir = row.values[1] as String;
var name = row.values[2] as String;
var file = File('$cachePath/$dir/$name');
var fileSize = 0;
if(await file.exists()){
fileSize = await file.length();
await file.delete();
}
_db.execute('''
DELETE FROM cache
WHERE key = ?
''', [key]);
if(_currentSize != null) {
_currentSize = _currentSize! - fileSize;
}
}
}
}
class CachingFile{
CachingFile._(this.key, this.dir, this.name, this.file);
final String key;
final String dir;
final String name;
final File file;
final List<int> _buffer = [];
Future<void> writeBytes(List<int> data) async{
_buffer.addAll(data);
if(_buffer.length > 1024 * 1024){
await file.writeAsBytes(_buffer, mode: FileMode.append);
_buffer.clear();
}
}
Future<void> close() async{
if(_buffer.isNotEmpty){
await file.writeAsBytes(_buffer, mode: FileMode.append);
}
CacheManager()._db.execute('''
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]);
}
Future<void> cancel() async{
await file.deleteIfExists();
}
}

View File

@@ -0,0 +1,193 @@
import 'dart:async' show Future, StreamController, scheduleMicrotask;
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui show Codec;
import 'dart:ui';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pixes/network/app_dio.dart';
import 'package:pixes/network/network.dart';
import 'cache_manager.dart';
class BadRequestException implements Exception {
final String message;
BadRequestException(this.message);
@override
String toString() {
return message;
}
}
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
extends ImageProvider<T> {
const BaseImageProvider();
@override
ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadBufferAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: 1.0,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>(
'Image provider: $this \n Image key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
}
Future<ui.Codec> _loadBufferAsync(
T key,
StreamController<ImageChunkEvent> chunkEvents,
ImageDecoderCallback decode,
) async {
try {
int retryTime = 1;
bool stop = false;
chunkEvents.onCancel = () {
stop = true;
};
Uint8List? data;
while (data == null && !stop) {
try {
data = await load(chunkEvents);
} catch (e) {
if (e.toString().contains("Your IP address")) {
rethrow;
}
if (e is BadRequestException) {
rethrow;
}
if (e.toString().contains("handshake")) {
if (retryTime < 5) {
retryTime = 5;
}
}
retryTime <<= 1;
if (retryTime > (2 << 3) || stop) {
rethrow;
}
await Future.delayed(Duration(seconds: retryTime));
}
}
if(stop) {
throw Exception("Image loading is stopped");
}
if(data!.isEmpty) {
throw Exception("Empty image data");
}
try {
final buffer = await ImmutableBuffer.fromUint8List(data);
return await decode(buffer);
} catch (e) {
await CacheManager().delete(this.key);
Object error = e;
if (data.length < 200) {
// data is too short, it's likely that the data is text, not image
try {
var text = utf8.decoder.convert(data);
error = Exception("Expected image data, but got text: $text");
} catch (e) {
// ignore
}
}
throw error;
}
} catch (e) {
scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);
String get key;
@override
bool operator ==(Object other) {
return other is BaseImageProvider<T> && key == other.key;
}
@override
int get hashCode => key.hashCode;
@override
String toString() {
return "$runtimeType($key)";
}
}
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
final String url;
CachedImageProvider(this.url);
@override
String get key => url;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async{
var cached = await CacheManager().findCache(key);
if(cached != null) {
return await File(cached).readAsBytes();
}
var dio = AppDio();
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
var res = await dio.get(
url,
options: Options(
responseType: ResponseType.stream,
validateStatus: (status) => status != null && status < 500,
headers: {
"referer": "https://app-api.pixiv.net/",
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
"x-client-time": time,
"x-client-hash": hash,
"accept-enconding": "gzip",
}
)
);
if(res.statusCode != 200) {
throw BadRequestException("Failed to load image: ${res.statusCode}");
}
var data = <int>[];
var cachingFile = await CacheManager().openWrite(key);
await for (var chunk in res.data.stream) {
data.addAll(chunk);
await cachingFile.writeBytes(chunk);
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: data.length,
expectedTotalBytes: res.data.contentLength+1,
));
}
await cachingFile.close();
return Uint8List.fromList(data);
}
@override
Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<CachedImageProvider>(this);
}
}

91
lib/foundation/log.dart Normal file
View File

@@ -0,0 +1,91 @@
import 'package:flutter/foundation.dart';
import 'package:pixes/utils/ext.dart';
class LogItem {
final LogLevel level;
final String title;
final String content;
final DateTime time = DateTime.now();
@override
toString() => "${level.name} $title $time \n$content\n\n";
LogItem(this.level, this.title, this.content);
}
enum LogLevel { error, warning, info }
class Log {
static final List<LogItem> _logs = <LogItem>[];
static List<LogItem> get logs => _logs;
static const maxLogLength = 3000;
static const maxLogNumber = 500;
static bool ignoreLimitation = false;
static void printWarning(String text) {
print('\x1B[33m$text\x1B[0m');
}
static void printError(String text) {
print('\x1B[31m$text\x1B[0m');
}
static void addLog(LogLevel level, String title, String content) {
if (!ignoreLimitation && content.length > maxLogLength) {
content = "${content.substring(0, maxLogLength)}...";
}
if (kDebugMode) {
switch (level) {
case LogLevel.error:
printError(content);
case LogLevel.warning:
printWarning(content);
case LogLevel.info:
print(content);
}
}
var newLog = LogItem(level, title, content);
if (newLog == _logs.lastOrNull) {
return;
}
_logs.add(newLog);
if (_logs.length > maxLogNumber) {
var res = _logs.remove(
_logs.firstWhereOrNull((element) => element.level == LogLevel.info));
if (!res) {
_logs.removeAt(0);
}
}
}
static info(String title, String content) {
addLog(LogLevel.info, title, content);
}
static warning(String title, String content) {
addLog(LogLevel.warning, title, content);
}
static error(String title, String content) {
addLog(LogLevel.error, title, content);
}
static void clear() => _logs.clear();
@override
String toString() {
var res = "Logs\n\n";
for (var log in _logs) {
res += log.toString();
}
return res;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:fluent_ui/fluent_ui.dart';
import '../components/message.dart' as overlay;
import '../components/page_route.dart';
extension Navigation on BuildContext {
void pop<T>([T? result]) {
Navigator.of(this).pop(result);
}
Future<T?> to<T>(Widget Function() builder) {
return Navigator.of(this)
.push<T>(AppPageRoute(builder: (context) => builder()));
}
void showToast({required String message, IconData? icon}) {
overlay.showToast(this, message: message, icon: icon);
}
}

9
lib/foundation/pair.dart Normal file
View File

@@ -0,0 +1,9 @@
class Pair<M, V>{
M left;
V right;
Pair(this.left, this.right);
Pair.fromMap(Map<M, V> map, M key): left = key, right = map[key]
?? (throw Exception("Pair not found"));
}

View File

@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'pair.dart';
class SimpleController extends StateController{
final void Function()? refresh_;
SimpleController({this.refresh_});
@override
void refresh() {
(refresh_ ?? super.refresh)();
}
}
abstract class StateController{
static final _controllers = <StateControllerWrapped>[];
static T put<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){
_controllers.add(StateControllerWrapped(controller, autoRemove, tag));
return controller;
}
static T putIfNotExists<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){
return findOrNull<T>(tag: tag) ?? put(controller, tag: tag, autoRemove: autoRemove);
}
static T find<T extends StateController>({Object? tag}){
try {
return _controllers.lastWhere((element) =>
element.controller is T
&& (tag == null || tag == element.tag)).controller as T;
}
catch(e){
throw StateError("${T.runtimeType} with tag $tag Not Found");
}
}
static T? findOrNull<T extends StateController>({Object? tag}){
try {
return _controllers.lastWhere((element) =>
element.controller is T
&& (tag == null || tag == element.tag)).controller as T;
}
catch(e){
return null;
}
}
static void remove<T>([Object? tag, bool check = false]){
for(int i=_controllers.length-1; i>=0; i--){
var element = _controllers[i];
if(element.controller is T && (tag == null || tag == element.tag)){
if(check && !element.autoRemove){
continue;
}
_controllers.removeAt(i);
return;
}
}
}
static SimpleController putSimpleController(void Function() onUpdate, Object? tag, {void Function()? refresh}){
var controller = SimpleController(refresh_: refresh);
controller.stateUpdaters.add(Pair(null, onUpdate));
_controllers.add(StateControllerWrapped(controller, false, tag));
return controller;
}
List<Pair<Object?, void Function()>> stateUpdaters = [];
void update([List<Object>? ids]){
if(ids == null){
for(var element in stateUpdaters){
element.right();
}
}else{
for(var element in stateUpdaters){
if(ids.contains(element.left)) {
element.right();
}
}
}
}
void dispose(){
_controllers.removeWhere((element) => element.controller == this);
}
void refresh(){
update();
}
}
class StateControllerWrapped{
StateController controller;
bool autoRemove;
Object? tag;
StateControllerWrapped(this.controller, this.autoRemove, this.tag);
}
class StateBuilder<T extends StateController> extends StatefulWidget {
const StateBuilder({super.key, this.init, this.dispose, this.initState, this.tag,
required this.builder, this.id});
final T? init;
final void Function(T controller)? dispose;
final void Function(T controller)? initState;
final Object? tag;
final Widget Function(T controller) builder;
Widget builderWrapped(StateController controller){
return builder(controller as T);
}
void initStateWrapped(StateController controller){
return initState?.call(controller as T);
}
void disposeWrapped(StateController controller){
return dispose?.call(controller as T);
}
final Object? id;
@override
State<StateBuilder> createState() => _StateBuilderState<T>();
}
class _StateBuilderState<T extends StateController> extends State<StateBuilder> {
late T controller;
@override
void initState() {
if(widget.init != null) {
StateController.put(widget.init!, tag: widget.tag, autoRemove: true);
}
try {
controller = StateController.find<T>(tag: widget.tag);
}
catch(e){
throw "Controller Not Found";
}
controller.stateUpdaters.add(Pair(widget.id, () {
if(mounted){
setState(() {});
}
}));
widget.initStateWrapped(controller);
super.initState();
}
@override
void dispose() {
widget.disposeWrapped(controller);
StateController.remove<T>(widget.tag, true);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.builderWrapped(controller);
}
abstract class StateWithController<T extends StatefulWidget> extends State<T>{
late final SimpleController _controller;
void refresh(){
_controller.update();
}
@override
@mustCallSuper
void initState() {
_controller = StateController.putSimpleController(() => setState(() {}), tag, refresh: refresh);
super.initState();
}
@override
@mustCallSuper
void dispose() {
_controller.dispose();
super.dispose();
}
void update(){
_controller.update();
}
Object? get tag;
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/widgets.dart';
extension WidgetExtension on Widget{
Widget padding(EdgeInsetsGeometry padding){
return Padding(padding: padding, child: this);
}
Widget paddingLeft(double padding){
return Padding(padding: EdgeInsets.only(left: padding), child: this);
}
Widget paddingRight(double padding){
return Padding(padding: EdgeInsets.only(right: padding), child: this);
}
Widget paddingTop(double padding){
return Padding(padding: EdgeInsets.only(top: padding), child: this);
}
Widget paddingBottom(double padding){
return Padding(padding: EdgeInsets.only(bottom: padding), child: this);
}
Widget paddingVertical(double padding){
return Padding(padding: EdgeInsets.symmetric(vertical: padding), child: this);
}
Widget paddingHorizontal(double padding){
return Padding(padding: EdgeInsets.symmetric(horizontal: padding), child: this);
}
Widget paddingAll(double padding){
return Padding(padding: EdgeInsets.all(padding), child: this);
}
Widget toCenter(){
return Center(child: this);
}
Widget toAlign(AlignmentGeometry alignment){
return Align(alignment: alignment, child: this);
}
Widget sliverPadding(EdgeInsetsGeometry padding){
return SliverPadding(padding: padding, sliver: this);
}
Widget sliverPaddingAll(double padding){
return SliverPadding(padding: EdgeInsets.all(padding), sliver: this);
}
Widget sliverPaddingVertical(double padding){
return SliverPadding(padding: EdgeInsets.symmetric(vertical: padding), sliver: this);
}
Widget sliverPaddingHorizontal(double padding){
return SliverPadding(padding: EdgeInsets.symmetric(horizontal: padding), sliver: this);
}
}