forked from firka/firka
precache slides
slides take a long time to decode, so we cache them
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Taken from https://gist.github.com/darmawan01/9be266df44594ea59f07032e325ffa3b
|
||||
// and adapted to use assets
|
||||
|
||||
final _globalImageCache = <String, Uint8List>{};
|
||||
|
||||
Future<void> precacheAsset(AssetBundle bundle, String asset) async {
|
||||
if (!_globalImageCache.containsKey(asset)) {
|
||||
final data = await bundle.load(asset);
|
||||
_globalImageCache[asset] = data.buffer.asUint8List();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> precacheAssets(AssetBundle bundle, List<String> assets) async {
|
||||
for (final asset in assets) {
|
||||
await precacheAsset(bundle, asset);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> _cacheLoad(AssetBundle bundle, String asset) async {
|
||||
if (!_globalImageCache.containsKey(asset)) {
|
||||
final data = await bundle.load(asset);
|
||||
_globalImageCache[asset] = data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
return Future.value(_globalImageCache[asset]!);
|
||||
}
|
||||
|
||||
class CacheMemoryImageProvider extends ImageProvider<CacheMemoryImageProvider> {
|
||||
final AssetBundle bundle;
|
||||
final String path;
|
||||
Uint8List? _img;
|
||||
|
||||
CacheMemoryImageProvider(this.bundle, this.path);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
CacheMemoryImageProvider key, ImageDecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(decode),
|
||||
scale: 1.0,
|
||||
debugLabel: path,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('Tag: $path');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Codec> _loadAsync(ImageDecoderCallback decode) async {
|
||||
_img ??= await _cacheLoad(bundle, path);
|
||||
|
||||
// the DefaultCacheManager() encapsulation, it get cache from local storage.
|
||||
final Uint8List bytes = _img!;
|
||||
|
||||
if (bytes.lengthInBytes == 0) {
|
||||
// The file may become available later.
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
throw StateError('$path is empty and cannot be loaded as an image.');
|
||||
}
|
||||
final buffer = await ImmutableBuffer.fromUint8List(bytes);
|
||||
|
||||
return await decode(buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CacheMemoryImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<CacheMemoryImageProvider>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
bool res = other is CacheMemoryImageProvider && other.path == path;
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => path.hashCode;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'${objectRuntimeType(this, 'CacheImageProvider')}("$path")';
|
||||
}
|
||||
183
firka/lib/helpers/image_preloader.dart
Normal file
183
firka/lib/helpers/image_preloader.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ImagePreloader {
|
||||
static final Map<String, ui.Image> _cache = {};
|
||||
static final Map<String, Future<ui.Image>> _loadingFutures = {};
|
||||
|
||||
static Future<ui.Image> preloadAssetImage(
|
||||
AssetBundle bundle, String assetPath) async {
|
||||
if (_cache.containsKey(assetPath)) {
|
||||
return _cache[assetPath]!;
|
||||
}
|
||||
|
||||
if (_loadingFutures.containsKey(assetPath)) {
|
||||
return _loadingFutures[assetPath]!;
|
||||
}
|
||||
|
||||
final future = _loadAssetImage(bundle, assetPath);
|
||||
_loadingFutures[assetPath] = future;
|
||||
|
||||
try {
|
||||
final image = await future;
|
||||
_cache[assetPath] = image;
|
||||
return image;
|
||||
} finally {
|
||||
_loadingFutures.remove(assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ui.Image> preloadNetworkImage(String url) async {
|
||||
if (_cache.containsKey(url)) {
|
||||
return _cache[url]!;
|
||||
}
|
||||
|
||||
if (_loadingFutures.containsKey(url)) {
|
||||
return _loadingFutures[url]!;
|
||||
}
|
||||
|
||||
final future = _loadNetworkImage(url);
|
||||
_loadingFutures[url] = future;
|
||||
|
||||
try {
|
||||
final image = await future;
|
||||
_cache[url] = image;
|
||||
return image;
|
||||
} finally {
|
||||
_loadingFutures.remove(url);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<ui.Image>> preloadMultipleAssets(
|
||||
AssetBundle bundle, List<String> assetPaths) async {
|
||||
final futures =
|
||||
assetPaths.map((path) => preloadAssetImage(bundle, path)).toList();
|
||||
return await Future.wait(futures);
|
||||
}
|
||||
|
||||
static Future<List<ui.Image>> preloadWithProgress(
|
||||
AssetBundle bundle,
|
||||
List<String> assetPaths,
|
||||
Function(int loaded, int total)? onProgress,
|
||||
) async {
|
||||
final List<ui.Image> results = [];
|
||||
|
||||
for (int i = 0; i < assetPaths.length; i++) {
|
||||
final image = await preloadAssetImage(bundle, assetPaths[i]);
|
||||
results.add(image);
|
||||
onProgress?.call(i + 1, assetPaths.length);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
static ui.Image? getCachedImage(String key) {
|
||||
return _cache[key];
|
||||
}
|
||||
|
||||
static bool isCached(String key) {
|
||||
return _cache.containsKey(key);
|
||||
}
|
||||
|
||||
static int getCacheSize() {
|
||||
return _cache.length;
|
||||
}
|
||||
|
||||
static void clearImage(String key) {
|
||||
_cache.remove(key);
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
for (final image in _cache.values) {
|
||||
image.dispose();
|
||||
}
|
||||
_cache.clear();
|
||||
_loadingFutures.clear();
|
||||
}
|
||||
|
||||
static void trimCache(int maxSize) {
|
||||
if (_cache.length <= maxSize) return;
|
||||
|
||||
final keys = _cache.keys.toList();
|
||||
final keysToRemove = keys.take(_cache.length - maxSize);
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
_cache[key]?.dispose();
|
||||
_cache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ui.Image> _loadAssetImage(
|
||||
AssetBundle bundle, String assetPath) async {
|
||||
final ByteData data = await bundle.load(assetPath);
|
||||
final Uint8List bytes = data.buffer.asUint8List();
|
||||
return await _decodeImageFromBytes(bytes);
|
||||
}
|
||||
|
||||
static Future<ui.Image> _loadNetworkImage(String url) async {
|
||||
throw UnimplementedError(
|
||||
'Network image loading not implemented in this example');
|
||||
}
|
||||
|
||||
static Future<ui.Image> _decodeImageFromBytes(Uint8List bytes) async {
|
||||
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
|
||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
||||
return frameInfo.image;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Only works on non animated images.
|
||||
class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
|
||||
final AssetBundle assetBundle;
|
||||
final String assetPath;
|
||||
|
||||
const PreloadedImageProvider(this.assetBundle, this.assetPath);
|
||||
|
||||
@override
|
||||
Future<PreloadedImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<PreloadedImageProvider>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
PreloadedImageProvider key, ImageDecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(_loadAsync(key));
|
||||
}
|
||||
|
||||
Future<ImageInfo> _loadAsync(PreloadedImageProvider key) async {
|
||||
// clone the image before using it to prevent from the original image
|
||||
// getting disposed
|
||||
final cachedImage = ImagePreloader.getCachedImage(key.assetPath)?.clone();
|
||||
|
||||
if (cachedImage != null) {
|
||||
return ImageInfo(image: cachedImage);
|
||||
}
|
||||
|
||||
try {
|
||||
final image =
|
||||
await ImagePreloader.preloadAssetImage(assetBundle, key.assetPath);
|
||||
return ImageInfo(image: image.clone());
|
||||
} catch (e) {
|
||||
final ByteData data = await assetBundle.load(key.assetPath);
|
||||
final Uint8List bytes = data.buffer.asUint8List();
|
||||
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
|
||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
||||
return ImageInfo(image: frameInfo.image.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is PreloadedImageProvider && other.assetPath == assetPath;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetPath.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'PreloadedImageProvider("$assetPath")';
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:firka/helpers/api/client/kreta_client.dart';
|
||||
import 'package:firka/helpers/api/consts.dart';
|
||||
import 'package:firka/helpers/db/models/token_model.dart';
|
||||
import 'package:firka/helpers/firka_bundle.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:firka/ui/phone/widgets/login_webview.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../../../helpers/api/token_grant.dart';
|
||||
import '../../../../helpers/cache_memory_image_provider.dart';
|
||||
import '../../../../helpers/image_preloader.dart';
|
||||
import '../../../model/style.dart';
|
||||
import '../home/home_screen.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
final AppInitialization data;
|
||||
@@ -28,7 +21,6 @@ class LoginScreen extends StatefulWidget {
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
late LoginWebviewWidget _loginWebView;
|
||||
|
||||
bool _preloadDone = false;
|
||||
|
||||
@override
|
||||
@@ -45,32 +37,41 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
systemNavigationBarColor: Color(0xFFFAFFF0),
|
||||
));
|
||||
|
||||
() async {
|
||||
final firkaBundle = FirkaBundle();
|
||||
_preloadImages();
|
||||
}
|
||||
|
||||
await precacheAssets(firkaBundle, [
|
||||
"assets/images/carousel/slide1.webp",
|
||||
"assets/images/carousel/slide1_background.webp",
|
||||
"assets/images/carousel/slide2.webp",
|
||||
"assets/images/carousel/slide2_background.webp",
|
||||
"assets/images/carousel/slide3.webp",
|
||||
"assets/images/carousel/slide3_foreground.webp",
|
||||
"assets/images/carousel/slide4.webp",
|
||||
"assets/images/carousel/slide4_background.webp"
|
||||
]);
|
||||
Future<void> _preloadImages() async {
|
||||
final imagePaths = [
|
||||
"assets/images/carousel/slide1.webp",
|
||||
"assets/images/carousel/slide2.webp",
|
||||
"assets/images/carousel/slide3.webp",
|
||||
"assets/images/carousel/slide4.webp",
|
||||
"assets/images/logos/colored_logo.webp",
|
||||
];
|
||||
|
||||
try {
|
||||
// Preload with progress tracking
|
||||
await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths);
|
||||
|
||||
// All images are now decoded and cached
|
||||
setState(() {
|
||||
_preloadDone = true;
|
||||
});
|
||||
}();
|
||||
} catch (e) {
|
||||
print('Error preloading images: $e');
|
||||
// Fallback: continue anyway
|
||||
setState(() {
|
||||
_preloadDone = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_preloadDone) {
|
||||
return MaterialApp(
|
||||
home: SizedBox(),
|
||||
);
|
||||
home: SizedBox(),
|
||||
);
|
||||
}
|
||||
|
||||
final paddingWidthHorizontal = MediaQuery.of(context).size.width -
|
||||
@@ -145,7 +146,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
image: DecorationImage(
|
||||
image: CacheMemoryImageProvider(
|
||||
image: PreloadedImageProvider(
|
||||
DefaultAssetBundle.of(context),
|
||||
'assets/images/logos/colored_logo.webp'),
|
||||
fit: BoxFit.cover,
|
||||
@@ -217,12 +218,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: Transform.scale(
|
||||
scale: slides[index]['scale']
|
||||
as double,
|
||||
child: Image(
|
||||
image: CacheMemoryImageProvider(
|
||||
DefaultAssetBundle.of(
|
||||
context),
|
||||
slides[index]['background']!
|
||||
as String),
|
||||
child: Image.asset(
|
||||
slides[index]['background']!
|
||||
as String,
|
||||
bundle: DefaultAssetBundle.of(
|
||||
context),
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
)),
|
||||
@@ -238,7 +238,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Image(
|
||||
image: CacheMemoryImageProvider(
|
||||
image: PreloadedImageProvider(
|
||||
DefaultAssetBundle.of(context),
|
||||
slides[index]['picture']! as String),
|
||||
fit: BoxFit.cover,
|
||||
@@ -266,12 +266,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: Transform.scale(
|
||||
scale: slides[index]['scale']
|
||||
as double,
|
||||
child: Image(
|
||||
image: CacheMemoryImageProvider(
|
||||
DefaultAssetBundle.of(
|
||||
context),
|
||||
slides[index]['foreground']!
|
||||
as String),
|
||||
child: Image.asset(
|
||||
slides[index]['foreground']!
|
||||
as String,
|
||||
bundle: DefaultAssetBundle.of(
|
||||
context),
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context)
|
||||
.size
|
||||
|
||||
Reference in New Issue
Block a user