[add] GameListFrame paginator

This commit is contained in:
2020-03-11 17:27:12 +03:00
parent d375f07bed
commit 4c4555c036
10 changed files with 104 additions and 38 deletions

View File

@@ -34,37 +34,41 @@ class DefaultConverter<D> extends Converter<D, String> {
}
}
class DataFilter<D> extends DefaultConverter<Filter> {
private var builder:D -> Filter;
class MetaBuilder<D> extends DefaultConverter<DataMeta> {
private var builder:D -> DataMeta;
public function new(builder:D -> Filter) {
public function new(builder:D -> DataMeta) {
super();
this.builder = builder;
}
public function build(item:D):Filter {
public function build(item:D):DataMeta {
return builder(item);
}
}
typedef DataMeta = Filter;
typedef IdValue<I> = {id:I, meta:DataMeta};
class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
inline private static var DATA_KEY = "item";
private var name:String;
private var filterConverter:DataFilter<D>;
private var metaBuilder:MetaBuilder<D>;
private var idConverter:Converter<I, String>;
private var dataConverter:Converter<D, Dynamic>;
private var indexData:SharedObject;
public function new(
name:String,
filterConverter:DataFilter<D>,
metaBuilder:MetaBuilder<D>,
idConverter:Converter<I, String> = null,
dataConverter:Converter<D, Dynamic> = null
) {
this.name = name;
this.filterConverter = filterConverter;
this.metaBuilder = metaBuilder;
this.idConverter = idConverter != null ? idConverter : new DefaultConverter();
this.dataConverter = dataConverter != null ? dataConverter : new DefaultConverter();
this.indexData = SharedObject.getLocal('${name}/index');
@@ -78,21 +82,38 @@ class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
return idConverter.deserialize(data);
}
private function buildFilter(item:D):Filter {
return filterConverter.build(item);
private function buildMeta(item:D):DataMeta {
return metaBuilder.build(item);
}
private function checkFilter(filter:Null<Filter>, data:Filter):Bool {
private function checkFilter(filter:Null<Filter>, meta:DataMeta):Bool {
if (filter != null) {
for (k => v in filter) {
if (data.get(k) != v) {
return false;
}
if (meta.get(k) != v) {
return false;
}
}
}
return true;
}
private function sort(order:Order, values:Array<IdValue<I>>):Void {
if (order != null) {
values.sort((a:IdValue<I>, b:IdValue<I>) -> {
for (item in order) {
var av = a.meta.get(item.key);
var bv = b.meta.get(item.key);
if (av > bv) {
return item.reverse ? - 1 : 1;
} else if (av < bv) {
return item.reverse ? 1 : -1;
}
}
return 0;
});
}
}
private function serialize(item:D):Dynamic {
return dataConverter.serialize(item);
}
@@ -103,23 +124,25 @@ class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
public function getIndexPage(page:Page):Promise<DataPage<I>> {
var data:DynamicAccess<String> = indexData.data;
var result:Array<I> = [];
var values:Array<IdValue<I>> = [];
for (k => v in data) {
var filter = this.filterConverter.deserialize(v);
if (checkFilter(page.filter, filter)) {
result.push(desirealizeId(k));
var meta = metaBuilder.deserialize(v);
if (checkFilter(page.filter, meta)) {
values.push({id: desirealizeId(k), meta: meta});
}
}
sort(page.order, values);
var result:Array<I> = values.slice(page.index * page.count, page.index * page.count + page.count).map(value -> value.id);
return Promise.promise({
page: page,
total: result.length,
total: values.length,
data: result,
});
}
public function getPage(page:Page):Promise<DataPage<D>> {
return getIndexPage(page).pipe((indexPage:DataPage<I>) -> {
Promise.whenAll([for (id in indexPage.data) get(id)]).then((data:Array<D>) -> {
return Promise.whenAll([for (id in indexPage.data) get(id)]).then((data:Array<D>) -> {
return {
page: indexPage.page,
total: indexPage.total,
@@ -144,7 +167,7 @@ class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
var itemData = SharedObject.getLocal('${name}/${stringId}');
itemData.setProperty(DATA_KEY, serialize(item));
itemData.flush();
indexData.setProperty(stringId, filterConverter.serialize(filterConverter.build(item)));
indexData.setProperty(stringId, metaBuilder.serialize(metaBuilder.build(item)));
indexData.flush();
return Promise.promise(item);
}

View File

@@ -4,10 +4,12 @@ import promhx.Promise;
typedef Filter = Map<String, String>;
typedef Order = Array<{key: String, ?reverse:Bool}>;
typedef Page = {
var index:Int;
var count:Int;
@:optional var order:Array<String>;
@:optional var order:Order;
@:optional var filter:Filter;
}

View File

@@ -27,9 +27,10 @@ class Game implements IGame {
case READY:
shuffle();
state.status = STARTED;
signal.emit(START(state));
case _:
signal.emit(RESUME(state));
}
signal.emit(START(state));
}
public function shuffle():Void {

View File

@@ -14,6 +14,7 @@ enum GameChange {
enum GameEvent {
START(state:GameState);
RESUME(state:GameState);
ACTION(action:GameAction);
CHANGE(change:GameChange);
COMPLETE;

View File

@@ -71,7 +71,7 @@ class Render extends SpriteView implements IRender {
public function onGameEvent(event:GameEvent):Void {
switch event {
case START(state):
case START(state) | RESUME(state):
onStart(state);
case CHANGE(PART_UPDATE(id, TABLE(point))):
var part:PartView = parts[id];

View File

@@ -6,12 +6,12 @@ import ru.m.data.DataStorage;
@:provide class GameStorage extends DataStorage<ImageId, GameState> {
inline private static var NAME = "game";
inline private static var VERSION = 3;
inline private static var VERSION = 4;
public function new() {
super(
'${NAME}/${VERSION}',
new DataFilter<GameState>(item -> ["status" => item.status]),
new MetaBuilder<GameState>(item -> ["status" => item.status, "date" => Date.now().toString()]),
new Converter<ImageId, String>(id -> id.toString(), data -> ImageId.fromString(data))
);
}

View File

@@ -40,7 +40,9 @@ import ru.m.puzzlez.view.popup.PreviewPopup;
}
override public function onHide():Void {
save();
if (saveTimer != null) {
save();
}
if (game != null) {
render.signal.disconnect(game.signal.emit);
game.stop();
@@ -50,8 +52,8 @@ import ru.m.puzzlez.view.popup.PreviewPopup;
}
private function toSave():Void {
if (saveTimer != null) {
saveTimer = Timer.delay(save, 500);
if (saveTimer == null) {
saveTimer = Timer.delay(save, 5000);
}
}

View File

@@ -1,6 +1,7 @@
package ru.m.puzzlez.view;
import haxework.view.data.DataView;
import haxework.view.form.ToggleButtonView;
import haxework.view.frame.FrameSwitcher;
import haxework.view.frame.FrameView;
import haxework.view.popup.ConfirmView;
@@ -14,7 +15,8 @@ import ru.m.puzzlez.view.PuzzleImageView;
public static var ID(default, never) = "game_list";
@:view var images:ActionDataView<ImageId, PuzzleImageView, Action>;
@:view("images") var imagesView:ActionDataView<ImageId, PuzzleImageView, Action>;
@:view("pages") var pagesView:DataView<Int, ToggleButtonView>;
@:provide var switcher:FrameSwitcher;
@:provide var storage:GameStorage;
@@ -22,28 +24,48 @@ import ru.m.puzzlez.view.PuzzleImageView;
private var status:GameStatus;
private var page:Page;
private var data(default, set):DataPage<ImageId>;
private function set_data(value) {
data = value;
imagesView.data = data.data;
pagesView.data = [for (i in 0...Std.int(data.total / data.page.count) + 1) i];
return data;
}
public function new() {
super(ID);
page = {index: 0, count: 10};
page = {index: 0, count: 6, order: [{key: "date", reverse: true}]};
}
private function pageFactory(index:Int, value:Int):ToggleButtonView {
var result = new ToggleButtonView();
result.text = '${value}';
result.on = data.page.index == value;
return result;
}
override public function onShow(data:GameStatus):Void {
status = data;
page.filter = ["status" => status];
storage.getIndexPage(page).then(page -> images.data = page.data);
refresh();
}
private function start(id:ImageId):Void {
storage.get(id).then(state -> switcher.change(GameFrame.ID, state));
}
private function refresh():Void {
storage.getIndexPage(page).then(data -> this.data = data);
}
private function onAction(imageId:ImageId, action:Action):Void {
switch action {
case CLEAN:
ConfirmView.confirm("Delete state?").then(result -> {
if (result) {
storage.delete(imageId);
storage.getIndexPage(page).then(page -> images.data = page.data);
refresh();
}
});
case _:

View File

@@ -13,6 +13,18 @@ views:
+onDataAction: ~onAction
geometry.margin: 5
overflow.y: scroll
- id: pages
$type: haxework.view.data.DataView
geometry.width: 100%
layout:
$type: haxework.view.layout.TailLayout
margin: 5
factory: ~pageFactory
+onDataSelect: |
~(index) -> {
page.index = index;
refresh();
}
- $type: haxework.view.form.ButtonView
text: Back
geometry.position: absolute

View File

@@ -1,17 +1,18 @@
package ru.m.puzzlez.view;
import ru.m.update.Updater;
import haxework.view.data.DataView;
import haxework.view.form.ButtonView;
import haxework.view.frame.FrameSwitcher;
import haxework.view.frame.FrameView;
import haxework.view.popup.ConfirmView;
import ru.m.data.IDataSource;
import ru.m.puzzlez.core.GameState.GameStatus;
import ru.m.puzzlez.source.AssetSource;
import ru.m.puzzlez.source.FileSource;
import ru.m.puzzlez.source.PixabaySource;
import ru.m.puzzlez.storage.GameStorage;
import ru.m.puzzlez.storage.ImageStorage;
import ru.m.update.Updater;
@:template class StartFrame extends FrameView<Dynamic> {
public static var ID = "start";
@@ -38,12 +39,14 @@ import ru.m.puzzlez.storage.ImageStorage;
}
private function refresh():Void {
gameStorage.getIndexPage({index: 0, count: 0, filter: ["status" => STARTED]}).then(page -> {
var count = page.total;
loadButton.text = 'Resume (${count})';
loadButton.disabled = count == 0;
var startedRequest:Page = {index: 0, count: 0, filter: ["status" => STARTED]};
gameStorage.getIndexPage(startedRequest).then(page -> {
var total = page.total;
loadButton.text = 'Resume (${total})';
loadButton.disabled = total == 0;
});
gameStorage.getIndexPage({index: 0, count: 0, filter: ["status" => COMPLETE]}).then(page -> {
var completeRequest:Page = {index: 0, count: 0, filter: ["status" => COMPLETE]};
gameStorage.getIndexPage(completeRequest).then(page -> {
var total = page.total;
completeButton.text = 'Complete (${total})';
completeButton.disabled = total == 0;