|
@@ -0,0 +1,242 @@
|
|
|
+import 'dart:async';
|
|
|
+import 'dart:convert';
|
|
|
+import 'dart:io';
|
|
|
+import 'dart:typed_data';
|
|
|
+
|
|
|
+import 'package:fiscommon/helpers/http.dart';
|
|
|
+import 'package:fiscommon/index.dart';
|
|
|
+import 'package:flutter/foundation.dart';
|
|
|
+import 'package:flutter/services.dart';
|
|
|
+import 'package:path_provider/path_provider.dart';
|
|
|
+
|
|
|
+import 'manifest.dart';
|
|
|
+import 'theme.dart';
|
|
|
+
|
|
|
+/// 资源来源
|
|
|
+enum FResourceFromType {
|
|
|
+ local, // 本地
|
|
|
+ network, // 网络
|
|
|
+}
|
|
|
+
|
|
|
+/// 资源配置信息实体
|
|
|
+class FResourceInfo {
|
|
|
+ final String key;
|
|
|
+ final FResourceFromType from;
|
|
|
+ final String path;
|
|
|
+
|
|
|
+ const FResourceInfo({
|
|
|
+ required this.key,
|
|
|
+ required this.from,
|
|
|
+ required this.path,
|
|
|
+ });
|
|
|
+
|
|
|
+ factory FResourceInfo.fromJson(Map<String, dynamic> json) {
|
|
|
+ return FResourceInfo(
|
|
|
+ key: json['key'],
|
|
|
+ from: FResourceFromType.values[json['from']],
|
|
|
+ path: json['path'],
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// 资源索引键
|
|
|
+class FResourceKey {
|
|
|
+ final String value;
|
|
|
+ const FResourceKey(this.value);
|
|
|
+}
|
|
|
+
|
|
|
+/// 主题资源包Package名称
|
|
|
+const String themePackageName = "fisresource";
|
|
|
+
|
|
|
+/// Fis资源集
|
|
|
+abstract class FResourceBundle {
|
|
|
+ late String _resourcePath;
|
|
|
+
|
|
|
+ /// 资源路径
|
|
|
+ String get resourcePath => _resourcePath;
|
|
|
+
|
|
|
+ /// 设置资源路径
|
|
|
+ void setResourcePath(String value) => _resourcePath = value;
|
|
|
+
|
|
|
+ final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
|
|
|
+ final Map<String, Future<dynamic>> _structuredDataCache =
|
|
|
+ <String, Future<dynamic>>{};
|
|
|
+ Map<String, FResourceInfo> _resourceMap = Map<String, FResourceInfo>();
|
|
|
+
|
|
|
+ /// 主题清单内存映射
|
|
|
+ late FThemeManifestInfo _manifestInfoRefer;
|
|
|
+
|
|
|
+ @protected
|
|
|
+ Future<ByteData> _innerLoad(FResourceInfo info);
|
|
|
+
|
|
|
+ /// 加载资源数据
|
|
|
+ Future<ByteData> load(FResourceKey key) async {
|
|
|
+ FResourceInfo? resourceInfo = _getInfo(key.value);
|
|
|
+ if (resourceInfo == null) return ByteData(0);
|
|
|
+ if (resourceInfo.from == FResourceFromType.network) {
|
|
|
+ return await _loadFromNetwork(resourceInfo);
|
|
|
+ }
|
|
|
+ ByteData data;
|
|
|
+ if (FTheme.ins.defaultName == _manifestInfoRefer.name) {
|
|
|
+ var path = "packages/$themePackageName/assets/${resourceInfo.path}";
|
|
|
+ data = await rootBundle.load(path);
|
|
|
+ } else {
|
|
|
+ data = await _innerLoad(resourceInfo);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<ByteData> _loadFromNetwork(FResourceInfo resourceInfo) async {
|
|
|
+ try {
|
|
|
+ var bytes = await FHttpHelper.downloadBytes(resourceInfo.path);
|
|
|
+ if (bytes != null) {
|
|
|
+ return bytes.buffer.asByteData();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // TODO: add log
|
|
|
+ }
|
|
|
+ return ByteData(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取字符串
|
|
|
+ Future<String> loadString(FResourceKey key, {bool cache = true}) {
|
|
|
+ if (cache)
|
|
|
+ return _stringCache.putIfAbsent(key.value, () => _innerLoadString(key));
|
|
|
+ return _innerLoadString(key);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取结构化数据
|
|
|
+ Future<T> loadStructuredData<T>(
|
|
|
+ FResourceKey key, Future<T> parser(String value)) {
|
|
|
+ String keyString = key.value;
|
|
|
+ if (_structuredDataCache.containsKey(keyString))
|
|
|
+ return _structuredDataCache[keyString]! as Future<T>;
|
|
|
+ Completer<T>? completer;
|
|
|
+ Future<T>? result;
|
|
|
+ loadString(key, cache: false).then<T>(parser).then<void>((T value) {
|
|
|
+ result = SynchronousFuture<T>(value);
|
|
|
+ _structuredDataCache[keyString] = result!;
|
|
|
+ if (completer != null) {
|
|
|
+ completer.complete(value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (result != null) {
|
|
|
+ return result!;
|
|
|
+ }
|
|
|
+ completer = Completer<T>();
|
|
|
+ _structuredDataCache[keyString] = completer.future;
|
|
|
+ return completer.future;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取字节数组
|
|
|
+ Future<Uint8List> loadAsBytes(FResourceKey key) async {
|
|
|
+ var byteData = await load(key);
|
|
|
+ return byteData.buffer.asUint8List();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 载入资源map和初始化
|
|
|
+ void init(FThemeManifestInfo manifestInfo) {
|
|
|
+ try {
|
|
|
+ _manifestInfoRefer = manifestInfo;
|
|
|
+ _resourceMap = Map.fromIterable(manifestInfo.resources,
|
|
|
+ key: (item) => (item as FResourceInfo).key, value: (item) => item);
|
|
|
+ clearCache();
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 收回指定缓存
|
|
|
+ void evict(FResourceKey key) {
|
|
|
+ _stringCache.remove(key.value);
|
|
|
+ _structuredDataCache.remove(key.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 清除缓存
|
|
|
+ void clearCache() {
|
|
|
+ _stringCache.clear();
|
|
|
+ _structuredDataCache.clear();
|
|
|
+ print('FResourceBundle clear cache!');
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<String> _innerLoadString(FResourceKey key, {bool cache = true}) async {
|
|
|
+ final ByteData data = await load(key);
|
|
|
+ if (data.lengthInBytes < 50 * 1024) {
|
|
|
+ return utf8.decode(data.buffer.asUint8List());
|
|
|
+ }
|
|
|
+ return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
|
|
|
+ }
|
|
|
+
|
|
|
+ static String _utf8decode(ByteData data) {
|
|
|
+ return utf8.decode(data.buffer.asUint8List());
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ String toString() => '${describeIdentity(this)}()';
|
|
|
+
|
|
|
+ FResourceInfo? _getInfo(String key) {
|
|
|
+ if (_resourceMap.containsKey(key)) {
|
|
|
+ return _resourceMap[key];
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _FResourceBundleNative extends FResourceBundle {
|
|
|
+ Directory? _storageDirectory;
|
|
|
+
|
|
|
+ @override
|
|
|
+ Future<ByteData> _innerLoad(FResourceInfo info) async {
|
|
|
+ try {
|
|
|
+ final String storagePath = await _getStoragePath();
|
|
|
+ final String themeName = super._manifestInfoRefer.name;
|
|
|
+ final String filePath = "$storagePath/themes/$themeName/${info.path}";
|
|
|
+ final Uint8List bytes = await File(filePath).readAsBytes();
|
|
|
+ return bytes.buffer.asByteData();
|
|
|
+ } catch (e) {
|
|
|
+ // TODO: add log
|
|
|
+ }
|
|
|
+ return ByteData(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<String> _getStoragePath() async {
|
|
|
+ if (_storageDirectory == null) {
|
|
|
+ _storageDirectory = await getApplicationDocumentsDirectory();
|
|
|
+ }
|
|
|
+ return _storageDirectory!.path;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _FResourceBundleWeb extends FResourceBundle {
|
|
|
+ @override
|
|
|
+ Future<ByteData> _innerLoad(FResourceInfo info) async {
|
|
|
+ String url =
|
|
|
+ "${_getUrlRoot()}/${super._manifestInfoRefer.name}/${info.path}";
|
|
|
+ Uint8List? data = await FHttpHelper.downloadBytes(url);
|
|
|
+ if (data != null) {
|
|
|
+ return data.buffer.asByteData();
|
|
|
+ }
|
|
|
+ return ByteData(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ @protected
|
|
|
+ String _getUrlRoot() => resourcePath;
|
|
|
+}
|
|
|
+
|
|
|
+class _FResourceBundleShellWeb extends _FResourceBundleWeb {
|
|
|
+ @override
|
|
|
+ String _getUrlRoot() => "http://resource.fis.plus/themes";
|
|
|
+}
|
|
|
+
|
|
|
+FResourceBundle _createBundle() {
|
|
|
+ switch (FPlatform.current) {
|
|
|
+ case FPlatformEnum.android:
|
|
|
+ case FPlatformEnum.iOS:
|
|
|
+ return _FResourceBundleNative();
|
|
|
+ case FPlatformEnum.web:
|
|
|
+ return _FResourceBundleWeb();
|
|
|
+ case FPlatformEnum.webOnWin:
|
|
|
+ case FPlatformEnum.webOnMac:
|
|
|
+ return _FResourceBundleShellWeb();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+final FResourceBundle fRootBundle = _createBundle();
|