diff --git a/package.json b/package.json index e9f3d0f..b3f4f8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "puzzlez", - "version": "0.0.4", + "version": "0.1.0", "private": true, "devDependencies": { "dateformat": "^3.0.3", diff --git a/src/haxe/ru/m/AbstractEnumTools.hx b/src/haxe/ru/m/AbstractEnumTools.hx new file mode 100644 index 0000000..55a03b9 --- /dev/null +++ b/src/haxe/ru/m/AbstractEnumTools.hx @@ -0,0 +1,38 @@ +package ru.m; + +#if macro +import haxe.macro.Context; +import haxe.macro.Expr; +using haxe.macro.Tools; +#end + +class AbstractEnumTools { + public static macro function getValues(typePath:Expr):Expr { + // Get the type from a given expression converted to string. + // This will work for identifiers and field access which is what we need, + // it will also consider local imports. If expression is not a valid type path or type is not found, + // compiler will give a error here. + var type = Context.getType(typePath.toString()); + + // Switch on the type and check if it's an abstract with @:enum metadata + switch (type.follow()) { + case TAbstract(_.get() => ab, _) if (ab.meta.has(":enum")): + // @:enum abstract values are actually static fields of the abstract implementation class, + // marked with @:enum and @:impl metadata. We generate an array of expressions that access those fields. + // Note that this is a bit of implementation detail, so it can change in future Haxe versions, but it's been + // stable so far. + var valueExprs = []; + for (field in ab.impl.get().statics.get()) { + if (field.meta.has(":enum") && field.meta.has(":impl")) { + var fieldName = field.name; + valueExprs.push(macro $typePath.$fieldName); + } + } + // Return collected expressions as an array declaration. + return macro $a{valueExprs}; + default: + // The given type is not an abstract, or doesn't have @:enum metadata, show a nice error message. + throw new Error(type.toString() + " should be @:enum abstract", typePath.pos); + } + } +} diff --git a/src/haxe/ru/m/puzzlez/Const.hx b/src/haxe/ru/m/puzzlez/Const.hx new file mode 100755 index 0000000..10646b2 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/Const.hx @@ -0,0 +1,20 @@ +package ru.m.puzzlez; + +import flash.Lib; +import flash.system.Capabilities; + +class Const { + public static var FPS:Int; + public static var BUILD:String; + public static var VERSION:String; + public static var NAME:String; + public static var DEBUG:Bool; + + public static function init():Void { + FPS = Std.parseInt(Lib.current.stage.application.meta.get("fps")); + BUILD = CompilationOption.get("build"); + VERSION = Lib.current.stage.application.meta.get("version"); + NAME = Lib.current.stage.application.meta.get("name"); + DEBUG = Capabilities.isDebugger; + } +} diff --git a/src/haxe/ru/m/puzzlez/PuzzlezApp.hx b/src/haxe/ru/m/puzzlez/PuzzlezApp.hx index ed754ba..7ee74ff 100644 --- a/src/haxe/ru/m/puzzlez/PuzzlezApp.hx +++ b/src/haxe/ru/m/puzzlez/PuzzlezApp.hx @@ -12,7 +12,9 @@ class PuzzlezApp extends App { LinuxIcon.apply(); #end var app = new PuzzlezApp(new PuzzlezTheme()); - app.start(new PuzzlezAppView()); + var view = new PuzzlezAppView(); + app.start(view); + view.launch(); L.d("Puzzlez", "started"); } } diff --git a/src/haxe/ru/m/puzzlez/core/ImageSource.hx b/src/haxe/ru/m/puzzlez/core/ImageSource.hx index 221e186..98be924 100644 --- a/src/haxe/ru/m/puzzlez/core/ImageSource.hx +++ b/src/haxe/ru/m/puzzlez/core/ImageSource.hx @@ -2,5 +2,5 @@ package ru.m.puzzlez.core; enum ImageSource { ASSET(name:String); - URL(url:String); + URL(url:String, ?preview:String); } diff --git a/src/haxe/ru/m/puzzlez/render/ProgressView.hx b/src/haxe/ru/m/puzzlez/render/ProgressView.hx index b170807..1ef54f9 100644 --- a/src/haxe/ru/m/puzzlez/render/ProgressView.hx +++ b/src/haxe/ru/m/puzzlez/render/ProgressView.hx @@ -6,12 +6,16 @@ class ProgressView extends LabelView { public function new() { super(); - text = 'Loading 0/0'; - update(); + text = 'Loading...'; } public function setProgress(current:Int, total:Int):Void { text = 'Loading ${current}/${total}'; + } + + override private function set_text(value:String):String { + var result = super.set_text(value); update(); + return result; } } diff --git a/src/haxe/ru/m/puzzlez/render/Render.hx b/src/haxe/ru/m/puzzlez/render/Render.hx index 6796fea..4c36cb7 100644 --- a/src/haxe/ru/m/puzzlez/render/Render.hx +++ b/src/haxe/ru/m/puzzlez/render/Render.hx @@ -84,7 +84,7 @@ class Render extends SpriteView implements IRender { table.graphics.drawRect(state.preset.imageRect.x, state.preset.imageRect.y, state.preset.imageRect.width, state.preset.imageRect.height); table.graphics.endFill(); table.graphics.lineStyle(); - progress.setProgress(0, state.parts.length); + progress.text = "Loading image"; content.addChild(progress.content); imageStorage.resolve(state.preset.image).then(onImageResolved); toUpdate(); diff --git a/src/haxe/ru/m/puzzlez/storage/AssetStorage.hx b/src/haxe/ru/m/puzzlez/storage/AssetStorage.hx index a3cdb9f..ae4fb83 100644 --- a/src/haxe/ru/m/puzzlez/storage/AssetStorage.hx +++ b/src/haxe/ru/m/puzzlez/storage/AssetStorage.hx @@ -5,14 +5,14 @@ import openfl.utils.AssetType; import promhx.Promise; import ru.m.puzzlez.core.ImageSource; -@:provide class AssetStorage implements ISourceStorage { +@:provide class AssetStorage implements ISourceStorage { var data:Promise>; public function new() { } - public function resolve(?type:String):Promise> { + public function resolve(?type:Dynamic):Promise> { if (data == null) { data = Promise.promise([for (name in Assets.list(AssetType.IMAGE).filter(function(name:String) return name.substr(0, 15) == "resources/image")) ASSET(name)]); } diff --git a/src/haxe/ru/m/puzzlez/storage/ISourceStorage.hx b/src/haxe/ru/m/puzzlez/storage/ISourceStorage.hx index e365e16..61e72af 100644 --- a/src/haxe/ru/m/puzzlez/storage/ISourceStorage.hx +++ b/src/haxe/ru/m/puzzlez/storage/ISourceStorage.hx @@ -1,8 +1,8 @@ package ru.m.puzzlez.storage; -import ru.m.puzzlez.core.ImageSource; import promhx.Promise; +import ru.m.puzzlez.core.ImageSource; -interface ISourceStorage { - public function resolve(?type:String):Promise>; +interface ISourceStorage { + public function resolve(?type:T):Promise>; } diff --git a/src/haxe/ru/m/puzzlez/storage/ImageStorage.hx b/src/haxe/ru/m/puzzlez/storage/ImageStorage.hx index 13e617c..913102f 100644 --- a/src/haxe/ru/m/puzzlez/storage/ImageStorage.hx +++ b/src/haxe/ru/m/puzzlez/storage/ImageStorage.hx @@ -17,7 +17,7 @@ import ru.m.puzzlez.core.ImageSource; @:provide class ImageStorage extends SharedObjectStorage { - private var cache:Map>; + private var cache:Map>; private var enabled:Bool; public function new() { @@ -42,25 +42,25 @@ import ru.m.puzzlez.core.ImageSource; return def.promise(); } - public function resolve(source:ImageSource):Promise { - if (cache.exists(source)) { - return cache.get(source); + public function resolve(source:ImageSource, preview:Bool = false):Promise { + var key = '${source}_${preview}'; + if (cache.exists(key)) { + return cache.get(key); } - var key = Std.string(source); var result:Promise = enabled && exists(key) ? unserialize(Reflect.field(so.data, key)) : (switch source { case ASSET(name): Promise.promise(Assets.getBitmapData(name)); - case URL(url): - new ImageLoader().GET(url); + case URL(url, previewUrl): + new ImageLoader().GET(preview && previewUrl != null ? previewUrl : url); }).then(function(image) { if (enabled) { so.setProperty(key, serialize(image)); } return image; }); - cache.set(source, result); + cache.set(key, result); return result; } } diff --git a/src/haxe/ru/m/puzzlez/storage/PixabayStorage.hx b/src/haxe/ru/m/puzzlez/storage/PixabayStorage.hx index 8729f17..9bcd55e 100644 --- a/src/haxe/ru/m/puzzlez/storage/PixabayStorage.hx +++ b/src/haxe/ru/m/puzzlez/storage/PixabayStorage.hx @@ -5,35 +5,41 @@ import haxework.storage.SharedObjectStorage; import promhx.Promise; import ru.m.puzzlez.core.ImageSource; +typedef PixabayImage = { + var id:Int; + var largeImageURL:String; + var webformatURL:String; + var previewURL:String; +} + typedef PixabayResponse = { - hits:Array<{ - largeImageURL:String, - }> + var hits:Array; } -enum PixabayCategory { - FASHION; - NATURE; - BACKGROUNDS; - SCIENCE; - EDUCATION; - PEOPLE; - FEELINGS; - RELIGION; - HEALTH; - PLACES; - ANIMALS; - INDUSTRY; - FOOD; - COMPUTER; - SPORTS; - TRANSPORTATION; - TRAVEL; BUILDINGS; - BUSINESS; - MUSIC; +@:enum abstract PixabayCategory(String) from String to String { + var FASHION = "fashion"; + var NATURE = "nature"; + var BACKGROUNDS = "backgrounds"; + var SCIENCE = "science"; + var EDUCATION = "education"; + var PEOPLE = "people"; + var FEELINGS = "feelings"; + var RELIGION = "religion"; + var HEALTH = "health"; + var PLACES = "places"; + var ANIMALS = "animals"; + var INDUSTRY = "industry"; + var FOOD = "food"; + var COMPUTER = "computer"; + var SPORTS = "sports"; + var TRANSPORTATION = "transportation"; + var TRAVEL = "travel"; + var BUILDINGS = "buildings"; + var BUSINESS = "business"; + var MUSIC = "music"; } -@:provide class PixabayStorage extends SharedObjectStorage implements ISourceStorage { +@:provide class PixabayStorage extends SharedObjectStorage implements ISourceStorage { private var key:String; private var enabled:Bool; @@ -44,13 +50,13 @@ enum PixabayCategory { enabled = false; } - public function resolve(?type:String):Promise> { + public function resolve(?type:PixabayCategory):Promise> { return enabled && exists(type) ? Promise.promise(read(type)) : new JsonLoader() - .GET('https://pixabay.com/api/?key=${key}&q=${type}') + .GET('https://pixabay.com/api/?key=${key}&category=${type}') .then(function(result:PixabayResponse) { - var result = [for (item in result.hits) URL(item.largeImageURL)]; + var result = [for (item in result.hits) URL(item.largeImageURL, item.previewURL)]; if (enabled) { write(type, result); } diff --git a/src/haxe/ru/m/puzzlez/view/GameFrame.hx b/src/haxe/ru/m/puzzlez/view/GameFrame.hx new file mode 100644 index 0000000..e9745d1 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/GameFrame.hx @@ -0,0 +1,42 @@ +package ru.m.puzzlez.view; + +import haxework.view.frame.FrameSwitcher; +import haxework.view.frame.FrameView; +import ru.m.puzzlez.core.Game; +import ru.m.puzzlez.core.GameUtil; +import ru.m.puzzlez.core.IGame; +import ru.m.puzzlez.core.ImageSource; +import ru.m.puzzlez.render.IRender; + +@:template class GameFrame extends FrameView { + public static var ID = "game"; + + @:view private var render:IRender; + private var game:IGame; + @:provide var switcher:FrameSwitcher; + + public function new() { + super(ID); + } + + override public function onShow(image:ImageSource):Void { + onHide(); + game = new Game(GameUtil.buildPreset(image)); + game.signal.connect(render.onGameEvent); + render.signal.connect(game.signal.emit); + game.start(); + } + + override public function onHide():Void { + if (game != null) { + render.signal.disconnect(game.signal.emit); + game.stop(); + game.dispose(); + game = null; + } + } + + private function back():Void { + switcher.change(ImagesFrame.ID); + } +} diff --git a/src/haxe/ru/m/puzzlez/view/GameFrame.yaml b/src/haxe/ru/m/puzzlez/view/GameFrame.yaml new file mode 100644 index 0000000..0a63e5b --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/GameFrame.yaml @@ -0,0 +1,17 @@ +--- +style: frame +geometry.width: 100% +geometry.height: 100% +overflow.x: crop +overflow.y: crop +views: + - id: render + $type: ru.m.puzzlez.render.Render + geometry.width: 100% + geometry.height: 100% + - $type: haxework.view.form.ButtonView + text: Back + geometry.position: absolute + geometry.hAlign: right + geometry.vAlign: top + +onPress: ~back() diff --git a/src/haxe/ru/m/puzzlez/view/ImagesFrame.hx b/src/haxe/ru/m/puzzlez/view/ImagesFrame.hx new file mode 100644 index 0000000..502d971 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/ImagesFrame.hx @@ -0,0 +1,51 @@ +package ru.m.puzzlez.view; + +import haxework.view.data.DataView; +import haxework.view.frame.FrameSwitcher; +import haxework.view.frame.FrameView; +import haxework.view.ImageView; +import haxework.view.utils.DrawUtil; +import openfl.Assets; +import ru.m.puzzlez.core.ImageSource; +import ru.m.puzzlez.storage.ImageStorage; + +@:template class ImagesFrame extends FrameView> { + public static var ID = "images"; + + @:view var images:DataView; + @:provide var imageStorage:ImageStorage; + @:provide var switcher:FrameSwitcher; + + public function new() { + super(ID); + } + + override public function onShow(data:Array):Void { + if (data != null) { + images.data = data; + } + } + + override public function onHide():Void { + //images.data = []; + } + + private function imageViewFactory(index:Int, image:ImageSource):ImageView { + var result = new ImageView(); + result.style = "view"; + result.stretch = false; + result.fillType = FillType.COVER; + result.setSize(192, 128); + result.image = Assets.getBitmapData("resources/icon.png"); + imageStorage.resolve(image, true).then(function(image) result.image = image); + return result; + } + + private function start(image:ImageSource):Void { + switcher.change(GameFrame.ID, image); + } + + private function back():Void { + switcher.change(StartFrame.ID); + } +} diff --git a/src/haxe/ru/m/puzzlez/view/ImagesFrame.yaml b/src/haxe/ru/m/puzzlez/view/ImagesFrame.yaml new file mode 100644 index 0000000..3a43d75 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/ImagesFrame.yaml @@ -0,0 +1,20 @@ +--- +style: frame +views: + - id: images + $type: haxework.view.data.DataView + layout: + $type: haxework.view.layout.TailLayout + margin: 5 + vAlign: middle + geometry.stretch: true + factory: ~imageViewFactory + +onDataSelect: ~start + geometry.margin: 5 + overflow.y: scroll + - $type: haxework.view.form.ButtonView + text: Back + geometry.position: absolute + geometry.hAlign: right + geometry.vAlign: top + +onPress: ~back() diff --git a/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx b/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx index 06417fe..0af34e7 100644 --- a/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx +++ b/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx @@ -1,66 +1,33 @@ package ru.m.puzzlez.view; -import haxework.view.data.DataView; -import haxework.view.form.ButtonView; +import flash.events.KeyboardEvent; +import flash.ui.Keyboard; +import haxework.resources.IResources; +import haxework.view.frame.FrameSwitcher; import haxework.view.group.VGroupView; -import haxework.view.ImageView; -import haxework.view.utils.DrawUtil; -import openfl.utils.Assets; -import ru.m.puzzlez.core.Game; -import ru.m.puzzlez.core.GameUtil; -import ru.m.puzzlez.core.IGame; -import ru.m.puzzlez.core.ImageSource; -import ru.m.puzzlez.render.IRender; -import ru.m.puzzlez.storage.AssetStorage; -import ru.m.puzzlez.storage.ImageStorage; -import ru.m.puzzlez.storage.PixabayStorage; @:template class PuzzlezAppView extends VGroupView { - @:view private var images:DataView; - @:view private var render:IRender; - private var game:IGame; - - @:provide var imageStorage:ImageStorage; - @:provide var assetStorage:AssetStorage; - @:provide var pixabayStorage:PixabayStorage; + @:view("switcher") var switcherView:FrameSwitcher; + @:provide var switcher:FrameSwitcher; + @:provide var resources:IResources; public function new() { super(); - assetStorage.resolve().then(function(data) images.data = data); + Const.init(); + resources.text.put("version", Const.VERSION); + resources.text.put("name", Const.NAME); + switcher = switcherView; } - private function imageViewFactory(index:Int, image:ImageSource):ImageView { - var result = new ImageView(); - result.style = "view"; - result.stretch = false; - result.fillType = FillType.COVER; - result.setSize(192, 128); - result.image = Assets.getBitmapData("resources/icon.png"); - imageStorage.resolve(image).then(function(image) result.image = image); - return result; - } - - private function moreImages(view:ButtonView):Void { - view.visible = false; - view.toUpdateParent(); - pixabayStorage.resolve("nature").then(function(data) images.data = images.data.concat(data)); - } - - public function start(image:ImageSource):Void { - stop(); - game = new Game(GameUtil.buildPreset(image)); - game.signal.connect(render.onGameEvent); - render.signal.connect(game.signal.emit); - game.start(); - } - - public function stop():Void { - if (game != null) { - render.signal.disconnect(game.signal.emit); - game.stop(); - game.dispose(); - game = null; - } + public function launch():Void { + content.stage.stageFocusRect = false; + switcher.change(StartFrame.ID); + stage.addEventListener(KeyboardEvent.KEY_DOWN, function(event:KeyboardEvent) { + switch event.keyCode { + case Keyboard.ESCAPE: + switcher.change(StartFrame.ID); + } + }); } } diff --git a/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml b/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml index 3be1156..429a517 100644 --- a/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml +++ b/src/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml @@ -1,42 +1,10 @@ -style: background -layout.hAlign: center -layout.vAlign: middle +--- views: - - $type: haxework.view.group.HGroupView + - $type: haxework.view.frame.FrameSwitcher + id: switcher geometry.stretch: true - geometry.margin: 5 - views: - - $type: haxework.view.group.VGroupView - geometry.height: 100% - layout.hAlign: center - views: - - $type: haxework.view.form.LabelView - text: Puzzle'z - font.size: 42 - - $type: haxework.view.form.ButtonView - text: Shuffle - +onPress: ~game.shuffle() - - id: images - $type: haxework.view.data.DataView - layout: - $type: haxework.view.layout.VerticalLayout - margin: 5 - geometry.height: 100% - factory: ~imageViewFactory - +onDataSelect: ~start - geometry.margin: 5 - overflow.y: scroll - - $type: haxework.view.form.ButtonView - text: More - +onPress: ~moreImages - - $type: haxework.view.group.GroupView - style: frame - geometry.width: 100% - geometry.height: 100% - overflow.x: crop - overflow.y: crop - views: - - id: render - $type: ru.m.puzzlez.render.Render - geometry.width: 100% - geometry.height: 100% + style: dark + factory: + _start_: {$class: ru.m.puzzlez.view.StartFrame} + _images_: {$class: ru.m.puzzlez.view.ImagesFrame} + _game_: {$class: ru.m.puzzlez.view.GameFrame} diff --git a/src/haxe/ru/m/puzzlez/view/StartFrame.hx b/src/haxe/ru/m/puzzlez/view/StartFrame.hx new file mode 100644 index 0000000..2ebbce6 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/StartFrame.hx @@ -0,0 +1,48 @@ +package ru.m.puzzlez.view; + +import haxework.view.data.DataView; +import haxework.view.form.ButtonView; +import haxework.view.frame.FrameSwitcher; +import haxework.view.frame.FrameView; +import ru.m.puzzlez.core.ImageSource; +import ru.m.puzzlez.storage.AssetStorage; +import ru.m.puzzlez.storage.ISourceStorage; +import ru.m.puzzlez.storage.PixabayStorage; + +typedef Source = { + var storage:ISourceStorage; + @:optional var type:T; +} + +@:template class StartFrame extends FrameView { + public static var ID = "start"; + + @:view var sources:DataView, ButtonView>; + @:provide var assetStorage:AssetStorage; + @:provide var pixabayStorage:PixabayStorage; + @:provide var switcher:FrameSwitcher; + + public function new() { + super(ID); + var data:Array> = []; + data.push({storage: assetStorage}); + for (type in AbstractEnumTools.getValues(PixabayCategory)) { + data.push({storage: pixabayStorage, type: type}); + } + sources.data = data; + } + + private function sourceViewFactory(index:Int, source:Source):ButtonView { + var result = new ButtonView(); + result.text = Std.string(source.type != null ? source.type : "custom"); + return result; + } + + private function load(source:Source):Void { + source.storage.resolve(source.type).then(onLoaded); + } + + private function onLoaded(result:Array):Void { + switcher.change(ImagesFrame.ID, result); + } +} diff --git a/src/haxe/ru/m/puzzlez/view/StartFrame.yaml b/src/haxe/ru/m/puzzlez/view/StartFrame.yaml new file mode 100644 index 0000000..32a84c6 --- /dev/null +++ b/src/haxe/ru/m/puzzlez/view/StartFrame.yaml @@ -0,0 +1,19 @@ +--- +style: frame +views: + - $type: haxework.view.form.LabelView + text: $r:text:name + geometry.margin.top: 15 + geometry.hAlign: center + font.size: 40 + - id: sources + $type: haxework.view.data.DataView + layout: + $type: haxework.view.layout.TailLayout + margin: 10 + vAlign: middle + geometry.stretch: true + factory: ~sourceViewFactory + +onDataSelect: ~load + geometry.margin: 5 + overflow.y: scroll