diff --git a/firka/lib/helpers/cache_memory_image_provider.dart b/firka/lib/helpers/cache_memory_image_provider.dart new file mode 100644 index 0000000..52ef7d6 --- /dev/null +++ b/firka/lib/helpers/cache_memory_image_provider.dart @@ -0,0 +1,86 @@ +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/ui/phone/screens/login/login_screen.dart b/firka/lib/ui/phone/screens/login/login_screen.dart index 704fff7..84afa46 100644 --- a/firka/lib/ui/phone/screens/login/login_screen.dart +++ b/firka/lib/ui/phone/screens/login/login_screen.dart @@ -4,6 +4,7 @@ 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:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,7 @@ 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 '../../../model/style.dart'; import '../home/home_screen.dart'; @@ -26,6 +28,8 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { late WebViewController _webViewController; + bool _preloadDone = false; + @override void initState() { super.initState(); @@ -87,6 +91,25 @@ class _LoginScreenState extends State { statusBarColor: Colors.transparent, systemNavigationBarColor: Color(0xFFFAFFF0), )); + + () async { + final firkaBundle = FirkaBundle(); + + await precacheAssets(firkaBundle, [ + "assets/images/carousel/slide1.png", + "assets/images/carousel/slide1_background.gif", + "assets/images/carousel/slide2.png", + "assets/images/carousel/slide2_background.gif", + "assets/images/carousel/slide3.png", + "assets/images/carousel/slide3_foreground.gif", + "assets/images/carousel/slide4.png", + "assets/images/carousel/slide4_background.gif" + ]); + + setState(() { + _preloadDone = true; + }); + }(); } @override @@ -96,6 +119,12 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { + if (!_preloadDone) { + return MaterialApp( + home: SizedBox(), + ); + } + final paddingWidthHorizontal = MediaQuery.of(context).size.width - MediaQuery.of(context).size.width * 0.95; List> slides = [ @@ -167,8 +196,9 @@ class _LoginScreenState extends State { height: 30, clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - image: const DecorationImage( - image: AssetImage( + image: DecorationImage( + image: CacheMemoryImageProvider( + DefaultAssetBundle.of(context), 'assets/images/logos/colored_logo.png'), fit: BoxFit.cover, ), @@ -240,8 +270,11 @@ class _LoginScreenState extends State { scale: slides[index]['scale'] as double, child: Image( - image: AssetImage(slides[index] - ['background']! as String), + image: CacheMemoryImageProvider( + DefaultAssetBundle.of( + context), + slides[index]['background']! + as String), fit: BoxFit.contain, width: double.infinity, )), @@ -257,7 +290,8 @@ class _LoginScreenState extends State { child: SizedBox( width: MediaQuery.of(context).size.width, child: Image( - image: AssetImage( + image: CacheMemoryImageProvider( + DefaultAssetBundle.of(context), slides[index]['picture']! as String), fit: BoxFit.cover, width: double.infinity, @@ -285,8 +319,11 @@ class _LoginScreenState extends State { scale: slides[index]['scale'] as double, child: Image( - image: AssetImage(slides[index] - ['foreground']! as String), + image: CacheMemoryImageProvider( + DefaultAssetBundle.of( + context), + slides[index]['foreground']! + as String), fit: BoxFit.cover, width: MediaQuery.of(context) .size