mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 04:57:23 +00:00
Initial commit
This commit is contained in:
41
lib/foundation/app.dart
Normal file
41
lib/foundation/app.dart
Normal 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();
|
255
lib/foundation/cache_manager.dart
Normal file
255
lib/foundation/cache_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
193
lib/foundation/image_provider.dart
Normal file
193
lib/foundation/image_provider.dart
Normal 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
91
lib/foundation/log.dart
Normal 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;
|
||||
}
|
||||
}
|
19
lib/foundation/navigation.dart
Normal file
19
lib/foundation/navigation.dart
Normal 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
9
lib/foundation/pair.dart
Normal 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"));
|
||||
}
|
195
lib/foundation/state_controller.dart
Normal file
195
lib/foundation/state_controller.dart
Normal 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;
|
||||
}
|
59
lib/foundation/widget_utils.dart
Normal file
59
lib/foundation/widget_utils.dart
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user