WT 1.1.5+2 remote images for menu
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:aitrainer_app/service/logging.dart';
|
||||
import 'package:aitrainer_app/util/not_found_exception.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:network_image_to_byte/network_image_to_byte.dart';
|
||||
import 'dart:collection';
|
||||
|
||||
class ImageCache with Logging {
|
||||
static final ImageCache _singleton = ImageCache._internal();
|
||||
final LinkedHashMap<String, String> _images = LinkedHashMap();
|
||||
final LinkedHashMap<String, bool> _imageMap = LinkedHashMap();
|
||||
var downloadSize = 0;
|
||||
|
||||
// Create storage
|
||||
final storage = FlutterSecureStorage();
|
||||
|
||||
factory ImageCache() {
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
ImageCache._internal();
|
||||
|
||||
LinkedHashMap getImageList() => _images;
|
||||
|
||||
void clearImageList() => _images.clear();
|
||||
|
||||
bool existsImageInMap(int id, String url) {
|
||||
final String imageKey = generateMd5(url + "_" + id.toString());
|
||||
return _imageMap[imageKey] != null && _imageMap[imageKey] == true;
|
||||
}
|
||||
|
||||
String getImageString(int id, String url) {
|
||||
final String imageKey = generateMd5(url + "_" + id.toString());
|
||||
return _images[imageKey];
|
||||
}
|
||||
|
||||
Future<void> putImageToList(int id, String url) async {
|
||||
final String imageKey = generateMd5(url + "_" + id.toString());
|
||||
|
||||
// get from storage
|
||||
final String imageString = await getImageAs64BaseString(id, url);
|
||||
if (imageString != null) {
|
||||
_images[imageKey] = imageString;
|
||||
_imageMap[imageKey] = true;
|
||||
}
|
||||
|
||||
/* final String imageString = await getImageAs64BaseString(id, url);
|
||||
if (imageString != null) {
|
||||
_imageMap[imageKey] = imageString;
|
||||
_imageDown[imageKey] = true;
|
||||
}
|
||||
}
|
||||
_images[imageKey] = imageString;
|
||||
_imageMap[imageKey] = true; */
|
||||
}
|
||||
|
||||
Future<void> saveImageToPrefs(String key, String value) async {
|
||||
//log(" save the key " + key);
|
||||
await storage.write(key: key, value: value);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> emptyPrefs() async {
|
||||
await storage.deleteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String> loadImageFromPrefs(String key) async {
|
||||
String value = await storage.read(key: key);
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<bool> existImageInPrefs(String key) async {
|
||||
return await storage.containsKey(key: key);
|
||||
}
|
||||
|
||||
// encodes bytes list as string
|
||||
static String base64String(Uint8List data) {
|
||||
return base64Encode(data);
|
||||
}
|
||||
|
||||
// decode bytes from a string
|
||||
Image imageFrom64BaseString(String base64String) {
|
||||
if (base64String == null) {
|
||||
return null;
|
||||
}
|
||||
return Image.memory(
|
||||
base64Decode(base64String),
|
||||
fit: BoxFit.fill,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Image> getImage(int id, String name) async {
|
||||
if (storage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name == null || name.length == 0) {
|
||||
return null;
|
||||
}
|
||||
final String imageKey = generateMd5(name + "_" + id.toString());
|
||||
final String imageString = await storage.read(key: imageKey);
|
||||
final Image image = imageFrom64BaseString(imageString);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
Future<String> getImageAs64BaseString(int id, String name) async {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
final String imageKey = generateMd5(name + "_" + id.toString());
|
||||
String imageString;
|
||||
if (await storage.containsKey(key: imageKey)) {
|
||||
//log(" .. get from storage");
|
||||
imageString = await storage.read(key: imageKey);
|
||||
} else {
|
||||
imageString = await downloadAndSaveImage(id, name);
|
||||
//log(" .. downloaded");
|
||||
}
|
||||
return imageString;
|
||||
}
|
||||
|
||||
Future<String> downloadAndSaveImage(int id, String url) async {
|
||||
final String imageKey = generateMd5(url + "_" + id.toString());
|
||||
if (!await existImageInPrefs(imageKey)) {
|
||||
try {
|
||||
if (url.contains("http")) {
|
||||
log(" ... direct download " + url);
|
||||
Uint8List byteImage = await networkImageToByte(url);
|
||||
this.downloadSize += byteImage.length;
|
||||
final String imageAsString = base64String(byteImage);
|
||||
|
||||
ImageCache().saveImageToPrefs(imageKey, imageAsString);
|
||||
return imageAsString;
|
||||
}
|
||||
} on NotFoundException catch (_) {
|
||||
print(url + " not found");
|
||||
} on Exception catch (e) {
|
||||
print(e);
|
||||
}
|
||||
} else {
|
||||
//log(" .. from storage");
|
||||
final String storageString = await storage.read(key: imageKey);
|
||||
if (storageString != null) {
|
||||
//log(" .. storage String: " + storageString);
|
||||
} else {
|
||||
// log(" .. storage String is NULL");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String generateMd5(String input) {
|
||||
String converted = input.replaceAll(RegExp(r'\.'), 'a');
|
||||
converted = converted.replaceAll(RegExp(r'\/'), 'b');
|
||||
converted = converted.replaceAll(RegExp(r':'), 'c');
|
||||
//print("key: " + converted);
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
+93
-26
@@ -1,11 +1,30 @@
|
||||
/// Tree view widget library
|
||||
library tree_view;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:aitrainer_app/util/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class TreeViewStream {
|
||||
static final TreeViewStream _singleton = TreeViewStream._internal();
|
||||
final StreamController<bool> streamController = StreamController<bool>.broadcast();
|
||||
double positionY = 0;
|
||||
|
||||
Stream get stream => streamController.stream;
|
||||
StreamController getStreamController() => streamController;
|
||||
|
||||
factory TreeViewStream() => _singleton;
|
||||
|
||||
TreeViewStream._internal();
|
||||
|
||||
void dispose() {
|
||||
streamController.close();
|
||||
}
|
||||
}
|
||||
|
||||
class TreeView extends InheritedWidget {
|
||||
final List<Widget> children;
|
||||
final bool startExpanded;
|
||||
@@ -24,33 +43,77 @@ class TreeView extends InheritedWidget {
|
||||
);
|
||||
|
||||
static TreeView of(BuildContext context) {
|
||||
//return context.inheritFromWidgetOfExactType(TreeView);
|
||||
return context.dependOnInheritedWidgetOfExactType(aspect: TreeView);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(TreeView oldWidget) {
|
||||
if (oldWidget.children == this.children &&
|
||||
oldWidget.startExpanded == this.startExpanded) {
|
||||
if (oldWidget.children == this.children && oldWidget.startExpanded == this.startExpanded) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _TreeViewData extends StatelessWidget {
|
||||
class _TreeViewData extends StatefulWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
_TreeViewData({
|
||||
this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
__TreeViewDataState createState() => __TreeViewDataState();
|
||||
}
|
||||
|
||||
class __TreeViewDataState extends State<_TreeViewData> {
|
||||
final ScrollController _controller = ScrollController();
|
||||
final Stream stream = TreeViewStream().stream;
|
||||
var subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// We require the initializers to run after the loading screen is rendered
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
final double cHeight = MediaQuery.of(context).size.height;
|
||||
subscription = stream.listen((value) {
|
||||
if (value) {
|
||||
final double positionY = TreeViewStream().positionY;
|
||||
print("pos " +
|
||||
positionY.toString() +
|
||||
" height: " +
|
||||
cHeight.toString() +
|
||||
" controller offset " +
|
||||
_controller.offset.toString() +
|
||||
" controller initial " +
|
||||
_controller.initialScrollOffset.toString());
|
||||
if (positionY > cHeight - 190) {
|
||||
final double offset = positionY + 40;
|
||||
print("antimateTo " + offset.toString());
|
||||
_controller.animateTo(offset, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: children.length,
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: _controller,
|
||||
itemCount: widget.children.length,
|
||||
itemBuilder: (context, index) {
|
||||
return children.elementAt(index);
|
||||
return widget.children.elementAt(index);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -92,14 +155,9 @@ class TreeViewChild extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TreeViewChildState extends State<TreeViewChild> with Common, SingleTickerProviderStateMixin {
|
||||
class TreeViewChildState extends State<TreeViewChild> with Common {
|
||||
bool isExpanded;
|
||||
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
Color _color;
|
||||
double _opacity = 0;
|
||||
|
||||
AnimationController _controller;
|
||||
Animation<double> sizeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -115,7 +173,8 @@ class TreeViewChildState extends State<TreeViewChild> with Common, SingleTicker
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return (Column(
|
||||
key: listKey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
@@ -124,27 +183,35 @@ class TreeViewChildState extends State<TreeViewChild> with Common, SingleTicker
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
child:
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds:200),
|
||||
reverseDuration: Duration(milliseconds:200),
|
||||
switchInCurve: Curves.easeIn,
|
||||
child: isExpanded ? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
) : Offstage(),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 200),
|
||||
reverseDuration: Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeIn,
|
||||
child: isExpanded
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
)
|
||||
: Offstage(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
void toggleExpanded() {
|
||||
setState(() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
_color = isExpanded ? Colors.black12 : Colors.transparent;
|
||||
_opacity = isExpanded ? 1 : 0;
|
||||
TreeViewStream().positionY = getPosition();
|
||||
TreeViewStream().getStreamController().add(this.isExpanded);
|
||||
});
|
||||
}
|
||||
|
||||
double getPosition() {
|
||||
RenderBox box = listKey.currentContext.findRenderObject();
|
||||
Offset position = box.localToGlobal(Offset.zero); //this is global position
|
||||
double y = position.dy;
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user