[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> { class MetaBuilder<D> extends DefaultConverter<DataMeta> {
private var builder:D -> Filter; private var builder:D -> DataMeta;
public function new(builder:D -> Filter) { public function new(builder:D -> DataMeta) {
super(); super();
this.builder = builder; this.builder = builder;
} }
public function build(item:D):Filter { public function build(item:D):DataMeta {
return builder(item); return builder(item);
} }
} }
typedef DataMeta = Filter;
typedef IdValue<I> = {id:I, meta:DataMeta};
class DataStorage<I, D:{id:I}> implements IDataSource<I, D> { class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
inline private static var DATA_KEY = "item"; inline private static var DATA_KEY = "item";
private var name:String; private var name:String;
private var filterConverter:DataFilter<D>; private var metaBuilder:MetaBuilder<D>;
private var idConverter:Converter<I, String>; private var idConverter:Converter<I, String>;
private var dataConverter:Converter<D, Dynamic>; private var dataConverter:Converter<D, Dynamic>;
private var indexData:SharedObject; private var indexData:SharedObject;
public function new( public function new(
name:String, name:String,
filterConverter:DataFilter<D>, metaBuilder:MetaBuilder<D>,
idConverter:Converter<I, String> = null, idConverter:Converter<I, String> = null,
dataConverter:Converter<D, Dynamic> = null dataConverter:Converter<D, Dynamic> = null
) { ) {
this.name = name; this.name = name;
this.filterConverter = filterConverter; this.metaBuilder = metaBuilder;
this.idConverter = idConverter != null ? idConverter : new DefaultConverter(); this.idConverter = idConverter != null ? idConverter : new DefaultConverter();
this.dataConverter = dataConverter != null ? dataConverter : new DefaultConverter(); this.dataConverter = dataConverter != null ? dataConverter : new DefaultConverter();
this.indexData = SharedObject.getLocal('${name}/index'); this.indexData = SharedObject.getLocal('${name}/index');
@@ -78,21 +82,38 @@ class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
return idConverter.deserialize(data); return idConverter.deserialize(data);
} }
private function buildFilter(item:D):Filter { private function buildMeta(item:D):DataMeta {
return filterConverter.build(item); 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) { if (filter != null) {
for (k => v in filter) { for (k => v in filter) {
if (data.get(k) != v) { if (meta.get(k) != v) {
return false; return false;
} }
} }
} }
return true; 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 { private function serialize(item:D):Dynamic {
return dataConverter.serialize(item); 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>> { public function getIndexPage(page:Page):Promise<DataPage<I>> {
var data:DynamicAccess<String> = indexData.data; var data:DynamicAccess<String> = indexData.data;
var result:Array<I> = []; var values:Array<IdValue<I>> = [];
for (k => v in data) { for (k => v in data) {
var filter = this.filterConverter.deserialize(v); var meta = metaBuilder.deserialize(v);
if (checkFilter(page.filter, filter)) { if (checkFilter(page.filter, meta)) {
result.push(desirealizeId(k)); 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({ return Promise.promise({
page: page, page: page,
total: result.length, total: values.length,
data: result, data: result,
}); });
} }
public function getPage(page:Page):Promise<DataPage<D>> { public function getPage(page:Page):Promise<DataPage<D>> {
return getIndexPage(page).pipe((indexPage:DataPage<I>) -> { 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 { return {
page: indexPage.page, page: indexPage.page,
total: indexPage.total, total: indexPage.total,
@@ -144,7 +167,7 @@ class DataStorage<I, D:{id:I}> implements IDataSource<I, D> {
var itemData = SharedObject.getLocal('${name}/${stringId}'); var itemData = SharedObject.getLocal('${name}/${stringId}');
itemData.setProperty(DATA_KEY, serialize(item)); itemData.setProperty(DATA_KEY, serialize(item));
itemData.flush(); itemData.flush();
indexData.setProperty(stringId, filterConverter.serialize(filterConverter.build(item))); indexData.setProperty(stringId, metaBuilder.serialize(metaBuilder.build(item)));
indexData.flush(); indexData.flush();
return Promise.promise(item); return Promise.promise(item);
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,12 +6,12 @@ import ru.m.data.DataStorage;
@:provide class GameStorage extends DataStorage<ImageId, GameState> { @:provide class GameStorage extends DataStorage<ImageId, GameState> {
inline private static var NAME = "game"; inline private static var NAME = "game";
inline private static var VERSION = 3; inline private static var VERSION = 4;
public function new() { public function new() {
super( super(
'${NAME}/${VERSION}', '${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)) 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 { override public function onHide():Void {
save(); if (saveTimer != null) {
save();
}
if (game != null) { if (game != null) {
render.signal.disconnect(game.signal.emit); render.signal.disconnect(game.signal.emit);
game.stop(); game.stop();
@@ -50,8 +52,8 @@ import ru.m.puzzlez.view.popup.PreviewPopup;
} }
private function toSave():Void { private function toSave():Void {
if (saveTimer != null) { if (saveTimer == null) {
saveTimer = Timer.delay(save, 500); saveTimer = Timer.delay(save, 5000);
} }
} }

View File

@@ -1,6 +1,7 @@
package ru.m.puzzlez.view; package ru.m.puzzlez.view;
import haxework.view.data.DataView; import haxework.view.data.DataView;
import haxework.view.form.ToggleButtonView;
import haxework.view.frame.FrameSwitcher; import haxework.view.frame.FrameSwitcher;
import haxework.view.frame.FrameView; import haxework.view.frame.FrameView;
import haxework.view.popup.ConfirmView; import haxework.view.popup.ConfirmView;
@@ -14,7 +15,8 @@ import ru.m.puzzlez.view.PuzzleImageView;
public static var ID(default, never) = "game_list"; 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 switcher:FrameSwitcher;
@:provide var storage:GameStorage; @:provide var storage:GameStorage;
@@ -22,28 +24,48 @@ import ru.m.puzzlez.view.PuzzleImageView;
private var status:GameStatus; private var status:GameStatus;
private var page:Page; 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() { public function new() {
super(ID); 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 { override public function onShow(data:GameStatus):Void {
status = data; status = data;
page.filter = ["status" => status]; page.filter = ["status" => status];
storage.getIndexPage(page).then(page -> images.data = page.data); refresh();
} }
private function start(id:ImageId):Void { private function start(id:ImageId):Void {
storage.get(id).then(state -> switcher.change(GameFrame.ID, state)); 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 { private function onAction(imageId:ImageId, action:Action):Void {
switch action { switch action {
case CLEAN: case CLEAN:
ConfirmView.confirm("Delete state?").then(result -> { ConfirmView.confirm("Delete state?").then(result -> {
if (result) { if (result) {
storage.delete(imageId); storage.delete(imageId);
storage.getIndexPage(page).then(page -> images.data = page.data); refresh();
} }
}); });
case _: case _:

View File

@@ -13,6 +13,18 @@ views:
+onDataAction: ~onAction +onDataAction: ~onAction
geometry.margin: 5 geometry.margin: 5
overflow.y: scroll 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 - $type: haxework.view.form.ButtonView
text: Back text: Back
geometry.position: absolute geometry.position: absolute

View File

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