From b4fd3fbcd21fa4eb6a94420b76a9951bf5f93a05 Mon Sep 17 00:00:00 2001 From: shmyga Date: Thu, 10 Sep 2020 10:43:12 +0300 Subject: [PATCH] [refactoring] restore FileStorage --- src/app/haxe/ru/m/data/DataSource.hx | 17 +- src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx | 2 + .../ru/m/puzzlez/source/FileImageSource.hx | 30 +++ .../ru/m/puzzlez/source/PixabayImageSource.hx | 2 +- .../haxe/ru/m/puzzlez/storage/FileStorage.hx | 11 ++ .../ru/m/puzzlez/view/ImageSourceFrame.hx | 8 +- src/app/haxe/ru/m/puzzlez/view/StartFrame.hx | 15 +- .../haxe/ru/m/puzzlez/view/StartFrame.yaml | 6 + .../ru/m/puzzlez/view/common/ImageDataList.hx | 2 +- .../ru/m/storage/SharedObjectDataStorage.hx | 176 ++++++++++++++++++ 10 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 src/app/haxe/ru/m/puzzlez/source/FileImageSource.hx create mode 100644 src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx create mode 100644 src/app/haxe/ru/m/storage/SharedObjectDataStorage.hx diff --git a/src/app/haxe/ru/m/data/DataSource.hx b/src/app/haxe/ru/m/data/DataSource.hx index 9352c59..6087489 100644 --- a/src/app/haxe/ru/m/data/DataSource.hx +++ b/src/app/haxe/ru/m/data/DataSource.hx @@ -2,12 +2,17 @@ package ru.m.data; import promhx.Promise; -typedef PageParams = Map; +typedef Filter = Map; +typedef Order = Array<{ + var field:String; + @:optional var reverse:Bool; +}>; typedef Page = { var index:Int; var count:Int; - @:optional var params:PageParams; + @:optional var filter:Filter; + @:optional var order:Order; } typedef DataPage = { @@ -18,5 +23,11 @@ typedef DataPage = { interface DataSource { public function getPage(page:Page):Promise>; - // public function get(id):D; +} + +interface DataStorage extends DataSource { + public function getIndexPage(page:Page):Promise>; + public function get(id:I):Promise; + public function save(item:D):Promise; + public function delete(id:I):Promise; } diff --git a/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx b/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx index 8003694..f7081f1 100644 --- a/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx +++ b/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx @@ -7,6 +7,7 @@ import ru.m.puzzlez.image.ImageSourceBundle; import ru.m.puzzlez.render.part.IPartBuilder; import ru.m.puzzlez.settings.Settings; import ru.m.puzzlez.source.AssetImageSource; +import ru.m.puzzlez.source.FileImageSource; import ru.m.puzzlez.source.PixabayImageSource; import ru.m.puzzlez.view.PuzzlezAppView; import ru.m.update.Updater; @@ -21,6 +22,7 @@ class PuzzlezApp { Settings; IPartBuilder; sourceBundle.register(new AssetImageSource()); + sourceBundle.register(new FileImageSource()); sourceBundle.register(new PixabayImageSource()); L.push(new TraceLogger()); updater = new Updater(Const.instance.VERSION, "https://shmyga.ru/repo/puzzlez/packages.json"); diff --git a/src/app/haxe/ru/m/puzzlez/source/FileImageSource.hx b/src/app/haxe/ru/m/puzzlez/source/FileImageSource.hx new file mode 100644 index 0000000..89ad002 --- /dev/null +++ b/src/app/haxe/ru/m/puzzlez/source/FileImageSource.hx @@ -0,0 +1,30 @@ +package ru.m.puzzlez.source; + +import promhx.Promise; +import ru.m.data.DataSource; +import ru.m.puzzlez.image.ImageSource; +import ru.m.puzzlez.proto.game.ImageId; +import ru.m.puzzlez.storage.FileStorage; + +class FileImageSource implements ImageSource { + public var id(default, never):String = "file"; + + @:provide private var storage:FileStorage; + + public function new() { + } + + public function getPage(page:Page):Promise> { + return storage.getIndexPage(page).then((response:DataPage) -> { + return { + page: response.page, + data: response.data.map(key -> new ImageId().setSource(id).setId(key)), + total: response.total, + } + }); + } + + public function load(id:String):Promise { + return storage.get(id).then(bytes -> ImageValue.BYTES(bytes)); + } +} diff --git a/src/app/haxe/ru/m/puzzlez/source/PixabayImageSource.hx b/src/app/haxe/ru/m/puzzlez/source/PixabayImageSource.hx index d2f77c3..6f31634 100644 --- a/src/app/haxe/ru/m/puzzlez/source/PixabayImageSource.hx +++ b/src/app/haxe/ru/m/puzzlez/source/PixabayImageSource.hx @@ -17,7 +17,7 @@ class PixabayImageSource implements ImageSource { } public function getPage(page:Page):Promise> { - return this.api.getPage(page.index + 1, page.count, page.params.get("category")).then((response:PixabayResponse) -> { + return this.api.getPage(page.index + 1, page.count, page.filter.get("category")).then((response:PixabayResponse) -> { var data:Array = []; for (hit in response.hits) { imageUrlsCache.set(hit.id, hit.largeImageURL); diff --git a/src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx b/src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx new file mode 100644 index 0000000..42e54eb --- /dev/null +++ b/src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx @@ -0,0 +1,11 @@ +package ru.m.puzzlez.storage; + +import haxe.io.Bytes; +import ru.m.storage.SharedObjectDataStorage; + +@:provide class FileStorage extends SharedObjectDataStorage { + + public function new() { + super("file", new DefaultHelper()); + } +} diff --git a/src/app/haxe/ru/m/puzzlez/view/ImageSourceFrame.hx b/src/app/haxe/ru/m/puzzlez/view/ImageSourceFrame.hx index bf66a1a..5f140df 100644 --- a/src/app/haxe/ru/m/puzzlez/view/ImageSourceFrame.hx +++ b/src/app/haxe/ru/m/puzzlez/view/ImageSourceFrame.hx @@ -3,7 +3,7 @@ package ru.m.puzzlez.view; import hw.view.form.LabelView; import hw.view.frame.FrameSwitcher; import hw.view.frame.FrameView; -import ru.m.data.DataSource.PageParams; +import ru.m.data.DataSource; import ru.m.puzzlez.image.ImageSourceBundle; import ru.m.puzzlez.proto.game.ImageId; import ru.m.puzzlez.view.common.ImageDataList; @@ -11,7 +11,7 @@ import ru.m.puzzlez.view.common.ImageDataList; typedef ImageSourceConfig = { var title:String; var sourceId:String; - @:optional var params:PageParams; + @:optional var filter:Filter; } @:template class ImageSourceFrame extends FrameView { @@ -30,15 +30,13 @@ typedef ImageSourceConfig = { images.reset(); if (data != null) { header.text = data.title; - images.page.params = data.params; + images.page.filter = data.filter; images.source = sourceBundle.get(data.sourceId); images.refresh(); } } private function start(imageId:ImageId):Void { - //var state = GameUtil.buildState(GameUtil.buildPreset(imageId, 2, 2)); - //switcher.change(GameFrame.ID, state); switcher.change(PresetFrame.ID, imageId); } diff --git a/src/app/haxe/ru/m/puzzlez/view/StartFrame.hx b/src/app/haxe/ru/m/puzzlez/view/StartFrame.hx index 557d0e1..00129f4 100644 --- a/src/app/haxe/ru/m/puzzlez/view/StartFrame.hx +++ b/src/app/haxe/ru/m/puzzlez/view/StartFrame.hx @@ -5,6 +5,8 @@ import hw.view.form.ButtonView; import hw.view.frame.FrameSwitcher; import hw.view.frame.FrameView; import ru.m.pixabay.PixabayApi; +import ru.m.puzzlez.FileUtil; +import ru.m.puzzlez.storage.FileStorage; import ru.m.puzzlez.view.ImageSourceFrame; import ru.m.update.Updater; @@ -16,14 +18,17 @@ import ru.m.update.Updater; @:provide var switcher:FrameSwitcher; @:provide static var appUpdater:Updater; + @:provide var fileStorage:FileStorage; + + private var fileSource:ImageSourceConfig = {title: "Files", sourceId: "file"}; public function new() { super(ID); var data:Array = []; data.push({title: "Assets", sourceId: "asset"}); - // data.push({title: "Files", sourceId: "file"}); + data.push(fileSource); for (type in AbstractEnumTools.getValues(PixabayCategory)) { - data.push({title: type, sourceId: "pixabay", params: ["category" => type]}); + data.push({title: type, sourceId: "pixabay", filter: ["category" => type]}); } sources.data = data; } @@ -34,6 +39,12 @@ import ru.m.update.Updater; return result; } + public function upload():Void { + FileUtil.browse() + .pipe((data:FileContent) -> fileStorage.save(data.content)) + .then(_ -> showSource(fileSource)); + } + public function showSource(source:ImageSourceConfig):Void { switcher.change(ImageSourceFrame.ID, source); } diff --git a/src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml b/src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml index 40301f3..bd370cd 100644 --- a/src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml +++ b/src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml @@ -27,6 +27,12 @@ views: +onDataSelect: ~showSource geometry.margin: 5 overflow.y: scroll + - $type: hw.view.group.HGroupView + geometry.width: 100% + views: + - $type: hw.view.form.ButtonView + text: Upload + +onPress: ~upload() - $type: hw.view.form.LabelView text: $r:text:app.version geometry.position: absolute diff --git a/src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx b/src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx index 5099390..80bd28f 100644 --- a/src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx +++ b/src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx @@ -35,7 +35,7 @@ import ru.m.puzzlez.proto.game.ImageId; public function new() { super(); loading = new LoadingWrapper(imagesView); - page = {index: 0, count: 6}; + page = {index: 0, count: 6, order: [{field: "date", reverse: true}]}; } private function pageFactory(index:Int, value:Int):ToggleButtonView { diff --git a/src/app/haxe/ru/m/storage/SharedObjectDataStorage.hx b/src/app/haxe/ru/m/storage/SharedObjectDataStorage.hx new file mode 100644 index 0000000..0b2ab99 --- /dev/null +++ b/src/app/haxe/ru/m/storage/SharedObjectDataStorage.hx @@ -0,0 +1,176 @@ +package ru.m.storage; + +import haxe.Unserializer; +import flash.net.SharedObject; +import haxe.crypto.Md5; +import haxe.io.Bytes; +import haxe.Serializer; +import promhx.Promise; +import ru.m.data.DataSource; + +typedef DataMeta = Map; + +typedef IdMeta = { + var id:I; + var meta:DataMeta; +} + +interface DataHelper { + public function idKey(id:I):String; + + public function keyId(key:String):I; + + public function dataId(data:D):I; + + public function dataBytes(data:D):Bytes; + + public function bytesData(bytes:Bytes):D; + + public function dataMeta(data:D):DataMeta; +} + +class DefaultHelper implements DataHelper { + public function new() { + + } + + public function idKey(id:String):String { + return id; + } + + public function keyId(key:String):String { + return key; + } + + public function dataId(data:Bytes):String { + return Md5.make(data).toHex(); + } + + public function dataBytes(data:Bytes):Bytes { + return data; + } + + public function bytesData(bytes:Bytes):Bytes { + return bytes; + } + + public function dataMeta(data:Bytes):DataMeta { + return ["date" => Date.now().toString()]; + } +} + +class SharedObjectDataStorage implements DataStorage { + private static var DATA_KEY:String = "data"; + + public var name(default, null):String; + public var version(default, null):Int; + + private var helper:DataHelper; + + private var _indexObject:SharedObject; + private var indexObject(get, null):SharedObject; + + private function get_indexObject():SharedObject { + if (_indexObject == null) { + _indexObject = SharedObject.getLocal('${name}/${version}/index'); + } + return _indexObject; + } + + public function new(name:String, helper:DataHelper, version:Int = 1) { + this.name = name; + this.version = version; + this.helper = helper; + } + + private function resolveDataObject(id:I):SharedObject { + var idKey = helper.idKey(id); + return SharedObject.getLocal('${name}/${version}/${Md5.encode(idKey)}'); + } + + private function applyFilter(filter:Null, meta:DataMeta):Bool { + if (filter != null) { + for (k in filter.keys()) { + if (meta.exists(k) && meta.get(k) != filter.get(k)) { + return false; + } + } + } + return true; + } + + private function applyOrder(order:Order, values:Array>):Void { + if (order != null) { + values.sort((a:IdMeta, b:IdMeta) -> { + for (item in order) { + var av = a.meta.get(item.field); + var bv = b.meta.get(item.field); + if (av > bv) { + return item.reverse ? -1 : 1; + } else if (av < bv) { + return item.reverse ? 1 : -1; + } + } + return 0; + }); + } + } + + public function getIndexPage(page:Page):Promise> { + var data = indexObject.data; + var values:Array> = []; + for (k in Reflect.fields(data)) { + var meta = Unserializer.run(Reflect.field(data, k)); + if (applyFilter(page.filter, meta)) { + values.push({id: helper.keyId(k), meta: meta}); + } + } + applyOrder(page.order, values); + var result:Array = values.slice(page.index * page.count, page.index * page.count + page.count).map(value -> value.id); + return Promise.promise({ + page: page, + total: values.length, + data: result, + }); + } + + public function getPage(page:Page):Promise> { + return getIndexPage(page).pipe((indexPage:DataPage) -> { + return Promise.whenAll([for (id in indexPage.data) get(id)]).then((data:Array) -> { + return { + page: indexPage.page, + total: indexPage.total, + data: data, + } + }); + }); + } + + public function get(id:I):Promise { + var dataObject = resolveDataObject(id); + if (Reflect.hasField(dataObject.data, DATA_KEY)) { + return Promise.promise(helper.bytesData(Bytes.ofHex(Reflect.field(dataObject.data, DATA_KEY)))); + } + return Promise.promise(null); + } + + public function save(item:D):Promise { + var id = helper.dataId(item); + var dataObject = resolveDataObject(id); + dataObject.setProperty(DATA_KEY, helper.dataBytes(item).toHex()); + dataObject.flush(); + indexObject.setProperty(helper.idKey(id), Serializer.run(helper.dataMeta(item))); + indexObject.flush(); + return Promise.promise(item); + } + + public function delete(id:I):Promise { + var dataObject = resolveDataObject(id); + if (Reflect.hasField(dataObject.data, DATA_KEY)) { + dataObject.clear(); + return Promise.promise(true); + } + Reflect.deleteField(dataObject.data, helper.idKey(id)); + return Promise.promise(false); + } +}