mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00

* feat: 增加图片收藏 * feat: 主体图片收藏页面实现 * feat: 点击打开大图浏览 * feat: 数据结构变更 * feat: 基本完成 * feat: 翻译与bug修复 * feat: 实机测试和问题修复 * feat: jm导入, pica历史记录nhentai有问题, 一键反转 * fix: 大小写不一致, 一个htManga, 一个htmanga * feat: 拉取收藏优化 * feat: 改成以ep为准 * feat: 兜底一些可能报错场景 * chore: 没有用到 * feat: 尽量保证和网络收藏顺序一致 * feat: 支持显示热点tag * feat: 支持双击收藏, 不过此时禁止放大图片 * fix: 自动塞封面逻辑完善, 切换快速收藏图片立刻生效 * Refactor * fix updateValue * feat: 双击功能提示 * fix: 被确定取消收藏的才删除 * Refactor ImageFavoritesPage * translate author * feat: 功能提示改到dialog中 * fix text editing * fix text editing * feat: 功能提示放到邮件或长按菜单中 * fix: 修复tag过滤不生效问题 * Improve image loading * The default value of quickCollectImage should be false. * Refactor DragListener * Refactor ImageFavoriteItem & ImageFavoritePhotoView * Refactor * Fix `ImageFavoriteManager.has` * Fix UI * Improve UI --------- Co-authored-by: nyne <me@nyne.dev>
162 lines
4.4 KiB
Dart
162 lines
4.4 KiB
Dart
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
|
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'dart:ui' as ui show Codec;
|
|
import 'dart:ui';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:venera/foundation/cache_manager.dart';
|
|
import 'package:venera/foundation/log.dart';
|
|
|
|
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|
extends ImageProvider<T> {
|
|
const BaseImageProvider();
|
|
|
|
static double? _effectiveScreenWidth;
|
|
|
|
static const double _normalComicImageRatio = 0.72;
|
|
|
|
static const double _minComicImageWidth = 1920 * _normalComicImageRatio;
|
|
|
|
static TargetImageSize _getTargetSize(width, height) {
|
|
if (_effectiveScreenWidth == null) {
|
|
final screens = PlatformDispatcher.instance.displays;
|
|
for (var screen in screens) {
|
|
if (screen.size.width > screen.size.height) {
|
|
_effectiveScreenWidth = max(
|
|
_effectiveScreenWidth ?? 0,
|
|
screen.size.height * _normalComicImageRatio,
|
|
);
|
|
} else {
|
|
_effectiveScreenWidth =
|
|
max(_effectiveScreenWidth ?? 0, screen.size.width);
|
|
}
|
|
}
|
|
if (_effectiveScreenWidth! < _minComicImageWidth) {
|
|
_effectiveScreenWidth = _minComicImageWidth;
|
|
}
|
|
}
|
|
if (width > _effectiveScreenWidth!) {
|
|
height = (height * _effectiveScreenWidth! / width).round();
|
|
width = _effectiveScreenWidth!.round();
|
|
}
|
|
return TargetImageSize(width: width, height: height);
|
|
}
|
|
|
|
@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("Invalid Status Code: 404")) {
|
|
rethrow;
|
|
}
|
|
if (e.toString().contains("Invalid Status Code: 403")) {
|
|
rethrow;
|
|
}
|
|
if (e.toString().contains("handshake")) {
|
|
if (retryTime < 5) {
|
|
retryTime = 5;
|
|
}
|
|
}
|
|
retryTime <<= 1;
|
|
if (retryTime > (1 << 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,
|
|
getTargetSize: enableResize ? _getTargetSize : null,
|
|
);
|
|
} catch (e) {
|
|
await CacheManager().delete(this.key);
|
|
if (data.length < 2 * 1024) {
|
|
// data is too short, it's likely that the data is text, not image
|
|
try {
|
|
var text =
|
|
const Utf8Codec(allowMalformed: false).decoder.convert(data);
|
|
throw Exception("Expected image data, but got text: $text");
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
rethrow;
|
|
}
|
|
} catch (e, s) {
|
|
scheduleMicrotask(() {
|
|
PaintingBinding.instance.imageCache.evict(key);
|
|
});
|
|
Log.error("Image Loading", e, s);
|
|
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)";
|
|
}
|
|
|
|
bool get enableResize => false;
|
|
}
|
|
|
|
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
|