diff --git a/firka/lib/helpers/cache_memory_image_provider.dart b/firka/lib/helpers/cache_memory_image_provider.dart deleted file mode 100644 index 323c4ebe..00000000 --- a/firka/lib/helpers/cache_memory_image_provider.dart +++ /dev/null @@ -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 = {}; - -Future precacheAsset(AssetBundle bundle, String asset) async { - if (!_globalImageCache.containsKey(asset)) { - final data = await bundle.load(asset); - _globalImageCache[asset] = data.buffer.asUint8List(); - } -} - -Future precacheAssets(AssetBundle bundle, List assets) async { - for (final asset in assets) { - await precacheAsset(bundle, asset); - } -} - -Future _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 { - 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 _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 obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(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")'; -} diff --git a/firka/lib/helpers/image_preloader.dart b/firka/lib/helpers/image_preloader.dart new file mode 100644 index 00000000..787017a5 --- /dev/null +++ b/firka/lib/helpers/image_preloader.dart @@ -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 _cache = {}; + static final Map> _loadingFutures = {}; + + static Future 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 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> preloadMultipleAssets( + AssetBundle bundle, List assetPaths) async { + final futures = + assetPaths.map((path) => preloadAssetImage(bundle, path)).toList(); + return await Future.wait(futures); + } + + static Future> preloadWithProgress( + AssetBundle bundle, + List assetPaths, + Function(int loaded, int total)? onProgress, + ) async { + final List 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 _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 _loadNetworkImage(String url) async { + throw UnimplementedError( + 'Network image loading not implemented in this example'); + } + + static Future _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 { + final AssetBundle assetBundle; + final String assetPath; + + const PreloadedImageProvider(this.assetBundle, this.assetPath); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + PreloadedImageProvider key, ImageDecoderCallback decode) { + return OneFrameImageStreamCompleter(_loadAsync(key)); + } + + Future _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")'; +} diff --git a/firka/lib/ui/phone/screens/login/login_screen.dart b/firka/lib/ui/phone/screens/login/login_screen.dart index 967206f1..631018dd 100644 --- a/firka/lib/ui/phone/screens/login/login_screen.dart +++ b/firka/lib/ui/phone/screens/login/login_screen.dart @@ -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 { late LoginWebviewWidget _loginWebView; - bool _preloadDone = false; @override @@ -45,32 +37,41 @@ class _LoginScreenState extends State { 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 _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 { 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 { 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 { 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 { 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