[server] add module
This commit is contained in:
38
src/app/haxe/ru/m/AbstractEnumTools.hx
Normal file
38
src/app/haxe/ru/m/AbstractEnumTools.hx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/app/haxe/ru/m/Device.hx
Normal file
77
src/app/haxe/ru/m/Device.hx
Normal file
@@ -0,0 +1,77 @@
|
||||
package ru.m;
|
||||
|
||||
import flash.display.StageDisplayState;
|
||||
import flash.events.FullScreenEvent;
|
||||
import flash.Lib;
|
||||
import hw.signal.Signal;
|
||||
|
||||
enum abstract Platform(String) from String to String {
|
||||
var ANDROID = "android";
|
||||
var LINUX = "linux";
|
||||
var WINDOWS = "windows";
|
||||
var FLASH = "flash";
|
||||
var HTML5 = "html5";
|
||||
var UNKNOWN = "unknown";
|
||||
}
|
||||
|
||||
class Device {
|
||||
|
||||
public static var platform(get, null):Platform;
|
||||
|
||||
private static function get_platform():Platform {
|
||||
#if android
|
||||
return ANDROID;
|
||||
#elseif linux
|
||||
return LINUX;
|
||||
#elseif windows
|
||||
return WINDOWS;
|
||||
#elseif flash
|
||||
return FLASH;
|
||||
#elseif html5
|
||||
return HTML5;
|
||||
#else
|
||||
return UNKNOWN;
|
||||
#end
|
||||
}
|
||||
|
||||
private static var MOBILES(default, never):Array<String> = [
|
||||
"Android", "webOS", "iPhone", "iPad", "iPod", "BlackBerry", "Windows Phone",
|
||||
];
|
||||
|
||||
public static function isMobile():Bool {
|
||||
#if android
|
||||
return true;
|
||||
#elseif js
|
||||
var userAgent = js.Browser.navigator.userAgent;
|
||||
for (mobile in MOBILES) {
|
||||
if (userAgent.indexOf(mobile) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
public static var fullScreenSignal(get, null):Signal<Bool>;
|
||||
|
||||
private static function get_fullScreenSignal():Signal<Bool> {
|
||||
if (fullScreenSignal == null) {
|
||||
fullScreenSignal = new Signal();
|
||||
Lib.current.stage.addEventListener(FullScreenEvent.FULL_SCREEN, (event:FullScreenEvent) -> fullScreenSignal.emit(event.fullScreen));
|
||||
}
|
||||
return fullScreenSignal;
|
||||
}
|
||||
|
||||
public static var fullScreenSupport(get, never):Bool;
|
||||
|
||||
public static function get_fullScreenSupport():Bool {
|
||||
return Lib.current.stage.allowsFullScreen;
|
||||
}
|
||||
|
||||
public static function toggleFullScreen():Void {
|
||||
Lib.current.stage.displayState = Lib.current.stage.displayState == StageDisplayState.NORMAL ?
|
||||
StageDisplayState.FULL_SCREEN : StageDisplayState.NORMAL;
|
||||
}
|
||||
}
|
||||
38
src/app/haxe/ru/m/ModernFileReference.hx
Normal file
38
src/app/haxe/ru/m/ModernFileReference.hx
Normal file
@@ -0,0 +1,38 @@
|
||||
package ru.m;
|
||||
|
||||
import flash.net.FileFilter;
|
||||
import flash.net.FileReference;
|
||||
|
||||
class Callback<T> {
|
||||
private var callback:T -> Void;
|
||||
|
||||
public function new(callback: T -> Void) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public function execute(result:T):Void {
|
||||
this.callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
class ModernFileReference extends FileReference {
|
||||
#if android
|
||||
private static var fileUtilBrowse = lime.system.JNI.createStaticMethod(
|
||||
"ru.m.android.FileUtil",
|
||||
"browse",
|
||||
"(Lorg/haxe/lime/HaxeObject;)V"
|
||||
);
|
||||
#end
|
||||
|
||||
override function browse(?typeFilter:Array<FileFilter>):Bool {
|
||||
#if android
|
||||
fileUtilBrowse(new Callback<haxe.io.BytesData>(function(result:haxe.io.BytesData):Void {
|
||||
data = result;
|
||||
dispatchEvent(new flash.events.Event(flash.events.Event.COMPLETE));
|
||||
}));
|
||||
return true;
|
||||
#else
|
||||
return super.browse(typeFilter);
|
||||
#end
|
||||
}
|
||||
}
|
||||
191
src/app/haxe/ru/m/data/DataStorage.hx
Normal file
191
src/app/haxe/ru/m/data/DataStorage.hx
Normal file
@@ -0,0 +1,191 @@
|
||||
package ru.m.data;
|
||||
|
||||
import flash.net.SharedObject;
|
||||
import haxe.DynamicAccess;
|
||||
import haxe.Serializer;
|
||||
import haxe.Unserializer;
|
||||
import promhx.Promise;
|
||||
import ru.m.data.IDataSource;
|
||||
|
||||
class Converter<D, S> {
|
||||
private var serializer:D -> S;
|
||||
private var desirealizer:S -> D;
|
||||
|
||||
public function new(serializer:D -> S, desirealizer:S -> D) {
|
||||
this.serializer = serializer;
|
||||
this.desirealizer = desirealizer;
|
||||
}
|
||||
|
||||
public inline function serialize(item:D):S {
|
||||
return serializer(item);
|
||||
}
|
||||
|
||||
public inline function deserialize(data:S):D {
|
||||
return desirealizer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultConverter<D> extends Converter<D, String> {
|
||||
public function new () {
|
||||
super(
|
||||
item -> Serializer.run(item),
|
||||
data -> new Unserializer(data).unserialize()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyConverter<T> extends Converter<T, T> {
|
||||
public function new() {
|
||||
super(item -> item, data -> data);
|
||||
}
|
||||
}
|
||||
|
||||
class MetaBuilder<D> extends DefaultConverter<DataMeta> {
|
||||
private var builder:D -> DataMeta;
|
||||
|
||||
public function new(builder:D -> DataMeta) {
|
||||
super();
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
public function build(item:D):DataMeta {
|
||||
return builder(item);
|
||||
}
|
||||
}
|
||||
|
||||
typedef DataMeta = Filter;
|
||||
|
||||
typedef IdValue<I> = {id:I, meta:DataMeta};
|
||||
|
||||
class DataStorage<I, D> implements IDataManager<I, D> implements IDataIndex<I> {
|
||||
|
||||
inline private static var DATA_KEY = "item";
|
||||
|
||||
private var name:String;
|
||||
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,
|
||||
metaBuilder:MetaBuilder<D>,
|
||||
idConverter:Converter<I, String> = null,
|
||||
dataConverter:Converter<D, Dynamic> = null
|
||||
) {
|
||||
this.name = name;
|
||||
this.metaBuilder = metaBuilder;
|
||||
this.idConverter = idConverter != null ? idConverter : new DefaultConverter();
|
||||
this.dataConverter = dataConverter != null ? dataConverter : new DefaultConverter();
|
||||
this.indexData = SharedObject.getLocal('${name}/index');
|
||||
}
|
||||
|
||||
private function serializeId(id:I):String {
|
||||
return idConverter.serialize(id);
|
||||
}
|
||||
|
||||
private function desirealizeId(data:String):I {
|
||||
return idConverter.deserialize(data);
|
||||
}
|
||||
|
||||
private function buildMeta(item:D):DataMeta {
|
||||
return metaBuilder.build(item);
|
||||
}
|
||||
|
||||
private function checkFilter(filter:Null<Filter>, meta:DataMeta):Bool {
|
||||
if (filter != null) {
|
||||
for (k => v in filter) {
|
||||
if (meta.exists(k) && 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);
|
||||
}
|
||||
|
||||
private function unserialize(data:Dynamic):D {
|
||||
return dataConverter.deserialize(data);
|
||||
}
|
||||
|
||||
public function getIndexPage(page:Page):Promise<DataPage<I>> {
|
||||
var data:DynamicAccess<String> = indexData.data;
|
||||
var values:Array<IdValue<I>> = [];
|
||||
for (k => v in data) {
|
||||
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: values.length,
|
||||
data: result,
|
||||
});
|
||||
}
|
||||
|
||||
public function getPage(page:Page):Promise<DataPage<D>> {
|
||||
return getIndexPage(page).pipe((indexPage:DataPage<I>) -> {
|
||||
return Promise.whenAll([for (id in indexPage.data) get(id)]).then((data:Array<D>) -> {
|
||||
return {
|
||||
page: indexPage.page,
|
||||
total: indexPage.total,
|
||||
data: data,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function get(id:I):Promise<D> {
|
||||
var stringId = serializeId(id);
|
||||
var itemData = SharedObject.getLocal('${name}/${stringId}');
|
||||
var result:D = null;
|
||||
if (Reflect.hasField(itemData.data, DATA_KEY)) {
|
||||
result = unserialize(Reflect.field(itemData.data, DATA_KEY));
|
||||
}
|
||||
return Promise.promise(result);
|
||||
}
|
||||
|
||||
public function save(item:D):Promise<D> {
|
||||
var meta = metaBuilder.build(item);
|
||||
var stringId = meta.get("id");
|
||||
var itemData = SharedObject.getLocal('${name}/${stringId}');
|
||||
itemData.setProperty(DATA_KEY, serialize(item));
|
||||
itemData.flush();
|
||||
indexData.setProperty(stringId, metaBuilder.serialize(meta));
|
||||
indexData.flush();
|
||||
return Promise.promise(item);
|
||||
}
|
||||
|
||||
public function delete(id:I):Promise<Bool> {
|
||||
var stringId = serializeId(id);
|
||||
var itemData = SharedObject.getLocal('${name}/${stringId}');
|
||||
itemData.clear();
|
||||
var data:DynamicAccess<String> = indexData.data;
|
||||
data.remove(stringId);
|
||||
indexData.flush();
|
||||
return Promise.promise(true);
|
||||
}
|
||||
}
|
||||
34
src/app/haxe/ru/m/data/IDataSource.hx
Normal file
34
src/app/haxe/ru/m/data/IDataSource.hx
Normal file
@@ -0,0 +1,34 @@
|
||||
package ru.m.data;
|
||||
|
||||
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:Order;
|
||||
@:optional var filter:Filter;
|
||||
}
|
||||
|
||||
typedef DataPage<T> = {
|
||||
var page:Page;
|
||||
var total:Int;
|
||||
var data:Array<T>;
|
||||
}
|
||||
|
||||
interface IDataIndex<I> {
|
||||
public function getIndexPage(page:Page):Promise<DataPage<I>>;
|
||||
}
|
||||
|
||||
interface IDataSource<I, D> {
|
||||
public function getPage(page:Page):Promise<DataPage<D>>;
|
||||
public function get(id:I):Promise<D>;
|
||||
}
|
||||
|
||||
interface IDataManager<I, D> extends IDataSource<I, D> {
|
||||
public function save(item:D):Promise<D>;
|
||||
public function delete(id:I):Promise<Bool>;
|
||||
}
|
||||
53
src/app/haxe/ru/m/draw/DrawPath.hx
Normal file
53
src/app/haxe/ru/m/draw/DrawPath.hx
Normal file
@@ -0,0 +1,53 @@
|
||||
package ru.m.draw;
|
||||
|
||||
import flash.display.Graphics;
|
||||
import flash.display.GraphicsPathCommand;
|
||||
import flash.Vector;
|
||||
|
||||
enum DrawCommand {
|
||||
MOVE_TO(x:Float, y:Float);
|
||||
LINE_TO(x:Float, y:Float);
|
||||
CURVE_TO(cx:Float, cy:Float, ax:Float, ay:Float);
|
||||
}
|
||||
|
||||
class DrawPath {
|
||||
public var commands(default, null):Array<DrawCommand>;
|
||||
|
||||
public function new(commands:Array<DrawCommand> = null) {
|
||||
this.commands = commands != null ? commands : [];
|
||||
}
|
||||
|
||||
public function draw(graphics:Graphics):Void {
|
||||
var commands = new Vector<GraphicsPathCommand>();
|
||||
var data = new Vector<Float>();
|
||||
for (command in this.commands) {
|
||||
switch command {
|
||||
case MOVE_TO(x, y):
|
||||
commands.push(GraphicsPathCommand.MOVE_TO);
|
||||
data.push(x);
|
||||
data.push(y);
|
||||
case LINE_TO(x, y):
|
||||
commands.push(GraphicsPathCommand.LINE_TO);
|
||||
data.push(x);
|
||||
data.push(y);
|
||||
case CURVE_TO(cx, cy, ax, ay):
|
||||
commands.push(GraphicsPathCommand.CURVE_TO);
|
||||
data.push(cx);
|
||||
data.push(cy);
|
||||
data.push(ax);
|
||||
data.push(ay);
|
||||
}
|
||||
}
|
||||
graphics.drawPath(commands, data);
|
||||
}
|
||||
|
||||
public function move(mx:Float, my:Float):DrawPath {
|
||||
var result = new DrawPath();
|
||||
result.commands = commands.map(command -> switch command {
|
||||
case MOVE_TO(x, y): MOVE_TO(x + mx, y + my);
|
||||
case LINE_TO(x, y): LINE_TO(x + mx, y + my);
|
||||
case CURVE_TO(cx, cy, ax, ay): CURVE_TO(cx + mx, cy + my, ax + mx, ay + my);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
7
src/app/haxe/ru/m/event/GestureEvent.hx
Normal file
7
src/app/haxe/ru/m/event/GestureEvent.hx
Normal file
@@ -0,0 +1,7 @@
|
||||
package ru.m.event;
|
||||
|
||||
import flash.events.Event;
|
||||
|
||||
class GestureEvent extends Event {
|
||||
|
||||
}
|
||||
73
src/app/haxe/ru/m/event/GestureManager.hx
Normal file
73
src/app/haxe/ru/m/event/GestureManager.hx
Normal file
@@ -0,0 +1,73 @@
|
||||
package ru.m.event;
|
||||
|
||||
import flash.geom.Point;
|
||||
import flash.display.DisplayObject;
|
||||
import flash.events.TouchEvent;
|
||||
|
||||
typedef Touch = {
|
||||
var id:Int;
|
||||
var point:Point;
|
||||
}
|
||||
|
||||
class GestureManager {
|
||||
|
||||
private var target:DisplayObject;
|
||||
private var touchesMap:Map<Int, Touch>;
|
||||
private var touches:Array<Touch>;
|
||||
private var distance:Float;
|
||||
|
||||
public function new(target:DisplayObject) {
|
||||
this.target = target;
|
||||
touchesMap = new Map();
|
||||
touches = new Array();
|
||||
target.addEventListener(TouchEvent.TOUCH_BEGIN, onTouchBegin);
|
||||
target.addEventListener(TouchEvent.TOUCH_MOVE, onTouchMove);
|
||||
target.addEventListener(TouchEvent.TOUCH_END, onTouchEnd);
|
||||
}
|
||||
|
||||
private function addTouch(id:Int, point:Point):Void {
|
||||
var touch:Touch = {id: id, point: point};
|
||||
touchesMap.set(touch.id, touch);
|
||||
touches.push(touch);
|
||||
if (touches.length == 2) {
|
||||
distance = Point.distance(touches[0].point, touches[1].point);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateTouch(id:Int, point:Point):Void {
|
||||
touchesMap.get(id).point = point;
|
||||
if (touches.length == 2) {
|
||||
var newDistance = Point.distance(touches[0].point, touches[1].point);
|
||||
var event = new ZoomGestureEvent(ZoomGestureEvent.GESTURE_ZOOM);
|
||||
event.zoom = (newDistance - distance) * 0.001;
|
||||
distance = newDistance;
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private function removeTouch(id:Int):Void {
|
||||
touches.remove(touchesMap.get(id));
|
||||
touchesMap.remove(id);
|
||||
}
|
||||
|
||||
private function onTouchBegin(event:TouchEvent):Void {
|
||||
addTouch(event.touchPointID, new Point(event.stageX, event.stageY));
|
||||
}
|
||||
|
||||
private function onTouchMove(event:TouchEvent):Void {
|
||||
updateTouch(event.touchPointID, new Point(event.stageX, event.stageY));
|
||||
}
|
||||
|
||||
private function onTouchEnd(event:TouchEvent):Void {
|
||||
removeTouch(event.touchPointID);
|
||||
}
|
||||
|
||||
public function dispose():Void {
|
||||
if (target != null) {
|
||||
target.removeEventListener(TouchEvent.TOUCH_BEGIN, onTouchBegin);
|
||||
target.removeEventListener(TouchEvent.TOUCH_MOVE, onTouchMove);
|
||||
target.removeEventListener(TouchEvent.TOUCH_END, onTouchEnd);
|
||||
target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/app/haxe/ru/m/event/ZoomGestureEvent.hx
Normal file
7
src/app/haxe/ru/m/event/ZoomGestureEvent.hx
Normal file
@@ -0,0 +1,7 @@
|
||||
package ru.m.event;
|
||||
|
||||
class ZoomGestureEvent extends GestureEvent {
|
||||
public static var GESTURE_ZOOM(default, never):String = "gesture_zoom";
|
||||
|
||||
public var zoom(default, default):Float;
|
||||
}
|
||||
89
src/app/haxe/ru/m/pixabay/PixabayApi.hx
Normal file
89
src/app/haxe/ru/m/pixabay/PixabayApi.hx
Normal file
@@ -0,0 +1,89 @@
|
||||
package ru.m.pixabay;
|
||||
|
||||
import hw.net.JsonLoader;
|
||||
import promhx.Promise;
|
||||
import ru.m.data.IDataSource;
|
||||
|
||||
typedef PixabayImage = {
|
||||
var id:Int;
|
||||
var largeImageURL:String;
|
||||
var webformatURL:String;
|
||||
var previewURL:String;
|
||||
}
|
||||
|
||||
typedef PixabayResponse = {
|
||||
var total:Int;
|
||||
var totalHits:Int;
|
||||
var hits:Array<PixabayImage>;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
enum abstract PixabayImageType(String) from String to String {
|
||||
var ALL = "all";
|
||||
var PHOTO = "photo";
|
||||
var ILLUSTRATION = "illustration";
|
||||
var VECTOR = "vector";
|
||||
}
|
||||
|
||||
class PixabayApi implements IDataSource<Int, PixabayImage> {
|
||||
private var baseUrl:String = "https://pixabay.com/api/";
|
||||
private var key:String;
|
||||
|
||||
public function new(key:String) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
private function buildRequest(queryMap:Map<String, Dynamic>):String {
|
||||
queryMap.set("key", key);
|
||||
var query = [for (k in queryMap.keys()) '${k}=${queryMap.get(k)}'].join("&");
|
||||
return '${baseUrl}?${query}';
|
||||
}
|
||||
|
||||
public function getPage(page:Page):Promise<DataPage<PixabayImage>> {
|
||||
var category = PixabayCategory.NATURE;
|
||||
if (page.filter != null && page.filter.exists("type")) {
|
||||
category = page.filter.get("type");
|
||||
}
|
||||
return new JsonLoader<PixabayResponse>()
|
||||
.GET(buildRequest([
|
||||
"category" => category,
|
||||
"image_type" => PixabayImageType.PHOTO,
|
||||
"editors_choice" => true,
|
||||
"per_page" => page.count,
|
||||
"page" => page.index + 1,
|
||||
]))
|
||||
.then((response:PixabayResponse) -> ({
|
||||
page: page,
|
||||
total: response.totalHits,
|
||||
data: response.hits,
|
||||
}));
|
||||
}
|
||||
|
||||
public function get(id:Int):Promise<PixabayImage> {
|
||||
return new JsonLoader<PixabayResponse>()
|
||||
.GET(buildRequest(["id" => id]))
|
||||
.then((response:PixabayResponse) -> response.hits[0]);
|
||||
}
|
||||
}
|
||||
44
src/app/haxe/ru/m/puzzlez/FileUtil.hx
Normal file
44
src/app/haxe/ru/m/puzzlez/FileUtil.hx
Normal file
@@ -0,0 +1,44 @@
|
||||
package ru.m.puzzlez;
|
||||
|
||||
import flash.events.Event;
|
||||
import flash.events.IOErrorEvent;
|
||||
import flash.events.ProgressEvent;
|
||||
import haxe.io.Bytes;
|
||||
import promhx.Deferred;
|
||||
import promhx.Promise;
|
||||
|
||||
typedef FileContent = {
|
||||
var name:String;
|
||||
var content:Bytes;
|
||||
}
|
||||
|
||||
class FileUtil {
|
||||
|
||||
public static function browse():Promise<FileContent> {
|
||||
var d = new Deferred<FileContent>();
|
||||
var file = new ModernFileReference();
|
||||
file.addEventListener(Event.SELECT, (event:Event) -> {
|
||||
cast(event.target, ModernFileReference).load();
|
||||
});
|
||||
file.addEventListener(IOErrorEvent.IO_ERROR, (event:IOErrorEvent) -> {
|
||||
d.throwError(event);
|
||||
});
|
||||
file.addEventListener(ProgressEvent.PROGRESS, (event:ProgressEvent) -> {
|
||||
//trace('progress', '${event}');
|
||||
});
|
||||
file.addEventListener(Event.COMPLETE, (event:Event) -> {
|
||||
var f:ModernFileReference = cast event.target;
|
||||
d.resolve({
|
||||
name: f.name,
|
||||
content: Bytes.ofData(f.data),
|
||||
});
|
||||
});
|
||||
file.browse();
|
||||
return d.promise();
|
||||
}
|
||||
|
||||
public static function save(content:FileContent):Void {
|
||||
var file = new ModernFileReference();
|
||||
file.save(content.content.getData(), content.name);
|
||||
}
|
||||
}
|
||||
27
src/app/haxe/ru/m/puzzlez/ImageUtil.hx
Normal file
27
src/app/haxe/ru/m/puzzlez/ImageUtil.hx
Normal file
@@ -0,0 +1,27 @@
|
||||
package ru.m.puzzlez;
|
||||
|
||||
import flash.display.Bitmap;
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.Loader;
|
||||
import flash.display.LoaderInfo;
|
||||
import flash.events.Event;
|
||||
import haxe.io.Bytes;
|
||||
import openfl.events.IOErrorEvent;
|
||||
import promhx.Deferred;
|
||||
import promhx.Promise;
|
||||
|
||||
class ImageUtil {
|
||||
|
||||
public static function bytesToImage(bytes:Bytes):Promise<BitmapData> {
|
||||
var def = new Deferred();
|
||||
var loader = new Loader();
|
||||
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, (event:Event) -> {
|
||||
def.resolve(cast(cast(event.target, LoaderInfo).content, Bitmap).bitmapData);
|
||||
});
|
||||
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, (event:IOErrorEvent) -> {
|
||||
def.throwError(event);
|
||||
});
|
||||
loader.loadBytes(bytes);
|
||||
return def.promise();
|
||||
}
|
||||
}
|
||||
29
src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx
Normal file
29
src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx
Normal file
@@ -0,0 +1,29 @@
|
||||
package ru.m.puzzlez;
|
||||
|
||||
import hw.log.TraceLogger;
|
||||
import hw.app.App;
|
||||
import hw.app.Const;
|
||||
import ru.m.puzzlez.storage.GameStorage;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
import ru.m.puzzlez.storage.SettingsStorage;
|
||||
import ru.m.puzzlez.view.PuzzlezAppView;
|
||||
import ru.m.update.Updater;
|
||||
|
||||
class PuzzlezApp {
|
||||
|
||||
@:provide static var updater:Updater;
|
||||
|
||||
public static function main() {
|
||||
// ToDo: fix @:provide macro
|
||||
GameStorage;
|
||||
ImageStorage;
|
||||
SettingsStorage;
|
||||
L.push(new TraceLogger());
|
||||
updater = new Updater(Const.instance.VERSION, "https://shmyga.ru/repo/puzzlez/packages.json");
|
||||
var app = new App();
|
||||
app.theme = new PuzzlezTheme();
|
||||
app.icon = openfl.Assets.getBitmapData("resources/icon.png");
|
||||
app.view = new PuzzlezAppView();
|
||||
L.d("Puzzlez", "started");
|
||||
}
|
||||
}
|
||||
80
src/app/haxe/ru/m/puzzlez/PuzzlezTheme.hx
Normal file
80
src/app/haxe/ru/m/puzzlez/PuzzlezTheme.hx
Normal file
@@ -0,0 +1,80 @@
|
||||
package ru.m.puzzlez;
|
||||
|
||||
import hw.color.Color;
|
||||
import hw.view.geometry.Box;
|
||||
import hw.view.geometry.HAlign;
|
||||
import hw.view.geometry.SizeValue;
|
||||
import hw.view.geometry.VAlign;
|
||||
import hw.view.theme.Theme;
|
||||
import openfl.Assets;
|
||||
import ru.m.skin.ButtonSVGSkin;
|
||||
|
||||
class PuzzlezTheme extends Theme {
|
||||
|
||||
private static var ICONS:Map<String, String> = [
|
||||
"close" => "times-circle-solid.svg",
|
||||
"setting" => "cog-solid.svg",
|
||||
"image" => "image-polaroid.svg",
|
||||
"lock" => "lock-alt-solid.svg",
|
||||
"restore" => "window-restore-solid.svg",
|
||||
"compress" => "compress-solid.svg",
|
||||
"expand" => "expand-solid.svg",
|
||||
];
|
||||
|
||||
public function new() {
|
||||
super({embed: true}, {light: "gray"}, {base: Device.isMobile() ? 32 : 22});
|
||||
register(new Style("frame", [
|
||||
"geometry.padding" => Box.fromFloat(8),
|
||||
]));
|
||||
register(new Style("view", [
|
||||
"skin.background.color" => colors.light,
|
||||
"skin.border.color" => colors.border,
|
||||
"geometry.padding" => Box.fromFloat(3),
|
||||
"geometry.width" => SizeValue.fromInt(320),
|
||||
"geometry.height" => SizeValue.fromInt(240),
|
||||
]));
|
||||
register(new Style("text.error", [
|
||||
"font.color" => Color.fromString("red"),
|
||||
], "text"));
|
||||
|
||||
var size = Device.isMobile() ? 72 : 42;
|
||||
var smallSize = Device.isMobile() ? 64 : 32;
|
||||
register(new Style("icon", [
|
||||
"geometry.width" => SizeValue.fromInt(size),
|
||||
"geometry.height" => SizeValue.fromInt(size),
|
||||
"skin" => function() return new ButtonSVGSkin(),
|
||||
"skin.color" => colors.light,
|
||||
]));
|
||||
for (key in ICONS.keys()) {
|
||||
register(new Style('icon.${key}', [
|
||||
"skin.svg" => Assets.getText('resources/icon/${ICONS.get(key)}'),
|
||||
]));
|
||||
}
|
||||
register(new Style("icon.small", [
|
||||
"geometry.width" => SizeValue.fromInt(smallSize),
|
||||
"geometry.height" => SizeValue.fromInt(smallSize),
|
||||
]));
|
||||
register(new Style("icon.red", [
|
||||
"skin.color" => 0xcc0000,
|
||||
]));
|
||||
register(new Style("icon.orange", [
|
||||
"skin.color" => 0xcc5500,
|
||||
]));
|
||||
register(new Style("icon.control", [
|
||||
"geometry.hAlign" => HAlign.RIGHT,
|
||||
"geometry.vAlign" => VAlign.TOP,
|
||||
"geometry.margin" => Box.fromFloat(3),
|
||||
]));
|
||||
|
||||
register(new Style("button.red", [
|
||||
"skin.color" => 0xcc0000,
|
||||
], "button"));
|
||||
|
||||
register(new Style("label.header", [
|
||||
"font.size" => 36,
|
||||
"geometry.hAlign" => HAlign.CENTER,
|
||||
"geometry.margin.top" => 10,
|
||||
"geometry.margin.bottom" => 10,
|
||||
], "label"));
|
||||
}
|
||||
}
|
||||
10
src/app/haxe/ru/m/puzzlez/render/Background.hx
Normal file
10
src/app/haxe/ru/m/puzzlez/render/Background.hx
Normal file
@@ -0,0 +1,10 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import hw.color.Color;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
|
||||
enum Background {
|
||||
NONE;
|
||||
COLOR(color:Color);
|
||||
IMAGE(id:ImageId);
|
||||
}
|
||||
68
src/app/haxe/ru/m/puzzlez/render/CompleteView.hx
Normal file
68
src/app/haxe/ru/m/puzzlez/render/CompleteView.hx
Normal file
@@ -0,0 +1,68 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import flash.display.Shape;
|
||||
import flash.geom.Matrix;
|
||||
import ru.m.puzzlez.core.GamePreset;
|
||||
|
||||
class CompleteView extends Shape {
|
||||
|
||||
public var parts:Array<PartView>;
|
||||
public var preset:GamePreset;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
parts = [];
|
||||
}
|
||||
|
||||
public function addChild(part:PartView):Void {
|
||||
if (part.parent != null) {
|
||||
part.parent.removeChild(part);
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
public function redraw():Void {
|
||||
if (preset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var partWidth = preset.imageRect.width / preset.grid.width;
|
||||
var partHeight = preset.imageRect.height / preset.grid.height;
|
||||
|
||||
graphics.clear();
|
||||
graphics.lineStyle(2, 0xCCCCCC);
|
||||
graphics.beginFill(0x555555, 0.4);
|
||||
graphics.drawRect(0, 0, preset.imageRect.width, preset.imageRect.height);
|
||||
graphics.endFill();
|
||||
graphics.lineStyle();
|
||||
|
||||
for (partView in parts) {
|
||||
var part = partView.part;
|
||||
|
||||
if (partView.currentImage != null) {
|
||||
var image = partView.currentImage;
|
||||
var m = new Matrix();
|
||||
m.translate(partView.x, partView.y);
|
||||
graphics.beginBitmapFill(image, m, false, true);
|
||||
graphics.drawRect(m.tx, m.ty, image.width, image.height);
|
||||
graphics.endFill();
|
||||
}
|
||||
|
||||
var rect = part.rect.clone();
|
||||
rect.x = part.gridX * part.rect.width;
|
||||
rect.y = part.gridY * part.rect.height;
|
||||
var path = RenderUtil.builder.build(rect, part.bounds);
|
||||
for (value in RenderUtil.borderSettings) {
|
||||
graphics.lineStyle(1, value.color, value.opacity);
|
||||
path.move(value.offset.x, value.offset.y).draw(graphics);
|
||||
graphics.lineStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function clean():Void {
|
||||
preset = null;
|
||||
parts = [];
|
||||
graphics.clear();
|
||||
}
|
||||
}
|
||||
13
src/app/haxe/ru/m/puzzlez/render/IRender.hx
Normal file
13
src/app/haxe/ru/m/puzzlez/render/IRender.hx
Normal file
@@ -0,0 +1,13 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import hw.signal.Signal;
|
||||
import hw.view.IView;
|
||||
import ru.m.puzzlez.core.GameEvent;
|
||||
|
||||
interface IRender extends IView<Dynamic> {
|
||||
public var signal(default, null):Signal<GameEvent>;
|
||||
public var scale(get, set):Float;
|
||||
public var manager(default, null):RenderManager;
|
||||
|
||||
public function onGameEvent(event:GameEvent):Void;
|
||||
}
|
||||
40
src/app/haxe/ru/m/puzzlez/render/ImagePartBuilder.hx
Normal file
40
src/app/haxe/ru/m/puzzlez/render/ImagePartBuilder.hx
Normal file
@@ -0,0 +1,40 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import ru.m.puzzlez.render.RenderUtil;
|
||||
import flash.display.BitmapData;
|
||||
import haxe.Timer;
|
||||
import promhx.PublicStream;
|
||||
import promhx.Stream;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
|
||||
typedef Result = {
|
||||
var part:Part;
|
||||
var image:PartImage;
|
||||
}
|
||||
|
||||
class ImagePartBuilder {
|
||||
|
||||
private var image:BitmapData;
|
||||
|
||||
public function new(image:BitmapData) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
private function buildPart(index:Int, count:Int, parts:Array<Part>, stream:PublicStream<Result>):Void {
|
||||
for (i in index...index + count) {
|
||||
if (i >= parts.length) {
|
||||
return;
|
||||
}
|
||||
var part = parts[i];
|
||||
var image = RenderUtil.cropImagePart(image, part);
|
||||
stream.update({part: part, image: image});
|
||||
}
|
||||
Timer.delay(() -> buildPart(index + count, count, parts, stream), 0);
|
||||
}
|
||||
|
||||
public function build(parts:Array<Part>):Stream<Result> {
|
||||
var stream = new PublicStream<Result>();
|
||||
Timer.delay(() -> buildPart(0, 5, parts, stream), 0);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
102
src/app/haxe/ru/m/puzzlez/render/PartView.hx
Normal file
102
src/app/haxe/ru/m/puzzlez/render/PartView.hx
Normal file
@@ -0,0 +1,102 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.Bitmap;
|
||||
import flash.display.PixelSnapping;
|
||||
import flash.display.Sprite;
|
||||
import hw.geom.Point;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
import ru.m.puzzlez.render.RenderUtil;
|
||||
|
||||
class PartView extends Sprite {
|
||||
|
||||
public var id(default, null):Int;
|
||||
public var part(default, null):Part;
|
||||
public var position(default, set):Point;
|
||||
public var completed(default, default):Bool;
|
||||
public var target(default, null):Point;
|
||||
|
||||
private function set_position(value:Point):Point {
|
||||
position = value.clone();
|
||||
refresh();
|
||||
return position;
|
||||
}
|
||||
|
||||
public var image(default, set):PartImage;
|
||||
|
||||
private function set_image(value:PartImage):PartImage {
|
||||
image = value;
|
||||
redraw();
|
||||
refresh();
|
||||
return image;
|
||||
}
|
||||
|
||||
public var currentImage(get, never):BitmapData;
|
||||
|
||||
private function get_currentImage():BitmapData {
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
return completed ? image.borderedImage : image.shadedImage;
|
||||
}
|
||||
|
||||
public var size(default, null):Point;
|
||||
|
||||
public function new(part:Part) {
|
||||
super();
|
||||
this.id = part.id;
|
||||
this.part = part;
|
||||
this.size = part.rect.size.clone();
|
||||
this.target = new Point(part.gridX * size.x, part.gridY * size.y);
|
||||
}
|
||||
|
||||
private function redraw():Void {
|
||||
throw "Unimplemented";
|
||||
}
|
||||
|
||||
private function refresh():Void {
|
||||
if (position != null && image != null) {
|
||||
x = position.x - (image.borderedImage.width - size.x) / 2;
|
||||
y = position.y - (image.borderedImage.height - size.y) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete():Void {
|
||||
position = target;
|
||||
completed = true;
|
||||
refresh();
|
||||
redraw();
|
||||
}
|
||||
|
||||
public static function factory(part:Part):PartView {
|
||||
return new SpritePartView(part);
|
||||
}
|
||||
}
|
||||
|
||||
class SpritePartView extends PartView {
|
||||
|
||||
override private function redraw():Void {
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
var image = currentImage;
|
||||
graphics.clear();
|
||||
graphics.beginBitmapFill(image, null, false, true);
|
||||
graphics.drawRect(0, 0, image.width, image.height);
|
||||
graphics.endFill();
|
||||
}
|
||||
}
|
||||
|
||||
class BitmapPartView extends PartView {
|
||||
private var bitmap:Bitmap;
|
||||
|
||||
public function new(part) {
|
||||
super(part);
|
||||
bitmap = new Bitmap(null, PixelSnapping.AUTO, true);
|
||||
addChild(bitmap);
|
||||
}
|
||||
|
||||
override private function redraw():Void {
|
||||
bitmap.bitmapData = currentImage;
|
||||
}
|
||||
}
|
||||
21
src/app/haxe/ru/m/puzzlez/render/ProgressView.hx
Normal file
21
src/app/haxe/ru/m/puzzlez/render/ProgressView.hx
Normal file
@@ -0,0 +1,21 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import hw.view.form.LabelView;
|
||||
|
||||
class ProgressView extends LabelView {
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
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;
|
||||
}
|
||||
}
|
||||
230
src/app/haxe/ru/m/puzzlez/render/Render.hx
Normal file
230
src/app/haxe/ru/m/puzzlez/render/Render.hx
Normal file
@@ -0,0 +1,230 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.PNGEncoderOptions;
|
||||
import flash.display.Sprite;
|
||||
import flash.events.MouseEvent;
|
||||
import flash.geom.Point as FlashPoint;
|
||||
import flash.geom.Rectangle;
|
||||
import flash.net.FileReference;
|
||||
import flash.utils.ByteArray;
|
||||
import hw.geom.Point;
|
||||
import hw.signal.Signal;
|
||||
import hw.view.popup.AlertView;
|
||||
import hw.view.SpriteView;
|
||||
import ru.m.puzzlez.core.GameEvent;
|
||||
import ru.m.puzzlez.core.GameState;
|
||||
import ru.m.puzzlez.core.PartLocation;
|
||||
import ru.m.puzzlez.render.ImagePartBuilder;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
import ru.m.puzzlez.storage.SettingsStorage;
|
||||
|
||||
class Render extends SpriteView implements IRender {
|
||||
|
||||
public var signal(default, null):Signal<GameEvent>;
|
||||
public var scale(get, set):Float;
|
||||
public var manager(default, null):RenderManager;
|
||||
|
||||
@:provide static var settings:SettingsStorage;
|
||||
|
||||
private function get_scale():Float {
|
||||
return tableView.scaleX;
|
||||
}
|
||||
|
||||
private function set_scale(value:Float):Float {
|
||||
var result = tableView.scaleX = tableView.scaleY = value;
|
||||
tableView.x = (width - state.preset.tableRect.width * value) / 2;
|
||||
tableView.y = (height - state.preset.tableRect.height * value) / 2;
|
||||
//setSize(table.width, table.height, 'table');
|
||||
return result;
|
||||
}
|
||||
|
||||
private var state:GameState;
|
||||
private var image:BitmapData;
|
||||
|
||||
private var progress:ProgressView;
|
||||
private var container:Sprite;
|
||||
private var tableView:Sprite;
|
||||
private var imageView:CompleteView;
|
||||
private var parts:Map<Int, PartView>;
|
||||
private var activePart:PartView;
|
||||
private var activePoint:Point;
|
||||
|
||||
private var movePoint:FlashPoint;
|
||||
|
||||
@:provide var imageStorage:ImageStorage;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
container = new Sprite();
|
||||
content.addChild(container);
|
||||
manager = new RenderManager(content, container);
|
||||
progress = new ProgressView();
|
||||
signal = new Signal();
|
||||
tableView = new Sprite();
|
||||
tableView.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
|
||||
imageView = new CompleteView();
|
||||
tableView.addChild(imageView);
|
||||
container.addChild(tableView);
|
||||
parts = new Map();
|
||||
}
|
||||
|
||||
public function onGameEvent(event:GameEvent):Void {
|
||||
switch event {
|
||||
case START(state) | RESUME(state):
|
||||
onStart(state);
|
||||
case CHANGE(PART_UPDATE(id, TABLE(point))):
|
||||
var part:PartView = parts[id];
|
||||
part.position = point;
|
||||
case CHANGE(PART_UPDATE(id, IMAGE)):
|
||||
var part:PartView = parts[id];
|
||||
part.complete();
|
||||
imageView.addChild(part);
|
||||
imageView.redraw();
|
||||
case COMPLETE:
|
||||
AlertView.alert("Complete!");
|
||||
case _:
|
||||
}
|
||||
}
|
||||
|
||||
private function onStart(state:GameState):Void {
|
||||
clean();
|
||||
this.state = state;
|
||||
for (part in state.parts) {
|
||||
var partView = PartView.factory(part);
|
||||
parts.set(part.id, partView);
|
||||
switch part.location {
|
||||
case TABLE(point):
|
||||
partView.position = point;
|
||||
tableView.addChild(partView);
|
||||
case IMAGE:
|
||||
partView.complete();
|
||||
imageView.addChild(partView);
|
||||
case _:
|
||||
}
|
||||
}
|
||||
|
||||
imageView.x = state.preset.imageRect.x;
|
||||
imageView.y = state.preset.imageRect.y;
|
||||
imageView.preset = state.preset;
|
||||
imageView.redraw();
|
||||
progress.text = "Loading image";
|
||||
content.addChild(progress.content);
|
||||
imageStorage.resolve(state.preset.imageId).then(onImageResolved);
|
||||
toUpdate();
|
||||
}
|
||||
|
||||
private function onImageResolved(image:BitmapData):Void {
|
||||
this.image = RenderUtil.cropImage(image, state.preset.imageRect);
|
||||
var builder = new ImagePartBuilder(this.image);
|
||||
var i = 0;
|
||||
builder.build(state.parts).then((result:Result) -> {
|
||||
parts[result.part.id].image = result.image;
|
||||
progress.setProgress(++i, state.parts.length);
|
||||
if (i >= state.parts.length - 1) {
|
||||
if (progress.content.parent != null) {
|
||||
progress.content.parent.removeChild(progress.content);
|
||||
}
|
||||
imageView.redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override public function update():Void {
|
||||
super.update();
|
||||
progress.x = (width - progress.width) / 2;
|
||||
progress.y = (height - progress.height) / 2;
|
||||
progress.update();
|
||||
if (state != null) {
|
||||
scale = Math.min(width / state.preset.tableRect.width, height / state.preset.tableRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
override public function redraw():Void {
|
||||
switch settings.background {
|
||||
case Background.NONE:
|
||||
super.redraw();
|
||||
case Background.COLOR(color):
|
||||
content.graphics.clear();
|
||||
content.graphics.beginFill(color);
|
||||
content.graphics.drawRect(0, 0, width, height);
|
||||
content.graphics.endFill();
|
||||
case Background.IMAGE(id):
|
||||
imageStorage.resolve(id).then(result -> {
|
||||
content.graphics.clear();
|
||||
content.graphics.beginBitmapFill(result);
|
||||
content.graphics.drawRect(0, 0, width, height);
|
||||
content.graphics.endFill();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function onMouseDown(event:MouseEvent):Void {
|
||||
var point:FlashPoint = new FlashPoint(event.stageX, event.stageY);
|
||||
var objects = tableView.getObjectsUnderPoint(point);
|
||||
objects.reverse();
|
||||
var pointPart:PartView = null;
|
||||
for (object in objects) {
|
||||
if (Std.is(object, PartView)) {
|
||||
var part:PartView = cast object;
|
||||
var partPoint = part.globalToLocal(point);
|
||||
var color = part.image.shadedImage.getPixel(Std.int(partPoint.x), Std.int(partPoint.y));
|
||||
if (color > 0) {
|
||||
pointPart = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pointPart != null) {
|
||||
event.stopImmediatePropagation();
|
||||
if (event.ctrlKey) {
|
||||
save(pointPart);
|
||||
return;
|
||||
}
|
||||
if (pointPart.completed) {
|
||||
return;
|
||||
}
|
||||
activePart = pointPart;
|
||||
tableView.setChildIndex(activePart, tableView.numChildren - 1);
|
||||
activePoint = tableView.globalToLocal(point);
|
||||
tableView.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
|
||||
tableView.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
|
||||
signal.emit(ACTION(PART_TAKE(activePart.id)));
|
||||
}
|
||||
}
|
||||
|
||||
private function onMouseMove(event:MouseEvent):Void {
|
||||
var newPoint:Point = tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY));
|
||||
var partPosition = activePart.position.add(newPoint).subtract(activePoint);
|
||||
signal.emit(ACTION(PART_MOVE(activePart.id, partPosition.clone())));
|
||||
activePoint = newPoint;
|
||||
}
|
||||
|
||||
private function onMouseUp(event:MouseEvent):Void {
|
||||
var newPoint:Point = tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY));
|
||||
var partPosition = activePart.position.add(newPoint).subtract(activePoint);
|
||||
signal.emit(ACTION(PART_PUT(activePart.id, partPosition.clone())));
|
||||
tableView.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
|
||||
tableView.stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
|
||||
activePart = null;
|
||||
activePoint = null;
|
||||
}
|
||||
|
||||
private function save(part:PartView):Void {
|
||||
var file = new FileReference();
|
||||
var image = part.image.shadedImage;
|
||||
var data = new ByteArray();
|
||||
image.encode(new Rectangle(0, 0, image.width, image.height), new PNGEncoderOptions(), data);
|
||||
file.save(data, "icon.png");
|
||||
}
|
||||
|
||||
public function clean() {
|
||||
for (partView in parts) {
|
||||
if (partView.parent != null) {
|
||||
partView.parent.removeChild(partView);
|
||||
}
|
||||
}
|
||||
parts = new Map();
|
||||
imageView.clean();
|
||||
}
|
||||
}
|
||||
68
src/app/haxe/ru/m/puzzlez/render/RenderManager.hx
Normal file
68
src/app/haxe/ru/m/puzzlez/render/RenderManager.hx
Normal file
@@ -0,0 +1,68 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import ru.m.event.ZoomGestureEvent;
|
||||
import ru.m.event.GestureManager;
|
||||
import flash.display.DisplayObject;
|
||||
import flash.events.MouseEvent;
|
||||
import flash.geom.Point;
|
||||
|
||||
class RenderManager {
|
||||
|
||||
public var locked(default, default):Bool;
|
||||
|
||||
private var content:DisplayObject;
|
||||
private var container:DisplayObject;
|
||||
private var movePoint:Point;
|
||||
private var gesture:GestureManager;
|
||||
|
||||
public function new(content:DisplayObject, container:DisplayObject) {
|
||||
this.content = content;
|
||||
this.container = container;
|
||||
content.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
|
||||
content.addEventListener(ZoomGestureEvent.GESTURE_ZOOM, onGestureZoom);
|
||||
gesture = new GestureManager(content);
|
||||
}
|
||||
|
||||
private function onGestureZoom(event:ZoomGestureEvent):Void {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
container.scaleX = container.scaleY += event.zoom;
|
||||
}
|
||||
|
||||
private function onMouseDown(event:MouseEvent):Void {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
movePoint = new Point(event.stageX, event.stageY);
|
||||
content.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
|
||||
content.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
|
||||
}
|
||||
|
||||
private function onMouseMove(event:MouseEvent):Void {
|
||||
var newPoint = new Point(event.stageX, event.stageY);
|
||||
var diff = newPoint.subtract(movePoint);
|
||||
container.x += diff.x;
|
||||
container.y += diff.y;
|
||||
movePoint = newPoint;
|
||||
}
|
||||
|
||||
private function onMouseUp(event:MouseEvent):Void {
|
||||
content.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
|
||||
content.stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
|
||||
movePoint = null;
|
||||
}
|
||||
|
||||
public function reset():Void {
|
||||
container.x = 0;
|
||||
container.y = 0;
|
||||
}
|
||||
|
||||
public function dispose():Void {
|
||||
if (gesture != null) {
|
||||
gesture.dispose();
|
||||
gesture = null;
|
||||
}
|
||||
content.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
|
||||
}
|
||||
}
|
||||
119
src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx
Normal file
119
src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx
Normal file
@@ -0,0 +1,119 @@
|
||||
package ru.m.puzzlez.render;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.Shape;
|
||||
import flash.geom.Matrix;
|
||||
import hw.color.Color;
|
||||
import hw.geom.Point;
|
||||
import hw.geom.Rectangle;
|
||||
import ru.m.draw.DrawPath;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
import ru.m.puzzlez.render.part.ClassicPartBuilder;
|
||||
import ru.m.puzzlez.render.part.IPartBuilder;
|
||||
import ru.m.puzzlez.render.part.PartMask;
|
||||
|
||||
typedef DrawSetting = {
|
||||
var offset:Point;
|
||||
var color:Color;
|
||||
var opacity:Float;
|
||||
}
|
||||
|
||||
typedef PartImage = {
|
||||
var borderedImage:BitmapData;
|
||||
var shadedImage:BitmapData;
|
||||
}
|
||||
|
||||
class RenderUtil {
|
||||
public static var shadowSettings(default, null):Array<DrawSetting> = [
|
||||
{offset: new Point(-2, -2), color: 0x000000, opacity: 0.4},
|
||||
{offset: new Point(2, 2), color: 0xffffff, opacity: 0.4},
|
||||
];
|
||||
|
||||
public static var borderSettings(default, null):Array<DrawSetting> = [
|
||||
{offset: new Point(-1, -1), color: 0x555555, opacity: 0.4},
|
||||
{offset: new Point(1, 1), color: 0xcccccc, opacity: 0.4},
|
||||
];
|
||||
|
||||
public static var builder(default, null):IPartBuilder = new ClassicPartBuilder();
|
||||
|
||||
public static function cropImage(source:BitmapData, rect:Rectangle):BitmapData {
|
||||
var image = new BitmapData(Std.int(rect.width), Std.int(rect.height));
|
||||
var matrix = new Matrix();
|
||||
var scale = Math.max(rect.width / source.width, rect.height / source.height);
|
||||
matrix.scale(scale, scale);
|
||||
matrix.translate((rect.width - source.width * scale) / 2, (rect.height - source.height * scale) / 2);
|
||||
image.draw(source, matrix);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static function buildPartGeometry(part:Part):{rect:Rectangle, drawRect:Rectangle} {
|
||||
var source = new Rectangle(part.rect.width * part.gridX, part.rect.height * part.gridY, part.rect.width, part.rect.height);
|
||||
var rect = source.clone();
|
||||
var offset = rect.width / 4 + rect.width * 0.05;
|
||||
rect.x -= offset;
|
||||
rect.y -= offset;
|
||||
rect.width += offset * 2;
|
||||
rect.height += offset * 2;
|
||||
var drawRect = source.clone();
|
||||
drawRect.x = offset;
|
||||
drawRect.y = offset ;
|
||||
return {rect:rect, drawRect:drawRect};
|
||||
}
|
||||
|
||||
private static function drawShadow(source:BitmapData, path:DrawPath, values:Array<DrawSetting>):BitmapData {
|
||||
var canvas = new Shape();
|
||||
canvas.cacheAsBitmap = true;
|
||||
for (value in values) {
|
||||
canvas.graphics.beginFill(value.color, value.opacity);
|
||||
path.move(value.offset.x, value.offset.y).draw(canvas.graphics);
|
||||
canvas.graphics.endFill();
|
||||
}
|
||||
canvas.graphics.beginBitmapFill(source, null, false, true);
|
||||
canvas.graphics.drawRect(0, 0, source.width, source.height);
|
||||
canvas.graphics.endFill();
|
||||
var image = new BitmapData(source.width, source.height, true, 0x00000000);
|
||||
image.draw(canvas, null, null, null, null, true);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static function drawBorder(source:BitmapData, path:DrawPath, values:Array<DrawSetting>):BitmapData {
|
||||
var canvas = new Shape();
|
||||
canvas.cacheAsBitmap = true;
|
||||
for (value in values) {
|
||||
canvas.graphics.lineStyle(2, value.color, value.opacity);
|
||||
path.move(value.offset.x, value.offset.y).draw(canvas.graphics);
|
||||
canvas.graphics.lineStyle();
|
||||
}
|
||||
canvas.graphics.beginBitmapFill(source, null, false, true);
|
||||
canvas.graphics.drawRect(0, 0, source.width, source.height);
|
||||
canvas.graphics.endFill();
|
||||
var image = new BitmapData(source.width, source.height, true, 0x00000000);
|
||||
image.draw(canvas, null, null, null, null, true);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static function cropImagePart(source:BitmapData, part:Part, completed:Bool = false):PartImage {
|
||||
var geometry = buildPartGeometry(part);
|
||||
var path = builder.build(geometry.drawRect, part.bounds);
|
||||
var canvas:Shape = new Shape();
|
||||
canvas.cacheAsBitmap = true;
|
||||
canvas.mask = new PartMask(path);
|
||||
var matrix = new Matrix();
|
||||
matrix.translate(-geometry.rect.x, -geometry.rect.y);
|
||||
canvas.graphics.beginBitmapFill(source, matrix, false, true);
|
||||
canvas.graphics.drawRect(0, 0, geometry.rect.width, geometry.rect.height);
|
||||
canvas.graphics.endFill();
|
||||
var image = new BitmapData(Std.int(geometry.rect.width), Std.int(geometry.rect.height), true, 0x00000000);
|
||||
image.draw(canvas, null, null, null, null, true);
|
||||
var shadedImage = drawShadow(image, path, shadowSettings);
|
||||
//var borderedImage = drawBorder(image, path, borderSettings);
|
||||
return {borderedImage: image, shadedImage: shadedImage};
|
||||
}
|
||||
|
||||
public static function containRectangle(source:Rectangle, target:Rectangle):Rectangle {
|
||||
var s = Math.min(1, Math.min(target.width / source.width, target.height / source.height));
|
||||
var width = source.width * s;
|
||||
var height = source.height * s;
|
||||
return new Rectangle((target.width - width) / 2, (target.height - height) / 2, width, height);
|
||||
}
|
||||
}
|
||||
33
src/app/haxe/ru/m/puzzlez/render/part/BasePartBuilder.hx
Normal file
33
src/app/haxe/ru/m/puzzlez/render/part/BasePartBuilder.hx
Normal file
@@ -0,0 +1,33 @@
|
||||
package ru.m.puzzlez.render.part;
|
||||
|
||||
import hw.geom.Rectangle;
|
||||
import ru.m.draw.DrawPath;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
import ru.m.puzzlez.render.part.IPartBuilder;
|
||||
|
||||
class BasePartBuilder implements IPartBuilder {
|
||||
|
||||
public function new() {
|
||||
}
|
||||
|
||||
private function createAngle(path:DrawPath, x:Float, y:Float, first: Bool = false):Void {
|
||||
path.commands.push(first ? MOVE_TO(x, y) : LINE_TO(x, y));
|
||||
}
|
||||
|
||||
private function createBound(path:DrawPath, fromX:Float, fromY:Float, toX:Float, toY:Float, bound:PartBound):Void {
|
||||
}
|
||||
|
||||
public function build(rect:Rectangle, bounds:PartBounds):DrawPath {
|
||||
var path = new DrawPath();
|
||||
createAngle(path, rect.left, rect.top, true);
|
||||
createBound(path, rect.left, rect.top, rect.right, rect.top, bounds.top);
|
||||
createAngle(path, rect.right, rect.top);
|
||||
createBound(path, rect.right, rect.top, rect.right, rect.bottom, bounds.right);
|
||||
createAngle(path, rect.right, rect.bottom);
|
||||
createBound(path, rect.right, rect.bottom, rect.left, rect.bottom, bounds.bottom);
|
||||
createAngle(path, rect.left, rect.bottom);
|
||||
createBound(path, rect.left, rect.bottom, rect.left, rect.top, bounds.left);
|
||||
createAngle(path, rect.left, rect.top);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
46
src/app/haxe/ru/m/puzzlez/render/part/ClassicPartBuilder.hx
Normal file
46
src/app/haxe/ru/m/puzzlez/render/part/ClassicPartBuilder.hx
Normal file
@@ -0,0 +1,46 @@
|
||||
package ru.m.puzzlez.render.part;
|
||||
|
||||
import ru.m.draw.DrawPath;
|
||||
import ru.m.puzzlez.core.BoundType;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
|
||||
class ClassicPartBuilder extends BasePartBuilder {
|
||||
|
||||
override private function createBound(path:DrawPath, fromX:Float, fromY:Float, toX:Float, toY:Float, bound:PartBound):Void {
|
||||
var dx = toX == fromX ? 0 : (toX - fromX) / Math.abs(toX - fromX);
|
||||
var dy = toY == fromY ? 0 : (toY - fromY) / Math.abs(toY - fromY);
|
||||
var boundLength = Math.max(Math.abs(toX - fromX), Math.abs(toY - fromY));
|
||||
var spikeWidth = boundLength * 0.2;
|
||||
var spikeOffset = (boundLength - spikeWidth) / 2;
|
||||
var spikeDepth = switch bound.spike {
|
||||
case NONE: 0;
|
||||
case IN: spikeWidth;
|
||||
case OUT: -spikeWidth;
|
||||
};
|
||||
var spikeSideOffset = switch bound.side {
|
||||
case NONE: 0;
|
||||
case IN: boundLength * -0.04;
|
||||
case OUT: boundLength * 0.04;
|
||||
}
|
||||
switch bound.spike {
|
||||
case NONE:
|
||||
case IN | OUT:
|
||||
path.commands.push(LINE_TO(
|
||||
fromX + spikeOffset * dx + spikeSideOffset * dy,
|
||||
fromY + spikeOffset * dy + spikeSideOffset * dx
|
||||
));
|
||||
path.commands.push(CURVE_TO(
|
||||
fromX + (spikeOffset * 0.7) * dx - spikeDepth * dy + spikeSideOffset * dy,
|
||||
fromY + spikeDepth * dx + (spikeOffset * 0.7) * dy + spikeSideOffset * dx,
|
||||
fromX + (spikeOffset + spikeWidth / 2) * dx - (spikeDepth * 1.1) * dy + spikeSideOffset * dy,
|
||||
fromY + (spikeDepth * 1.1) * dx + (spikeOffset + spikeWidth / 2) * dy + spikeSideOffset * dx
|
||||
));
|
||||
path.commands.push(CURVE_TO(
|
||||
fromX + (spikeOffset + spikeWidth + spikeOffset * 0.3) * dx - spikeDepth * dy + spikeSideOffset * dy,
|
||||
fromY + spikeDepth * dx + (spikeOffset + spikeWidth + spikeOffset * 0.3) * dy + spikeSideOffset * dx,
|
||||
fromX + (spikeOffset + spikeWidth) * dx + spikeSideOffset * dy,
|
||||
fromY + (spikeOffset + spikeWidth) * dy + spikeSideOffset * dx
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/app/haxe/ru/m/puzzlez/render/part/IPartBuilder.hx
Normal file
9
src/app/haxe/ru/m/puzzlez/render/part/IPartBuilder.hx
Normal file
@@ -0,0 +1,9 @@
|
||||
package ru.m.puzzlez.render.part;
|
||||
|
||||
import hw.geom.Rectangle;
|
||||
import ru.m.draw.DrawPath;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
|
||||
interface IPartBuilder {
|
||||
public function build(rect:Rectangle, bound:PartBounds):DrawPath;
|
||||
}
|
||||
14
src/app/haxe/ru/m/puzzlez/render/part/PartMask.hx
Normal file
14
src/app/haxe/ru/m/puzzlez/render/part/PartMask.hx
Normal file
@@ -0,0 +1,14 @@
|
||||
package ru.m.puzzlez.render.part;
|
||||
|
||||
import flash.display.Shape;
|
||||
import ru.m.draw.DrawPath;
|
||||
|
||||
class PartMask extends Shape {
|
||||
|
||||
public function new(path:DrawPath) {
|
||||
super();
|
||||
graphics.beginFill(0xff00ff, 1);
|
||||
path.draw(graphics);
|
||||
graphics.endFill();
|
||||
}
|
||||
}
|
||||
41
src/app/haxe/ru/m/puzzlez/render/part/SquarePartBuilder.hx
Normal file
41
src/app/haxe/ru/m/puzzlez/render/part/SquarePartBuilder.hx
Normal file
@@ -0,0 +1,41 @@
|
||||
package ru.m.puzzlez.render.part;
|
||||
|
||||
import ru.m.draw.DrawPath;
|
||||
import ru.m.puzzlez.core.BoundType;
|
||||
import ru.m.puzzlez.core.Part;
|
||||
|
||||
class SquarePartBuilder extends BasePartBuilder {
|
||||
|
||||
override private function createBound(path:DrawPath, fromX:Float, fromY:Float, toX:Float, toY:Float, bound:PartBound):Void {
|
||||
var dx = toX == fromX ? 0 : (toX - fromX) / Math.abs(toX - fromX);
|
||||
var dy = toY == fromY ? 0 : (toY - fromY) / Math.abs(toY - fromY);
|
||||
var boundLength = Math.max(Math.abs(toX - fromX), Math.abs(toY - fromY));
|
||||
var spikeWidth = boundLength * 0.2;
|
||||
var spikeOffset = (boundLength - spikeWidth) / 2;
|
||||
var spikeDepth = switch bound.spike {
|
||||
case NONE: 0;
|
||||
case IN: spikeWidth;
|
||||
case OUT: -spikeWidth;
|
||||
}
|
||||
switch bound.spike {
|
||||
case NONE:
|
||||
case IN | OUT:
|
||||
path.commands.push(LINE_TO(
|
||||
fromX + spikeOffset * dx,
|
||||
fromY + spikeOffset * dy
|
||||
))
|
||||
path.commands.push(LINE_TO(
|
||||
fromX + spikeOffset * dx - spikeDepth * dy,
|
||||
fromY + spikeDepth * dx + spikeOffset * dy
|
||||
));
|
||||
path.commands.push(LINE_TO(
|
||||
fromX + (spikeOffset + spikeWidth) * dx - spikeDepth * dy,
|
||||
fromY + spikeDepth * dx + (spikeOffset + spikeWidth) * dy
|
||||
));
|
||||
path.commands.push(LINE_TO(
|
||||
fromX + (spikeOffset + spikeWidth) * dx,
|
||||
fromY + (spikeOffset + spikeWidth) * dy
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/app/haxe/ru/m/puzzlez/source/AssetSource.hx
Normal file
39
src/app/haxe/ru/m/puzzlez/source/AssetSource.hx
Normal file
@@ -0,0 +1,39 @@
|
||||
package ru.m.puzzlez.source;
|
||||
|
||||
import ru.m.data.IDataSource;
|
||||
import flash.display.BitmapData;
|
||||
import openfl.Assets;
|
||||
import openfl.utils.AssetType;
|
||||
import promhx.Promise;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
|
||||
class AssetSource implements IImageSource {
|
||||
public static var ID:SourceId = "asset";
|
||||
|
||||
public var id(default, never):SourceId = ID;
|
||||
|
||||
private var data:Array<ImageId>;
|
||||
|
||||
public function new() {
|
||||
}
|
||||
|
||||
private function resolveData():Array<ImageId> {
|
||||
return [for (name in Assets.list(AssetType.IMAGE).filter((name:String) -> name.substr(0, 15) == "resources/image")) new ImageId(id, name)];
|
||||
}
|
||||
|
||||
public function getIndexPage(page:Page):Promise<DataPage<ImageId>> {
|
||||
if (data == null) {
|
||||
data = resolveData();
|
||||
}
|
||||
// ToDo: pagination
|
||||
return Promise.promise({
|
||||
page: page,
|
||||
total: data.length,
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
public function loadImage(id:ImageId, preview:Bool = false):Promise<BitmapData> {
|
||||
return Promise.promise(Assets.getBitmapData(id.id));
|
||||
}
|
||||
}
|
||||
40
src/app/haxe/ru/m/puzzlez/source/FileSource.hx
Normal file
40
src/app/haxe/ru/m/puzzlez/source/FileSource.hx
Normal file
@@ -0,0 +1,40 @@
|
||||
package ru.m.puzzlez.source;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import haxe.io.Bytes;
|
||||
import promhx.Promise;
|
||||
import ru.m.data.IDataSource;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.storage.FileStorage;
|
||||
|
||||
class FileSource implements IImageSource {
|
||||
public static var ID:SourceId = "file";
|
||||
|
||||
public var id(default, never):SourceId = ID;
|
||||
|
||||
private var storage:FileStorage;
|
||||
|
||||
public function new() {
|
||||
storage = new FileStorage();
|
||||
}
|
||||
|
||||
public function getIndexPage(page:Page):Promise<DataPage<ImageId>> {
|
||||
return storage.getIndexPage(page).then(data -> ({page: data.page, total: data.total, data: data.data.map(item -> new ImageId(id, item))}));
|
||||
}
|
||||
|
||||
public function loadImage(id:ImageId, preview:Bool = false):Promise<BitmapData> {
|
||||
return storage.get(id.id).pipe(ImageUtil.bytesToImage);
|
||||
}
|
||||
|
||||
public function append(data:Bytes):Promise<Dynamic> {
|
||||
return storage.save(data);
|
||||
}
|
||||
|
||||
public function remove(id:ImageId):Promise<Dynamic> {
|
||||
return storage.delete(id.id);
|
||||
}
|
||||
|
||||
public function clean():Void {
|
||||
|
||||
}
|
||||
}
|
||||
11
src/app/haxe/ru/m/puzzlez/source/IImageSource.hx
Normal file
11
src/app/haxe/ru/m/puzzlez/source/IImageSource.hx
Normal file
@@ -0,0 +1,11 @@
|
||||
package ru.m.puzzlez.source;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import promhx.Promise;
|
||||
import ru.m.data.IDataSource;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
|
||||
interface IImageSource extends IDataIndex<ImageId> {
|
||||
public var id(default, never):SourceId;
|
||||
public function loadImage(id:ImageId, preview:Bool = false):Promise<BitmapData>;
|
||||
}
|
||||
53
src/app/haxe/ru/m/puzzlez/source/PixabaySource.hx
Normal file
53
src/app/haxe/ru/m/puzzlez/source/PixabaySource.hx
Normal file
@@ -0,0 +1,53 @@
|
||||
package ru.m.puzzlez.source;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.utils.ByteArray;
|
||||
import haxe.io.Bytes;
|
||||
import hw.net.BytesLoader;
|
||||
import promhx.Promise;
|
||||
import ru.m.data.IDataSource;
|
||||
import ru.m.pixabay.PixabayApi;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.core.ImageListSource;
|
||||
import ru.m.puzzlez.storage.CacheStorage;
|
||||
|
||||
class PixabaySource implements IImageSource {
|
||||
public static var ID:SourceId = "pixabay";
|
||||
|
||||
public var id(default, never):SourceId = ID;
|
||||
|
||||
private var api:PixabayApi;
|
||||
|
||||
@:provide static var imageCache:CacheStorage;
|
||||
|
||||
public function new() {
|
||||
api = new PixabayApi("14915210-5eae157281211e0ad28bc8def");
|
||||
}
|
||||
|
||||
public function getIndexPage(page:Page):Promise<DataPage<ImageId>> {
|
||||
return api.getPage(page).then(data -> ({page: data.page, total: data.total, data: data.data.map(item -> new ImageId(id, Std.string(item.id)))}));
|
||||
}
|
||||
|
||||
public function loadImage(id:ImageId, preview:Bool = false):Promise<BitmapData> {
|
||||
var key = '${id}_${preview}';
|
||||
if (imageCache.exists(key)) {
|
||||
return ImageUtil.bytesToImage(imageCache.read(key));
|
||||
} else {
|
||||
return api.get(Std.parseInt(id.id))
|
||||
.pipe((data:PixabayImage) -> new BytesLoader().GET(preview ? data.webformatURL : data.largeImageURL))
|
||||
.pipe((data:ByteArray) -> {
|
||||
var bytes = Bytes.ofData(data);
|
||||
imageCache.write(key, bytes);
|
||||
return ImageUtil.bytesToImage(bytes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function categorySource(category:PixabayCategory):ImageListSource {
|
||||
return {
|
||||
title: category,
|
||||
source: this,
|
||||
filter: ["type" => category],
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/app/haxe/ru/m/puzzlez/storage/CacheStorage.hx
Normal file
66
src/app/haxe/ru/m/puzzlez/storage/CacheStorage.hx
Normal file
@@ -0,0 +1,66 @@
|
||||
package ru.m.puzzlez.storage;
|
||||
|
||||
import flash.net.SharedObject;
|
||||
import haxe.io.Bytes;
|
||||
|
||||
#if html5
|
||||
@:provide class CacheStorage {
|
||||
public function new() {
|
||||
}
|
||||
|
||||
public function exists(key:String):Bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function read(key:String):Null<Bytes> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function write(key:String, data:Bytes):Void {
|
||||
}
|
||||
|
||||
public function remove(key:String):Void {
|
||||
}
|
||||
|
||||
public function clean():Void {
|
||||
}
|
||||
}
|
||||
#else
|
||||
@:provide class CacheStorage {
|
||||
|
||||
public function new() {
|
||||
}
|
||||
|
||||
private inline function getSharedObject(key:String):SharedObject {
|
||||
key = StringTools.replace(key, "/", "_");
|
||||
key = StringTools.replace(key, ":", "_");
|
||||
key = StringTools.replace(key, ".", "_");
|
||||
return SharedObject.getLocal('cache/${key}');
|
||||
}
|
||||
|
||||
public function exists(key:String):Bool {
|
||||
var so = getSharedObject(key);
|
||||
return so.size > 0 && Reflect.hasField(so.data, "data");
|
||||
}
|
||||
|
||||
public function read(key:String):Null<Bytes> {
|
||||
var so = getSharedObject(key);
|
||||
return Bytes.ofData(Reflect.field(so.data, "data"));
|
||||
}
|
||||
|
||||
public function write(key:String, data:Bytes):Void {
|
||||
var so = getSharedObject(key);
|
||||
so.setProperty("data", data.getData());
|
||||
so.flush();
|
||||
}
|
||||
|
||||
public function remove(key:String):Void {
|
||||
var so = getSharedObject(key);
|
||||
so.clear();
|
||||
}
|
||||
|
||||
public function clean():Void {
|
||||
//SharedObject.deleteAll("cache/");
|
||||
}
|
||||
}
|
||||
#end
|
||||
19
src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx
Normal file
19
src/app/haxe/ru/m/puzzlez/storage/FileStorage.hx
Normal file
@@ -0,0 +1,19 @@
|
||||
package ru.m.puzzlez.storage;
|
||||
|
||||
import haxe.crypto.Md5;
|
||||
import haxe.io.Bytes;
|
||||
import ru.m.data.DataStorage;
|
||||
|
||||
class FileStorage extends DataStorage<String, Bytes> {
|
||||
inline private static var NAME = "file";
|
||||
inline private static var VERSION = 1;
|
||||
|
||||
public function new() {
|
||||
super(
|
||||
'${NAME}/${VERSION}',
|
||||
new MetaBuilder<Bytes>(item -> ["id" => Md5.make(item).toHex(), "date" => Date.now().toString()]),
|
||||
new EmptyConverter<String>(),
|
||||
new Converter<Bytes, String>(item -> item.toHex(), data -> Bytes.ofHex(data))
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/app/haxe/ru/m/puzzlez/storage/GameStorage.hx
Normal file
27
src/app/haxe/ru/m/puzzlez/storage/GameStorage.hx
Normal file
@@ -0,0 +1,27 @@
|
||||
package ru.m.puzzlez.storage;
|
||||
|
||||
import ru.m.data.DataStorage;
|
||||
import ru.m.puzzlez.core.GameState;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.core.ImageListSource;
|
||||
|
||||
@:provide class GameStorage extends DataStorage<ImageId, GameState> {
|
||||
inline private static var NAME = "game";
|
||||
inline private static var VERSION = 4;
|
||||
|
||||
public function new() {
|
||||
super(
|
||||
'${NAME}/${VERSION}',
|
||||
new MetaBuilder<GameState>(item -> ["id" => item.preset.imageId, "status" => item.status, "date" => Date.now().toString()]),
|
||||
new Converter<ImageId, String>(id -> id.toString(), data -> ImageId.fromString(data))
|
||||
);
|
||||
}
|
||||
|
||||
public function statusSource(status:GameStatus):ImageListSource {
|
||||
return {
|
||||
title: status,
|
||||
source: this,
|
||||
filter: ["status" => status],
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/app/haxe/ru/m/puzzlez/storage/ImageStorage.hx
Normal file
51
src/app/haxe/ru/m/puzzlez/storage/ImageStorage.hx
Normal file
@@ -0,0 +1,51 @@
|
||||
package ru.m.puzzlez.storage;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.net.SharedObject;
|
||||
import promhx.Promise;
|
||||
import Reflect;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.source.AssetSource;
|
||||
import ru.m.puzzlez.source.FileSource;
|
||||
import ru.m.puzzlez.source.IImageSource;
|
||||
import ru.m.puzzlez.source.PixabaySource;
|
||||
|
||||
@:provide class ImageStorage {
|
||||
|
||||
public var sources(default, null):Map<SourceId, IImageSource>;
|
||||
|
||||
private var cache:Map<String, Promise<BitmapData>>;
|
||||
|
||||
public function new() {
|
||||
sources = new Map();
|
||||
cache = new Map();
|
||||
register(new AssetSource());
|
||||
register(new FileSource());
|
||||
register(new PixabaySource());
|
||||
}
|
||||
|
||||
public function register(source:IImageSource):Void {
|
||||
sources.set(source.id, source);
|
||||
}
|
||||
|
||||
public function read(id:ImageId):BitmapData {
|
||||
var so = SharedObject.getLocal(id);
|
||||
return Reflect.field(so.data, "");
|
||||
}
|
||||
|
||||
public function write(id:ImageId, data:BitmapData):Void {
|
||||
var so = SharedObject.getLocal(id);
|
||||
so.setProperty("", data);
|
||||
so.flush();
|
||||
}
|
||||
|
||||
public function resolve(id:ImageId, preview:Bool = false):Promise<BitmapData> {
|
||||
var key = '${id}_${preview}';
|
||||
if (cache.exists(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
var result = sources.get(id.source).loadImage(id, preview);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
24
src/app/haxe/ru/m/puzzlez/storage/SettingsStorage.hx
Normal file
24
src/app/haxe/ru/m/puzzlez/storage/SettingsStorage.hx
Normal file
@@ -0,0 +1,24 @@
|
||||
package ru.m.puzzlez.storage;
|
||||
|
||||
import hw.storage.SharedObjectStorage;
|
||||
import ru.m.puzzlez.render.Background;
|
||||
|
||||
@:provide class SettingsStorage extends SharedObjectStorage {
|
||||
private inline static var VERSION = 1;
|
||||
private inline static var BACKGROUND_KEY = "background";
|
||||
|
||||
public var background(get, set):Background;
|
||||
|
||||
private inline function get_background():Background {
|
||||
return exists(BACKGROUND_KEY) ? read(BACKGROUND_KEY) : NONE;
|
||||
}
|
||||
|
||||
private inline function set_background(value:Background):Background {
|
||||
write(BACKGROUND_KEY, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public function new() {
|
||||
super('setting/${VERSION}');
|
||||
}
|
||||
}
|
||||
99
src/app/haxe/ru/m/puzzlez/view/GameFrame.hx
Normal file
99
src/app/haxe/ru/m/puzzlez/view/GameFrame.hx
Normal file
@@ -0,0 +1,99 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import haxe.Timer;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.frame.FrameView;
|
||||
import hw.view.popup.ConfirmView;
|
||||
import promhx.Promise;
|
||||
import ru.m.puzzlez.core.Game;
|
||||
import ru.m.puzzlez.core.GameEvent;
|
||||
import ru.m.puzzlez.core.GameState;
|
||||
import ru.m.puzzlez.core.IGame;
|
||||
import ru.m.puzzlez.render.IRender;
|
||||
import ru.m.puzzlez.storage.GameStorage;
|
||||
import ru.m.puzzlez.storage.SettingsStorage;
|
||||
import ru.m.puzzlez.view.popup.BackgroundPopup;
|
||||
import ru.m.puzzlez.view.popup.PreviewPopup;
|
||||
|
||||
@:template class GameFrame extends FrameView<GameState> {
|
||||
public static var ID = "game";
|
||||
|
||||
@:view private var render:IRender;
|
||||
private var game:IGame;
|
||||
@:provide var switcher:FrameSwitcher;
|
||||
@:provide var storage:GameStorage;
|
||||
@:provide var settings:SettingsStorage;
|
||||
|
||||
private var saveTimer:Timer;
|
||||
|
||||
public function new() {
|
||||
super(ID);
|
||||
}
|
||||
|
||||
override public function onShow(state:GameState):Void {
|
||||
onHide();
|
||||
game = new Game(state);
|
||||
game.signal.connect(render.onGameEvent);
|
||||
game.signal.connect(onGameEvent);
|
||||
render.signal.connect(game.signal.emit);
|
||||
game.start();
|
||||
}
|
||||
|
||||
override public function onHide():Void {
|
||||
if (saveTimer != null) {
|
||||
save();
|
||||
}
|
||||
if (game != null) {
|
||||
render.signal.disconnect(game.signal.emit);
|
||||
game.stop();
|
||||
game.dispose();
|
||||
game = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function toSave():Void {
|
||||
if (saveTimer == null) {
|
||||
saveTimer = Timer.delay(save, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private function save():Void {
|
||||
if (saveTimer != null) {
|
||||
saveTimer.stop();
|
||||
saveTimer = null;
|
||||
}
|
||||
if (game != null) {
|
||||
storage.save(game.state);
|
||||
}
|
||||
}
|
||||
|
||||
private function onGameEvent(event:GameEvent):Void {
|
||||
switch event {
|
||||
case START(_) | ACTION(PART_PUT(_, _)):
|
||||
toSave();
|
||||
case _:
|
||||
}
|
||||
}
|
||||
|
||||
private function showPreview():Void {
|
||||
PreviewPopup.instance.showPreview(game.state);
|
||||
}
|
||||
|
||||
private function choiseBackground():Void {
|
||||
BackgroundPopup.instance.choise(settings.background).then(background -> {
|
||||
if (background != null) {
|
||||
settings.background = background;
|
||||
render.toRedraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function back():Void {
|
||||
(game.state.status == COMPLETE ? Promise.promise(true) : ConfirmView.confirm("Exit?"))
|
||||
.then(result -> {
|
||||
if (result) {
|
||||
switcher.change(ImageListFrame.ID, storage.statusSource(game.state.status));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
41
src/app/haxe/ru/m/puzzlez/view/GameFrame.yaml
Normal file
41
src/app/haxe/ru/m/puzzlez/view/GameFrame.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
style: frame
|
||||
layout:
|
||||
$type: hw.view.layout.HorizontalLayout
|
||||
views:
|
||||
- $type: hw.view.group.VGroupView
|
||||
geometry.height: 100%
|
||||
geometry.padding: 5
|
||||
layout.margin: 10
|
||||
views:
|
||||
- $type: hw.view.form.ButtonView
|
||||
style: icon.image
|
||||
+onPress: ~showPreview()
|
||||
- $type: hw.view.SpriteView
|
||||
geometry.stretch: true
|
||||
- $type: hw.view.form.ToggleButtonView
|
||||
style: icon.lock
|
||||
on: true
|
||||
+onPress: |
|
||||
~button -> {
|
||||
render.manager.locked = !render.manager.locked;
|
||||
cast(button, hw.view.form.ToggleButtonView).on = !render.manager.locked;
|
||||
}
|
||||
- $type: hw.view.form.ButtonView
|
||||
style: icon.restore
|
||||
+onPress: ~render.manager.reset()
|
||||
- $type: hw.view.form.ButtonView
|
||||
style: icon.setting
|
||||
geometry.margin.top: 30
|
||||
+onPress: ~choiseBackground()
|
||||
- id: render
|
||||
$type: ru.m.puzzlez.render.Render
|
||||
geometry.width: 100%
|
||||
geometry.height: 100%
|
||||
- $type: hw.view.form.ButtonView
|
||||
style: icon.close
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: right
|
||||
geometry.vAlign: top
|
||||
geometry.margin: 5
|
||||
+onPress: ~back()
|
||||
51
src/app/haxe/ru/m/puzzlez/view/ImageListFrame.hx
Normal file
51
src/app/haxe/ru/m/puzzlez/view/ImageListFrame.hx
Normal file
@@ -0,0 +1,51 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.form.LabelView;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.frame.FrameView;
|
||||
import ru.m.puzzlez.core.ImageListSource;
|
||||
import ru.m.puzzlez.FileUtil;
|
||||
import ru.m.puzzlez.source.FileSource;
|
||||
import ru.m.puzzlez.storage.GameStorage;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
import ru.m.puzzlez.view.common.ImageDataList;
|
||||
|
||||
@:template class ImageListFrame extends FrameView<ImageListSource> {
|
||||
public static var ID = "image_list";
|
||||
|
||||
@:view("header") var headerView:LabelView;
|
||||
@:view("images") var imagesView:ImageDataList;
|
||||
@:view var select:ButtonView;
|
||||
|
||||
@:provide var gameStorage:GameStorage;
|
||||
@:provide var imageStorage:ImageStorage;
|
||||
@:provide var switcher:FrameSwitcher;
|
||||
|
||||
public function new() {
|
||||
super(ID);
|
||||
}
|
||||
|
||||
override public function onShow(data:ImageListSource):Void {
|
||||
imagesView.reset();
|
||||
if (data != null) {
|
||||
headerView.text = data.title;
|
||||
// ToDo:
|
||||
select.visible = Std.is(data.source, FileSource);
|
||||
imagesView.page.filter = data.filter;
|
||||
imagesView.source = data.source;
|
||||
imagesView.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private function selectFile():Void {
|
||||
FileUtil.browse().then((data:FileContent) -> {
|
||||
var fileSource:FileSource = cast imageStorage.sources.get(FileSource.ID);
|
||||
fileSource.append(data.content).then(_ -> imagesView.refresh());
|
||||
});
|
||||
}
|
||||
|
||||
private function back():Void {
|
||||
switcher.change(StartFrame.ID);
|
||||
}
|
||||
}
|
||||
20
src/app/haxe/ru/m/puzzlez/view/ImageListFrame.yaml
Normal file
20
src/app/haxe/ru/m/puzzlez/view/ImageListFrame.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
style: frame
|
||||
views:
|
||||
- id: header
|
||||
$type: hw.view.form.LabelView
|
||||
style: label.header
|
||||
- id: images
|
||||
$type: ru.m.puzzlez.view.common.ImageDataList
|
||||
geometry.stretch: true
|
||||
- id: select
|
||||
$type: hw.view.form.ButtonView
|
||||
text: Select...
|
||||
+onPress: ~selectFile()
|
||||
visible: false
|
||||
- $type: hw.view.form.ButtonView
|
||||
text: Back
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: right
|
||||
geometry.vAlign: top
|
||||
+onPress: ~back()
|
||||
108
src/app/haxe/ru/m/puzzlez/view/LoadingWrapper.hx
Normal file
108
src/app/haxe/ru/m/puzzlez/view/LoadingWrapper.hx
Normal file
@@ -0,0 +1,108 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import hw.view.form.LabelView;
|
||||
import flash.text.TextFormatAlign;
|
||||
import hw.view.geometry.HAlign;
|
||||
import hw.view.geometry.Position;
|
||||
import hw.view.geometry.VAlign;
|
||||
import hw.view.group.IGroupView;
|
||||
import hw.view.IView;
|
||||
import hw.view.text.TextView;
|
||||
import promhx.Promise;
|
||||
|
||||
enum State {
|
||||
NONE;
|
||||
LOADING;
|
||||
ERROR(error:Dynamic);
|
||||
}
|
||||
|
||||
class LoadingWrapper {
|
||||
public var promise(null, set):Promise<Dynamic>;
|
||||
|
||||
private function set_promise(value:Promise<Dynamic>):Promise<Dynamic> {
|
||||
state = LOADING;
|
||||
value
|
||||
.then(_ -> state = NONE)
|
||||
.catchError(error -> state = ERROR(error));
|
||||
return value;
|
||||
}
|
||||
|
||||
public var state(default, set):State;
|
||||
|
||||
private function set_state(value:State):State {
|
||||
if (state != value) {
|
||||
state = value;
|
||||
switch state {
|
||||
case NONE:
|
||||
overlay = null;
|
||||
case LOADING:
|
||||
overlay = loadingView;
|
||||
case ERROR(error):
|
||||
L.e("wrapper", "", error);
|
||||
cast(errorView, TextView).text = Std.string(error);
|
||||
overlay = errorView;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private var view:IGroupView;
|
||||
|
||||
private var overlay(default, set):IView<Dynamic>;
|
||||
|
||||
private function set_overlay(value:IView<Dynamic>):IView<Dynamic> {
|
||||
if (overlay != null && view.containsView(overlay)) {
|
||||
view.removeView(overlay);
|
||||
}
|
||||
overlay = value;
|
||||
if (overlay != null) {
|
||||
view.addView(overlay);
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
|
||||
private var loadingView(get, null):IView<Dynamic>;
|
||||
|
||||
private function get_loadingView():IView<Dynamic> {
|
||||
if (loadingView == null) {
|
||||
loadingView = buildLoadingView();
|
||||
}
|
||||
return loadingView;
|
||||
}
|
||||
|
||||
private var errorView(get, null):IView<Dynamic>;
|
||||
|
||||
private function get_errorView():IView<Dynamic> {
|
||||
if (errorView == null) {
|
||||
errorView = buildErrorView();
|
||||
}
|
||||
return errorView;
|
||||
}
|
||||
|
||||
public function new(view:IGroupView) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
private function buildLoadingView():IView<Dynamic> {
|
||||
var view = new LabelView();
|
||||
view.geometry.position = ABSOLUTE;
|
||||
view.geometry.hAlign = CENTER;
|
||||
view.geometry.vAlign = MIDDLE;
|
||||
view.text = "Loading...";
|
||||
return view;
|
||||
}
|
||||
|
||||
private function buildErrorView():IView<Dynamic> {
|
||||
var view = new TextView();
|
||||
view.geometry.position = ABSOLUTE;
|
||||
view.geometry.hAlign = CENTER;
|
||||
view.geometry.vAlign = MIDDLE;
|
||||
view.geometry.stretch = true;
|
||||
view.style = "text.error";
|
||||
return view;
|
||||
}
|
||||
|
||||
public function reset():Void {
|
||||
overlay = null;
|
||||
}
|
||||
}
|
||||
63
src/app/haxe/ru/m/puzzlez/view/PresetFrame.hx
Normal file
63
src/app/haxe/ru/m/puzzlez/view/PresetFrame.hx
Normal file
@@ -0,0 +1,63 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import hw.geom.IntPoint;
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ToggleButtonView;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.frame.FrameView;
|
||||
import ru.m.puzzlez.core.GameUtil;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
import ru.m.puzzlez.view.common.PresetView;
|
||||
|
||||
@:template class PresetFrame extends FrameView<ImageId> {
|
||||
public static var ID = "preset";
|
||||
|
||||
@:view("image") var imageView:PresetView;
|
||||
@:view("sizes") var sizesView:DataView<IntPoint, ToggleButtonView>;
|
||||
|
||||
@:provide var imageStorage:ImageStorage;
|
||||
@:provide var switcher:FrameSwitcher;
|
||||
|
||||
private var imageId:ImageId;
|
||||
|
||||
public function new() {
|
||||
super(ID);
|
||||
sizesView.data = [
|
||||
new IntPoint(3, 2),
|
||||
new IntPoint(4, 3),
|
||||
new IntPoint(5, 4),
|
||||
new IntPoint(6, 5),
|
||||
new IntPoint(7, 6),
|
||||
new IntPoint(8, 7),
|
||||
new IntPoint(10, 8),
|
||||
];
|
||||
}
|
||||
|
||||
private function factory(index:Int, size:IntPoint):ToggleButtonView {
|
||||
var result = new ToggleButtonView();
|
||||
result.text = '${size.x}x${size.y}';
|
||||
return result;
|
||||
}
|
||||
|
||||
private function selectSize(size:IntPoint):Void {
|
||||
imageView.state = GameUtil.buildState(GameUtil.buildPreset(imageId, size.x, size.y));
|
||||
for (i in 0...sizesView.dataViews.length) {
|
||||
sizesView.dataViews[i].on = size == sizesView.data[i];
|
||||
}
|
||||
}
|
||||
|
||||
override public function onShow(data:ImageId):Void {
|
||||
super.onShow(data);
|
||||
imageId = data;
|
||||
selectSize(sizesView.data[sizesView.data.length - 1]);
|
||||
}
|
||||
|
||||
private function start():Void {
|
||||
switcher.change(GameFrame.ID, imageView.state);
|
||||
}
|
||||
|
||||
private function back():Void {
|
||||
switcher.change(ImageListFrame.ID);
|
||||
}
|
||||
}
|
||||
36
src/app/haxe/ru/m/puzzlez/view/PresetFrame.yaml
Normal file
36
src/app/haxe/ru/m/puzzlez/view/PresetFrame.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
style: frame
|
||||
layout.margin: 10
|
||||
views:
|
||||
- $type: hw.view.form.LabelView
|
||||
text: Puzzle configure
|
||||
style: label.header
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.hAlign: center
|
||||
layout.vAlign: middle
|
||||
views:
|
||||
- id: sizes
|
||||
$type: hw.view.data.DataView
|
||||
layout:
|
||||
$type: hw.view.layout.TailLayout
|
||||
hAlign: center
|
||||
stretch: true
|
||||
margin: 5
|
||||
factory: ~factory
|
||||
+onDataSelect: ~selectSize
|
||||
- $type: hw.view.form.ButtonView
|
||||
style: button.active
|
||||
geometry.margin.left: 15
|
||||
text: Start
|
||||
+onPress: ~start()
|
||||
- id: image
|
||||
$type: ru.m.puzzlez.view.common.PresetView
|
||||
geometry.stretch: true
|
||||
geometry.margin: 15
|
||||
- $type: hw.view.form.ButtonView
|
||||
text: Back
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: right
|
||||
geometry.vAlign: top
|
||||
+onPress: ~back()
|
||||
34
src/app/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx
Normal file
34
src/app/haxe/ru/m/puzzlez/view/PuzzlezAppView.hx
Normal file
@@ -0,0 +1,34 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import flash.events.KeyboardEvent;
|
||||
import flash.ui.Keyboard;
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.group.VGroupView;
|
||||
import hw.app.App;
|
||||
|
||||
@:template class PuzzlezAppView extends VGroupView {
|
||||
|
||||
@:view("switcher") var switcherView:FrameSwitcher;
|
||||
@:view("fullscreen") var fullscreenButton:ButtonView;
|
||||
|
||||
@:provide static var switcher:FrameSwitcher;
|
||||
@:provide static var app:App;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
fullscreenButton.visible = app.fullScreenSupport;
|
||||
app.fullScreenSignal.connect(fs -> fullscreenButton.style = 'icon.${fs ? "compress" : "expand"}');
|
||||
switcher = switcherView;
|
||||
switcher.change(StartFrame.ID);
|
||||
stage.addEventListener(KeyboardEvent.KEY_DOWN, (event:KeyboardEvent) -> {
|
||||
switch event.keyCode {
|
||||
case Keyboard.ESCAPE:
|
||||
switcher.change(StartFrame.ID);
|
||||
case Keyboard.F:
|
||||
app.fullScreen = !app.fullScreen;
|
||||
case _:
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
20
src/app/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml
Normal file
20
src/app/haxe/ru/m/puzzlez/view/PuzzlezAppView.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
views:
|
||||
- $type: hw.view.frame.FrameSwitcher
|
||||
id: switcher
|
||||
geometry.stretch: true
|
||||
style: dark
|
||||
factory:
|
||||
_start_: {$class: ru.m.puzzlez.view.StartFrame}
|
||||
_image_list_: {$class: ru.m.puzzlez.view.ImageListFrame}
|
||||
_preset_: {$class: ru.m.puzzlez.view.PresetFrame}
|
||||
_game_: {$class: ru.m.puzzlez.view.GameFrame}
|
||||
- id: fullscreen
|
||||
$type: hw.view.form.ButtonView
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: right
|
||||
geometry.vAlign: bottom
|
||||
geometry.margin: 10
|
||||
style: icon.expand
|
||||
+onPress: ~_ -> app.fullScreen = !app.fullScreen
|
||||
visible: false
|
||||
81
src/app/haxe/ru/m/puzzlez/view/StartFrame.hx
Normal file
81
src/app/haxe/ru/m/puzzlez/view/StartFrame.hx
Normal file
@@ -0,0 +1,81 @@
|
||||
package ru.m.puzzlez.view;
|
||||
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.frame.FrameView;
|
||||
import ru.m.data.IDataSource;
|
||||
import ru.m.pixabay.PixabayApi;
|
||||
import ru.m.puzzlez.core.GameState.GameStatus;
|
||||
import ru.m.puzzlez.core.ImageListSource;
|
||||
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";
|
||||
|
||||
@:view var sources:DataView<ImageListSource, ButtonView>;
|
||||
@:view("load") var loadButton:ButtonView;
|
||||
@:view("complete") var completeButton:ButtonView;
|
||||
@:view("update") var updateButton:ButtonView;
|
||||
|
||||
@:provide var storage:ImageStorage;
|
||||
@:provide var switcher:FrameSwitcher;
|
||||
@:provide var gameStorage:GameStorage;
|
||||
@:provide static var appUpdater:Updater;
|
||||
|
||||
public function new() {
|
||||
super(ID);
|
||||
var data:Array<ImageListSource> = [];
|
||||
data.push({title: "Assets", source: storage.sources.get(AssetSource.ID)});
|
||||
data.push({title: "Files", source: storage.sources.get(FileSource.ID)});
|
||||
var pixabay:PixabaySource = cast storage.sources.get(PixabaySource.ID);
|
||||
for (type in AbstractEnumTools.getValues(PixabayCategory)) {
|
||||
data.push(pixabay.categorySource(type));
|
||||
}
|
||||
sources.data = data;
|
||||
}
|
||||
|
||||
private function refresh():Void {
|
||||
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;
|
||||
});
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
override public function onShow(data:Dynamic):Void {
|
||||
refresh();
|
||||
appUpdater.check().then((info:Null<PackageInfo>) -> {
|
||||
if (info != null) {
|
||||
updateButton.visible = true;
|
||||
updateButton.text = 'Update ${info.version}';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function sourceViewFactory(index:Int, source:ImageListSource):ButtonView {
|
||||
var result = new ButtonView();
|
||||
result.text = source.title;
|
||||
return result;
|
||||
}
|
||||
|
||||
private function load(source:ImageListSource):Void {
|
||||
switcher.change(ImageListFrame.ID, source);
|
||||
}
|
||||
|
||||
private function games(status:GameStatus):Void {
|
||||
switcher.change(ImageListFrame.ID, gameStorage.statusSource(status));
|
||||
}
|
||||
}
|
||||
57
src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml
Normal file
57
src/app/haxe/ru/m/puzzlez/view/StartFrame.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
style: frame
|
||||
views:
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.hAlign: center
|
||||
geometry.margin.top: 15
|
||||
layout.vAlign: middle
|
||||
views:
|
||||
- $type: hw.view.ImageView
|
||||
image: $a:image:resources/icon.png
|
||||
stretch: false
|
||||
fillType: CONTAIN
|
||||
geometry.width: 96
|
||||
geometry.height: 96
|
||||
- $type: hw.view.form.LabelView
|
||||
text: $r:text:app.name
|
||||
font.size: 50
|
||||
- id: sources
|
||||
$type: hw.view.data.DataView
|
||||
layout:
|
||||
$type: hw.view.layout.TailLayout
|
||||
rowSize: 5
|
||||
margin: 10
|
||||
vAlign: middle
|
||||
geometry.stretch: true
|
||||
factory: ~sourceViewFactory
|
||||
+onDataSelect: ~load
|
||||
geometry.margin: 5
|
||||
overflow.y: scroll
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.vAlign: middle
|
||||
layout.margin: 10
|
||||
views:
|
||||
- id: load
|
||||
$type: hw.view.form.ButtonView
|
||||
text: Load
|
||||
+onPress: ~games('started')
|
||||
- id: complete
|
||||
$type: hw.view.form.ButtonView
|
||||
text: Complete
|
||||
+onPress: ~games('complete')
|
||||
- $type: hw.view.SpriteView
|
||||
geometry.width: 100%
|
||||
- $type: hw.view.form.LabelView
|
||||
text: $r:text:app.version
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: right
|
||||
geometry.vAlign: top
|
||||
- id: update
|
||||
$type: hw.view.form.ButtonView
|
||||
style: button.active
|
||||
geometry.position: absolute
|
||||
geometry.hAlign: left
|
||||
geometry.vAlign: top
|
||||
+onPress: ~appUpdater.download()
|
||||
visible: false
|
||||
92
src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx
Normal file
92
src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.hx
Normal file
@@ -0,0 +1,92 @@
|
||||
package ru.m.puzzlez.view.common;
|
||||
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ToggleButtonView;
|
||||
import hw.view.frame.FrameSwitcher;
|
||||
import hw.view.group.VGroupView;
|
||||
import hw.view.popup.ConfirmView;
|
||||
import ru.m.data.IDataSource;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.source.FileSource;
|
||||
import ru.m.puzzlez.storage.GameStorage;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
import ru.m.puzzlez.view.common.PuzzleImageView;
|
||||
|
||||
@:template class ImageDataList extends VGroupView {
|
||||
|
||||
public var source:IDataIndex<ImageId>;
|
||||
public var page:Page;
|
||||
|
||||
@:view("images") var imagesView:ActionDataView<ImageId, PuzzleImageView, Action>;
|
||||
@:view("paginator") var paginatorView:PaginatorView;
|
||||
private var loading:LoadingWrapper;
|
||||
|
||||
@:provide var switcher:FrameSwitcher;
|
||||
@:provide var gameStorage:GameStorage;
|
||||
@:provide var imageStorage:ImageStorage;
|
||||
|
||||
public var data(default, set):DataPage<ImageId>;
|
||||
|
||||
private function set_data(value:DataPage<ImageId>):DataPage<ImageId> {
|
||||
data = value;
|
||||
imagesView.data = data.data;
|
||||
paginatorView.page = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
loading = new LoadingWrapper(imagesView);
|
||||
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;
|
||||
}
|
||||
|
||||
private function start(imageId:ImageId):Void {
|
||||
gameStorage.get(imageId).then(state -> {
|
||||
if (state != null) {
|
||||
switcher.change(GameFrame.ID, state);
|
||||
} else {
|
||||
switcher.change(PresetFrame.ID, imageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function onAction(imageId:ImageId, action:Action):Void {
|
||||
switch action {
|
||||
case REMOVE:
|
||||
var fileSource:FileSource = cast imageStorage.sources.get(FileSource.ID);
|
||||
if (fileSource != null) {
|
||||
ConfirmView.confirm("Delete image?").then(result -> {
|
||||
if (result) {
|
||||
fileSource.remove(imageId).then(_ -> refresh());
|
||||
}
|
||||
});
|
||||
}
|
||||
case CLEAN:
|
||||
ConfirmView.confirm("Delete state?").then(result -> {
|
||||
if (result) {
|
||||
gameStorage.delete(imageId).then(_ -> refresh());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh():Void {
|
||||
loading.promise = source.getIndexPage(page).then(data -> this.data = data);
|
||||
}
|
||||
|
||||
public function reset():Void {
|
||||
page.index = 0;
|
||||
data = {
|
||||
page: page,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
23
src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.yaml
Normal file
23
src/app/haxe/ru/m/puzzlez/view/common/ImageDataList.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
views:
|
||||
- id: images
|
||||
$type: hw.view.data.ActionDataView
|
||||
layout:
|
||||
$type: hw.view.layout.TailLayout
|
||||
rowSize: 3
|
||||
margin: 5
|
||||
vAlign: middle
|
||||
geometry.stretch: true
|
||||
factory: ~ru.m.puzzlez.view.common.PuzzleImageView.factory
|
||||
+onDataSelect: ~start
|
||||
+onDataAction: ~onAction
|
||||
geometry.margin: 5
|
||||
overflow.y: scroll
|
||||
- id: paginator
|
||||
$type: ru.m.puzzlez.view.common.PaginatorView
|
||||
geometry.width: 100%
|
||||
+onPageSelect: |
|
||||
~(index) -> {
|
||||
page.index = index;
|
||||
refresh();
|
||||
}
|
||||
82
src/app/haxe/ru/m/puzzlez/view/common/PaginatorView.hx
Normal file
82
src/app/haxe/ru/m/puzzlez/view/common/PaginatorView.hx
Normal file
@@ -0,0 +1,82 @@
|
||||
package ru.m.puzzlez.view.common;
|
||||
|
||||
import hw.signal.Signal;
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ToggleButtonView;
|
||||
import hw.view.layout.TailLayout;
|
||||
import ru.m.data.IDataSource.DataPage;
|
||||
|
||||
enum PaginatorAction {
|
||||
START;
|
||||
JUMP(distance:Int);
|
||||
INDEX(index:Int);
|
||||
END;
|
||||
}
|
||||
|
||||
class PaginatorButtonView extends ToggleButtonView {
|
||||
public var action(default, set):PaginatorAction;
|
||||
|
||||
private function set_action(value:PaginatorAction):PaginatorAction {
|
||||
action = value;
|
||||
text = switch action {
|
||||
case INDEX(index): Std.string(index);
|
||||
case x: Std.string(x);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
public static function factory(index:Int, action:PaginatorAction):PaginatorButtonView {
|
||||
var result = new PaginatorButtonView();
|
||||
result.action = action;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class PaginatorView extends DataView<PaginatorAction, PaginatorButtonView> {
|
||||
|
||||
public var page(default, set):DataPage<Dynamic>;
|
||||
public var onPageSelect(default, null):Signal<Int>;
|
||||
|
||||
private function set_page(value:DataPage<Dynamic>):DataPage<Dynamic> {
|
||||
page = value;
|
||||
refresh();
|
||||
return page;
|
||||
}
|
||||
|
||||
public function new() {
|
||||
super(new TailLayout());
|
||||
layout.margin = 3;
|
||||
onPageSelect = new Signal();
|
||||
factory = PaginatorButtonView.factory;
|
||||
onDataSelect.connect(onAction);
|
||||
}
|
||||
|
||||
private function onAction(action:PaginatorAction):Void {
|
||||
onPageSelect.emit(switch action {
|
||||
case START: 0;
|
||||
case INDEX(index): index;
|
||||
case JUMP(distance): page.page.index + distance;
|
||||
case END: Std.int((page.total - 1) / page.page.count);
|
||||
});
|
||||
}
|
||||
|
||||
private function refresh():Void {
|
||||
var total:Int = Std.int((page.total - 1) / page.page.count);
|
||||
var start:Int = Std.int(Math.max(page.page.index - 2, 0));
|
||||
var end:Int = Std.int(Math.min(page.page.index + 2, total));
|
||||
var data = [for (index in start...end + 1) INDEX(index)];
|
||||
if (start > 0) {
|
||||
data.unshift(START);
|
||||
}
|
||||
if (end < total) {
|
||||
data.push(END);
|
||||
}
|
||||
this.data = data;
|
||||
for (button in dataViews) {
|
||||
button.on = switch button.action {
|
||||
case INDEX(index): index == page.page.index;
|
||||
case x: false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/app/haxe/ru/m/puzzlez/view/common/PresetView.hx
Normal file
85
src/app/haxe/ru/m/puzzlez/view/common/PresetView.hx
Normal file
@@ -0,0 +1,85 @@
|
||||
package ru.m.puzzlez.view.common;
|
||||
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.Graphics;
|
||||
import flash.display.Shape;
|
||||
import hw.view.group.GroupView;
|
||||
import ru.m.puzzlez.core.GameState;
|
||||
import ru.m.puzzlez.render.RenderUtil;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
|
||||
class PresetView extends GroupView {
|
||||
@:provide var imageStorage:ImageStorage;
|
||||
|
||||
public var scale(get, set):Float;
|
||||
|
||||
private function get_scale():Float {
|
||||
return table.scaleX;
|
||||
}
|
||||
|
||||
private function set_scale(value:Float):Float {
|
||||
var result = table.scaleX = table.scaleY = value;
|
||||
table.x = (width - state.preset.imageRect.width * value) / 2;
|
||||
table.y = (height - state.preset.imageRect.height * value) / 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
public var state(default, set):GameState;
|
||||
|
||||
private function set_state(value:GameState):GameState {
|
||||
state = value;
|
||||
this.image = null;
|
||||
table.graphics.clear();
|
||||
loading.promise = imageStorage.resolve(state.preset.imageId).then(image -> {
|
||||
this.image = RenderUtil.cropImage(image, state.preset.imageRect);
|
||||
toRedraw();
|
||||
toUpdate();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
private var loading:LoadingWrapper;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
table = new Shape();
|
||||
content.addChild(table);
|
||||
loading = new LoadingWrapper(this);
|
||||
}
|
||||
|
||||
private var table:Shape;
|
||||
private var image:BitmapData;
|
||||
|
||||
override public function redraw():Void {
|
||||
if (state == null || image == null) {
|
||||
return;
|
||||
}
|
||||
var preset = state.preset;
|
||||
var partWidth = preset.imageRect.width / preset.grid.width;
|
||||
var partHeight = preset.imageRect.height / preset.grid.height;
|
||||
var graphics:Graphics = table.graphics;
|
||||
graphics.clear();
|
||||
graphics.beginBitmapFill(image, null, false, true);
|
||||
graphics.drawRect(0, 0, preset.imageRect.width, preset.imageRect.height);
|
||||
graphics.endFill();
|
||||
|
||||
for (part in state.parts) {
|
||||
var rect = part.rect.clone();
|
||||
rect.x = part.gridX * part.rect.width;
|
||||
rect.y = part.gridY * part.rect.height;
|
||||
var path = RenderUtil.builder.build(rect, part.bounds);
|
||||
for (value in RenderUtil.borderSettings) {
|
||||
graphics.lineStyle(1, value.color, value.opacity);
|
||||
path.move(value.offset.x, value.offset.y).draw(graphics);
|
||||
graphics.lineStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public function update():Void {
|
||||
super.update();
|
||||
if (state != null) {
|
||||
scale = Math.min(width / state.preset.imageRect.width, height / state.preset.imageRect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/app/haxe/ru/m/puzzlez/view/common/PuzzleImageView.hx
Normal file
78
src/app/haxe/ru/m/puzzlez/view/common/PuzzleImageView.hx
Normal file
@@ -0,0 +1,78 @@
|
||||
package ru.m.puzzlez.view.common;
|
||||
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.form.LabelView;
|
||||
import hw.view.group.GroupView;
|
||||
import hw.view.ImageView;
|
||||
import ru.m.puzzlez.core.GameUtil;
|
||||
import ru.m.puzzlez.core.Id;
|
||||
import ru.m.puzzlez.source.FileSource;
|
||||
import ru.m.puzzlez.storage.GameStorage;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
|
||||
enum Action {
|
||||
CLEAN;
|
||||
REMOVE;
|
||||
}
|
||||
|
||||
@:template class PuzzleImageView extends GroupView {
|
||||
|
||||
public var imageId(default, set):ImageId;
|
||||
|
||||
private function set_imageId(value:ImageId):ImageId {
|
||||
if (imageId != value) {
|
||||
imageId = value;
|
||||
loading.promise = imageStorage.resolve(imageId, true).then(data -> imageView.image = data);
|
||||
}
|
||||
return imageId;
|
||||
}
|
||||
|
||||
public var text(default, set):String;
|
||||
|
||||
private function set_text(value:String):String {
|
||||
if (text != value) {
|
||||
text = value;
|
||||
labelView.text = text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@:view("image") var imageView:ImageView;
|
||||
@:view("label") var labelView:LabelView;
|
||||
@:view("clean") var cleanButton:ButtonView;
|
||||
@:view("remove") var removeButton:ButtonView;
|
||||
@:provide static var imageStorage:ImageStorage;
|
||||
@:provide static var gameStorage:GameStorage;
|
||||
private var loading:LoadingWrapper;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
cleanButton.visible = false;
|
||||
removeButton.visible = false;
|
||||
loading = new LoadingWrapper(this);
|
||||
}
|
||||
|
||||
private function emit(action:Action):Void {
|
||||
var dataView:ActionDataView<Dynamic, PuzzleImageView, Action> = Std.instance(parent, ActionDataView);
|
||||
if (dataView != null) {
|
||||
var index = dataView.dataViews.indexOf(this);
|
||||
dataView.onDataAction.emit(dataView.data[index], action);
|
||||
}
|
||||
}
|
||||
|
||||
public static function factory(index:Int, imageId:ImageId):PuzzleImageView {
|
||||
var result = new PuzzleImageView();
|
||||
result.imageId = imageId;
|
||||
gameStorage.get(imageId).then(state -> {
|
||||
if (state != null) {
|
||||
var progress = GameUtil.calcProgress(state);
|
||||
result.text = '${progress.complete}/${progress.total}';
|
||||
result.cleanButton.visible = true;
|
||||
} else if (imageId.source == FileSource.ID) {
|
||||
result.removeButton.visible = true;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
20
src/app/haxe/ru/m/puzzlez/view/common/PuzzleImageView.yaml
Normal file
20
src/app/haxe/ru/m/puzzlez/view/common/PuzzleImageView.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
style: view
|
||||
views:
|
||||
- id: image
|
||||
$type: hw.view.ImageView
|
||||
geometry.stretch: true
|
||||
stretch: false
|
||||
fillType: COVER
|
||||
- id: label
|
||||
$type: hw.view.form.LabelView
|
||||
- id: remove
|
||||
$type: hw.view.form.ButtonView
|
||||
propagation: false
|
||||
style: icon.control.small.close.red
|
||||
+onPress: ~emit(Action.REMOVE)
|
||||
- id: clean
|
||||
$type: hw.view.form.ButtonView
|
||||
propagation: false
|
||||
style: icon.control.small.close.orange
|
||||
+onPress: ~emit(Action.CLEAN)
|
||||
94
src/app/haxe/ru/m/puzzlez/view/popup/BackgroundPopup.hx
Normal file
94
src/app/haxe/ru/m/puzzlez/view/popup/BackgroundPopup.hx
Normal file
@@ -0,0 +1,94 @@
|
||||
package ru.m.puzzlez.view.popup;
|
||||
|
||||
import hw.color.Color;
|
||||
import hw.view.data.DataView;
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.popup.PopupView;
|
||||
import hw.view.skin.Skin;
|
||||
import hw.view.SpriteView;
|
||||
import hw.view.utils.DrawUtil;
|
||||
import openfl.Assets;
|
||||
import openfl.utils.AssetType;
|
||||
import promhx.Promise;
|
||||
import ru.m.puzzlez.core.Id.ImageId;
|
||||
import ru.m.puzzlez.render.Background;
|
||||
import ru.m.puzzlez.storage.ImageStorage;
|
||||
|
||||
@:singleton @:template class BackgroundPopup extends PopupView<Background> {
|
||||
|
||||
private static var colorsList:Array<Color> = [
|
||||
'#FFFFFF',
|
||||
'#001f3f',
|
||||
'#0074D9',
|
||||
'#7FDBFF',
|
||||
'#39CCCC',
|
||||
'#3D9970',
|
||||
'#2ECC40',
|
||||
'#01FF70',
|
||||
'#FFDC00',
|
||||
'#FF851B',
|
||||
'#FF4136',
|
||||
'#85144b',
|
||||
'#F012BE',
|
||||
'#B10DC9',
|
||||
'#111111',
|
||||
'#AAAAAA',
|
||||
'#DDDDDD',
|
||||
];
|
||||
|
||||
@:view("selected") var selectedView:SpriteView;
|
||||
@:view("colors") var colorsView:DataView<Color, ButtonView>;
|
||||
@:view("textures") var texturesView:DataView<ImageId, ButtonView>;
|
||||
|
||||
public var selected(default, set):Background;
|
||||
|
||||
@:provide static var imageStorage:ImageStorage;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
colorsView.data = colorsList;
|
||||
var textures = [];
|
||||
for (name in Assets.list(AssetType.IMAGE)) {
|
||||
if (StringTools.startsWith(name, 'resources/texture')) {
|
||||
textures.push(new ImageId('asset', name));
|
||||
}
|
||||
}
|
||||
texturesView.data = textures;
|
||||
}
|
||||
|
||||
private function set_selected(value:Background):Background {
|
||||
selected = value;
|
||||
switch selected {
|
||||
case NONE:
|
||||
selectedView.skin = null;
|
||||
case COLOR(color):
|
||||
selectedView.skin = Skin.color(color);
|
||||
case IMAGE(id):
|
||||
imageStorage.resolve(id).then(result -> {
|
||||
selectedView.skin = Skin.bitmap(result, REPEAT);
|
||||
selectedView.toRedraw();
|
||||
});
|
||||
}
|
||||
selectedView.toRedraw();
|
||||
return selected;
|
||||
}
|
||||
|
||||
private function colorButtonFactory(index:Int, color:Color):ButtonView {
|
||||
var result = new ButtonView();
|
||||
result.text = " ";
|
||||
result.skin = Skin.buttonColor(color);
|
||||
return result;
|
||||
}
|
||||
|
||||
private function textureButtonFactory(index:Int, imageId:ImageId):ButtonView {
|
||||
var result = new ButtonView();
|
||||
result.text = " ";
|
||||
result.skin = Skin.buttonBitmap(Assets.getBitmapData(imageId.id), REPEAT);
|
||||
return result;
|
||||
}
|
||||
|
||||
public function choise(current:Background):Promise<Background> {
|
||||
selected = current;
|
||||
return show();
|
||||
}
|
||||
}
|
||||
49
src/app/haxe/ru/m/puzzlez/view/popup/BackgroundPopup.yaml
Normal file
49
src/app/haxe/ru/m/puzzlez/view/popup/BackgroundPopup.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
view:
|
||||
$type: hw.view.group.VGroupView
|
||||
geometry.width: 660
|
||||
geometry.height: 200
|
||||
geometry.padding: 10
|
||||
geometry.hAlign: center
|
||||
geometry.vAlign: middle
|
||||
layout.margin: 10
|
||||
style: frame
|
||||
views:
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.margin: 10
|
||||
views:
|
||||
- id: header
|
||||
text: Choise background
|
||||
$type: hw.view.form.LabelView
|
||||
- id: selected
|
||||
$type: hw.view.SpriteView
|
||||
geometry.width: 100%
|
||||
geometry.height: 200
|
||||
- id: colors
|
||||
$type: hw.view.data.DataView
|
||||
factory: ~colorButtonFactory
|
||||
geometry.width: 100%
|
||||
layout:
|
||||
$type: hw.view.layout.TailLayout
|
||||
margin: 5
|
||||
+onDataSelect: ~(color) -> selected = COLOR(color)
|
||||
- id: textures
|
||||
$type: hw.view.data.DataView
|
||||
factory: ~textureButtonFactory
|
||||
geometry.width: 100%
|
||||
layout:
|
||||
$type: hw.view.layout.TailLayout
|
||||
margin: 5
|
||||
+onDataSelect: ~(imageId) -> selected = IMAGE(imageId)
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.hAlign: center
|
||||
layout.margin: 10
|
||||
views:
|
||||
- $type: hw.view.form.ButtonView
|
||||
text: Default
|
||||
+onPress: ~close(NONE)
|
||||
- $type: hw.view.form.ButtonView
|
||||
text: OK
|
||||
+onPress: ~close(selected)
|
||||
22
src/app/haxe/ru/m/puzzlez/view/popup/PreviewPopup.hx
Normal file
22
src/app/haxe/ru/m/puzzlez/view/popup/PreviewPopup.hx
Normal file
@@ -0,0 +1,22 @@
|
||||
package ru.m.puzzlez.view.popup;
|
||||
|
||||
import flash.events.MouseEvent;
|
||||
import hw.view.popup.PopupView;
|
||||
import promhx.Promise;
|
||||
import ru.m.puzzlez.core.GameState;
|
||||
import ru.m.puzzlez.view.common.PresetView;
|
||||
|
||||
@:singleton @:template class PreviewPopup extends PopupView<Dynamic> {
|
||||
|
||||
@:view("preview") var previewView:PresetView;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
content.addEventListener(MouseEvent.CLICK, _ -> close(null));
|
||||
}
|
||||
|
||||
public function showPreview(state:GameState):Promise<Dynamic> {
|
||||
previewView.state = state;
|
||||
return show();
|
||||
}
|
||||
}
|
||||
29
src/app/haxe/ru/m/puzzlez/view/popup/PreviewPopup.yaml
Normal file
29
src/app/haxe/ru/m/puzzlez/view/popup/PreviewPopup.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
view:
|
||||
$type: hw.view.group.VGroupView
|
||||
geometry.width: 100%
|
||||
geometry.height: 100%
|
||||
geometry.padding: 10
|
||||
geometry.hAlign: center
|
||||
geometry.vAlign: middle
|
||||
layout.margin: 10
|
||||
style: frame
|
||||
views:
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.margin: 10
|
||||
views:
|
||||
- id: header
|
||||
text: Image preview
|
||||
$type: hw.view.form.LabelView
|
||||
- id: preview
|
||||
$type: ru.m.puzzlez.view.common.PresetView
|
||||
geometry.stretch: true
|
||||
- $type: hw.view.group.HGroupView
|
||||
geometry.width: 100%
|
||||
layout.hAlign: center
|
||||
layout.margin: 10
|
||||
views:
|
||||
- $type: hw.view.form.ButtonView
|
||||
text: OK
|
||||
+onPress: ~close(null)
|
||||
68
src/app/haxe/ru/m/skin/ButtonSVGSkin.hx
Normal file
68
src/app/haxe/ru/m/skin/ButtonSVGSkin.hx
Normal file
@@ -0,0 +1,68 @@
|
||||
package ru.m.skin;
|
||||
|
||||
import format.SVG;
|
||||
import hw.color.Color;
|
||||
import hw.view.form.ButtonView;
|
||||
import hw.view.form.ToggleButtonView;
|
||||
import hw.view.skin.ISkin;
|
||||
|
||||
using StringTools;
|
||||
using hw.color.ColorUtil;
|
||||
|
||||
@:style class ButtonSVGSkin implements ISkin<ButtonView> {
|
||||
|
||||
@:style(null) public var svg:String;
|
||||
@:style(0) public var color:Null<Color>;
|
||||
@:style(false) public var solid:Null<Bool>;
|
||||
|
||||
private var svgs:Map<ButtonState, SVG>;
|
||||
private var needUpdate:Bool;
|
||||
|
||||
public function new(?svg:String, ?color:Color, ?solid:Bool) {
|
||||
this.svg = svg;
|
||||
this.color = color;
|
||||
this.solid = solid;
|
||||
this.needUpdate = true;
|
||||
}
|
||||
|
||||
private inline function buildSVG(color:Color):SVG {
|
||||
return new SVG(svg.replace("currentColor", '#${color}'));
|
||||
}
|
||||
|
||||
private function update():Void {
|
||||
if (needUpdate && svg != null) {
|
||||
var color = color;
|
||||
if (solid) {
|
||||
color = color.multiply(1.5);
|
||||
}
|
||||
svgs = new Map();
|
||||
svgs.set(UP, buildSVG(color));
|
||||
svgs.set(DOWN, buildSVG(color.diff(-24)));
|
||||
svgs.set(OVER, buildSVG(color.diff(24)));
|
||||
svgs.set(DISABLED, buildSVG(color.grey()));
|
||||
needUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function draw(view:ButtonView):Void {
|
||||
update();
|
||||
var svg = svgs.get(view.state);
|
||||
var graphics = view.content.graphics;
|
||||
var color = this.color;
|
||||
if (Std.is(view, ToggleButtonView)) {
|
||||
if (!cast(view, ToggleButtonView).on) {
|
||||
color = color.multiply(0.5);
|
||||
}
|
||||
}
|
||||
graphics.beginFill(0, 0);
|
||||
graphics.drawRect(0, 0, view.width, view.height);
|
||||
graphics.beginFill(color);
|
||||
if (!solid) {
|
||||
graphics.lineStyle(2, color.multiply(1.5));
|
||||
}
|
||||
// ToDo: padding
|
||||
svg.render(graphics, 0, 0, Std.int(view.width * 0.8), Std.int(view.height * 0.8));
|
||||
graphics.lineStyle();
|
||||
graphics.endFill();
|
||||
}
|
||||
}
|
||||
83
src/app/haxe/ru/m/update/Updater.hx
Normal file
83
src/app/haxe/ru/m/update/Updater.hx
Normal file
@@ -0,0 +1,83 @@
|
||||
package ru.m.update;
|
||||
|
||||
import hw.net.JsonLoader;
|
||||
import openfl.Lib;
|
||||
import openfl.net.URLRequest;
|
||||
import promhx.Promise;
|
||||
import ru.m.Device;
|
||||
import ru.m.update.Version;
|
||||
|
||||
@:enum abstract PackageType(String) from String to String {
|
||||
var APK = "apk";
|
||||
var DEB = "deb";
|
||||
var EXE = "exe";
|
||||
var ARCHIVE = "archive";
|
||||
}
|
||||
|
||||
typedef PackageInfo = {
|
||||
var platform:Platform;
|
||||
var type:PackageType;
|
||||
var path:String;
|
||||
var filename:String;
|
||||
var url:String;
|
||||
var version:String;
|
||||
}
|
||||
|
||||
typedef PackagesBundle = {
|
||||
var name:String;
|
||||
var version:String;
|
||||
var packages:Array<PackageInfo>;
|
||||
}
|
||||
|
||||
class Updater {
|
||||
|
||||
private static inline var TAG = "Update";
|
||||
|
||||
public var type(get, null):PackageType;
|
||||
public var bundle(get, null):Promise<PackagesBundle>;
|
||||
|
||||
private function get_bundle():Promise<PackagesBundle> {
|
||||
if (bundle == null) {
|
||||
bundle = new JsonLoader().GET(url);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private var url:String;
|
||||
private var version:Version;
|
||||
|
||||
public function new(version:Version, url:String) {
|
||||
this.url = url;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
private function get_type():PackageType {
|
||||
return switch Device.platform {
|
||||
case ANDROID: APK;
|
||||
case LINUX: DEB;
|
||||
case WINDOWS: EXE;
|
||||
case _: null;
|
||||
}
|
||||
}
|
||||
|
||||
public function check():Promise<Null<PackageInfo>> {
|
||||
return bundle.then((bundle:PackagesBundle) -> Lambda.find(bundle.packages, item -> (
|
||||
item.platform == Device.platform &&
|
||||
item.type == type &&
|
||||
version < Version.fromString(item.version)
|
||||
)));
|
||||
}
|
||||
|
||||
public function download():Promise<Bool> {
|
||||
return bundle.then((bundle:PackagesBundle) -> {
|
||||
for (item in bundle.packages) {
|
||||
if (item.platform == Device.platform && item.type == type) {
|
||||
L.i(TAG, 'download: ${item.url}');
|
||||
Lib.getURL(new URLRequest(item.url));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
52
src/app/haxe/ru/m/update/Version.hx
Normal file
52
src/app/haxe/ru/m/update/Version.hx
Normal file
@@ -0,0 +1,52 @@
|
||||
package ru.m.update;
|
||||
|
||||
abstract Version(Array<Int>) {
|
||||
|
||||
public function new(mayor:Int, minor:Int, patch:Int, snapshot:Bool = false) {
|
||||
this = [
|
||||
mayor,
|
||||
minor,
|
||||
patch,
|
||||
snapshot ? -1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
@:arrayAccess public inline function get(key:Int) return this[key];
|
||||
|
||||
public function compare(other:Version):Int {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
for (i in 0...4) {
|
||||
var d = this[i] - other[i];
|
||||
if (d != 0) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@:op(A > B) static function gt(a:Version, b:Version):Bool return a.compare(b) > 0;
|
||||
|
||||
@:op(A == B) static function eq(a:Version, b:Version):Bool return a.compare(b) == 0;
|
||||
|
||||
@:op(A < B) static function lt(a:Version, b:Version):Bool return a.compare(b) < 0;
|
||||
|
||||
@:from public static function fromString(value:String):Version {
|
||||
var r = ~/(\d+)\.(\d+)\.(\d+)(-SNAPSHOT)?/;
|
||||
return if (r.match(value)) {
|
||||
new Version(
|
||||
Std.parseInt(r.matched(1)),
|
||||
Std.parseInt(r.matched(2)),
|
||||
Std.parseInt(r.matched(3)),
|
||||
r.matched(4) != null
|
||||
);
|
||||
} else {
|
||||
new Version(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@:to public function toString():String {
|
||||
return '${this[0]}.${this[1]}.${this[2]}${this[3] < 0 ? "-SNAPSHOT" : ""}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user