├── .gitignore ├── .travis.yml ├── .yardopts ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── doc.rb ├── flow-logo.png ├── flow ├── base64 │ ├── README.md │ ├── android │ │ └── base64.rb │ └── cocoa │ │ └── base64.rb ├── digest │ ├── README.md │ ├── android │ │ └── digest.rb │ └── cocoa │ │ ├── digest.h │ │ ├── digest.m │ │ └── digest.rb ├── json │ ├── README.md │ ├── android │ │ └── json.rb │ └── cocoa │ │ └── json.rb ├── location │ ├── README.md │ ├── android │ │ └── location_services.rb │ ├── cocoa │ │ └── location_services.rb │ └── location.rb ├── net │ ├── README.md │ ├── actions.rb │ ├── android │ │ ├── cookies.rb │ │ ├── reachability.rb │ │ ├── request.rb │ │ └── response_proxy.rb │ ├── authorization.rb │ ├── cocoa │ │ ├── reachability.rb │ │ ├── request.rb │ │ └── response_proxy.rb │ ├── config.rb │ ├── expectation.rb │ ├── header.rb │ ├── mime_types.rb │ ├── net.rb │ ├── response.rb │ ├── session.rb │ └── stubbable.rb ├── store │ ├── README.md │ ├── android │ │ └── store.rb │ └── cocoa │ │ └── store.rb ├── task │ ├── README.md │ ├── android │ │ └── task.rb │ ├── cocoa │ │ └── task.rb │ └── task.rb └── ui │ ├── README.md │ ├── alert.rb │ ├── android │ ├── activity_indicator.rb │ ├── alert.rb │ ├── application.rb │ ├── button.rb │ ├── camera.rb │ ├── color.rb │ ├── control.rb │ ├── font.rb │ ├── gesture.rb │ ├── gradient.rb │ ├── image.rb │ ├── label.rb │ ├── list.rb │ ├── navigation.rb │ ├── screen.rb │ ├── shared_text.rb │ ├── text.rb │ ├── text_input.rb │ ├── ui.rb │ ├── view.rb │ └── web.rb │ ├── cocoa │ ├── activity_indicator.rb │ ├── alert.rb │ ├── application.rb │ ├── button.rb │ ├── camera.rb │ ├── color.rb │ ├── control.rb │ ├── font.rb │ ├── gesture.rb │ ├── gradient.rb │ ├── image.rb │ ├── label.rb │ ├── list.rb │ ├── navigation.rb │ ├── screen.rb │ ├── shared_text.rb │ ├── text.rb │ ├── text_input.rb │ ├── ui.rb │ ├── view.rb │ └── web.rb │ ├── color.rb │ ├── css_layout.h │ ├── css_node.c │ ├── eventable.rb │ ├── font.rb │ ├── list_row.rb │ └── view.rb ├── include └── rubymotion.h ├── lib ├── android.rb ├── cocoa.rb ├── common.rb ├── motion-flow.rb └── motion-flow │ ├── base64.rb │ ├── digest.rb │ ├── json.rb │ ├── loader.rb │ ├── location.rb │ ├── net.rb │ ├── store.rb │ ├── task.rb │ └── ui.rb ├── motion-flow.gemspec ├── samples ├── reddit │ ├── Gemfile │ ├── Gemfile.lock │ ├── Rakefile │ ├── app │ │ ├── android │ │ │ ├── main_activity.rb │ │ │ └── timeline_adapter.rb │ │ ├── ios │ │ │ └── app_delegate.rb │ │ ├── osx │ │ │ ├── app_delegate.rb │ │ │ ├── menu.rb │ │ │ └── reddit_controller.rb │ │ ├── post.rb │ │ ├── post_row.rb │ │ ├── posts_list.rb │ │ ├── posts_screen.rb │ │ └── reddit_fetcher.rb │ ├── config │ │ ├── android.rb │ │ ├── ios.rb │ │ └── osx.rb │ └── resources │ │ ├── Default-568h@2x.png │ │ ├── Default-667h@2x.png │ │ └── Default-736h@3x.png └── ui_demo │ ├── Gemfile │ ├── Gemfile.lock │ ├── Rakefile │ ├── app │ ├── android │ │ └── main_activity.rb │ ├── ios │ │ └── app_delegate.rb │ └── welcome_screen.rb │ ├── config │ ├── android.rb │ └── ios.rb │ └── resources │ ├── Default-568h@2x.png │ ├── Default-667h@2x.png │ ├── Default-736h@3x.png │ ├── Starjedi.ttf │ └── rubymotion-logo.png ├── template └── flow │ └── files │ ├── .gitignore │ ├── Gemfile │ ├── Rakefile │ ├── app │ ├── android │ │ └── main_activity.rb │ └── ios │ │ └── app_delegate.rb │ ├── config │ ├── android.rb.erb │ ├── ios.rb.erb │ └── osx.rb.erb │ └── resources │ ├── Default-568h@2x.png │ ├── Default-667h@2x.png │ └── Default-736h@3x.png └── test ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── android │ └── main_activity.rb ├── ios │ └── app_delegate.rb └── osx │ └── app_delegate.rb ├── config ├── android.rb ├── ios.rb └── osx.rb ├── server.rb └── spec ├── base64_spec.rb ├── digest_spec.rb ├── helpers ├── android │ └── constants.rb └── cocoa │ └── constants.rb ├── json_spec.rb ├── net ├── authorization_spec.rb ├── config_spec.rb ├── expectation_spec.rb ├── header_spec.rb ├── response_spec.rb └── session_spec.rb ├── net_spec.rb ├── store_spec.rb ├── task_spec.rb └── ui ├── color_spec.rb ├── label.rb └── view.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | test/build 3 | ui_app/build 4 | samples/*/build 5 | **/*/.repl_history 6 | flow/digest/build-* 7 | *.swp 8 | build 9 | flow/**/*.a 10 | doc 11 | .yardoc 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | rvm: 3 | - 2.2 4 | osx_image: xcode7.3 5 | before_install: 6 | - (ruby --version) 7 | - sudo chown -R travis ~/Library/RubyMotion 8 | - sudo mkdir -p ~/Library/RubyMotion/build 9 | - sudo chown -R travis ~/Library/RubyMotion/build 10 | script: 11 | - bundle install 12 | - rake build:ios 13 | - cd test && bundle install --gemfile=./Gemfile 14 | - ruby server.rb & 15 | - rake ios:clean:all 16 | - rake ios:spec 17 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | doc.rb 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rake' 3 | gem 'motion-gradle' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | motion-gradle (2.1.0) 5 | rake (12.3.0) 6 | 7 | PLATFORMS 8 | ruby 9 | 10 | DEPENDENCIES 11 | motion-gradle 12 | rake 13 | 14 | BUNDLED WITH 15 | 1.16.0 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/HipByte/Flow.svg?branch=master)](https://travis-ci.org/HipByte/Flow) 2 | 3 | # Flow 4 | 5 | flow logo 6 | 7 | Flow is a set of cross-platform libraries for RubyMotion. It can be seen as RubyMotion's missing standard library. 8 | 9 | Each library implements the following requirements: 10 | 11 | * Simple Ruby API 12 | * 100% cross-platform (iOS, Android and OS X) 13 | * No external dependencies 14 | * Covered by tests 15 | * [API Documentation](http://www.rubymotion.com/developers/motion-flow/) 16 | 17 | **WARNING**: Flow is currently a work in progress. Some specs might be broken, APIs might change, and documentation might be missing. We are working toward a stable release. If you want to help please get in touch. 18 | 19 | ## Libraries 20 | 21 | Flow is currently composed of the following libraries: 22 | 23 | * [**UI**](https://github.com/Hipbyte/Flow/tree/master/flow/ui) - User interface framework 24 | * [**Net**](https://github.com/Hipbyte/Flow/tree/master/flow/net) - HTTP networking and host reachability 25 | * [**JSON**](https://github.com/Hipbyte/Flow/tree/master/flow/json) - JSON serialization 26 | * [**Digest**](https://github.com/Hipbyte/Flow/tree/master/flow/digest) - Digest cryptography 27 | * [**Store**](https://github.com/Hipbyte/Flow/tree/master/flow/store) - Key-value store 28 | * [**Base64**](https://github.com/Hipbyte/Flow/tree/master/flow/base64) - Base64 encoding/decoding 29 | * [**Location**](https://github.com/Hipbyte/Flow/tree/master/flow/location) - Location management and (reverse) geocoding 30 | * [**Task**](https://github.com/Hipbyte/Flow/tree/master/flow/task) - Lightweight tasks scheduler 31 | 32 | ### Installation 33 | 34 | `motion-flow` requires RubyMotion >= 4.12. Make sure [iOS](http://www.rubymotion.com/developers/guides/manuals/cocoa/getting-started/) and [Android](http://www.rubymotion.com/developers/guides/manuals/android/getting-started/) are correctly setup. 35 | 36 | ``` 37 | $ gem install motion-flow 38 | ``` 39 | 40 | If you are targeting Android, you need to install the dependencies with Gradle: 41 | 42 | ``` 43 | $ bundle && rake android:gradle:install 44 | ``` 45 | 46 | ### Projects 47 | 48 | #### Flow projects 49 | 50 | Flow comes with its own RubyMotion template, which creates a hybrid (iOS + Android + OS X) project. 51 | 52 | ``` 53 | $ motion create --template=flow Hello 54 | $ cd Hello 55 | $ rake -T 56 | ``` 57 | 58 | #### RubyMotion projects 59 | 60 | Flow can be added as a dependency of an existing iOS, Android or OS X RubyMotion project, by adding the `motion-flow` gem in the project's `Gemfile`. 61 | 62 | ### Code organization 63 | 64 | Each Flow library is contained in subdirectories inside the `flow` directory. 65 | Platform-specific code is contained inside subdirectories of each library 66 | (E.g. `cocoa` and `android`). 67 | 68 | ### Documentation 69 | 70 | The documentation is written separately in the `doc.rb` file. If you work on a PR, 71 | please modify this file accordingly. 72 | 73 | #### Generate documentation 74 | 75 | ``` 76 | yard 77 | ``` 78 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | EXTENSIONS = [ 2 | { :files => ['flow/ui/css_node.c'], 3 | :ios => { :lib => 'flow/ui/cocoa/libcss_node.a' }, 4 | :android => { :lib_armeabi => 'flow/ui/android/armeabi-v7a/libcss_node.a', 5 | :lib_x86 => 'flow/ui/android/x86/libcss_node.a' } }, 6 | { :files => ['flow/digest/cocoa/digest.m'], 7 | :ios => { :lib => 'flow/digest/cocoa/libdigest.a' } } 8 | ] 9 | 10 | BUILD_DIR = 'build' 11 | XCODE_PATH = '/Applications/Xcode.app' 12 | XCODE_IOS_DEPLOYMENT_TARGET = '7.0' 13 | ANDROID_NDK_PATH = File.expand_path(ENV['RUBYMOTION_ANDROID_NDK'] || '~/.rubymotion-android/ndk') 14 | ANDROID_API = '16' 15 | 16 | desc 'Build the extensions' 17 | task 'build' => [:"build:ios", :"build:android"] 18 | 19 | desc 'Build the extensions for iOS' 20 | task 'build:ios' do 21 | EXTENSIONS.each do |extension| 22 | src_paths = extension[:files] 23 | if extension[:ios] 24 | # iOS 25 | toolchain_bin = XCODE_PATH + '/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin' 26 | cc = toolchain_bin + '/clang' 27 | build_dir = File.join(BUILD_DIR, 'ios') 28 | objs = [] 29 | %w{iPhoneSimulator iPhoneOS}.each do |platform| 30 | sdk_path = "#{XCODE_PATH}/Contents/Developer/Platforms/#{platform}.platform/Developer/SDKs/#{platform}.sdk" 31 | cflags = "-isysroot \"#{sdk_path}\" -F#{sdk_path}/System/Library/Frameworks -fobjc-legacy-dispatch -fobjc-abi-version=2 -I. -Werror -I./include " 32 | case platform 33 | when 'iPhoneSimulator' 34 | cflags << " -arch i386 -arch x86_64 -mios-simulator-version-min=#{XCODE_IOS_DEPLOYMENT_TARGET} -DCC_TARGET_OS_IPHONE" 35 | when 'iPhoneOS' 36 | cflags << " -arch armv7 -arch armv7s -arch arm64 -mios-version-min=#{XCODE_IOS_DEPLOYMENT_TARGET} -DCC_TARGET_OS_IPHONE" 37 | end 38 | src_paths.each do |src_path| 39 | obj_path = File.join(build_dir, platform, src_path + '.o') 40 | if !File.exist?(obj_path) or File.mtime(src_path) > File.mtime(obj_path) 41 | mkdir_p File.dirname(obj_path) 42 | sh "#{cc} #{cflags} -c #{src_path} -o #{obj_path}" 43 | end 44 | objs << obj_path 45 | end 46 | end 47 | lib_path = extension[:ios][:lib] 48 | if !File.exist?(lib_path) or objs.any? { |x| File.mtime(x) > File.mtime(lib_path) } 49 | mkdir_p File.dirname(lib_path) 50 | rm_f lib_path 51 | sh "/usr/bin/ar rcu #{lib_path} #{objs.join(' ')}" 52 | sh "/usr/bin/ranlib #{lib_path}" 53 | end 54 | end 55 | end 56 | end 57 | 58 | desc 'Build the extensions for Android' 59 | task 'build:android' do 60 | EXTENSIONS.each do |extension| 61 | src_paths = extension[:files] 62 | if extension[:android] 63 | # Android 64 | cc = File.join(ANDROID_NDK_PATH, 'toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++') 65 | toolchain_bin = "#{ANDROID_NDK_PATH}/toolchains/x86-4.9/prebuilt/darwin-x86_64/i686-linux-android/bin" 66 | ar = toolchain_bin + "/ar" 67 | ranlib = toolchain_bin + "/ranlib" 68 | %w{armeabi x86}.each do |arch| 69 | cflags = 70 | case arch 71 | when 'x86' 72 | "-mno-sse -mno-mmx -no-canonical-prefixes -msoft-float -target i686-none-linux-android -gcc-toolchain \"#{ANDROID_NDK_PATH}/toolchains/x86-4.8/prebuilt/darwin-x86_64\" -MMD -MP -fpic -ffunction-sections -funwind-tables -fexceptions -fstack-protector -fno-strict-aliasing -O0 -fno-omit-frame-pointer -DANDROID -I\"#{ANDROID_NDK_PATH}/platforms/android-#{ANDROID_API}/arch-x86/usr/include\" -Wformat -Werror=format-security -DCC_TARGET_OS_ANDROID -I./include" 73 | when 'armeabi' 74 | "-no-canonical-prefixes -target armv5te-none-linux-androideabi -march=armv5te -mthumb -msoft-float -marm -gcc-toolchain \"#{ANDROID_NDK_PATH}/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64\" -mtune=xscale -MMD -MP -fpic -ffunction-sections -funwind-tables -fexceptions -fstack-protector -fno-strict-aliasing -fno-omit-frame-pointer -DANDROID -I\"#{ANDROID_NDK_PATH}/platforms/android-#{ANDROID_API}/arch-arm/usr/include\" -Wformat -Werror=format-security -DCC_TARGET_OS_ANDROID -I./include" 75 | end 76 | objs = [] 77 | src_paths.each do |src_path| 78 | obj_path = File.join(BUILD_DIR, 'android-' + arch, src_path + '.o') 79 | if !File.exist?(obj_path) or File.mtime(src_path) > File.mtime(obj_path) 80 | mkdir_p File.dirname(obj_path) 81 | sh "#{cc} #{cflags} -c #{src_path} -o #{obj_path}" 82 | end 83 | objs << obj_path 84 | end 85 | lib_path = extension[:android]["lib_#{arch}".intern] 86 | if !File.exist?(lib_path) or objs.any? { |x| File.mtime(x) > File.mtime(lib_path) } 87 | mkdir_p File.dirname(lib_path) 88 | rm_f lib_path 89 | sh "#{ar} rcu #{lib_path} #{objs.join(' ')}" 90 | sh "#{ranlib} #{lib_path}" 91 | end 92 | end 93 | end 94 | end 95 | end 96 | 97 | desc 'Clean up build files' 98 | task 'clean' do 99 | rm_rf BUILD_DIR 100 | EXTENSIONS.each do |extension| 101 | if extension[:ios] 102 | rm_f extension[:ios][:lib] 103 | end 104 | if extension[:android] 105 | extension[:android].each { |_, path| rm_f path } 106 | end 107 | end 108 | end 109 | 110 | desc 'Create motion-flow.gem file' 111 | task 'gem' => [:build] do 112 | sh "gem build motion-flow.gemspec" 113 | end 114 | 115 | task :default => :build 116 | -------------------------------------------------------------------------------- /flow-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/flow-logo.png -------------------------------------------------------------------------------- /flow/base64/README.md: -------------------------------------------------------------------------------- 1 | # Base64 2 | 3 | Base64 encoding/decoding library. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/Base64.html 8 | -------------------------------------------------------------------------------- /flow/base64/android/base64.rb: -------------------------------------------------------------------------------- 1 | class Base64 2 | def self.encode(string) 3 | bytes = Java::Lang::String.new(string).getBytes("UTF-8") 4 | Android::Util::Base64.encodeToString(bytes, Android::Util::Base64::NO_WRAP) 5 | end 6 | 7 | def self.decode(string) 8 | java_string = Java::Lang::String.new(string) 9 | bytes = Android::Util::Base64.decode(java_string, Android::Util::Base64::NO_WRAP) 10 | Java::Lang::String.new(bytes, "UTF-8") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /flow/base64/cocoa/base64.rb: -------------------------------------------------------------------------------- 1 | class Base64 2 | def self.encode(string) 3 | data = string.dataUsingEncoding(NSUTF8StringEncoding) 4 | data.base64EncodedStringWithOptions(0) 5 | end 6 | 7 | def self.decode(string) 8 | data = NSData.alloc.initWithBase64EncodedString(string, options: 0) 9 | NSString.alloc.initWithData(data, encoding: NSUTF8StringEncoding) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /flow/digest/README.md: -------------------------------------------------------------------------------- 1 | # Digest 2 | 3 | Cryprographic digest functions. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/Digest.html 8 | -------------------------------------------------------------------------------- /flow/digest/android/digest.rb: -------------------------------------------------------------------------------- 1 | module Digest 2 | class Base 3 | def initialize(algo) 4 | @digest = Java::Security::MessageDigest.getInstance(algo) 5 | end 6 | 7 | def update(str) 8 | @digest.update(str.chars.map { |x| x.ord }) 9 | self 10 | end 11 | 12 | def reset 13 | @digest.reset 14 | self 15 | end 16 | 17 | def digest 18 | @digest.digest.map { |x| String.format('%02x', x) }.join 19 | end 20 | 21 | # To be called from subclasses. 22 | def self.digest(str) 23 | self.new.update(str).digest 24 | end 25 | end 26 | 27 | class MD5 < Base; def initialize; super('MD5'); end; end 28 | class SHA1 < Base; def initialize; super('SHA1'); end; end 29 | class SHA224 < Base; def initialize; super('SHA224'); end; end 30 | class SHA256 < Base; def initialize; super('SHA256'); end; end 31 | class SHA384 < Base; def initialize; super('SHA384'); end; end 32 | class SHA512 < Base; def initialize; super('SHA512'); end; end 33 | end 34 | -------------------------------------------------------------------------------- /flow/digest/cocoa/digest.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FlowDigest : NSObject 4 | { 5 | void *_opaque; 6 | } 7 | 8 | + (id)digestWithAlgo:(NSString *)algo; 9 | 10 | - (void)update:(NSData *)data; 11 | - (void)reset; 12 | - (NSData *)digest; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /flow/digest/cocoa/digest.m: -------------------------------------------------------------------------------- 1 | #import "digest.h" 2 | #include 3 | 4 | struct FlowDigestOpaque { 5 | void *ctx; 6 | int (*init)(void *); 7 | int (*update)(void *, const void *, long); 8 | int (*final)(unsigned char *, void *); 9 | int digest_len; 10 | }; 11 | 12 | @implementation FlowDigest 13 | 14 | #define FLOW_OPAQUE ((struct FlowDigestOpaque *)_opaque) 15 | 16 | - (id)initWithAlgo:(NSString *)algo 17 | { 18 | self = [self init]; 19 | if (self == nil) { 20 | return nil; 21 | } 22 | 23 | struct FlowDigestOpaque *opaque = (struct FlowDigestOpaque *) 24 | malloc(sizeof(struct FlowDigestOpaque)); 25 | assert(opaque != NULL); 26 | 27 | #define INIT_OPAQUE2(ctx_algo, algo) \ 28 | opaque->ctx = (void *)malloc(sizeof(CC_##ctx_algo##_CTX)); \ 29 | assert(opaque->ctx != NULL); \ 30 | opaque->init = (int (*)(void *))CC_##algo##_Init; \ 31 | opaque->update = (int (*)(void *, const void *, long))CC_##algo##_Update; \ 32 | opaque->final = (int (*)(unsigned char *, void *))CC_##algo##_Final; \ 33 | opaque->digest_len = CC_##algo##_DIGEST_LENGTH; 34 | 35 | #define INIT_OPAQUE(algo) INIT_OPAQUE2(algo, algo) 36 | 37 | if ([algo isEqualToString:@"MD5"]) { 38 | INIT_OPAQUE(MD5) 39 | } 40 | else if ([algo isEqualToString:@"SHA1"]) { 41 | INIT_OPAQUE(SHA1) 42 | } 43 | else if ([algo isEqualToString:@"SHA224"]) { 44 | INIT_OPAQUE2(SHA256, SHA224) 45 | } 46 | else if ([algo isEqualToString:@"SHA256"]) { 47 | INIT_OPAQUE(SHA256) 48 | } 49 | else if ([algo isEqualToString:@"SHA384"]) { 50 | INIT_OPAQUE2(SHA512, SHA384) 51 | } 52 | else if ([algo isEqualToString:@"SHA512"]) { 53 | INIT_OPAQUE(SHA512) 54 | } 55 | else { 56 | NSLog(@"incorrect algorithm name %@", algo); 57 | free(opaque); 58 | abort(); 59 | } 60 | 61 | #undef INIT_OPAQUE 62 | #undef INIT_OPAQUE2 63 | 64 | _opaque = opaque; 65 | [self reset]; 66 | return self; 67 | } 68 | 69 | - (void)dealloc 70 | { 71 | free(FLOW_OPAQUE->ctx); 72 | free(_opaque); 73 | _opaque = NULL; 74 | [super dealloc]; 75 | } 76 | 77 | + (id)digestWithAlgo:(NSString *)algo 78 | { 79 | return [[[FlowDigest alloc] initWithAlgo:algo] autorelease]; 80 | } 81 | 82 | - (void)update:(NSData *)data 83 | { 84 | FLOW_OPAQUE->update(FLOW_OPAQUE->ctx, [data bytes], [data length]); 85 | } 86 | 87 | - (void)reset 88 | { 89 | FLOW_OPAQUE->init(FLOW_OPAQUE->ctx); 90 | } 91 | 92 | - (NSData *)digest 93 | { 94 | unsigned char *md = (unsigned char *)malloc(FLOW_OPAQUE->digest_len); 95 | assert(md != NULL); 96 | 97 | FLOW_OPAQUE->final(md, FLOW_OPAQUE->ctx); 98 | 99 | return [NSData dataWithBytesNoCopy:md length:FLOW_OPAQUE->digest_len]; 100 | } 101 | 102 | @end 103 | 104 | -------------------------------------------------------------------------------- /flow/digest/cocoa/digest.rb: -------------------------------------------------------------------------------- 1 | module Digest 2 | class Base 3 | def initialize(algo) 4 | @digest = FlowDigest.digestWithAlgo(algo) 5 | end 6 | 7 | def update(str) 8 | @digest.update(str.dataUsingEncoding(NSUTF8StringEncoding)) 9 | self 10 | end 11 | 12 | def reset 13 | @digest.reset 14 | self 15 | end 16 | 17 | def digest 18 | data = @digest.digest 19 | ptr = data.bytes 20 | digest = '' 21 | data.length.times do |i| 22 | byte = ptr[i] 23 | digest << '%02x' % byte 24 | end 25 | digest 26 | end 27 | 28 | # To be called from subclasses. 29 | def self.digest(str) 30 | self.new.update(str).digest 31 | end 32 | end 33 | 34 | class MD5 < Base; def initialize; super('MD5'); end; end 35 | class SHA1 < Base; def initialize; super('SHA1'); end; end 36 | class SHA224 < Base; def initialize; super('SHA224'); end; end 37 | class SHA256 < Base; def initialize; super('SHA256'); end; end 38 | class SHA384 < Base; def initialize; super('SHA384'); end; end 39 | class SHA512 < Base; def initialize; super('SHA512'); end; end 40 | end 41 | -------------------------------------------------------------------------------- /flow/json/README.md: -------------------------------------------------------------------------------- 1 | # JSON 2 | 3 | JSON serialization/deserialization library. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/JSON.html 8 | 9 | - http://www.rubymotion.com/developers/motion-flow/Object.html 10 | -------------------------------------------------------------------------------- /flow/json/android/json.rb: -------------------------------------------------------------------------------- 1 | class JSON 2 | def self.load(str) 3 | tok = Org::JSON::JSONTokener.new(str) 4 | obj = tok.nextValue 5 | if obj == nil 6 | raise "Can't deserialize object from JSON" 7 | end 8 | 9 | # Transform pure-Java JSON objects to Ruby types. 10 | convert_java(obj) 11 | end 12 | 13 | def self.convert_java(obj) 14 | case obj 15 | when Org::JSON::JSONArray 16 | obj.length.times.map { |i| convert_java(obj.get(i)) } 17 | when Org::JSON::JSONObject 18 | iter = obj.keys 19 | hash = Hash.new 20 | loop do 21 | break unless iter.hasNext 22 | key = iter.next 23 | value = obj.get(key) 24 | hash[convert_java(key)] = convert_java(value) 25 | end 26 | hash 27 | when Java::Lang::String 28 | obj.to_s 29 | when Org::JSON::JSONObject::NULL 30 | nil 31 | else 32 | obj 33 | end 34 | end 35 | end 36 | 37 | class Object 38 | def to_json 39 | # The Android JSON API expects real Java String objects. 40 | @@fix_string ||= (lambda do |obj| 41 | case obj 42 | when String, Symbol 43 | obj = obj.toString 44 | when Hash 45 | map = Hash.new 46 | obj.each do |key, value| 47 | key = key.toString if key.is_a?(String) || key.is_a?(Symbol) 48 | value = @@fix_string.call(value) 49 | map[key] = value 50 | end 51 | obj = map 52 | when Array 53 | obj = obj.map do |item| 54 | (item.is_a?(String) || item.is_a?(Symbol)) ? item.toString : @@fix_string.call(item) 55 | end 56 | end 57 | obj 58 | end) 59 | 60 | obj = Org::JSON::JSONObject.wrap(@@fix_string.call(self)) 61 | if obj == nil 62 | raise "Can't serialize object to JSON" 63 | end 64 | obj.toString.to_s 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /flow/json/cocoa/json.rb: -------------------------------------------------------------------------------- 1 | class JSON 2 | def self.load(string) 3 | error_ptr = Pointer.new(:id) 4 | obj = NSJSONSerialization.JSONObjectWithData(string.dataUsingEncoding(NSUTF8StringEncoding), options:0, error:error_ptr) 5 | if obj == nil 6 | raise error_ptr[0].description 7 | end 8 | obj 9 | end 10 | end 11 | 12 | class Object 13 | def to_json 14 | raise "Invalid JSON object" unless NSJSONSerialization.isValidJSONObject(self) 15 | error_ptr = Pointer.new(:id) 16 | data = NSJSONSerialization.dataWithJSONObject(self, options:0, error:error_ptr) 17 | if data == nil 18 | raise error_ptr[0].description 19 | end 20 | data.to_str 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /flow/location/README.md: -------------------------------------------------------------------------------- 1 | # Location 2 | 3 | Location management and geocoding / reverse geocoding library. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/Location.html 8 | -------------------------------------------------------------------------------- /flow/location/android/location_services.rb: -------------------------------------------------------------------------------- 1 | class MonitorDelegate 2 | def initialize(callback) 3 | @callback = callback 4 | end 5 | 6 | def onLocationChanged(location) 7 | @callback.call(Location._from_jlocation(location), nil) 8 | end 9 | 10 | def onProviderDisabled(provider) 11 | end 12 | 13 | def onProviderEnabled(provider) 14 | end 15 | 16 | def onStatusChanged(provider, status, extra) 17 | end 18 | end 19 | 20 | class Location 21 | def self._from_jlocation(jlocation) 22 | obj = Location.new 23 | obj.latitude = jlocation.latitude 24 | obj.longitude = jlocation.longitude 25 | obj.altitude = jlocation.altitude 26 | obj.time = Time.new(jlocation.time) 27 | obj.speed = jlocation.speed 28 | obj.accuracy = jlocation.accuracy 29 | obj 30 | end 31 | 32 | def self.context=(context) 33 | @context = context 34 | @location_service = context.getSystemService(Android::Content::Context::LOCATION_SERVICE) 35 | end 36 | 37 | def self._context 38 | @context 39 | end 40 | 41 | def self._location_service 42 | @location_service or raise "Call `Location.context = self' in your main activity" 43 | end 44 | 45 | class Monitor 46 | def self.enabled? 47 | Location._location_service.isProviderEnabled('gps') 48 | end 49 | 50 | def initialize(options, callback) 51 | @options = options 52 | @delegate = MonitorDelegate.new(callback) 53 | start 54 | end 55 | 56 | def start 57 | Location._location_service.requestLocationUpdates('gps', 1000, @options[:distance_filter], @delegate) 58 | end 59 | 60 | def stop 61 | Location._location_service.removeUpdates(@delegate) 62 | end 63 | end 64 | 65 | class Geocoder 66 | def self.enabled? 67 | Android::Location::Geocoder.isPresent 68 | end 69 | 70 | def initialize(obj, block) 71 | geocoder = Android::Location::Geocoder.new(Location._context) 72 | addresses = 73 | if location_given = obj.is_a?(Location) 74 | geocoder.getFromLocation(obj.latitude, obj.longitude, 1) 75 | else 76 | geocoder.getFromLocationName(obj.to_s, 1) 77 | end 78 | if addresses and !addresses.empty? 79 | address = addresses.first 80 | unless location_given 81 | obj = Location.new 82 | obj.latitude = address.latitude 83 | obj.longitude = address.longitude 84 | end 85 | obj.name = address.featureName 86 | obj.address = begin 87 | str = '' 88 | i = 0 89 | count = address.getMaxAddressLineIndex 90 | while i < count 91 | str << address.getAddressLine(i) + "\n" 92 | i += 1 93 | end 94 | str 95 | end 96 | obj.locality = address.locality 97 | obj.postal_code = address.postalCode 98 | obj.sub_area = address.subAdminArea 99 | obj.area = address.adminArea 100 | obj.country = address.countryCode 101 | block.call(obj, nil) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /flow/location/cocoa/location_services.rb: -------------------------------------------------------------------------------- 1 | class Location 2 | attr_accessor :_cllocation 3 | def self._from_cllocation(cllocation) 4 | obj = Location.new 5 | obj._cllocation = cllocation 6 | obj.latitude = cllocation.coordinate.latitude 7 | obj.longitude = cllocation.coordinate.longitude 8 | obj.altitude = cllocation.altitude 9 | obj.time = cllocation.timestamp 10 | obj.speed = cllocation.speed 11 | obj.accuracy = cllocation.horizontalAccuracy 12 | obj 13 | end 14 | 15 | class Monitor 16 | def self.enabled? 17 | CLLocationManager.locationServicesEnabled and CLLocationManager.authorizationStatus == KCLAuthorizationStatusAuthorized 18 | end 19 | 20 | def initialize(options, callback) 21 | @callback = callback 22 | @location_manager = CLLocationManager.new 23 | @location_manager.desiredAccuracy = KCLLocationAccuracyBest 24 | @location_manager.distanceFilter = options[:distance_filter] 25 | @location_manager.delegate = self 26 | if @location_manager.respond_to?(:requestAlwaysAuthorization) 27 | # iOS 8 or above. 28 | @location_manager.requestAlwaysAuthorization 29 | end 30 | start 31 | end 32 | 33 | def start 34 | @location_manager.startUpdatingLocation 35 | end 36 | 37 | def stop 38 | @location_manager.stopUpdatingLocation 39 | end 40 | 41 | def locationManager(manager, didUpdateLocations:locations) 42 | unless locations.empty? 43 | @callback.call(Location._from_cllocation(locations.last), nil) 44 | end 45 | end 46 | 47 | def locationManager(manager, didFailWithError:error) 48 | @callback.call(nil, error.localizedDescription) 49 | end 50 | end 51 | 52 | class Geocoder 53 | def self.enabled? 54 | true 55 | end 56 | 57 | def initialize(obj, block) 58 | @callback = block 59 | @geocoder = CLGeocoder.new 60 | if obj.is_a?(Location) 61 | @geocoder.reverseGeocodeLocation(obj._cllocation, completionHandler:lambda do |placemarks, error| 62 | _handle(obj, placemarks, error) 63 | end) 64 | else 65 | @geocoder.geocodeAddressString(obj.to_s, completionHandler:lambda do |placemarks, error| 66 | _handle(nil, placemarks, error) 67 | end) 68 | end 69 | end 70 | 71 | def _handle(location, placemarks, error) 72 | if placemarks 73 | placemark = placemarks.last 74 | location ||= Location._from_cllocation(placemark.location) 75 | location.name = placemark.name 76 | location.address = ABCreateStringWithAddressDictionary(placemark.addressDictionary, true) 77 | location.locality = placemark.locality 78 | location.postal_code = placemark.postalCode 79 | location.sub_area = placemark.subAdministrativeArea 80 | location.area = placemark.administrativeArea 81 | location.country = placemark.ISOcountryCode 82 | @callback.call(location, nil) 83 | else 84 | @callback.call(nil, error.localizedDescription) 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /flow/location/location.rb: -------------------------------------------------------------------------------- 1 | class Location 2 | attr_accessor :latitude, :longitude, :altitude, :time, :speed, :accuracy 3 | attr_accessor :name, :address, :locality, :postal_code, :sub_area, :area, :country 4 | 5 | def self.monitor_enabled? 6 | Location::Monitor.enabled? 7 | end 8 | 9 | def self.monitor(options={}, &block) 10 | options[:distance_filter] ||= 0 11 | Location::Monitor.new(options, block) 12 | end 13 | 14 | def self.geocode_enabled? 15 | Location::Geocoder.enabled? 16 | end 17 | 18 | def self.geocode(str, &block) 19 | Location::Geocoder.new(str, block) 20 | end 21 | 22 | def geocode(&block) 23 | Location::Geocoder.new(self, block) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /flow/net/README.md: -------------------------------------------------------------------------------- 1 | # Net 2 | 3 | HTTP networking / host reachability library. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/Net.html 8 | -------------------------------------------------------------------------------- /flow/net/actions.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Request 3 | module Actions 4 | [:get, :post, :put, :delete, :patch, :options, :head].each do |http_method| 5 | define_method(http_method) do |base_url, *options, callback| 6 | options = options.shift || {} 7 | request = Request.new(base_url, options.merge(method: http_method)) 8 | request.run(&callback) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /flow/net/android/cookies.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | def self.setup_cookies 3 | @setup_cookies ||= begin 4 | Java::Net::CookieHandler.setDefault(Java::Net::CookieManager.new) 5 | true 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /flow/net/android/reachability.rb: -------------------------------------------------------------------------------- 1 | class ReachabilityDelegate < Android::Net::ConnectivityManager::NetworkCallback 2 | def initialize(hostname, block) 3 | @hostname = hostname 4 | @block = block 5 | end 6 | 7 | def onAvailable(network) 8 | begin 9 | sockaddr = Java::Net::InetSocketAddress.new(@hostname, 80) 10 | sock = Java::Net::Socket.new 11 | sock.connect(sockaddr, 2000) # 2 seconds timeout 12 | sock.close 13 | @block.call(true) 14 | rescue 15 | @block.call(false) 16 | end 17 | end 18 | 19 | def onLost(network) 20 | @block.call(false) 21 | end 22 | end 23 | 24 | module Net 25 | def self.context=(context) 26 | @connectivity = context.getSystemService(Android::Content::Context::CONNECTIVITY_SERVICE) 27 | end 28 | 29 | def self._connectivity 30 | @connectivity or raise "Call `Net.context = self' in your main activity" 31 | end 32 | 33 | class Reachability 34 | def initialize(hostname, &block) 35 | builder = Android::Net::NetworkRequest::Builder.new 36 | builder.addCapability(Android::Net::NetworkCapabilities::NET_CAPABILITY_INTERNET) 37 | builder.addTransportType(Android::Net::NetworkCapabilities::TRANSPORT_WIFI) 38 | builder.addTransportType(Android::Net::NetworkCapabilities::TRANSPORT_CELLULAR) 39 | @delegate = ReachabilityDelegate.new(hostname, block) 40 | Net._connectivity.registerNetworkCallback(builder.build, @delegate) 41 | end 42 | 43 | def stop 44 | if @delegate 45 | Net._connectivity.unregisterNetworkCallback(@delegate) 46 | @delegate = nil 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /flow/net/android/request.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Request 3 | extend Actions 4 | include Request::Stubbable 5 | 6 | attr_reader :configuration, :session, :base_url 7 | 8 | def initialize(url, options = {}, session = nil) 9 | Net.setup_cookies 10 | 11 | @base_url = url 12 | @url = Java::Net::URL.new(url) 13 | @options = options 14 | @session = session 15 | @configuration = {} 16 | 17 | set_defaults 18 | configure 19 | end 20 | 21 | def run(&callback) 22 | return if stub!(&callback) 23 | 24 | Task.background do 25 | configuration[:headers].each do |key, value| 26 | url_connection.setRequestProperty(key, value) 27 | end 28 | 29 | if do_method? and body = configuration[:body] 30 | stream = url_connection.getOutputStream 31 | body = body.to_json if json? 32 | stream.write(Java::Lang::String.new(body).getBytes("UTF-8")) 33 | end 34 | 35 | response_code = url_connection.getResponseCode 36 | stream = response_code >= 400 ? url_connection.getErrorStream : url_connection.getInputStream 37 | 38 | response = nil 39 | if url_connection.getContentType.match(/^image\//) 40 | # Response as bitmap. 41 | response = Android::Graphics::BitmapFactory.decodeStream(stream) 42 | else 43 | # Response as text. 44 | input_reader = Java::Io::InputStreamReader.new(stream) 45 | input = Java::Io::BufferedReader.new(input_reader) 46 | response = Java::Lang::StringBuffer.new 47 | loop do 48 | line = input.readLine 49 | break unless line 50 | response.append(line) 51 | end 52 | input.close 53 | response = response.toString 54 | end 55 | 56 | Task.main do 57 | callback.call(ResponseProxy.build_response(url_connection, response)) 58 | end 59 | end 60 | end 61 | 62 | private 63 | 64 | def do_method? 65 | case configuration[:method] 66 | when :post, :put, :patch, :delete 67 | true 68 | end 69 | end 70 | 71 | def json? 72 | Net::MimeTypes::JSON.include?(configuration[:headers].fetch('Content-Type', nil)) 73 | end 74 | 75 | def url_connection 76 | @url_connection ||= begin 77 | connection = @url.openConnection 78 | connection.setRequestMethod(configuration[:method].to_s.upcase) 79 | connection.setDoOutput(true) if do_method? 80 | connection 81 | end 82 | end 83 | 84 | def set_defaults 85 | configuration[:headers] = { 86 | 'User-Agent' => Config.user_agent, 87 | 'Content-Type' => 'application/x-www-form-urlencoded' 88 | } 89 | configuration[:method] = :get 90 | configuration[:body] = "" 91 | configuration[:connect_timeout] = Config.connect_timeout 92 | configuration[:read_timeout] = Config.read_timeout 93 | end 94 | 95 | def configure 96 | if session 97 | configuration[:headers].merge!(session.headers) 98 | if session.authorization 99 | configuration[:headers].merge!({'Authorization' => session.authorization.to_s}) 100 | end 101 | end 102 | 103 | configuration.merge!(@options) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /flow/net/android/response_proxy.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class ResponseProxy 3 | def self.build_response(connection, response) 4 | new(connection, response).response 5 | end 6 | 7 | def initialize(connection, response) 8 | @connection = connection 9 | @response = response 10 | end 11 | 12 | def response 13 | Response.new({ 14 | status_code: status_code, 15 | status_message: status_message, 16 | headers: headers, 17 | body: build_body 18 | }) 19 | end 20 | 21 | private 22 | 23 | def mime_type 24 | @connection.getContentType 25 | end 26 | 27 | def status_code 28 | @connection.getResponseCode 29 | end 30 | 31 | def status_message 32 | @connection.getResponseMessage 33 | end 34 | 35 | def headers 36 | hash = Hash.new 37 | hash.putAll(@connection.getHeaderFields) 38 | hash.each do |k,v| 39 | array = [] 40 | array.addAll(v) 41 | hash[k] = array.join(',') 42 | end 43 | hash 44 | end 45 | 46 | def json? 47 | mime_type && Net::MimeTypes.json.any? { |json_type| mime_type.include?(json_type) } 48 | end 49 | 50 | def build_body 51 | json? ? JSON.load(@response) : @response 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /flow/net/authorization.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Authorization 3 | def initialize(options = {}) 4 | @options = options 5 | 6 | if !basic? && !token? 7 | raise "Invalid arguments given for Authorization" 8 | end 9 | end 10 | 11 | def to_s 12 | if basic? 13 | base_64 = Base64.encode("#{username}:#{password}") 14 | return "Basic #{base_64}" 15 | end 16 | if token? 17 | return 'Token token="' + token + '"' 18 | end 19 | end 20 | 21 | def username 22 | @options.fetch(:username, false) 23 | end 24 | 25 | def password 26 | @options.fetch(:password, false) 27 | end 28 | 29 | def token 30 | @options.fetch(:token, false) 31 | end 32 | 33 | def basic? 34 | !!username && !!password 35 | end 36 | 37 | def token? 38 | !!token 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /flow/net/cocoa/reachability.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Reachability 3 | def initialize(hostname, &block) 4 | @internal_callback = Proc.new do |target, flags, pointer| 5 | block.call(reachable_with_flags?(flags)) 6 | end 7 | block.weak! 8 | @reachability = SCNetworkReachabilityCreateWithName(KCFAllocatorDefault, 9 | hostname.UTF8String) 10 | 11 | start_tracking_network 12 | end 13 | 14 | def stop 15 | SCNetworkReachabilityUnscheduleFromRunLoop(@reachability, 16 | CFRunLoopGetCurrent(), 17 | KCFRunLoopDefaultMode) 18 | SCNetworkReachabilitySetCallback(@reachability, nil, @context) 19 | end 20 | 21 | protected 22 | 23 | def reachable_with_flags?(flags) 24 | return false unless flags 25 | 26 | if (flags & KSCNetworkReachabilityFlagsReachable) == 0 27 | return false 28 | end 29 | 30 | true 31 | end 32 | 33 | def start_tracking_network 34 | @context = Pointer.new(SCNetworkReachabilityContext.type) 35 | if SCNetworkReachabilitySetCallback(@reachability, @internal_callback, 36 | @context) 37 | if SCNetworkReachabilityScheduleWithRunLoop(@reachability, 38 | CFRunLoopGetCurrent(), 39 | KCFRunLoopDefaultMode) 40 | return true 41 | end 42 | end 43 | 44 | false 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /flow/net/cocoa/request.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Request 3 | extend Actions 4 | include Request::Stubbable 5 | 6 | attr_reader :configuration, :session, :base_url 7 | 8 | def initialize(url, options = {}, session = nil) 9 | @base_url = url 10 | @url = NSURL.URLWithString(url) 11 | @options = options 12 | @session = session 13 | @configuration = {} 14 | 15 | set_defaults 16 | configure 17 | end 18 | 19 | def run(&callback) 20 | return if stub!(&callback) 21 | 22 | Task.background do 23 | handler = lambda { |body, response, error| 24 | if response.nil? && error 25 | raise error.localizedDescription 26 | end 27 | Task.main do 28 | callback.call(ResponseProxy.build_response(body, response)) 29 | end 30 | } 31 | task = ns_url_session.dataTaskWithRequest(ns_mutable_request, 32 | completionHandler:handler) 33 | task.resume 34 | end 35 | end 36 | 37 | private 38 | 39 | def ns_url_session 40 | @ns_url_session ||= NSURLSession.sessionWithConfiguration(ns_url_session_configuration, delegate:self, delegateQueue:nil) 41 | end 42 | 43 | def ns_mutable_request 44 | @ns_mutable_request ||= begin 45 | request = NSMutableURLRequest.requestWithURL(@url) 46 | request.setHTTPMethod(configuration[:method].to_s.upcase) 47 | request.setHTTPBody(build_body(configuration[:body]), dataUsingEncoding:NSUTF8StringEncoding) 48 | request 49 | end 50 | end 51 | 52 | def ns_url_session_configuration 53 | @ns_url_session_configuration ||= begin 54 | config = NSURLSessionConfiguration.defaultSessionConfiguration 55 | config.setHTTPAdditionalHeaders(configuration[:headers]) 56 | config.timeoutIntervalForRequest = configuration[:connect_timeout] 57 | config.timeoutIntervalForResource = configuration[:read_timeout] 58 | config.HTTPMaximumConnectionsPerHost = 1 59 | config 60 | end 61 | end 62 | 63 | def json? 64 | Net::MimeTypes::JSON.include?(configuration[:headers].fetch('Content-Type', nil)) 65 | end 66 | 67 | def build_body(body) 68 | (json? and body != '') ? body.to_json.dataUsingEncoding(NSUTF8StringEncoding) : body.dataUsingEncoding(NSUTF8StringEncoding) 69 | end 70 | 71 | def set_defaults 72 | configuration[:headers] = { 73 | 'User-Agent' => Config.user_agent, 74 | 'Content-Type' => 'application/x-www-form-urlencoded' 75 | } 76 | configuration[:method] = :get 77 | configuration[:body] = "" 78 | configuration[:connect_timeout] = Config.connect_timeout 79 | configuration[:read_timeout] = Config.read_timeout 80 | end 81 | 82 | def configure 83 | if session 84 | configuration[:headers].merge!(session.headers) 85 | if session.authorization 86 | configuration[:headers].merge!({'Authorization' => session.authorization.to_s}) 87 | end 88 | end 89 | 90 | configuration.merge!(@options) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /flow/net/cocoa/response_proxy.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class ResponseProxy 3 | def self.build_response(raw_body, response) 4 | new(raw_body, response).response 5 | end 6 | 7 | def initialize(raw_body, response) 8 | @raw_body = raw_body 9 | @response = response 10 | end 11 | 12 | def response 13 | Response.new({ 14 | status_code: status_code, 15 | status_message: status_message, 16 | headers: headers, 17 | body: build_body 18 | }) 19 | end 20 | 21 | private 22 | 23 | def status_message 24 | message = CFHTTPMessageCreateResponse(KCFAllocatorDefault, 25 | @response.statusCode, nil, KCFHTTPVersion1_1) 26 | CFHTTPMessageCopyResponseStatusLine(message) 27 | end 28 | 29 | def headers 30 | @response.allHeaderFields 31 | end 32 | 33 | def mime_type 34 | @response.MIMEType 35 | end 36 | 37 | def status_code 38 | @response.statusCode 39 | end 40 | 41 | def json? 42 | mime_type && Net::MimeTypes.json.any? { |json_type| mime_type.include?(json_type) } 43 | end 44 | 45 | def build_body 46 | json? && !@raw_body.length.zero? ? JSON.load(@raw_body.to_str) : @raw_body 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /flow/net/config.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Config 3 | USER_AGENT = "Flow - https://github.com/HipByte/flow" 4 | 5 | class << self 6 | attr_accessor :user_agent 7 | attr_accessor :connect_timeout 8 | attr_accessor :read_timeout 9 | 10 | def user_agent 11 | @user_agent ||= Net::Config::USER_AGENT 12 | end 13 | 14 | def connect_timeout 15 | @connect_timeout ||= 30 16 | end 17 | 18 | def read_timeout 19 | @read_timeout ||= 604800 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /flow/net/expectation.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Expectation 3 | attr_reader :response 4 | attr_reader :url 5 | 6 | def initialize(url) 7 | @url = url 8 | end 9 | 10 | def and_return(response) 11 | @response = response 12 | end 13 | 14 | def response 15 | @response.mock = true 16 | @response 17 | end 18 | 19 | def matches?(request) 20 | url_match?(request.base_url) 21 | end 22 | 23 | class << self 24 | def all 25 | @expectations ||= [] 26 | end 27 | 28 | def clear 29 | all.clear 30 | end 31 | 32 | def response_for(request) 33 | expectation = find_by(request) 34 | return nil if expectation.nil? 35 | expectation.response 36 | end 37 | 38 | def find_by(request) 39 | all.find do |expectation| 40 | expectation.matches?(request) 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def url_match?(request_url) 48 | case @url 49 | when String 50 | @url == request_url 51 | when Regexp 52 | @url === request_url 53 | when nil 54 | true 55 | else 56 | false 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /flow/net/header.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Header 3 | SHORTHANDS = { 4 | content_type: 'Content-Type', 5 | accept: 'Accept', 6 | json: 'application/json' 7 | } 8 | 9 | attr_reader :field, :value 10 | 11 | def initialize(field, value) 12 | @field = SHORTHANDS.fetch(field, field) 13 | @value = SHORTHANDS.fetch(value, value) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /flow/net/mime_types.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module MimeTypes 3 | JSON = %w( 4 | application/json 5 | application/vnd.api+json 6 | ) 7 | 8 | def self.json 9 | JSON 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /flow/net/net.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class << self 3 | def build(base_url, &block) 4 | Session.build(base_url, &block) 5 | end 6 | 7 | def reachable?(hostname = 'www.google.com', &block) 8 | Reachability.new(hostname, &block) 9 | end 10 | 11 | def stub(base_url) 12 | expectation = Expectation.all.find{ |e| e.base_url == base_url } 13 | if expectation.nil? 14 | expectation = Expectation.new(base_url) 15 | Expectation.all << expectation 16 | end 17 | expectation 18 | end 19 | 20 | [:get, :post, :put, :delete, :patch, :options, :head].each do |http_method| 21 | define_method(http_method) do |base_url, *options, &callback| 22 | Request.send(http_method, base_url, options.shift || {}, callback) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /flow/net/response.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Response 3 | attr_accessor :options, :mock 4 | 5 | def initialize(options = {}) 6 | @options = options 7 | @headers = options[:headers] 8 | @mock = false 9 | end 10 | 11 | def status 12 | options[:status_code] 13 | end 14 | 15 | def status_message 16 | options[:status_message] 17 | end 18 | 19 | def mime_type 20 | options[:mime_type] 21 | end 22 | 23 | def body 24 | options.fetch(:body, "") 25 | end 26 | 27 | def headers 28 | @headers 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /flow/net/session.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Session 3 | attr_reader :authorization 4 | 5 | def initialize(base_url, &block) 6 | @base_url = base_url 7 | @authorization = false 8 | @headers = [] 9 | self.instance_eval(&block) 10 | end 11 | 12 | def self.build(base_url, &block) 13 | new(base_url, &block) 14 | end 15 | 16 | [:get, :post, :put, :delete, :patch, :options, :head].each do |http_method| 17 | define_method(http_method) do |endpoint, *options, &callback| 18 | options = (options.shift || {}).merge({method: http_method}) 19 | request = Request.new("#{@base_url}#{endpoint}", options, self) 20 | request.run(&callback) 21 | end 22 | end 23 | 24 | def header(field, value) 25 | @headers << Header.new(field, value) 26 | end 27 | 28 | def headers 29 | hash = {} 30 | @headers.map {|header| hash[header.field] = header.value} 31 | hash 32 | end 33 | 34 | def authorize(options) 35 | @authorization = Authorization.new(options) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /flow/net/stubbable.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Request 3 | module Stubbable 4 | def stub!(&callback) 5 | if response = Expectation.response_for(self) 6 | callback.call(response) 7 | true 8 | end 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /flow/store/README.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | Simple key/value storage library. 4 | 5 | This library was inspired by https://github.com/GantMan/PackingPeanut. 6 | 7 | ## Documentation 8 | 9 | - http://www.rubymotion.com/developers/motion-flow/Store.html 10 | -------------------------------------------------------------------------------- /flow/store/android/store.rb: -------------------------------------------------------------------------------- 1 | class Store 2 | DoesNotExist = '<____does_not_exist____>' 3 | def self.[](key) 4 | val = _storage.getString(key.to_s, DoesNotExist) 5 | val == DoesNotExist ? nil : JSON.load(val) 6 | end 7 | 8 | def self.[]=(key, value) 9 | editor = _storage.edit 10 | editor.putString(key.to_s, value.to_json) 11 | editor.commit 12 | end 13 | 14 | def self.delete(key) 15 | editor = _storage.edit 16 | editor.remove(key.to_s) 17 | editor.commit 18 | end 19 | 20 | def self.all 21 | all = {} 22 | _storage.getAll.each { |key, value| all[key] = JSON.load(value) } 23 | all 24 | end 25 | 26 | def self.context=(context) 27 | @storage = context.getSharedPreferences('userdefaults', Android::Content::Context::MODE_PRIVATE) 28 | end 29 | 30 | def self._storage 31 | @storage or raise "Call `Store.context = self' in your main activity" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /flow/store/cocoa/store.rb: -------------------------------------------------------------------------------- 1 | class Store 2 | def self.[](key) 3 | _storage.objectForKey(key.to_s) 4 | end 5 | 6 | def self.[]=(key, value) 7 | _storage.setObject(value, forKey:key.to_s) 8 | end 9 | 10 | def self.delete(key) 11 | _storage.removeObjectForKey(key.to_s) 12 | end 13 | 14 | def self.all 15 | _storage.dictionaryRepresentation 16 | end 17 | 18 | def self._storage 19 | NSUserDefaults.standardUserDefaults 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /flow/task/README.md: -------------------------------------------------------------------------------- 1 | # Task 2 | 3 | Task manager / scheduler. A simpler (but portable) version of Apple's dispatch library. Uses `Dispatch` on Cocoa and `java.util.concurrent.Executors` on Android. 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/Task.html 8 | -------------------------------------------------------------------------------- /flow/task/android/task.rb: -------------------------------------------------------------------------------- 1 | class Task 2 | def self.main? 3 | Android::Os::Looper.myLooper == Android::Os::Looper.getMainLooper 4 | end 5 | 6 | class Timer 7 | def initialize(interval, repeats, block) 8 | interval *= 1000 9 | if interval <= 0 10 | raise ArgError, "negative or too small interval" 11 | end 12 | handle = Android::Os::Handler.new 13 | runnable = Proc.new { handle.post block } 14 | @timer = Java::Util::Concurrent::Executors.newSingleThreadScheduledExecutor 15 | @future = 16 | if repeats 17 | @timer.scheduleAtFixedRate(runnable, interval, interval, Java::Util::Concurrent::TimeUnit::MILLISECONDS) 18 | else 19 | @timer.schedule(runnable, interval, Java::Util::Concurrent::TimeUnit::MILLISECONDS) 20 | end 21 | end 22 | 23 | def stop 24 | if @future 25 | @future.cancel(true) 26 | @future = nil 27 | end 28 | end 29 | end 30 | 31 | class Queue 32 | def initialize 33 | @queue = Java::Util::Concurrent::Executors.newSingleThreadExecutor 34 | end 35 | 36 | def schedule(&block) 37 | @queue.execute(block) 38 | end 39 | 40 | def wait 41 | @queue.submit(-> {}).get 42 | end 43 | 44 | def self.schedule_on_main(block) 45 | @main_handle ||= begin 46 | looper = Android::Os::Looper.getMainLooper 47 | Android::Os::Handler.new(looper) 48 | end 49 | @main_handle.post(block) 50 | end 51 | 52 | def self.schedule_on_background(block) 53 | @thread_pool ||= Java::Util::Concurrent::Executors.newCachedThreadPool 54 | @thread_pool.execute(block) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /flow/task/cocoa/task.rb: -------------------------------------------------------------------------------- 1 | class Task 2 | def self.main? 3 | NSThread.isMainThread 4 | end 5 | 6 | class Timer 7 | def initialize(interval, repeats, block) 8 | queue = Dispatch::Queue.current 9 | @timer = Dispatch::Source.timer(interval, interval, 0.0, queue) do |src| 10 | begin 11 | block.call 12 | ensure 13 | stop unless repeats 14 | end 15 | end 16 | end 17 | 18 | def stop 19 | if @timer 20 | @timer.cancel! 21 | @timer = nil 22 | end 23 | end 24 | end 25 | 26 | class Queue 27 | @@counter = 0 28 | def initialize 29 | @queue = Dispatch::Queue.new("com.hipbyte.flow.queue#{@@counter += 1}") 30 | end 31 | 32 | def schedule(&block) 33 | @queue.async(&block) 34 | end 35 | 36 | def wait 37 | @queue.sync {} 38 | end 39 | 40 | def self.schedule_on_main(block) 41 | Dispatch::Queue.main.async(&block) 42 | end 43 | 44 | def self.schedule_on_background(block) 45 | Dispatch::Queue.concurrent.async(&block) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /flow/task/task.rb: -------------------------------------------------------------------------------- 1 | class Task 2 | def self.every(interval, &block) 3 | Task::Timer.new(interval, true, block) 4 | end 5 | 6 | def self.after(delay, &block) 7 | Task::Timer.new(delay, false, block) 8 | end 9 | 10 | def self.main(&block) 11 | Task::Queue.schedule_on_main(block) 12 | end 13 | 14 | def self.background(&block) 15 | Task::Queue.schedule_on_background(block) 16 | end 17 | 18 | def self.queue 19 | Task::Queue.new 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /flow/ui/README.md: -------------------------------------------------------------------------------- 1 | # UI 2 | 3 | Cross platform UI toolkit 4 | 5 | ## Documentation 6 | 7 | - http://www.rubymotion.com/developers/motion-flow/UI.html 8 | 9 | - http://www.rubymotion.com/developers/motion-flow/CSSNode.html 10 | -------------------------------------------------------------------------------- /flow/ui/alert.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | def self.alert(opt={}, &block) 3 | alert = UI::Alert.new 4 | alert.title = (opt[:title] or raise ":title needed") 5 | alert.message = (opt[:message] or raise ":message needed") 6 | 7 | buttons = [:cancel, :default] 8 | has_button = false 9 | buttons.each do |button| 10 | if title = opt[button] 11 | alert.set_button(title, button) 12 | has_button = true 13 | end 14 | end 15 | alert.set_button('Cancel', :cancel) unless has_button 16 | 17 | alert.show(&block) 18 | @last_alert = alert # Keep a strong reference to the alert object has it has to remain alive. 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /flow/ui/android/activity_indicator.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class ActivityIndicator < UI::View 3 | def initialize 4 | super 5 | self.hidden = true 6 | end 7 | 8 | def start 9 | self.hidden = false 10 | end 11 | 12 | def stop 13 | self.hidden = true 14 | end 15 | 16 | def animating? 17 | !hidden? 18 | end 19 | 20 | def color=(color) 21 | proxy.indeterminateDrawable.tint = UI::Color(color).proxy 22 | end 23 | 24 | def proxy 25 | @proxy ||= begin 26 | progress_bar = Android::Widget::ProgressBar.new(UI.context) 27 | progress_bar.setIndeterminate(true) 28 | progress_bar 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /flow/ui/android/alert.rb: -------------------------------------------------------------------------------- 1 | class FlowUIAlertClickListener 2 | def initialize(alert) 3 | @alert = alert 4 | end 5 | 6 | def onClick(alert_dialog, button_type) 7 | type = case button_type 8 | when Android::App::AlertDialog::BUTTON_NEGATIVE 9 | :cancel 10 | else 11 | :default 12 | end 13 | @alert._clicked(type) 14 | end 15 | end 16 | 17 | module UI 18 | class Alert 19 | def title=(title) 20 | proxy.setTitle(title) 21 | end 22 | 23 | def message=(message) 24 | proxy.setMessage(message) 25 | end 26 | 27 | def set_button(title, type) 28 | button_type = case type 29 | when :cancel 30 | Android::App::AlertDialog::BUTTON_NEGATIVE 31 | when :default 32 | Android::App::AlertDialog::BUTTON_NEUTRAL 33 | else 34 | raise "expected :cancel or :default" 35 | end 36 | @listener ||= FlowUIAlertClickListener.new(self) 37 | proxy.setButton(button_type, title, @listener) 38 | end 39 | 40 | def show(&block) 41 | @complete_block = (block or raise "expected block") 42 | proxy.show 43 | end 44 | 45 | def _clicked(type) 46 | @complete_block.call(type) 47 | @listener = nil 48 | end 49 | 50 | def proxy 51 | @proxy ||= Android::App::AlertDialog.new(UI.context) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /flow/ui/android/application.rb: -------------------------------------------------------------------------------- 1 | class UI::Application 2 | attr_reader :navigation 3 | 4 | @@instance = nil 5 | def initialize(navigation, context) 6 | UI.context = context 7 | @navigation = navigation 8 | UI.context.contentView = proxy 9 | UI.context.supportActionBar.elevation = UI.density # one bottom border line 10 | @@instance = self 11 | end 12 | 13 | def self.instance 14 | @@instance 15 | end 16 | 17 | def start 18 | @navigation.start 19 | end 20 | 21 | def open_url(url) 22 | _open_url(Android::Content::Intent::ACTION_VIEW, url) 23 | end 24 | 25 | def open_phone_call(number) 26 | _open_url(Android::Content::Intent::ACTION_CALL, "tel:" + number) 27 | end 28 | 29 | def _open_url(action, url) 30 | intent = Android::Content::Intent.new(action, Android::Net::Uri.parse(url)) 31 | UI.context.startActivity(intent) 32 | end 33 | 34 | def proxy 35 | @proxy ||= begin 36 | frame_layout = Android::Widget::FrameLayout.new(UI.context) 37 | frame_layout.id = Android::View::View.generateViewId 38 | frame_layout 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /flow/ui/android/button.rb: -------------------------------------------------------------------------------- 1 | class FlowUIButtonClickListener 2 | def initialize(view) 3 | @view = view 4 | end 5 | 6 | def onClick(button) 7 | @view.trigger :tap 8 | end 9 | end 10 | 11 | module UI 12 | class Button < UI::Control 13 | include Eventable 14 | 15 | def initialize 16 | super 17 | calculate_measure(true) 18 | end 19 | 20 | def height=(val) 21 | super 22 | calculate_measure(false) 23 | end 24 | 25 | def measure(width, height) 26 | dimension = [width, height] 27 | unless width.nan? 28 | spacing_mult = @line_height ? 0 : 1 29 | spacing_add = @line_height || 0 30 | layout = Android::Text::StaticLayout.new(proxy.text, proxy.paint, width, Android::Text::Layout::Alignment::ALIGN_NORMAL, spacing_mult, spacing_add, true) 31 | dimension[1] = layout.height 32 | end 33 | dimension 34 | end 35 | 36 | def color 37 | @type == :text ? UI::Color(proxy.textColor) : nil 38 | end 39 | 40 | def color=(color) 41 | _change_type :text 42 | proxy.textColor = UI::Color(color).proxy 43 | end 44 | 45 | def title 46 | @type == :text ? proxy.text : nil 47 | end 48 | 49 | def title=(text) 50 | _change_type :text 51 | proxy.text = text 52 | end 53 | 54 | def font 55 | @type == :text ? UI::Font._wrap(proxy.typeface, proxy.textSize) : nil 56 | end 57 | 58 | def font=(font) 59 | _change_type :text 60 | font = UI::Font(font) 61 | proxy.setTypeface(font.proxy) 62 | proxy.setTextSize(font.size) 63 | end 64 | 65 | def image=(source) 66 | _change_type :image 67 | drawable = UI::Image._drawable_from_source(source) 68 | proxy.imageDrawable = drawable 69 | proxy.setPadding(0, 0, 0, 0) 70 | proxy.scaleType = Android::Widget::ImageView::ScaleType::FIT_CENTER 71 | proxy.backgroundColor = Android::Graphics::Color::TRANSPARENT 72 | end 73 | 74 | def _change_type(type) 75 | if @type != type 76 | @type = type 77 | @proxy = nil 78 | end 79 | end 80 | 81 | def proxy 82 | @proxy ||= begin 83 | case @type 84 | when :text 85 | button = Android::Widget::Button.new(UI.context) 86 | button.setPadding(0, 0, 0, 0) 87 | button.allCaps = false 88 | when :image 89 | button = Android::Widget::ImageButton.new(UI.context) 90 | else 91 | raise "incorrect button type `#{@type}'" 92 | end 93 | button.onClickListener = FlowUIButtonClickListener.new(self) 94 | button 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /flow/ui/android/camera.rb: -------------------------------------------------------------------------------- 1 | class FlowUICameraSurfaceHolderCallback 2 | def initialize(view) 3 | @view = view 4 | end 5 | 6 | def surfaceCreated(holder) 7 | @view._surface_created 8 | end 9 | 10 | def surfaceChanged(holder, format, width, height) 11 | # Do nothing. 12 | end 13 | 14 | def surfaceDestroyed(holder) 15 | @view._surface_destroyed 16 | end 17 | end 18 | 19 | class FlowUICameraDetectorProcessor 20 | def initialize(view) 21 | @view = view 22 | end 23 | 24 | def receiveDetections(detections) 25 | Task.main do 26 | barcodes = detections.detectedItems 27 | barcodes.size.times do |i| 28 | barcode = barcodes.valueAt(i) 29 | @view.trigger :barcode_scanned, barcode.rawValue 30 | end 31 | end 32 | end 33 | 34 | def release 35 | end 36 | end 37 | 38 | module UI 39 | class Camera < UI::View 40 | include Eventable 41 | 42 | attr_accessor :detect_barcode_types, :facing 43 | 44 | def start 45 | barcode_formats = (@detect_barcode_types or []).map do |format| 46 | case format 47 | when :aztec 48 | Com::Google::Android::Gms::Vision::Barcode::Barcode::AZTEC 49 | when :code_128 50 | Com::Google::Android::Gms::Vision::Barcode::Barcode::CODE_128 51 | when :code_39 52 | Com::Google::Android::Gms::Vision::Barcode::Barcode::CODE_39 53 | when :code_93 54 | Com::Google::Android::Gms::Vision::Barcode::Barcode::CODE_93 55 | when :codabar 56 | Com::Google::Android::Gms::Vision::Barcode::Barcode::CODABAR 57 | when :data_matrix 58 | Com::Google::Android::Gms::Vision::Barcode::Barcode::DATA_MATRIX 59 | when :ean_13 60 | Com::Google::Android::Gms::Vision::Barcode::Barcode::EAN_13 61 | when :ean_8 62 | Com::Google::Android::Gms::Vision::Barcode::Barcode::EAN_8 63 | when :itf 64 | Com::Google::Android::Gms::Vision::Barcode::Barcode::ITF 65 | when :pdf_417 66 | Com::Google::Android::Gms::Vision::Barcode::Barcode::PDF417 67 | when :qrcode 68 | Com::Google::Android::Gms::Vision::Barcode::Barcode::QR_CODE 69 | when :upc_a 70 | Com::Google::Android::Gms::Vision::Barcode::Barcode::UPC_A 71 | when :upc_e 72 | Com::Google::Android::Gms::Vision::Barcode::Barcode::UPC_E 73 | else 74 | raise "Incorrect value `#{format}' for `detect_barcode_types'" 75 | end 76 | end.inject(0) { |b, m| b | m } 77 | 78 | facing = case (@facing or :back) 79 | when :front 80 | Com::Google::Android::Gms::Vision::CameraSource::CAMERA_FACING_FRONT 81 | when :back 82 | Com::Google::Android::Gms::Vision::CameraSource::CAMERA_FACING_BACK 83 | else 84 | raise "Incorrect value `#{@facing}' for `facing'" 85 | end 86 | 87 | barcode_detector = Com::Google::Android::Gms::Vision::Barcode::BarcodeDetector::Builder.new(UI.context).setBarcodeFormats(barcode_formats).build 88 | barcode_detector.setProcessor(FlowUICameraDetectorProcessor.new(self)) 89 | #puts "barcode_detector.isOperational -> #{barcode_detector.isOperational}" 90 | 91 | @camera_source = Com::Google::Android::Gms::Vision::CameraSource::Builder.new(UI.context, barcode_detector).setFacing(facing).setAutoFocusEnabled(true).build 92 | 93 | if proxy.holder.isCreating 94 | @start_after_created = true 95 | else 96 | _start 97 | end 98 | end 99 | 100 | def _start 101 | Task.after 0.5 { @camera_source.start(proxy.holder) } 102 | end 103 | 104 | def stop 105 | if @camera_source 106 | @camera_source.stop 107 | @camera_source = nil 108 | end 109 | end 110 | 111 | def _surface_created 112 | _start if @start_after_created 113 | end 114 | 115 | def _surface_destroyed 116 | stop 117 | end 118 | 119 | def take_capture 120 | puts "Not yet implemented" 121 | end 122 | 123 | def proxy 124 | @proxy ||= begin 125 | surface_view = Android::View::SurfaceView.new(UI.context) 126 | surface_view.holder.addCallback(FlowUICameraSurfaceHolderCallback.new(self)) 127 | surface_view 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /flow/ui/android/color.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Color 3 | def self._native?(color) 4 | color.is_a?(Fixnum) 5 | end 6 | 7 | def self.rgba(r, g, b, a) 8 | new Android::Graphics::Color.argb(a, r, g, b) 9 | end 10 | 11 | def red 12 | Android::Graphics::Color.red(proxy) 13 | end 14 | 15 | def green 16 | Android::Graphics::Color.green(proxy) 17 | end 18 | 19 | def blue 20 | Android::Graphics::Color.blue(proxy) 21 | end 22 | 23 | def alpha 24 | Android::Graphics::Color.alpha(proxy) 25 | end 26 | 27 | def to_a 28 | [red, green, blue, alpha] 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /flow/ui/android/control.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Control < UI::View 3 | def blur 4 | proxy.clearFocus 5 | end 6 | 7 | def focus 8 | proxy.requestFocus 9 | end 10 | 11 | def focus? 12 | proxy.hasFocus 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /flow/ui/android/font.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Font 3 | def self._wrap(font, size) 4 | new(font, size, nil) 5 | end 6 | 7 | BuiltinFonts = { 8 | 'ComingSoon' => 'casual', 9 | 'DancingScript-Regular' => 'cursive', 10 | 'DroidSansMono' => 'monospace', 11 | 'Roboto-Regular' => 'sans-serif', 12 | 'Roboto-Black' => 'sans-serif-black', 13 | 'RobotoCondensed-Regular' => 'sans-serif-condensed', 14 | 'RobotoCondensed-Light' => 'sans-serif-condensed-light', 15 | 'Roboto-Light' => 'sans-serif-light', 16 | 'Roboto-Medium' => 'sans-serif-medium', 17 | 'CarroisGothicSC-Regular' => 'sans-serif-smallcaps', 18 | 'Roboto-Thin' => 'sans-serif-thin', 19 | 'NotoSerif-Regular' => 'serif', 20 | 'CutiveMono' => 'serif-monospace' 21 | } 22 | 23 | def initialize(obj, size, trait=nil, extension=:ttf) 24 | if obj.is_a?(Android::Graphics::Typeface) 25 | @proxy = obj 26 | else 27 | family_name = BuiltinFonts[obj] 28 | if family_name 29 | style = case trait 30 | when :bold 31 | Android::Graphics::Typeface::BOLD 32 | when :italic 33 | Android::Graphics::Typeface::ITALIC 34 | when :bold_italic 35 | Android::Graphics::Typeface::BOLD_ITALIC 36 | else 37 | Android::Graphics::Typeface::NORMAL 38 | end 39 | @proxy = Android::Graphics::Typeface.create(family_name, style) 40 | else 41 | @proxy = Android::Graphics::Typeface.createFromAsset(UI.context.getAssets, obj + "." + extension.to_s) 42 | end 43 | end 44 | @size = size 45 | end 46 | 47 | def name 48 | nil # TODO 49 | end 50 | 51 | def size 52 | @size 53 | end 54 | 55 | def trait 56 | @trait ||= begin 57 | case proxy.getStyle 58 | when Android::Graphics::Typeface::BOLD 59 | :bold 60 | when Android::Graphics::Typeface::ITALIC 61 | :italic 62 | when Android::Graphics::Typeface::BOLD_ITALIC 63 | :bold_italic 64 | else 65 | :normal 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /flow/ui/android/gesture.rb: -------------------------------------------------------------------------------- 1 | class FlowUIGestureListener < Android::View::GestureDetector::SimpleOnGestureListener 2 | attr_accessor :gestures 3 | 4 | def onDown(e) 5 | true 6 | end 7 | 8 | def onFling(e1, e2, velocity_x, velocity_y) 9 | @gestures.any? { |x| x.on_fling(e1, e2) } 10 | end 11 | end 12 | 13 | class FlowUIGestureTouchListener 14 | def initialize(detector) 15 | @detector = detector 16 | end 17 | 18 | def onTouch(view, event) 19 | @detector.onTouchEvent(event) 20 | end 21 | end 22 | 23 | module UI 24 | class SwipeGesture 25 | def initialize(view, args, block) 26 | unless listener = view.instance_variable_get(:'@_gesture_listener') 27 | listener = FlowUIGestureListener.new 28 | listener.gestures = [] 29 | @detector = Android::View::GestureDetector.new(UI.context, listener) 30 | view.proxy.onTouchListener = FlowUIGestureTouchListener.new(@detector) 31 | view.instance_variable_set(:'@_gesture_listener', listener) 32 | end 33 | listener.gestures << self 34 | @args = (args || [:right]) 35 | @block = block 36 | end 37 | 38 | def on_fling(e1, e2) 39 | e1_x, e1_y = e1.rawX, e1.rawY 40 | e2_x, e2_y = e2.rawX, e2.rawY 41 | delta_x = (e2_x - e1_x).abs 42 | delta_y = (e2_y - e1_y).abs 43 | threshold = 100 44 | (@args.any? do |arg| 45 | case arg 46 | when :left 47 | e2_x < e1_x and delta_x > threshold 48 | when :right 49 | e2_x > e1_x and delta_x > threshold 50 | when :up 51 | e2_x > e1_x and delta_y > threshold 52 | when :down 53 | e2_x < e1_x and delta_y > threshold 54 | end 55 | end and (@block.call; true)) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /flow/ui/android/gradient.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Gradient 3 | attr_reader :colors 4 | 5 | def colors=(colors) 6 | raise ArgError, "must receive an array of 2 or 3 colors" if colors.size < 2 or colors.size > 3 7 | @colors = colors.map { |x| UI::Color(x).proxy } 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /flow/ui/android/image.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Image < UI::View 3 | attr_reader :source 4 | 5 | def self._drawable_from_source(source) 6 | if source.is_a?(Android::Graphics::Bitmap) 7 | drawable = Android::Graphics::Drawable::BitmapDrawable.new(UI.context.resources, source) 8 | else 9 | candidates = [source] 10 | if UI.density > 0 11 | base = source.sub(/\.png$/, '') 12 | (1...UI.density.to_i).each do |i| 13 | candidates.unshift base + "@#{i + 1}x.png" 14 | end 15 | end 16 | 17 | @asset_files ||= UI.context.assets.list('') 18 | idx = candidates.index { |x| @asset_files.include?(x) } 19 | raise "Couldn't find an asset file named `#{source}'" unless idx 20 | 21 | stream = UI.context.assets.open(candidates[idx]) 22 | drawable = Android::Graphics::Drawable::Drawable.createFromStream(stream, nil) 23 | 24 | image_density = UI.density - idx 25 | if image_density != UI.density 26 | bitmap = drawable.bitmap 27 | scale = (UI.density / image_density) 28 | size_x = drawable.intrinsicWidth * scale 29 | size_y = drawable.intrinsicHeight * scale 30 | bitmap_resized = Android::Graphics::Bitmap.createScaledBitmap(bitmap, size_x, size_y, false) 31 | drawable = Android::Graphics::Drawable::BitmapDrawable.new(UI.context.resources, bitmap_resized) 32 | end 33 | end 34 | drawable 35 | end 36 | 37 | def source=(source) 38 | if @source != source 39 | @source = source 40 | drawable = self.class._drawable_from_source(source) 41 | if self.border_radius and self.border_radius > 0 42 | drawable = Android::Support::V4::Graphics::Drawable::RoundedBitmapDrawableFactory.create(UI.context.resources, drawable.bitmap) 43 | drawable.antiAlias = true 44 | drawable.cornerRadius = self.border_radius * UI.density 45 | end 46 | proxy.imageDrawable = drawable 47 | if width.nan? and height.nan? 48 | self.width = drawable.intrinsicWidth 49 | self.height = drawable.intrinsicHeight 50 | end 51 | end 52 | end 53 | 54 | def filter=(color) 55 | proxy.setColorFilter(UI::Color(color).proxy, Android::Graphics::PorterDuff::Mode::MULTIPLY) 56 | end 57 | 58 | RESIZE_MODES = { 59 | cover: Android::Widget::ImageView::ScaleType::CENTER_CROP, 60 | contain: Android::Widget::ImageView::ScaleType::CENTER_INSIDE, 61 | stretch: Android::Widget::ImageView::ScaleType::FIT_XY 62 | } 63 | 64 | def resize_mode=(resize_mode) 65 | proxy.scaleType = RESIZE_MODES.fetch(resize_mode.to_sym) do 66 | raise "Incorrect value, expected one of: #{RESIZE_MODES.keys.join(',')}" 67 | end 68 | end 69 | 70 | def resize_mode 71 | RESIZE_MODES.key(proxy.scaleType) 72 | end 73 | 74 | def proxy 75 | @proxy ||= Android::Widget::ImageView.new(UI.context) 76 | end 77 | 78 | def _regenerate_background 79 | # Do nothing. 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /flow/ui/android/label.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Label < UI::View 3 | include UI::SharedText 4 | 5 | def initialize 6 | super 7 | calculate_measure(true) 8 | self.text_alignment = :left 9 | end 10 | 11 | def height=(val) 12 | super 13 | calculate_measure(false) 14 | end 15 | 16 | def measure(width, height) 17 | dimension = [width, height] 18 | unless width.nan? 19 | spacing_mult = @line_height ? 0 : 1 20 | spacing_add = @line_height || 0 21 | layout = Android::Text::StaticLayout.new(proxy.text, proxy.paint, width, Android::Text::Layout::Alignment::ALIGN_NORMAL, spacing_mult, spacing_add, true) 22 | dimension[1] = layout.height 23 | end 24 | dimension 25 | end 26 | 27 | def proxy 28 | @proxy ||= Android::Widget::TextView.new(UI.context) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /flow/ui/android/list.rb: -------------------------------------------------------------------------------- 1 | class FlowUIListViewAdapter < Android::Widget::BaseAdapter 2 | def initialize(list) 3 | @list = list 4 | @cached_rows = {} 5 | end 6 | 7 | def getCount 8 | @list.data_source.size 9 | end 10 | 11 | def getItem(pos) 12 | @list.data_source[pos] 13 | end 14 | 15 | def getItemId(pos) 16 | pos 17 | end 18 | 19 | def getView(pos, convert_view, parent_view) 20 | data = @list.data_source[pos] 21 | (@cached_rows[data] ||= begin 22 | view = @list.render_row_block.call(0, pos).new 23 | view.list = @list if view.respond_to?(:list=) 24 | view.width = parent_view.width / UI.density 25 | view.update(data) if view.respond_to?(:update) 26 | view.update_layout 27 | view._autolayout_when_resized = true 28 | view 29 | end).proxy 30 | end 31 | 32 | def row_at_index(pos) 33 | # TODO this should be optimized 34 | @cached_rows[@list.data_source[pos]] 35 | end 36 | end 37 | 38 | class FlowUIListItemClickListener 39 | def initialize(list) 40 | @list = list 41 | end 42 | 43 | def onItemClick(parent, view, position, id) 44 | row = @list.row_at_index(position) 45 | @list.trigger :select, @list.data_source[position], position, row 46 | end 47 | end 48 | 49 | module UI 50 | class List < UI::View 51 | include Eventable 52 | 53 | def initialize 54 | super 55 | @data_source = [] 56 | @render_row_block = lambda { |section_index, row_index| ListRow } 57 | end 58 | 59 | attr_reader :data_source 60 | 61 | def data_source=(data_source) 62 | if @data_source != data_source 63 | @data_source = data_source 64 | proxy.adapter.notifyDataSetChanged 65 | end 66 | end 67 | 68 | attr_reader :render_row_block 69 | 70 | def render_row(&block) 71 | @render_row_block = block 72 | end 73 | 74 | def row_at_index(pos) 75 | proxy.adapter.row_at_index(pos) 76 | end 77 | 78 | def proxy 79 | @proxy ||= begin 80 | list_view = Android::Widget::ListView.new(UI.context) 81 | list_view.adapter = FlowUIListViewAdapter.new(self) 82 | list_view.onItemClickListener = FlowUIListItemClickListener.new(self) 83 | list_view.divider = nil 84 | list_view.dividerHeight = 0 85 | list_view.itemsCanFocus = true 86 | list_view.descendantFocusability = Android::View::ViewGroup::FOCUS_AFTER_DESCENDANTS 87 | list_view 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /flow/ui/android/navigation.rb: -------------------------------------------------------------------------------- 1 | class UI::Navigation 2 | attr_reader :root_screen 3 | 4 | def initialize(root_screen) 5 | @root_screen = root_screen 6 | @root_screen.navigation = self 7 | @current_screens = [] 8 | end 9 | 10 | def screen 11 | @current_screens.last 12 | end 13 | 14 | def hide_bar 15 | bar = UI.context.supportActionBar 16 | if bar.isShowing 17 | bar.hide 18 | Task.after 0.05 do 19 | screen.view.height += (bar.height / UI.density) 20 | screen.view.update_layout 21 | end 22 | end 23 | end 24 | 25 | def show_bar 26 | bar = UI.context.supportActionBar 27 | if !bar.isShowing 28 | bar.show 29 | Task.after 0.05 do 30 | screen.view.height -= (bar.height / UI.density) 31 | screen.view.update_layout 32 | end 33 | end 34 | end 35 | 36 | def title=(title) 37 | UI.context.supportActionBar.title = title 38 | end 39 | 40 | def bar_color=(color) 41 | UI.context.supportActionBar.backgroundDrawable = Android::Graphics::Drawable::ColorDrawable.new(UI::Color(color).proxy) 42 | end 43 | 44 | def items=(items) 45 | fragment = @current_screens.last.proxy 46 | 47 | has_menu = false 48 | has_menu |= (fragment._buttons = items[:buttons]) 49 | has_menu |= (fragment._options_menu_items = items[:options_menu_items]) 50 | has_menu |= (UI.context.supportActionBar.displayHomeAsUpEnabled = (items[:home_button_enabled] or false)) 51 | fragment.hasOptionsMenu = has_menu 52 | end 53 | 54 | BACK_STACK_ROOT_TAG = 'FlowBackStackRootTag' 55 | 56 | def _fragment_tag(fragment) 57 | "fragment-#{fragment.hashCode}" 58 | end 59 | 60 | def start 61 | replace @root_screen, false 62 | end 63 | 64 | def push(screen, animated=true) 65 | screen.navigation = self 66 | 67 | fragment = screen.proxy 68 | fragment._animate = animated ? :slide : false 69 | 70 | current_fragment = @current_screens.last.proxy 71 | current_fragment._animate = animated ? :fade : false 72 | 73 | transaction = proxy.beginTransaction 74 | transaction.hide(current_fragment) 75 | transaction.add(UI::Application.instance.proxy.id, fragment, _fragment_tag(fragment)) 76 | transaction.addToBackStack(BACK_STACK_ROOT_TAG) 77 | transaction.commit 78 | 79 | @current_screens << screen 80 | end 81 | 82 | def pop(animated=true) 83 | if @current_screens.size > 1 84 | current_screen = @current_screens.pop 85 | current_screen.proxy._animate = animated ? :slide : false 86 | previous_screen = @current_screens.last 87 | previous_screen.proxy._animate = animated ? :fade : false 88 | 89 | previous_screen.before_on_show 90 | proxy.popBackStack 91 | previous_screen.on_show 92 | 93 | current_screen 94 | else 95 | nil 96 | end 97 | end 98 | 99 | def replace(new_screen, animated=true) 100 | # TODO: honor `animated' 101 | proxy.popBackStack(BACK_STACK_ROOT_TAG, Android::App::FragmentManager::POP_BACK_STACK_INCLUSIVE) 102 | 103 | new_screen.navigation = self 104 | fragment = new_screen.proxy 105 | transaction = proxy.beginTransaction 106 | if already_added = fragment.isAdded 107 | if first_screen = @current_screens.first 108 | transaction.hide(first_screen.proxy) 109 | end 110 | transaction.show(fragment) 111 | else 112 | transaction.add(UI::Application.instance.proxy.id, fragment, _fragment_tag(fragment)) 113 | end 114 | new_screen.before_on_show if already_added 115 | transaction.commit 116 | new_screen.on_show if already_added 117 | 118 | @current_screens = [new_screen] 119 | end 120 | 121 | def share_panel(text, animated=true) 122 | # TODO: honor `animated' 123 | Android::Support::V4::App::ShareCompat::IntentBuilder.from(UI.context).setText(text).setType("text/plain").startChooser 124 | end 125 | 126 | def proxy 127 | @proxy ||= UI.context.fragmentManager 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /flow/ui/android/screen.rb: -------------------------------------------------------------------------------- 1 | class FlowUIFragment < Android::App::Fragment 2 | def initialize(screen) 3 | @screen = screen 4 | end 5 | 6 | def onCreateView(inflater, proxy, savedInstanceState) 7 | @view ||= begin 8 | @screen.before_on_load 9 | @screen.view.proxy 10 | end 11 | end 12 | 13 | def onResume 14 | super 15 | @screen.before_on_show 16 | @screen.on_show 17 | end 18 | 19 | attr_accessor :_animate 20 | 21 | def onCreateAnimator(transit, enter, next_anim) 22 | if _animate 23 | animator = Android::Animation::ObjectAnimator.new 24 | animator.target = self 25 | animator.duration = 300 26 | case _animate 27 | when :fade 28 | animator.propertyName = "alpha" 29 | animator.setFloatValues(enter ? [0, 1] : [1, 0]) 30 | when :slide 31 | display = UI.context.windowManager.defaultDisplay 32 | size = Android::Graphics::Point.new 33 | display.getSize(size) 34 | animator.propertyName = "translationX" 35 | animator.setFloatValues(enter ? [size.x, 0] : [0, size.x]) 36 | else 37 | raise "incorrect _animate value `#{_animate}'" 38 | end 39 | animator 40 | else 41 | nil 42 | end 43 | end 44 | 45 | attr_accessor :_buttons, :_options_menu_items 46 | 47 | def onCreateOptionsMenu(menu, inflater) 48 | menu.clear 49 | @menu_actions = {} 50 | n = 0 51 | if _buttons 52 | _buttons.each do |opt| 53 | title = opt[:title] 54 | item = menu.add(Android::View::Menu::NONE, n, Android::View::Menu::NONE, title) 55 | mode = Android::View::MenuItem::SHOW_AS_ACTION_ALWAYS 56 | mode |= Android::View::MenuItem::SHOW_AS_ACTION_WITH_TEXT if title 57 | item.showAsAction = mode 58 | unless title 59 | drawable = UI::Image._drawable_from_source(opt[:image]) 60 | drawable.targetDensity = UI.context.resources.displayMetrics.densityDpi # XXX needed so that the image properly scales, to investigate why 61 | item.icon = drawable 62 | end 63 | @menu_actions[n] = opt[:action] 64 | n += 1 65 | end 66 | end 67 | if _options_menu_items 68 | _options_menu_items.each do |opt| 69 | menu.add(Android::View::Menu::NONE, n, Android::View::Menu::NONE, opt[:title]) 70 | @menu_actions[n] = opt[:action] 71 | n += 1 72 | end 73 | end 74 | end 75 | 76 | def onOptionsItemSelected(menu_item) 77 | id = menu_item.itemId 78 | if id == Android::R::Id::Home 79 | @screen.navigation.pop 80 | elsif action = @menu_actions[id] 81 | @screen.send(action) 82 | end 83 | end 84 | end 85 | 86 | module UI 87 | class Screen 88 | attr_accessor :navigation 89 | 90 | def before_on_load 91 | CSSNode.set_scale(UI.density) 92 | view.background_color = :white 93 | on_load 94 | end 95 | def on_load; end 96 | 97 | def before_on_show; end 98 | def on_show; end 99 | 100 | def view 101 | @view ||= begin 102 | view = UI::View.new 103 | view.proxy.setLayoutParams(Android::View::ViewGroup::LayoutParams.new(Android::View::ViewGroup::LayoutParams::MATCH_PARENT, Android::View::ViewGroup::LayoutParams::MATCH_PARENT)) 104 | metrics = Android::Util::DisplayMetrics.new 105 | main_screen_metrics = UI.context.windowManager.defaultDisplay.getMetrics(metrics) 106 | 107 | view_height = main_screen_metrics.height 108 | view_height -= UI.status_bar_height # assume solid status bar by default 109 | 110 | view.width = main_screen_metrics.width / UI.density 111 | view.height = view_height / UI.density 112 | view 113 | end 114 | end 115 | 116 | def status_bar_style=(style) 117 | window = UI.context.window 118 | case o = style[:background] 119 | when :transparent 120 | window.addFlags(Android::View::WindowManager::LayoutParams::FLAG_TRANSLUCENT_STATUS) 121 | unless @status_bar_transparent 122 | view.height += (UI.status_bar_height / UI.density) 123 | view.update_layout 124 | @status_bar_transparent = true 125 | end 126 | else 127 | # A color. 128 | window.addFlags(Android::View::WindowManager::LayoutParams::FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 129 | window.statusBarColor = UI.color(o).proxy 130 | end 131 | end 132 | 133 | def self.size 134 | metrics = Android::Util::DisplayMetrics.new 135 | main_screen_metrics = UI.context.windowManager.defaultDisplay.getMetrics(metrics) 136 | [main_screen_metrics.width / UI.density, main_screen_metrics.height / UI.density] 137 | end 138 | 139 | def proxy 140 | @proxy ||= FlowUIFragment.new(self) 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /flow/ui/android/shared_text.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | module SharedText 3 | def text_proxy; proxy; end 4 | 5 | def text_alignment 6 | case (text_proxy.gravity & Android::View::Gravity::HORIZONTAL_GRAVITY_MASK) 7 | when Android::View::Gravity::LEFT 8 | :left 9 | when Android::View::Gravity::RIGHT 10 | :right 11 | else 12 | :center 13 | end 14 | end 15 | 16 | def text_alignment=(sym) 17 | val = Android::View::Gravity::CENTER_VERTICAL 18 | val |= case sym 19 | when :left 20 | Android::View::Gravity::LEFT 21 | when :center 22 | Android::View::Gravity::CENTER_HORIZONTAL 23 | when :right 24 | Android::View::Gravity::RIGHT 25 | else 26 | raise "Incorrect value, should be :left, :center or :right" 27 | end 28 | text_proxy.gravity = val 29 | end 30 | 31 | def color 32 | UI::Color(text_proxy.textColor) 33 | end 34 | 35 | def color=(color) 36 | text_proxy.textColor = text_proxy.hintTextColor = UI::Color(color).proxy 37 | end 38 | 39 | def text=(text) 40 | text_proxy.text = text 41 | end 42 | 43 | def text 44 | text_proxy.text.toString 45 | end 46 | 47 | def font 48 | UI::Font._wrap(text_proxy.typeface, text_proxy.textSize) 49 | end 50 | 51 | def font=(font) 52 | font = UI::Font(font) 53 | text_proxy.typeface = font.proxy 54 | text_proxy.textSize = font.size 55 | end 56 | 57 | def line_height=(spacing) 58 | if @line_height != spacing 59 | @line_height = spacing * UI.density 60 | text_proxy.setLineSpacing(@line_height, 0) 61 | end 62 | end 63 | 64 | def line_height 65 | @line_height / UI.density 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /flow/ui/android/text.rb: -------------------------------------------------------------------------------- 1 | class FlowUITextViewSpan < Android::Text::Style::ClickableSpan 2 | def initialize(view, link) 3 | @view, @link = view, link 4 | end 5 | 6 | def onClick(view) 7 | @view.trigger :link, @link 8 | end 9 | end 10 | 11 | module UI 12 | class Text < UI::View 13 | include UI::SharedText 14 | def text_proxy; proxy; @text_proxy; end 15 | include Eventable 16 | 17 | def editable? 18 | @editable 19 | end 20 | 21 | def editable=(flag) 22 | if @editable != flag 23 | @editable = flag 24 | @proxy = nil 25 | end 26 | end 27 | 28 | attr_accessor :link_color 29 | 30 | def links=(links) 31 | ss = Android::Text::SpannableString.new(self.text) 32 | 33 | links.each do |range, link| 34 | span = FlowUITextViewSpan.new(self, link) 35 | ss.setSpan(span, range.begin, range.end, Android::Text::Spanned::SPAN_EXCLUSIVE_EXCLUSIVE) 36 | 37 | if @link_color 38 | fspan = Android::Text::Style::ForegroundColorSpan.new(UI.Color(@link_color).proxy) 39 | ss.setSpan(fspan, range.begin, range.end, Android::Text::Spanned::SPAN_INCLUSIVE_INCLUSIVE) 40 | end 41 | end 42 | 43 | @text_proxy.text = ss 44 | @text_proxy.movementMethod = Android::Text::Method::LinkMovementMethod.getInstance 45 | end 46 | 47 | def proxy 48 | @proxy ||= begin 49 | scroll = Android::Widget::ScrollView.new(UI.context) 50 | @text_proxy = @editable ? Android::Widget::EditText.new(UI.context) : Android::Widget::TextView.new(UI.context) 51 | scroll.addView(@text_proxy) 52 | scroll 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /flow/ui/android/text_input.rb: -------------------------------------------------------------------------------- 1 | class FlowUITextInputTextChangedListener 2 | def initialize(view) 3 | @view = view 4 | end 5 | 6 | def afterTextChanged(s) 7 | # Do nothing. 8 | end 9 | 10 | def beforeTextChanged(s, start, count, after) 11 | # Do nothing. 12 | end 13 | 14 | def onTextChanged(s, start, before, count) 15 | @view.trigger(:change, @view.text) 16 | end 17 | end 18 | 19 | class FlowUITextInputDateListener 20 | def initialize(view) 21 | @view = view 22 | end 23 | 24 | def onFocusChange(view, has_focus) 25 | _show_date_picker_dialog if has_focus 26 | end 27 | 28 | def onClick(view) 29 | _show_date_picker_dialog 30 | end 31 | 32 | def onDateSet(view, year, month, day) 33 | @current_date = [year, month, day] 34 | @view.trigger :change, year, month + 1, day 35 | end 36 | 37 | def _show_date_picker_dialog 38 | if @current_date 39 | year, month, day = @current_date 40 | else 41 | calendar = Java::Util::Calendar.getInstance 42 | year = calendar.get(Java::Util::Calendar::YEAR) 43 | month = calendar.get(Java::Util::Calendar::MONTH) 44 | day = calendar.get(Java::Util::Calendar::DAY_OF_MONTH) 45 | end 46 | Android::App::DatePickerDialog.new(UI.context, self, year, month, day).show 47 | end 48 | end 49 | 50 | module UI 51 | class TextInput < UI::Control 52 | include UI::SharedText 53 | include Eventable 54 | 55 | def secure? 56 | (proxy.inputType & Android::Text::InputType::TYPE_TEXT_VARIATION_PASSWORD) != 0 57 | end 58 | 59 | def secure=(flag) 60 | type = Android::Text::InputType::TYPE_CLASS_TEXT 61 | type |= Android::Text::InputType::TYPE_TEXT_VARIATION_PASSWORD if flag 62 | proxy.inputType = type 63 | end 64 | 65 | def placeholder=(text) 66 | proxy.hint = text 67 | end 68 | 69 | def placeholder 70 | proxy.hint 71 | end 72 | 73 | def input_offset 74 | proxy.paddingLeft 75 | end 76 | 77 | def input_offset=(padding) 78 | proxy.setPadding(padding * UI.density, 0, 0, 0) 79 | end 80 | 81 | attr_reader :date_picker 82 | 83 | def date_picker=(flag) 84 | if @date_picker != flag 85 | @date_picker = flag 86 | if flag 87 | listener = FlowUITextInputDateListener.new(self) 88 | proxy.onFocusChangeListener = listener 89 | proxy.onClickListener = listener 90 | proxy.removeTextChangedListener(@text_changed_listener) 91 | proxy.keyListener = nil 92 | else 93 | proxy.onFocusChangeListener = nil 94 | proxy.onClickListener = nil 95 | proxy.addTextChangedListener(@text_changed_listener) 96 | proxy.keyListener = @key_listener 97 | end 98 | end 99 | end 100 | 101 | def proxy 102 | @proxy ||= begin 103 | edit_text = Android::Widget::EditText.new(UI.context) 104 | edit_text.setPadding(0, 0, 0, 0) 105 | edit_text.backgroundColor = Android::Graphics::Color::TRANSPARENT 106 | @text_changed_listener = FlowUITextInputTextChangedListener.new(self) 107 | edit_text.addTextChangedListener(@text_changed_listener) 108 | @key_listener = edit_text.keyListener 109 | edit_text 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /flow/ui/android/ui.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | def self.context 3 | @context or raise "Context missing" 4 | end 5 | 6 | def self.context=(context) 7 | @context = context 8 | end 9 | 10 | def self.density 11 | @density ||= UI.context.resources.displayMetrics.density 12 | end 13 | 14 | def self.status_bar_height 15 | @status_bar_height ||= begin 16 | resource_id = UI.context.resources.getIdentifier("status_bar_height", "dimen", "android") 17 | resource_id > 0 ? UI.context.resources.getDimensionPixelSize(resource_id) : 0 18 | end 19 | end 20 | 21 | def self.resource_str(name) 22 | if stream = UI.context.assets.open(name) 23 | input_reader = Java::Io::InputStreamReader.new(stream) 24 | input = Java::Io::BufferedReader.new(input_reader) 25 | buf = Java::Lang::StringBuffer.new 26 | loop do 27 | break unless line = input.readLine 28 | buf.append(line) 29 | end 30 | input.close 31 | buf.toString 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /flow/ui/android/view.rb: -------------------------------------------------------------------------------- 1 | class FlowUIViewLayoutChangeListener 2 | def initialize(view) 3 | @view = view 4 | end 5 | 6 | def onLayoutChange(view, left, top, right, bottom, old_left, old_top, old_right, old_bottom) 7 | if (right - left) != (old_right - old_left) or (top - bottom) != (old_top - old_bottom) 8 | @view.update_layout 9 | end 10 | end 11 | end 12 | 13 | module UI 14 | class View < CSSNode 15 | attr_accessor :_previous_width, :_previous_height 16 | 17 | # These properties are used when generating the background drawable right after layout. 18 | attr_accessor :background_color, :background_gradient, :border_color, :border_width, :border_radius, :shadow_offset, :shadow_color, :shadow_radius 19 | 20 | def hidden? 21 | proxy.visibility != Android::View::View::VISIBLE 22 | end 23 | 24 | def hidden=(flag) 25 | if flag != hidden? 26 | if flag 27 | if !self.width.nan? 28 | self._previous_width = self.width 29 | self.width = 0 30 | end 31 | 32 | if !self.height.nan? 33 | self._previous_height = self.height 34 | self.height = 0 35 | end 36 | else 37 | self.width = self._previous_width if self._previous_width 38 | self.height = self._previous_height if self._previous_height 39 | end 40 | 41 | proxy.visibility = flag ? Android::View::View::INVISIBLE : Android::View::View::VISIBLE 42 | 43 | self.root.update_layout 44 | end 45 | end 46 | 47 | def alpha 48 | proxy.alpha 49 | end 50 | 51 | def alpha=(value) 52 | proxy.alpha = value 53 | end 54 | 55 | def add_child(child) 56 | super 57 | proxy.addView(child.proxy) 58 | end 59 | 60 | def insert_child(index, child) 61 | super 62 | proxy.addView(child.proxy, index) 63 | end 64 | 65 | def delete_child(child) 66 | if super 67 | proxy.removeView(child.proxy) 68 | end 69 | end 70 | 71 | def update_layout 72 | super 73 | _apply_layout 74 | end 75 | 76 | def proxy 77 | @proxy ||= Android::Widget::FrameLayout.new(UI.context) 78 | end 79 | 80 | def _apply_layout 81 | if params = proxy.layoutParams 82 | left, top, width, height = layout 83 | if params.is_a?(Android::View::ViewGroup::MarginLayoutParams) 84 | params.leftMargin = left 85 | params.topMargin = top 86 | end 87 | resized = params.width != width or params.height != height 88 | params.width = width 89 | params.height = height 90 | proxy.layoutParams = params 91 | _regenerate_background if resized 92 | end 93 | children.each { |child| child._apply_layout } 94 | end 95 | 96 | def _regenerate_background 97 | return unless @background_gradient or @background_color or @border_width or @shadow_radius 98 | 99 | width, height = layout[2], layout[3] 100 | return unless width > 0 and height > 0 101 | 102 | bitmap = Android::Graphics::Bitmap.createBitmap(width, height, Android::Graphics::Bitmap::Config::ARGB_8888) 103 | canvas = Android::Graphics::Canvas.new(bitmap) 104 | 105 | paint = Android::Graphics::Paint.new 106 | if @background_gradient 107 | colors = @background_gradient.colors 108 | positions = case colors.size 109 | when 2 then [0, 1] 110 | when 3 then [0, 0.5, 1] 111 | else raise "invalid number of colors" 112 | end 113 | paint.shader = Android::Graphics::LinearGradient.new(0, 0, 0, height, colors, positions, Android::Graphics::Shader::TileMode::MIRROR) 114 | elsif @background_color 115 | paint.color = UI::Color(@background_color).proxy 116 | end 117 | 118 | shadow_radius = (@shadow_radius || 0) * UI.density 119 | if shadow_radius > 0 120 | x = y = 0 121 | if @shadow_offset 122 | x, y = @shadow_offset 123 | end 124 | color = UI::Color(@shadow_color || :black).proxy 125 | paint.setShadowLayer(shadow_radius, x, y, color) 126 | proxy.setLayerType(Android::View::View::LAYER_TYPE_SOFTWARE, paint) # disable hardware acceleration when drawing shadows, seems required on some devices 127 | proxy.setPadding(shadow_radius, shadow_radius, shadow_radius, shadow_radius) 128 | end 129 | 130 | corner_radius = (@border_radius || 0) * UI.density 131 | canvas.drawRoundRect(shadow_radius, shadow_radius, width - shadow_radius, height - shadow_radius, corner_radius, corner_radius, paint) 132 | 133 | border_width = (@border_width || 0) * UI.density 134 | if border_width > 0 135 | paint = Android::Graphics::Paint.new 136 | paint.color = UI::Color(@border_color || :black).proxy 137 | paint.style = Android::Graphics::Paint::Style::STROKE 138 | paint.strokeWidth = border_width 139 | canvas.drawRoundRect(border_width, border_width, width - border_width, height - border_width, corner_radius, corner_radius, paint) 140 | end 141 | 142 | proxy.background = Android::Graphics::Drawable::BitmapDrawable.new(UI.context.resources, bitmap) 143 | end 144 | 145 | def _autolayout_when_resized=(value) 146 | if value 147 | unless @layout_listener 148 | @layout_listener = FlowUIViewLayoutChangeListener.new(self) 149 | proxy.addOnLayoutChangeListener(@layout_listener) 150 | end 151 | else 152 | if @layout_listener 153 | proxy.removeOnLayoutChangeListener(@layout_listener) 154 | @layout_listener = nil 155 | end 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /flow/ui/android/web.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Web < UI::View 3 | def load_html(str) 4 | proxy.loadData(str, 'text/html', nil) 5 | end 6 | 7 | def proxy 8 | @proxy ||= begin 9 | web_view = Android::Webkit::WebView.new(UI.context) 10 | web_view.settings.javaScriptEnabled = true 11 | web_view 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /flow/ui/cocoa/activity_indicator.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class ActivityIndicator < UI::View 3 | def start 4 | proxy.startAnimating 5 | end 6 | 7 | def stop 8 | proxy.stopAnimating 9 | end 10 | 11 | def animating? 12 | proxy.animating? 13 | end 14 | 15 | def color=(color) 16 | proxy.color = UI::Color(color).proxy 17 | end 18 | 19 | def proxy 20 | @proxy ||= begin 21 | view = UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleWhiteLarge) 22 | view.color = UIColor.grayColor 23 | view 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /flow/ui/cocoa/alert.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Alert 3 | def title=(title) 4 | proxy.title = title 5 | end 6 | 7 | def message=(message) 8 | proxy.message = message 9 | end 10 | 11 | def set_button(title, type) 12 | @buttons ||= {} 13 | pos = proxy.addButtonWithTitle(title) 14 | if type == :cancel 15 | proxy.cancelButtonIndex = pos 16 | end 17 | @buttons[pos] = type 18 | end 19 | 20 | def show(&block) 21 | @complete_block = (block or raise "expected block") 22 | proxy.show 23 | end 24 | 25 | def alertView(view, clickedButtonAtIndex:pos) 26 | @complete_block.call(@buttons[pos] || :default) 27 | end 28 | 29 | def proxy 30 | @proxy ||= UIAlertView.alloc.initWithTitle("", message:"", delegate:self, cancelButtonTitle:nil, otherButtonTitles:nil) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /flow/ui/cocoa/application.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Application 3 | attr_reader :navigation 4 | 5 | @@instance = nil 6 | def initialize(navigation, context) 7 | @navigation = navigation 8 | context.window = proxy 9 | @@instance = self 10 | end 11 | 12 | def self.instance 13 | @@instance 14 | end 15 | 16 | def start 17 | proxy.rootViewController = @navigation.proxy 18 | proxy.makeKeyAndVisible 19 | true 20 | end 21 | 22 | def open_url(url) 23 | UIApplication.sharedApplication.openURL(NSURL.URLWithString(url)) 24 | end 25 | 26 | def open_phone_call(number) 27 | open_url("tel://#{number}") 28 | end 29 | 30 | def proxy 31 | @proxy ||= UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /flow/ui/cocoa/button.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Button < Control 3 | include Eventable 4 | 5 | def initialize 6 | super 7 | calculate_measure(true) 8 | end 9 | 10 | def measure(width, height) 11 | self.proxy.sizeToFit 12 | 13 | [ 14 | width.nan? ? self.proxy.frame.size.width : width, 15 | height.nan? ? self.proxy.frame.size.height : height 16 | ] 17 | end 18 | 19 | def color=(color) 20 | case color 21 | when Hash 22 | color.map do |state, color| 23 | proxy.setTitleColor(UI::Color(color).proxy, forState: CONTROL_STATES[state]) 24 | end 25 | else 26 | proxy.setTitleColor(UI::Color(color).proxy, forState: CONTROL_STATES[:normal]) 27 | end 28 | end 29 | 30 | def color(state = :normal) 31 | UI::Color(proxy.titleColorForState(CONTROL_STATES[state])) 32 | end 33 | 34 | def title=(title) 35 | case title 36 | when Hash 37 | title.map do |state, title| 38 | proxy.setTitle(title, forState: CONTROL_STATES[state]) 39 | end 40 | when String 41 | proxy.setTitle(title, forState: CONTROL_STATES[:normal]) 42 | end 43 | end 44 | 45 | def title(state = :normal) 46 | proxy.titleForState(CONTROL_STATES[state]) 47 | end 48 | 49 | def image=(image) 50 | proxy.setImage(UIImage.imageNamed(image), forState: CONTROL_STATES[:normal]) 51 | end 52 | 53 | def font 54 | UI::Font._wrap(proxy.titleLabel.font) 55 | end 56 | 57 | def font=(font) 58 | proxy.titleLabel.font = UI::Font(font).proxy 59 | end 60 | 61 | def on_tap 62 | trigger(:tap) 63 | end 64 | 65 | def proxy 66 | @proxy ||= begin 67 | ui_button = UIButton.buttonWithType(UIButtonTypeCustom) 68 | ui_button.translatesAutoresizingMaskIntoConstraints = false 69 | ui_button.addTarget(self, action: :on_tap, forControlEvents: UIControlEventTouchUpInside) 70 | ui_button 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /flow/ui/cocoa/camera.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Camera < View 3 | include Eventable 4 | 5 | attr_accessor :detect_barcode_types, :facing 6 | 7 | def start 8 | barcode_formats = (@detect_barcode_types or []).map do |format| 9 | case format 10 | when :aztec 11 | AVMetadataObjectTypeAztecCode 12 | when :code_128 13 | AVMetadataObjectTypeCode128Code 14 | when :code_39 15 | AVMetadataObjectTypeCode39Code 16 | when :code_93 17 | AVMetadataObjectTypeCode93Code 18 | when :codabar 19 | raise ":codabar not supported in iOS" 20 | when :data_matrix 21 | AVMetadataObjectTypeDataMatrixCode 22 | when :ean_13, :upc_a 23 | # UPCA is EAN13 according to https://developer.apple.com/library/ios/technotes/tn2325/_index.html 24 | AVMetadataObjectTypeEAN13Code 25 | when :ean_8 26 | AVMetadataObjectTypeEAN8Code 27 | when :itf 28 | AVMetadataObjectTypeITF14Code 29 | when :pdf_417 30 | AVMetadataObjectTypePDF417Code 31 | when :qrcode 32 | AVMetadataObjectTypeQRCode 33 | when :upc_e 34 | AVMetadataObjectTypeUPCECode 35 | else 36 | raise "Incorrect value `#{format}' for `detect_barcode_types'" 37 | end 38 | end 39 | 40 | position = case (@facing or :back) 41 | when :front 42 | AVCaptureDevicePositionFront 43 | when :back 44 | AVCaptureDevicePositionBack 45 | else 46 | raise "Incorrect value `#{@facing}' for `facing'" 47 | end 48 | 49 | @capture_session = AVCaptureSession.alloc.init 50 | @capture_session.sessionPreset = AVCaptureSessionPresetHigh 51 | 52 | devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) 53 | device = devices.find { |x| x.position == position } 54 | return false unless device 55 | 56 | error = Pointer.new(:id) 57 | input = AVCaptureDeviceInput.deviceInputWithDevice(device, error:error) 58 | raise error.description unless input 59 | 60 | @preview_layer = AVCaptureVideoPreviewLayer.alloc.initWithSession(@capture_session) 61 | @preview_layer.videoGravity = AVLayerVideoGravityResizeAspectFill 62 | layer_rect = proxy.layer.bounds 63 | @preview_layer.bounds = layer_rect 64 | @preview_layer.position = [CGRectGetMidX(layer_rect), CGRectGetMidY(layer_rect)] 65 | proxy.layer.addSublayer(@preview_layer) 66 | 67 | queue = Dispatch::Queue.new('camQueue') 68 | output = AVCaptureMetadataOutput.alloc.init 69 | output.setMetadataObjectsDelegate(self, queue:queue.dispatch_object) 70 | 71 | @capture_session.addInput input 72 | @capture_session.addOutput output 73 | 74 | output.metadataObjectTypes = barcode_formats 75 | 76 | @capture_session.startRunning 77 | end 78 | 79 | def captureOutput(capture_output, didOutputMetadataObjects:metadata_objects, fromConnection:connection) 80 | Task.main do 81 | metadata_objects.each do |barcode| 82 | trigger :barcode_scanned, barcode.stringValue 83 | end 84 | end 85 | end 86 | 87 | def stop 88 | if @capture_session 89 | @capture_session.stopRunning 90 | @capture_session = nil 91 | end 92 | if @preview_layer 93 | @preview_layer.removeFromSuperlayer 94 | @preview_layer = nil 95 | end 96 | end 97 | 98 | def take_capture 99 | puts "Not yet implemented" 100 | end 101 | 102 | # XXX Not defining #proxy here since we can reuse UI::View's. 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /flow/ui/cocoa/color.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Color 3 | def self._native?(color) 4 | color.is_a?(UIColor) 5 | end 6 | 7 | def self.rgba(r, g, b, a) 8 | new UIColor.colorWithRed(r/255.0, green:g/255.0, blue:b/255.0, alpha:a/255.0) 9 | end 10 | 11 | def to_a 12 | @ary_values ||= begin 13 | red_ptr = Pointer.new(:double) 14 | green_ptr = Pointer.new(:double) 15 | blue_ptr = Pointer.new(:double) 16 | alpha_ptr = Pointer.new(:double) 17 | 18 | if proxy.getRed(red_ptr, green:green_ptr, blue:blue_ptr, alpha:alpha_ptr) 19 | [red_ptr[0], green_ptr[0], blue_ptr[0], alpha_ptr[0]] 20 | else 21 | [0, 0, 0, 0] 22 | end.map { |x| (x * 255.0).round} 23 | end 24 | end 25 | 26 | def red 27 | to_a[0] 28 | end 29 | 30 | def green 31 | to_a[1] 32 | end 33 | 34 | def blue 35 | to_a[2] 36 | end 37 | 38 | def alpha 39 | to_a[3] 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /flow/ui/cocoa/control.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Control < View 3 | CONTROL_EVENTS = { 4 | touch: UIControlEventTouchUpInside, 5 | editing_changed: UIControlEventEditingChanged, 6 | editing_did_begin: UIControlEventEditingDidBegin, 7 | editing_did_end: UIControlEventEditingDidEnd 8 | } 9 | 10 | CONTROL_STATES = { 11 | normal: UIControlStateNormal, 12 | highlighted: UIControlStateHighlighted, 13 | disabled: UIControlStateDisabled, 14 | selected: UIControlStateSelected, 15 | focused: UIControlStateFocused 16 | } 17 | 18 | def blur 19 | proxy.resignFirstResponder 20 | end 21 | 22 | def focus 23 | proxy.becomeFirstResponder 24 | end 25 | 26 | def focus? 27 | proxy.firstResponder? 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /flow/ui/cocoa/font.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Font 3 | def self._wrap(font) 4 | new(font, nil, nil) 5 | end 6 | 7 | def initialize(obj, size, trait=nil, extension=nil) 8 | if obj.is_a?(UIFont) 9 | @proxy = obj 10 | else 11 | desc = UIFontDescriptor.fontDescriptorWithFontAttributes(UIFontDescriptorNameAttribute => obj) 12 | case trait 13 | when :bold 14 | desc = desc.fontDescriptorWithSymbolicTraits(UIFontDescriptorTraitBold) 15 | when :italic 16 | desc = desc.fontDescriptorWithSymbolicTraits(UIFontDescriptorTraitItalic) 17 | when :bold_italic 18 | desc = desc.fontDescriptorWithSymbolicTraits(UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic) 19 | end 20 | @proxy = UIFont.fontWithDescriptor(desc, size:size) 21 | end 22 | end 23 | 24 | def name 25 | @proxy.fontName 26 | end 27 | 28 | def size 29 | @proxy.pointSize 30 | end 31 | 32 | def trait 33 | @trait ||= begin 34 | traits = @proxy.fontDescriptor.symbolicTraits 35 | if (traits & UIFontDescriptorTraitItalic) != 0 36 | if (traits & UIFontDescriptorTraitBold) != 0 37 | :bold_italic 38 | else 39 | :italic 40 | end 41 | elsif (traits & UIFontDescriptorTraitBold) != 0 42 | :bold 43 | else 44 | :normal 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /flow/ui/cocoa/gesture.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class SwipeGesture 3 | def initialize(view, args, block) 4 | obj = UISwipeGestureRecognizer.alloc.initWithTarget(self, action:"_swipe:") 5 | obj.direction = (args || [:right]).inject(0) do |m, sym| 6 | m | (case sym 7 | when :right then UISwipeGestureRecognizerDirectionRight 8 | when :left then UISwipeGestureRecognizerDirectionLeft 9 | when :up then UISwipeGestureRecognizerDirectionUp 10 | when :down then UISwipeGestureRecognizerDirectionDown 11 | else 12 | raise "invalid swipe type #{sym}" 13 | end) 14 | end 15 | view.proxy.addGestureRecognizer(obj) 16 | @block = block 17 | end 18 | 19 | def _swipe(sender) 20 | @block.call 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /flow/ui/cocoa/gradient.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Gradient 3 | def colors=(colors) 4 | raise ArgError, "must receive an array of 2 or 3 colors" if colors.size < 2 or colors.size > 3 5 | proxy.colors = colors.map { |x| UI::Color(x).proxy.CGColor } 6 | end 7 | 8 | def proxy 9 | @proxy ||= CAGradientLayer.layer 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /flow/ui/cocoa/image.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Image < View 3 | attr_reader :source 4 | 5 | def source=(source) 6 | if @source != source 7 | @source = source 8 | 9 | image = case source 10 | when String 11 | UIImage.imageNamed(source) 12 | when NSData 13 | UIImage.imageWithData(source) 14 | else 15 | raise "Expected `String' or `NSData' object, got `#{source.class}'" 16 | end 17 | @original_image = proxy.image = image 18 | 19 | if width.nan? and height.nan? 20 | self.width = proxy.image.size.width 21 | self.height = proxy.image.size.height 22 | end 23 | end 24 | end 25 | 26 | def filter=(color) 27 | image = @original_image 28 | raise "source= must be set" unless image 29 | 30 | begin 31 | size = image.size 32 | UIGraphicsBeginImageContextWithOptions(size, false, image.scale) 33 | 34 | context = UIGraphicsGetCurrentContext() 35 | area = CGRectMake(0, 0, size.width, size.height) 36 | 37 | CGContextScaleCTM(context, 1, -1) 38 | CGContextTranslateCTM(context, 0, -area.size.height) 39 | 40 | CGContextSaveGState(context) 41 | CGContextClipToMask(context, area, image.CGImage) 42 | 43 | UI::Color(color).proxy.set 44 | CGContextFillRect(context, area) 45 | 46 | CGContextRestoreGState(context) 47 | 48 | CGContextSetBlendMode(context, KCGBlendModeMultiply) 49 | 50 | CGContextDrawImage(context, area, image.CGImage) 51 | 52 | filter_image = UIGraphicsGetImageFromCurrentImageContext() 53 | proxy.image = filter_image 54 | ensure 55 | UIGraphicsEndImageContext() 56 | end 57 | end 58 | 59 | RESIZE_MODES = { 60 | cover: UIViewContentModeScaleToFill, 61 | contain: UIViewContentModeScaleAspectFit, 62 | stretch: UIViewContentModeScaleAspectFill 63 | } 64 | 65 | def resize_mode=(resize_mode) 66 | proxy.contentMode = RESIZE_MODES.fetch(resize_mode.to_sym) do 67 | raise "Incorrect value, expected one of: #{RESIZE_MODES.keys.join(',')}" 68 | end 69 | end 70 | 71 | def resize_mode 72 | RESIZE_MODES.key(proxy.contentMode) 73 | end 74 | 75 | def proxy 76 | @proxy ||= begin 77 | ui_image_view = UIImageView.alloc.init 78 | ui_image_view.translatesAutoresizingMaskIntoConstraints = false 79 | ui_image_view 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /flow/ui/cocoa/label.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Label < View 3 | include UI::SharedText 4 | 5 | def initialize 6 | super 7 | calculate_measure(true) 8 | end 9 | 10 | def height=(val) 11 | super 12 | calculate_measure(false) 13 | end 14 | 15 | def measure(width, height) 16 | #https://developer.apple.com/reference/foundation/nsattributedstring/1524971-draw 17 | at = proxy.attributedText 18 | return [0, 0] if at == nil or at.length == 0 19 | size = [width.nan? ? Float::MAX : width, Float::MAX] 20 | rect = at.boundingRectWithSize(size, options:NSStringDrawingUsesLineFragmentOrigin, context:nil) 21 | [width, rect.size.height.ceil] 22 | end 23 | 24 | def proxy 25 | @proxy ||= begin 26 | label = UILabel.alloc.init 27 | label.translatesAutoresizingMaskIntoConstraints = false 28 | label.lineBreakMode = NSLineBreakByWordWrapping 29 | label.numberOfLines = 0 30 | label 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /flow/ui/cocoa/list.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class List < View 3 | include Eventable 4 | 5 | class CustomListCell < UITableViewCell 6 | IDENTIFIER = "CustomListCell" 7 | 8 | attr_reader :content_view 9 | 10 | def content_view=(content_view) 11 | if @content_view != content_view 12 | @content_view = content_view 13 | self.contentView.addSubview(@content_view.proxy) 14 | @content_view.width = contentView.frame.size.width 15 | end 16 | end 17 | 18 | def list=(list) 19 | @list = WeakRef.new(list) 20 | end 21 | 22 | def layoutSubviews 23 | @content_view.width = contentView.frame.size.width 24 | @content_view.update_layout 25 | super 26 | if path = @list.proxy.indexPathForCell(self) 27 | @list._set_row_height(path, @content_view) 28 | end 29 | end 30 | end 31 | 32 | def initialize 33 | super 34 | @data_source = [] 35 | @render_row_block = lambda { |section_index, row_index| ListRow } 36 | @cached_rows = {} 37 | @cached_rows_height = [] 38 | end 39 | 40 | def numberOfSectionsInTableView(table_view) 41 | 1 42 | end 43 | 44 | def tableView(table_view, numberOfRowsInSection: section) 45 | @data_source.size 46 | end 47 | 48 | def tableView(table_view, cellForRowAtIndexPath: index_path) 49 | row_klass = @render_row_block.call(index_path.section, index_path.row) 50 | data = @data_source[index_path.row] 51 | cell_identifier = CustomListCell::IDENTIFIER + row_klass.name 52 | cell = table_view.dequeueReusableCellWithIdentifier(cell_identifier) 53 | unless cell 54 | row = (@cached_rows[data] ||= row_klass.new) 55 | row.list = self if row.respond_to?(:list=) 56 | cell = CustomListCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier: cell_identifier) 57 | cell.selectionStyle = UITableViewCellSelectionStyleNone 58 | cell.content_view = row 59 | cell.list = self 60 | end 61 | cell.content_view.update(data) if cell.content_view.respond_to?(:update) 62 | cell.content_view.update_layout 63 | _set_row_height(index_path, cell.content_view, false) 64 | cell 65 | end 66 | 67 | def _set_row_height(index_path, cell, reload=true) 68 | row = index_path.row 69 | height = cell.layout[3] 70 | previous_height = @cached_rows_height[row] 71 | @cached_rows_height[row] = height 72 | if height != previous_height and reload 73 | # Calling beginUpdates and endUpdates seem to be the only way to tell the table view to recalculate the row heights. We also have to disable animations when this happens. 74 | state = UIView.areAnimationsEnabled 75 | begin 76 | UIView.animationsEnabled = false 77 | proxy.beginUpdates 78 | proxy.endUpdates 79 | ensure 80 | UIView.animationsEnabled = state 81 | end 82 | end 83 | end 84 | 85 | def tableView(table_view, heightForRowAtIndexPath: index_path) 86 | @cached_rows_height[index_path.row] or UITableViewAutomaticDimension 87 | end 88 | 89 | def _row_at_index_path(index_path) 90 | proxy.cellForRowAtIndexPath(index_path).content_view 91 | end 92 | 93 | def tableView(table_view, shouldHighlightRowAtIndexPath: index_path) 94 | view = _row_at_index_path(index_path) 95 | view.respond_to?(:selectable?) ? view.selectable? : true 96 | end 97 | 98 | def tableView(table_view, didSelectRowAtIndexPath: index_path) 99 | data = @data_source[index_path.row] 100 | row = @cached_rows[data] 101 | trigger :select, data, index_path.row, row 102 | end 103 | 104 | attr_reader :data_source 105 | 106 | def data_source=(data_source) 107 | if @data_source != data_source 108 | @data_source = data_source 109 | proxy.reloadData 110 | end 111 | end 112 | 113 | def render_row(&block) 114 | @render_row_block = block.weak! 115 | end 116 | 117 | def row_at_index(pos) 118 | _row_at_index_path(NSIndexPath.indexPathForItem(pos, inSection:0)) 119 | end 120 | 121 | def proxy 122 | @proxy ||= begin 123 | ui_table_view = UITableView.alloc.init 124 | ui_table_view.delegate = self 125 | ui_table_view.dataSource = self 126 | ui_table_view.separatorStyle = UITableViewCellSeparatorStyleNone 127 | ui_table_view 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /flow/ui/cocoa/navigation.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Navigation 3 | attr_reader :root_screen 4 | 5 | def initialize(root_screen) 6 | root_screen.navigation = self 7 | @root_screen = root_screen 8 | @current_screens = [@root_screen] 9 | end 10 | 11 | def screen 12 | @current_screens.last 13 | end 14 | 15 | def _height_of_navigation_bar 16 | rect = proxy.navigationBar.frame 17 | rect.origin.y + rect.size.height 18 | end 19 | 20 | def show_bar 21 | if proxy.isNavigationBarHidden 22 | proxy.navigationBarHidden = false 23 | screen = @current_screens.last 24 | screen.view.height -= _height_of_navigation_bar 25 | screen.view.update_layout 26 | end 27 | end 28 | 29 | def hide_bar 30 | if !proxy.isNavigationBarHidden 31 | screen = @current_screens.last 32 | screen.view.height += _height_of_navigation_bar 33 | screen.view.update_layout 34 | proxy.navigationBarHidden = true 35 | end 36 | end 37 | 38 | def bar_hidden? 39 | proxy.isNavigationBarHidden 40 | end 41 | 42 | def title=(title) 43 | @current_screens.last.proxy.title = title 44 | end 45 | 46 | def bar_color=(color) 47 | bar = proxy.navigationBar 48 | bar.barTintColor = UI::Color(color).proxy 49 | bar.translucent = false 50 | end 51 | 52 | def items=(items) 53 | current_screen = @current_screens.last 54 | navigation_item = current_screen.proxy.navigationItem 55 | buttons = [:back_button, :left_button, :right_button].map do |key| 56 | if opt = items[key] 57 | if title = opt[:title] 58 | UIBarButtonItem.alloc.initWithTitle(title, style:UIBarButtonItemStylePlain, target:current_screen, action:opt[:action]) 59 | elsif image = opt[:image] 60 | UIBarButtonItem.alloc.initWithImage(UIImage.imageNamed(image), style:UIBarButtonItemStylePlain, target:current_screen, action:opt[:action]) 61 | else 62 | nil 63 | end 64 | else 65 | nil 66 | end 67 | end 68 | navigation_item.backBarButtonItem = buttons[0] 69 | navigation_item.leftBarButtonItem = buttons[1] 70 | navigation_item.rightBarButtonItem = buttons[2] 71 | if items[:hide_back_button] 72 | navigation_item.hidesBackButton = true 73 | end 74 | end 75 | 76 | def start 77 | replace @root_screen, false 78 | end 79 | 80 | def push(screen, animated=true) 81 | @current_screens << screen 82 | screen.navigation = self 83 | proxy.pushViewController(screen.proxy, animated: animated) 84 | end 85 | 86 | def pop(animated=true) 87 | if @current_screens.size > 1 88 | screen = @current_screens.pop 89 | proxy.popViewControllerAnimated(animated) 90 | screen 91 | else 92 | nil 93 | end 94 | end 95 | 96 | def replace(new_screen, animated=true) 97 | new_screen.navigation = self 98 | @current_screens = [new_screen] 99 | proxy.setViewControllers([new_screen.proxy], animated:animated) 100 | end 101 | 102 | def share_panel(text, animated=true) 103 | controller = UIActivityViewController.alloc.initWithActivityItems([text], applicationActivities:nil) 104 | proxy.presentViewController(controller, animated:animated, completion:nil) 105 | end 106 | 107 | def proxy 108 | @proxy ||= UINavigationController.alloc.initWithRootViewController(@root_screen.proxy) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /flow/ui/cocoa/screen.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Controller < UIViewController 3 | include Eventable 4 | 5 | attr_accessor :navigation 6 | 7 | def initWithScreen(screen) 8 | if init 9 | @screen = screen 10 | on(:view_did_load) { @screen.before_on_load } 11 | on(:view_will_appear) { @screen.before_on_show } 12 | on(:view_did_appear) { @screen.on_show } 13 | end 14 | self 15 | end 16 | 17 | def loadView 18 | nav = @screen.navigation 19 | screen_size = UIScreen.mainScreen.bounds.size 20 | screen_view = @screen.view 21 | screen_view.width = screen_size.width 22 | screen_view.height = screen_size.height 23 | unless nav.bar_hidden? 24 | screen_view.height -= nav._height_of_navigation_bar 25 | end 26 | self.view = screen_view.proxy 27 | end 28 | 29 | def viewDidAppear(animated) 30 | super 31 | trigger(:view_did_appear) 32 | end 33 | 34 | def viewWillAppear(animated) 35 | super 36 | trigger(:view_will_appear) 37 | end 38 | 39 | def viewDidLoad 40 | super 41 | self.edgesForExtendedLayout = UIRectEdgeNone 42 | trigger(:view_did_load) 43 | end 44 | 45 | def viewWillTransitionToSize(size, withTransitionCoordinator:coordinator) 46 | super 47 | 48 | did_rotate = lambda do |context| 49 | @screen.view.width = size.width 50 | @screen.view.height = size.height 51 | @screen.view.update_layout 52 | end 53 | coordinator.animateAlongsideTransition(nil, completion:did_rotate) 54 | end 55 | 56 | def viewDidLayoutSubviews 57 | @screen.view.update_layout 58 | end 59 | 60 | def preferredStatusBarStyle 61 | @screen.status_bar_style or UIStatusBarStyleDefault 62 | end 63 | end 64 | 65 | class Screen 66 | attr_accessor :navigation 67 | 68 | def initialize 69 | @navigation = nil 70 | end 71 | 72 | def before_on_load 73 | view.background_color = :white 74 | on_load 75 | end 76 | def on_load; end 77 | 78 | def before_on_show; end 79 | def on_show; end 80 | 81 | def view 82 | @view ||= UI::View.new 83 | end 84 | 85 | attr_reader :status_bar_style 86 | 87 | def status_bar_style=(style) 88 | @status_bar_style = case style[:content] 89 | when :light 90 | UIStatusBarStyleLightContent 91 | when :dark, :default 92 | UIStatusBarStyleDefault 93 | else 94 | raise "invalid style" 95 | end 96 | proxy.setNeedsStatusBarAppearanceUpdate 97 | end 98 | 99 | def self.size 100 | UIScreen.mainScreen.bounds.size.to_a 101 | end 102 | 103 | def proxy 104 | @proxy ||= Controller.alloc.initWithScreen(self) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /flow/ui/cocoa/shared_text.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | module SharedText 3 | def text_alignment 4 | UI::TEXT_ALIGNMENT.key(proxy.textAlignment) 5 | end 6 | 7 | def text_alignment=(text_alignment) 8 | proxy.textAlignment = UI::TEXT_ALIGNMENT.fetch(text_alignment) do 9 | raise "Incorrect value, expected one of: #{UI::TEXT_ALIGNMENT.keys.join(',')}" 10 | end 11 | end 12 | 13 | def color 14 | UI::Color(proxy.textColor) 15 | end 16 | 17 | def color=(color) 18 | proxy.textColor = UI::Color(color).proxy 19 | end 20 | 21 | def _text=(text) 22 | if text 23 | proxy.text = text 24 | if text.size > 0 and @line_height 25 | at = proxy.attributedText.mutableCopy 26 | if ps = at.attribute(NSParagraphStyleAttributeName, atIndex:0, effectiveRange:nil) 27 | ps = ps.mutableCopy 28 | else 29 | ps = NSMutableParagraphStyle.new 30 | end 31 | ps.minimumLineHeight = @line_height 32 | at.addAttribute(NSParagraphStyleAttributeName, value:ps, range:[0, at.length]) 33 | proxy.attributedText = at 34 | end 35 | end 36 | end 37 | 38 | def text=(text) 39 | self._text = text 40 | end 41 | 42 | def text 43 | proxy.text 44 | end 45 | 46 | def font 47 | UI::Font._wrap(proxy.font) 48 | end 49 | 50 | def font=(font) 51 | proxy.font = UI::Font(font).proxy 52 | end 53 | 54 | def line_height=(spacing) 55 | if @line_height != spacing 56 | @line_height = spacing 57 | self._text = proxy.text 58 | end 59 | end 60 | 61 | def line_height 62 | @line_height 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /flow/ui/cocoa/text.rb: -------------------------------------------------------------------------------- 1 | class FlowUITextView < UITextView 2 | # Flow text views are not selectable at the moment. 3 | def canBecomeFirstResponder 4 | false 5 | end 6 | end 7 | 8 | module UI 9 | class Text < View 10 | include UI::SharedText 11 | include Eventable 12 | 13 | def editable=(flag) 14 | proxy.editable = flag 15 | end 16 | 17 | def editable? 18 | proxy.editable 19 | end 20 | 21 | attr_reader :link_color 22 | 23 | def link_color=(color) 24 | if @link_color != color 25 | @link_color = color 26 | proxy.linkTextAttributes = { NSForegroundColorAttributeName => UI.Color(color).proxy } 27 | end 28 | end 29 | 30 | def links=(links) 31 | at = proxy.attributedText.mutableCopy 32 | 33 | links.each do |range, link| 34 | range = [range.begin, range.end - range.begin] 35 | at.addAttribute(NSLinkAttributeName, value:_add_link(link), range:range) 36 | at.addAttribute(NSUnderlineStyleAttributeName, value:1, range:range) 37 | end 38 | 39 | proxy.attributedText = at 40 | proxy.dataDetectorTypes = UIDataDetectorTypeLink 41 | end 42 | 43 | def _add_link(value) 44 | @links ||= {} 45 | link = "link#{@links.size}" 46 | @links[link] = value 47 | NSURL.URLWithString(link + '://') 48 | end 49 | 50 | def textView(view, shouldInteractWithURL:url, inRange:range) 51 | if @links and value = @links[url.scheme] 52 | trigger :link, value 53 | false 54 | else 55 | true 56 | end 57 | end 58 | 59 | def proxy 60 | @proxy ||= begin 61 | view = FlowUITextView.alloc.init 62 | view.delegate = self 63 | view 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /flow/ui/cocoa/text_input.rb: -------------------------------------------------------------------------------- 1 | class FlowTextField < UITextField 2 | attr_accessor :_input_offset 3 | 4 | def textRectForBounds(bounds) 5 | _padded_rect(bounds) 6 | end 7 | 8 | def editingRectForBounds(bounds) 9 | _padded_rect(bounds) 10 | end 11 | 12 | def _padded_rect(rect) 13 | @_input_offset ? CGRectInset(rect, @_input_offset, 0) : rect 14 | end 15 | end 16 | 17 | module UI 18 | class TextInput < Control 19 | include Eventable 20 | 21 | def on(event, &block) 22 | case event 23 | when :change 24 | proxy.addTarget(self, action: :on_change, forControlEvents: UIControlEventEditingChanged) 25 | when :focus 26 | proxy.addTarget(self, action: :on_focus, forControlEvents: UIControlEventEditingDidBegin) 27 | when :blur 28 | proxy.addTarget(self, action: :on_blur, forControlEvents: UIControlEventEditingDidEnd) 29 | else 30 | raise "Expected event to be in : `:change, :focus, :blur`" 31 | end 32 | 33 | super 34 | end 35 | 36 | def on_focus 37 | trigger(:focus) 38 | end 39 | 40 | def on_blur 41 | trigger(:blur) 42 | end 43 | 44 | def on_change 45 | trigger(:change, self.text) 46 | end 47 | 48 | def text_alignment 49 | UI::TEXT_ALIGNMENT.key(proxy.textAlignment) 50 | end 51 | 52 | def text_alignment=(text_alignment) 53 | proxy.textAlignment = UI::TEXT_ALIGNMENT.fetch(text_alignment.to_sym) do 54 | raise "Incorrect value, expected one of: #{UI::TEXT_ALIGNMENT.keys.join(',')}" 55 | end 56 | end 57 | 58 | def color 59 | UI::Color(proxy.textColor) 60 | end 61 | 62 | def color=(color) 63 | proxy.textColor = UI::Color(color).proxy 64 | end 65 | 66 | def secure? 67 | proxy.secureTextEntry? 68 | end 69 | 70 | def secure=(is_secure) 71 | proxy.secureTextEntry = is_secure 72 | end 73 | 74 | def text=(text) 75 | if proxy.text != text 76 | proxy.text = text 77 | on_change unless @on_change_disabled 78 | end 79 | end 80 | 81 | def text 82 | proxy.text 83 | end 84 | 85 | def placeholder=(text) 86 | proxy.attributedPlaceholder = NSAttributedString.alloc.initWithString(text, attributes: { NSForegroundColorAttributeName => proxy.textColor }) 87 | end 88 | 89 | def placeholder 90 | proxy.placeholder 91 | end 92 | 93 | def font 94 | UI::Font._wrap(proxy.font) 95 | end 96 | 97 | def font=(font) 98 | proxy.font = UI::Font(font).proxy 99 | end 100 | 101 | def input_offset 102 | proxy._input_offset 103 | end 104 | 105 | def input_offset=(padding) 106 | proxy._input_offset = padding 107 | end 108 | 109 | attr_reader :date_picker 110 | 111 | def date_picker=(flag) 112 | if @date_picker != flag 113 | @date_picker = flag 114 | proxy.inputView = 115 | if flag 116 | date_picker = UIDatePicker.alloc.init 117 | date_picker.datePickerMode = UIDatePickerModeDate 118 | date_picker.addTarget(self, action:'_datePickerValueChanged:', forControlEvents:UIControlEventValueChanged) 119 | date_picker 120 | end 121 | end 122 | end 123 | 124 | def _datePickerValueChanged(sender) 125 | components = NSCalendar.currentCalendar.components(NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit, fromDate:sender.date) 126 | @on_change_disabled = true 127 | begin 128 | trigger :change, components.year, components.month, components.day 129 | ensure 130 | @on_change_disabled = nil 131 | end 132 | end 133 | 134 | def textField(view, shouldChangeCharactersInRange:range, replacementString:string) 135 | !@date_picker 136 | end 137 | 138 | def proxy 139 | @proxy ||= begin 140 | view = FlowTextField.alloc.init 141 | view.delegate = self 142 | view 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /flow/ui/cocoa/ui.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | TEXT_ALIGNMENT = { 3 | left: NSTextAlignmentLeft, 4 | center: NSTextAlignmentCenter, 5 | right: NSTextAlignmentRight, 6 | justify: NSTextAlignmentJustified 7 | } 8 | 9 | def self.resource_str(name) 10 | if md = name.match(/(.*)\.(.*)$/) 11 | if path = NSBundle.mainBundle.pathForResource(md[1], ofType:md[2]) 12 | File.read(path) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /flow/ui/cocoa/view.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class View < CSSNode 3 | ANIMATION_OPTIONS = { 4 | ease_out: UIViewAnimationOptionCurveEaseOut, 5 | ease_in: UIViewAnimationOptionCurveEaseIn, 6 | linear: UIViewAnimationOptionCurveLinear 7 | } 8 | 9 | attr_accessor :_previous_width, :_previous_height 10 | 11 | def animate(options = {}, &block) 12 | animation_options = options.fetch(:options, :linear) 13 | 14 | UIView.animateWithDuration(options.fetch(:duration, 0), 15 | delay: options.fetch(:delay, 0), 16 | options: ANIMATION_OPTIONS.values_at(*animation_options).reduce(&:|), 17 | animations: lambda { 18 | self.root.update_layout 19 | }, 20 | completion: lambda {|completion| 21 | block.call if block 22 | }) 23 | end 24 | 25 | def border_width=(width) 26 | proxy.layer.borderWidth = width 27 | end 28 | 29 | def border_color=(color) 30 | proxy.layer.borderColor = UI::Color(color).proxy.CGColor 31 | end 32 | 33 | def border_radius=(radius) 34 | layer = proxy.layer 35 | layer.masksToBounds = !!radius 36 | layer.cornerRadius = (radius or 0) 37 | end 38 | 39 | # Shadow attributes are applied on the layer of a separate super view that we create on demand, because it's not possible to have both shadow attributes and other attributes (ex. corner radius) at the same time in UIKit. 40 | def _shadow_layer 41 | @_shadow_layer ||= begin 42 | @_shadow_view = UIView.new 43 | @_shadow_view.addSubview(proxy) 44 | layer = @_shadow_view.layer 45 | layer.shadowOpacity = 1.0 46 | layer.masksToBounds = false 47 | layer 48 | end 49 | end 50 | 51 | def shadow_offset=(offset) 52 | _shadow_layer.shadowOffset = offset 53 | end 54 | 55 | def shadow_color=(color) 56 | _shadow_layer.shadowColor = UI::Color(color).proxy.CGColor 57 | end 58 | 59 | def shadow_radius=(radius) 60 | _shadow_layer.shadowRadius = radius 61 | end 62 | 63 | def border_width=(width) 64 | proxy.layer.borderWidth = width 65 | end 66 | 67 | def border_color 68 | proxy.layer.borderColor 69 | end 70 | 71 | def border_radius 72 | proxy.layer.cornerRadius 73 | end 74 | 75 | def border_width 76 | proxy.layer.borderWidth 77 | end 78 | 79 | def background_color 80 | UI::Color(proxy.backgroundColor) 81 | end 82 | 83 | def _reset_background_layer(layer=nil) 84 | @_background_layer.removeFromSuperlayer if @_background_layer 85 | @_background_layer = layer 86 | end 87 | 88 | def _layout_background_layer 89 | @_background_layer.frame = proxy.bounds if @_background_layer 90 | end 91 | 92 | def background_color=(color) 93 | proxy.backgroundColor = UI::Color(color).proxy 94 | _reset_background_layer 95 | end 96 | 97 | def background_gradient=(gradient) 98 | layer = gradient.proxy 99 | proxy.layer.insertSublayer(layer, atIndex:0) 100 | _reset_background_layer(layer) 101 | _layout_background_layer 102 | end 103 | 104 | def hidden? 105 | proxy.hidden? 106 | end 107 | 108 | def hidden=(hidden) 109 | if hidden 110 | if !self.width.nan? 111 | self._previous_width = self.width 112 | self.width = 0 113 | end 114 | 115 | if !self.height.nan? 116 | self._previous_height = self.height 117 | self.height = 0 118 | end 119 | else 120 | self.width = self._previous_width if self._previous_width 121 | self.height = self._previous_height if self._previous_height 122 | end 123 | 124 | proxy.hidden = hidden 125 | 126 | self.root.update_layout 127 | end 128 | 129 | def alpha 130 | proxy.alpha 131 | end 132 | 133 | def alpha=(value) 134 | proxy.alpha = value 135 | end 136 | 137 | def _proxy_or_shadow_view 138 | @_shadow_view or proxy 139 | end 140 | 141 | def add_child(child) 142 | super 143 | proxy.addSubview(child._proxy_or_shadow_view) 144 | end 145 | 146 | def insert_child(index, child) 147 | super 148 | proxy.insertSubview(child._proxy_or_shadow_view, atIndex:index) 149 | end 150 | 151 | def delete_child(child) 152 | if super 153 | child._proxy_or_shadow_view.removeFromSuperview 154 | end 155 | end 156 | 157 | def update_layout 158 | super 159 | _apply_layout([0, 0], _proxy_or_shadow_view.frame.origin) 160 | _layout_background_layer 161 | end 162 | 163 | def proxy 164 | @proxy ||= UIView.alloc.init 165 | end 166 | 167 | def _apply_layout(absolute_point, origin_point) 168 | left, top, width, height = layout 169 | 170 | top_left = [absolute_point[0] + left, absolute_point[1] + top] 171 | bottom_right = [absolute_point[0] + left + width, absolute_point[1] + top + height] 172 | new_frame = [[left + origin_point[0], top + origin_point[1]], [bottom_right[0] - top_left[0], bottom_right[1] - top_left[1]]] 173 | 174 | if @_shadow_view 175 | @_shadow_view.frame = new_frame 176 | new_frame[0] = [0, 0] 177 | end 178 | 179 | proxy.autoresizingMask = UIViewAutoresizingNone 180 | proxy.translatesAutoresizingMaskIntoConstraints = true 181 | proxy.frame = new_frame 182 | 183 | absolute_point[0] += left 184 | absolute_point[1] += top 185 | 186 | children.each { |x| x._apply_layout(absolute_point, [0, 0]) } 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /flow/ui/cocoa/web.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class Web < View 3 | attr_accessor :configuration 4 | 5 | def initialize(configuration = nil) 6 | super() 7 | @configuration = configuration || WKWebViewConfiguration.new 8 | end 9 | 10 | def load_html(string) 11 | proxy.loadHTMLString(string, baseURL: NSBundle.mainBundle.bundleURL) 12 | end 13 | 14 | def proxy 15 | @proxy ||= begin 16 | ui_web_view = WKWebView.alloc.initWithFrame([[0,0], [1024, 768]], configuration: @configuration) 17 | ui_web_view.translatesAutoresizingMaskIntoConstraints = false 18 | ui_web_view.UIDelegate = self 19 | ui_web_view.NavigationDelegate = self 20 | ui_web_view 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /flow/ui/eventable.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | # Simple placeholder implementation 3 | # will need to be rewrite/improved 4 | module Eventable 5 | def on(event, action = nil, &block) 6 | if action 7 | __events__[event.to_sym] ||= action 8 | elsif block 9 | __events__[event.to_sym] ||= block 10 | end 11 | end 12 | 13 | def trigger(event, *args) 14 | # if no listener found we will do nothing 15 | return unless registered_event = __events__.fetch(event, nil) 16 | 17 | case registered_event 18 | when String, Symbol 19 | self.send(registered_event, *args) 20 | else 21 | registered_event.call(*args) 22 | end 23 | end 24 | 25 | def __events__ 26 | @__events__ ||= {} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /flow/ui/font.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | def self.Font(font) 3 | case font 4 | when UI::Font 5 | self 6 | when Hash 7 | name = (font[:name] or raise ":name expected") 8 | size = (font[:size] or raise ":size expected") 9 | trait = (font[:trait] or :normal) 10 | extension = (font[:extension] or :ttf) 11 | UI::Font.new(name, size, trait, extension) 12 | when Array 13 | raise "Expected Array of 2 or 3 or 4 elements" if font.size < 2 or font.size > 4 14 | UI::Font.new(*font) 15 | else 16 | raise "Expected UI::Font or Hash or Array" 17 | end 18 | end 19 | 20 | class Font 21 | attr_reader :proxy 22 | 23 | def italic? 24 | trait == :italic or trait == :bold_italic 25 | end 26 | 27 | def bold? 28 | trait == :bold or trait == :bold_italic 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /flow/ui/list_row.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class ListRow < UI::View 3 | def initialize 4 | super 5 | self.flex_direction = :row 6 | 7 | @label = UI::Label.new 8 | @label.flex = 1 9 | @label.margin = [10, 15] 10 | @label.padding = 5 11 | self.add_child(@label) 12 | end 13 | 14 | def update(data) 15 | @label.text = data 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /flow/ui/view.rb: -------------------------------------------------------------------------------- 1 | module UI 2 | class View < CSSNode 3 | def added? 4 | parent != nil 5 | end 6 | 7 | def on_swipe_left(&block) 8 | on_swipe [:left], &block 9 | end 10 | 11 | def on_swipe_right(&block) 12 | on_swipe [:right], &block 13 | end 14 | 15 | def on_swipe_up(&block) 16 | on_swipe [:up], &block 17 | end 18 | 19 | def on_swipe_down(&block) 20 | on_swipe [:down], &block 21 | end 22 | 23 | def on_swipe(args, &block) 24 | on_gesture :swipe, args, block 25 | end 26 | 27 | def on_gesture(type, args=nil, block) 28 | (@gestures ||= []) << case type 29 | when :swipe 30 | UI::SwipeGesture 31 | else 32 | raise "invalid gesture type #{type}" 33 | end.new(self, args, block) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/android.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'common.rb') 2 | 3 | $:.unshift("/Library/RubyMotion/lib") 4 | require 'motion/project/template/android' 5 | require 'motion-gradle' 6 | 7 | Motion::Project::App.setup do |app| 8 | app.api_version = '23' unless Motion::Project::Config.starter? 9 | app.build_dir = 'build/android' 10 | app.assets_dirs << 'resources' 11 | app.resources_dirs = [] 12 | 13 | files = [] 14 | FLOW_COMPONENTS.each do |comp| 15 | libdir = File.join(File.dirname(__FILE__), '../flow/' + comp) 16 | files.concat(Dir.glob(File.join(libdir, '*.rb'))) 17 | files.concat(Dir.glob(File.join(libdir, 'android/*.rb'))) 18 | 19 | abis = %w{armeabi-v7a x86} 20 | if abis.all? { |x| File.exist?(File.join(libdir, 'android', x)) } 21 | abis.each do |abi| 22 | app.libs[abi] += Dir.glob(File.join(libdir, 'android', abi, "*.a")).map { |x| "\"#{x}\""} 23 | end 24 | end 25 | if comp == 'ui' 26 | app.custom_init_funcs << 'Init_CSSNode' 27 | end 28 | end 29 | app.files.unshift(*files) 30 | 31 | app.files.delete_if { |path| path.start_with?('./app/ios') or path.start_with?('./app/osx') } 32 | app.spec_files.delete_if { |path| path.start_with?('./spec/helpers/cocoa') } 33 | 34 | app.manifest.child('application') do |application| 35 | application['android:theme'] = '@style/Theme.AppCompat.Light' 36 | end 37 | 38 | app.gradle do 39 | repository 'https://maven.google.com' 40 | dependency 'com.android.support:appcompat-v7:24.2.1' 41 | dependency 'com.android.support:support-v4:24.2.1' 42 | dependency 'com.google.android.gms:play-services-vision:11.8.0' 43 | dependency 'com.google.android.gms:play-services-vision-common:11.8.0' 44 | end 45 | 46 | app.manifest_entry('application', 'meta-data', :name => 'com.google.android.gms.version', :value => '@integer/google_play_services_version') 47 | app.manifest_entry('application', 'meta-data', :name => 'com.google.android.gms.vision.DEPENDENCIES', :value => 'barcode') 48 | end 49 | -------------------------------------------------------------------------------- /lib/cocoa.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'common.rb') 2 | 3 | $:.unshift("/Library/RubyMotion/lib") 4 | template = ENV['template'] || 'ios' 5 | require "motion/project/template/#{template}" 6 | 7 | Motion::Project::App.setup do |app| 8 | app.build_dir = "build/#{template}" 9 | app.deployment_target = '7.0' if template == :ios && !Motion::Project::Config.starter? 10 | 11 | files = [] 12 | FLOW_COMPONENTS.each do |comp| 13 | libdir = File.join(File.dirname(__FILE__), '../flow/' + comp) 14 | files.concat(Dir.glob(File.join(libdir, '*.rb'))) 15 | files.concat(Dir.glob(File.join(libdir, 'cocoa/*.rb'))) 16 | 17 | exts = Dir.glob(File.join(libdir, "cocoa/*.a")) 18 | unless exts.empty? 19 | app.vendor_project libdir, :static, :products => exts, :source_files => [], :force_load => true 20 | if comp == 'ui' 21 | app.custom_init_funcs << 'Init_CSSNode' 22 | end 23 | end 24 | end 25 | app.files.unshift(*files) 26 | 27 | samples = %w(android ios osx).delete_if {|t| t == template} 28 | samples.each do |sample| 29 | app.files.delete_if { |path| path.start_with?("./app/#{sample}") } 30 | end 31 | app.spec_files.delete_if { |path| path.start_with?('./spec/helpers/android') } 32 | 33 | app.frameworks += ['SystemConfiguration', 'CoreLocation', 'AddressBookUI', 'AVFoundation'] 34 | end 35 | -------------------------------------------------------------------------------- /lib/common.rb: -------------------------------------------------------------------------------- 1 | FLOW_COMPONENTS = %w{net json digest store base64 location task ui} 2 | -------------------------------------------------------------------------------- /lib/motion-flow/base64.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | Motion::Flow.load_library 'base64' 3 | -------------------------------------------------------------------------------- /lib/motion-flow/digest.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | Motion::Flow.load_library 'digest' 3 | -------------------------------------------------------------------------------- /lib/motion-flow/json.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | Motion::Flow.load_library 'json' 3 | -------------------------------------------------------------------------------- /lib/motion-flow/loader.rb: -------------------------------------------------------------------------------- 1 | $:.unshift("/Library/RubyMotion/lib") 2 | 3 | module Motion 4 | class Flow 5 | def self.load_library(lib_name) 6 | lib_dir = File.join(File.dirname(__FILE__), '../../flow', lib_name) 7 | 8 | case Motion::Project::App.template 9 | when :android 10 | platform = 'android' 11 | when :ios, :tvos, :osx, :'ios-extension' 12 | platform = 'cocoa' 13 | else 14 | raise "Project template #{Motion::Project::App.template} not supported by Flow" 15 | end 16 | 17 | Motion::Project::App.setup do |app| 18 | app.files.concat(Dir.glob(File.join(lib_dir, '*.rb'))) 19 | app.files.concat(Dir.glob(File.join(lib_dir, platform, '*.rb'))) 20 | 21 | if platform == 'android' 22 | abis = %w{armeabi-v7a x86} 23 | if abis.all? { |x| File.exist?(File.join(lib_dir, 'android', x)) } 24 | abis.each do |abi| 25 | app.libs[abi] += Dir.glob(File.join(lib_dir, 'android', abi, "*.a")).map { |x| "\"#{x}\""} 26 | end 27 | end 28 | 29 | # TODO: figure out which of these vendor libs are required for each Flow lib. Figure out how to selectively add these only if they haven't already been added by the main project. 30 | # vendor_dir = File.join(File.dirname(__FILE__), '../../vendor/android') 31 | # v7_app_compat_dir = File.join(vendor_dir, 'support/v7/appcompat') 32 | # app.vendor_project(:jar => File.join(v7_app_compat_dir, "/libs/android-support-v4.jar")) 33 | # app.vendor_project(:jar => File.join(v7_app_compat_dir, "/libs/android-support-v7-appcompat.jar"), :resources => File.join(v7_app_compat_dir, "/res"), :manifest => File.join(v7_app_compat_dir, "/AndroidManifest.xml")) 34 | # 35 | # app.vendor_project(:jar => File.join(vendor_dir, 'google-play-services_lib/libs/google-play-services.jar'), :filter => ['^.com.google.android.gms.vision'], :resources => File.join(vendor_dir, 'google-play-services_lib/res'), :manifest => File.join(vendor_dir, 'google-play-services_lib/AndroidManifest.xml')) 36 | # app.manifest_entry('application', 'meta-data', :name => 'com.google.android.gms.version', :value => '@integer/google_play_services_version') 37 | # app.manifest_entry('application', 'meta-data', :name => 'com.google.android.gms.vision.DEPENDENCIES', :value => 'barcode') 38 | end 39 | 40 | if platform == 'cocoa' 41 | exts = Dir.glob(File.join(lib_dir, "cocoa/*.a")) 42 | unless exts.empty? 43 | app.vendor_project lib_dir, :static, :products => exts, :source_files => [], :force_load => true 44 | end 45 | # TODO: figure out which frameworks are required for each Flow lib 46 | app.frameworks += ['SystemConfiguration', 'CoreLocation', 'AddressBookUI'] 47 | end 48 | 49 | yield app if block_given? 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/motion-flow/location.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | Motion::Flow.load_library 'location' 3 | -------------------------------------------------------------------------------- /lib/motion-flow/net.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | 3 | # Load Dependencies 4 | require_relative 'base64' 5 | require_relative 'json' 6 | require_relative 'task' 7 | 8 | Motion::Flow.load_library 'net' 9 | -------------------------------------------------------------------------------- /lib/motion-flow/store.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | 3 | # Load Dependencies 4 | require_relative 'json' 5 | 6 | Motion::Flow.load_library 'store' 7 | -------------------------------------------------------------------------------- /lib/motion-flow/task.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | Motion::Flow.load_library 'task' 3 | -------------------------------------------------------------------------------- /lib/motion-flow/ui.rb: -------------------------------------------------------------------------------- 1 | require_relative 'loader' 2 | 3 | # Load Dependencies 4 | require_relative 'task' 5 | 6 | Motion::Flow.load_library 'ui' do |app| 7 | app.custom_init_funcs << 'Init_CSSNode' 8 | end 9 | -------------------------------------------------------------------------------- /motion-flow.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'motion-flow' 3 | spec.version = '0.1.9' 4 | spec.summary = 'Cross-platform app framework for RubyMotion' 5 | spec.description = "motion-flow allows you to write cross-platform 6 | native mobile apps in Ruby." 7 | spec.author = 'HipByte' 8 | spec.email = 'info@hipbyte.com' 9 | spec.homepage = 'http://www.rubymotion.com' 10 | spec.license = 'BSD-2-Clause' 11 | spec.files = Dir.glob('lib/**/*.rb') + 12 | Dir.glob('flow/**/*.rb') + 13 | Dir.glob('flow/**/*.a') + 14 | Dir.glob('template/**/*') + 15 | Dir.glob('vendor/**/*') 16 | spec.metadata = { "rubymotion_template_dir" => "template" } 17 | end 18 | -------------------------------------------------------------------------------- /samples/reddit/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'motion-flow', path: '../../' 5 | # Add your dependencies here: 6 | -------------------------------------------------------------------------------- /samples/reddit/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | motion-flow (0.1.9) 5 | motion-gradle 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | motion-gradle (2.1.0) 11 | rake (12.3.0) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | motion-flow! 18 | rake 19 | 20 | BUNDLED WITH 21 | 1.16.0 22 | -------------------------------------------------------------------------------- /samples/reddit/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require '../../lib/motion-flow.rb' 3 | -------------------------------------------------------------------------------- /samples/reddit/app/android/main_activity.rb: -------------------------------------------------------------------------------- 1 | class MainActivity < Android::App::Activity 2 | def onCreate(savedInstanceState) 3 | super 4 | 5 | @contentLayout = Android::Widget::FrameLayout.new(self) 6 | @contentLayout.setId(Android::View::View.generateViewId) 7 | 8 | @list = Android::Widget::ListView.new(self) 9 | @list.adapter = TimelineAdapter.new(self, Android::R::Layout::Simple_list_item_1) 10 | @list.choiceMode = Android::Widget::AbsListView::CHOICE_MODE_SINGLE 11 | @list.dividerHeight = 0 12 | @list.backgroundColor = Android::Graphics::Color::WHITE 13 | @list.onItemClickListener = self 14 | self.contentView = @list 15 | 16 | search('cats') 17 | end 18 | 19 | def search(query) 20 | RedditFetcher.fetch_posts(query) do |posts| 21 | @list.adapter.posts = posts 22 | @list.adapter.clear() 23 | @list.adapter.addAll(posts) 24 | @list.adapter.notifyDataSetChanged 25 | end 26 | end 27 | 28 | def onItemClick(parent, view, position, id) 29 | end 30 | 31 | def density 32 | @density ||= resources.displayMetrics.density 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /samples/reddit/app/android/timeline_adapter.rb: -------------------------------------------------------------------------------- 1 | class TimelineAdapter < Android::Widget::ArrayAdapter 2 | def posts=(posts) 3 | @posts = posts 4 | end 5 | 6 | # Instead of a generic TextView, we return a custom view for each schedule item. 7 | def getView(position, convertView, parent) 8 | avatar_view = Android::Widget::ImageView.new(context) 9 | layout_params = Android::Widget::LinearLayout::LayoutParams.new(100,100) 10 | layout_params.setMargins(10, 10, 10, 10) 11 | avatar_view.setLayoutParams(layout_params) 12 | avatar_view.visibility = Android::View::View::GONE 13 | 14 | post = @posts[position] 15 | 16 | if post.thumbnail 17 | Task.background do 18 | stream = Java::Net::URL.new(post.thumbnail).openStream 19 | bitmap = Android::Graphics::BitmapFactory.decodeStream(stream) 20 | 21 | Task.main do 22 | avatar_view.setImageBitmap(bitmap) 23 | avatar_view.visibility = Android::View::View::VISIBLE 24 | end 25 | end 26 | end 27 | 28 | title_view = Android::Widget::TextView.new(context) 29 | title_view.text = post.title 30 | title_view.textSize = 18 31 | title_view.setTypeface(nil, Android::Graphics::Typeface::BOLD) 32 | title_view.textColor = Android::Graphics::Color::BLACK 33 | 34 | subreddit_view = Android::Widget::TextView.new(context) 35 | subreddit_view.text = "@#{post.subreddit}" 36 | subreddit_view.textSize = 16 37 | subreddit_view.textColor = Android::Graphics::Color::BLACK 38 | subreddit_view.setPadding(10,0,0,0) 39 | 40 | title_layout = Android::Widget::LinearLayout.new(context) 41 | title_layout.orientation = Android::Widget::LinearLayout::HORIZONTAL 42 | title_layout.addView(title_view) 43 | title_layout.addView(subreddit_view) 44 | 45 | right_view = Android::Widget::LinearLayout.new(context) 46 | right_view.orientation = Android::Widget::LinearLayout::VERTICAL 47 | right_view.addView(title_layout) 48 | 49 | layout = Android::Widget::LinearLayout.new(context) 50 | layout.orientation = Android::Widget::LinearLayout::HORIZONTAL 51 | layout.addView(avatar_view) 52 | layout.addView(right_view) 53 | layout 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /samples/reddit/app/ios/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | attr_accessor :window 3 | 4 | def application(application, didFinishLaunchingWithOptions:launchOptions) 5 | posts_screen = PostsScreen.new 6 | navigation = UI::Navigation.new(posts_screen) 7 | flow_app = UI::Application.new(navigation, self) 8 | flow_app.start 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /samples/reddit/app/osx/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | attr_reader :window 3 | 4 | def applicationDidFinishLaunching(notification) 5 | buildMenu 6 | buildWindow 7 | 8 | @controller = RedditController.new 9 | 10 | RedditFetcher.fetch_posts('cats') do |posts| 11 | @controller.data = posts 12 | end 13 | end 14 | 15 | def buildWindow 16 | @window = NSWindow.alloc.initWithContentRect([[240, 180], [480, 360]], 17 | styleMask: NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask, 18 | backing: NSBackingStoreBuffered, 19 | defer: false) 20 | @window.title = NSBundle.mainBundle.infoDictionary['CFBundleName'] 21 | @window.orderFrontRegardless 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /samples/reddit/app/osx/menu.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def buildMenu 3 | @mainMenu = NSMenu.new 4 | 5 | appName = NSBundle.mainBundle.infoDictionary['CFBundleName'] 6 | 7 | find_menu = createMenu('Find') do 8 | addItemWithTitle('Find...', action: 'performFindPanelAction:', keyEquivalent: 'f') 9 | addItemWithTitle('Find Next', action: 'performFindPanelAction:', keyEquivalent: 'g') 10 | addItemWithTitle('Find Previous', action: 'performFindPanelAction:', keyEquivalent: 'G') 11 | addItemWithTitle('Use Selection for Find', action: 'performFindPanelAction:', keyEquivalent: 'e') 12 | addItemWithTitle('Jump to Selection', action: 'centerSelectionInVisibleArea:', keyEquivalent: 'j') 13 | end 14 | 15 | spelling_and_grammar_menu = createMenu('Spelling and Grammar') do 16 | addItemWithTitle('Show Spelling and Grammar', action: 'showGuessPanel:', keyEquivalent: ':') 17 | addItemWithTitle('Check Document Now', action: 'checkSpelling:', keyEquivalent: ';') 18 | addItem(NSMenuItem.separatorItem) 19 | addItemWithTitle('Check Spelling While Typing', action: 'toggleContinuousSpellChecking:', keyEquivalent: '') 20 | addItemWithTitle('Check Grammar With Spelling', action: 'toggleGrammarChecking:', keyEquivalent: '') 21 | addItemWithTitle('Correct Spelling Automatically', action: 'toggleAutomaticSpellingCorrection:', keyEquivalent: '') 22 | end 23 | 24 | substitutions_menu = createMenu('Substitutions') do 25 | addItemWithTitle('Show Substitutions', action: 'orderFrontSubstitutionsPanel:', keyEquivalent: 'f') 26 | addItem(NSMenuItem.separatorItem) 27 | addItemWithTitle('Smart Copy/Paste', action: 'toggleSmartInsertDelete:', keyEquivalent: 'f') 28 | addItemWithTitle('Smart Quotes', action: 'toggleAutomaticQuoteSubstitution:', keyEquivalent: 'g') 29 | addItemWithTitle('Smart Dashes', action: 'toggleAutomaticDashSubstitution:', keyEquivalent: '') 30 | addItemWithTitle('Smart Links', action: 'toggleAutomaticLinkDetection:', keyEquivalent: 'G') 31 | addItemWithTitle('Text Replacement', action: 'toggleAutomaticTextReplacement:', keyEquivalent: '') 32 | end 33 | 34 | transformations_menu = createMenu('Transformations') do 35 | addItemWithTitle('Make Upper Case', action: 'uppercaseWord:', keyEquivalent: '') 36 | addItemWithTitle('Make Lower Case', action: 'lowercaseWord:', keyEquivalent: '') 37 | addItemWithTitle('Capitalize', action: 'capitalizeWord:', keyEquivalent: '') 38 | end 39 | 40 | speech_menu = createMenu('Speech') do 41 | addItemWithTitle('Start Speaking', action: 'startSpeaking:', keyEquivalent: '') 42 | addItemWithTitle('Stop Speaking', action: 'stopSpeaking:', keyEquivalent: '') 43 | end 44 | 45 | addMenu(appName) do 46 | addItemWithTitle("About #{appName}", action: 'orderFrontStandardAboutPanel:', keyEquivalent: '') 47 | addItem(NSMenuItem.separatorItem) 48 | addItemWithTitle('Preferences', action: 'openPreferences:', keyEquivalent: ',') 49 | addItem(NSMenuItem.separatorItem) 50 | servicesItem = addItemWithTitle('Services', action: nil, keyEquivalent: '') 51 | NSApp.servicesMenu = servicesItem.submenu = NSMenu.new 52 | addItem(NSMenuItem.separatorItem) 53 | addItemWithTitle("Hide #{appName}", action: 'hide:', keyEquivalent: 'h') 54 | item = addItemWithTitle('Hide Others', action: 'hideOtherApplications:', keyEquivalent: 'H') 55 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 56 | addItemWithTitle('Show All', action: 'unhideAllApplications:', keyEquivalent: '') 57 | addItem(NSMenuItem.separatorItem) 58 | addItemWithTitle("Quit #{appName}", action: 'terminate:', keyEquivalent: 'q') 59 | end 60 | 61 | addMenu('File') do 62 | addItemWithTitle('New', action: 'newDocument:', keyEquivalent: 'n') 63 | addItemWithTitle('Open…', action: 'openDocument:', keyEquivalent: 'o') 64 | addItem(NSMenuItem.separatorItem) 65 | addItemWithTitle('Close', action: 'performClose:', keyEquivalent: 'w') 66 | addItemWithTitle('Save…', action: 'saveDocument:', keyEquivalent: 's') 67 | addItemWithTitle('Revert to Saved', action: 'revertDocumentToSaved:', keyEquivalent: '') 68 | addItem(NSMenuItem.separatorItem) 69 | addItemWithTitle('Page Setup…', action: 'runPageLayout:', keyEquivalent: 'P') 70 | addItemWithTitle('Print…', action: 'printDocument:', keyEquivalent: 'p') 71 | end 72 | 73 | addMenu('Edit') do 74 | addItemWithTitle('Undo', action: 'undo:', keyEquivalent: 'z') 75 | addItemWithTitle('Redo', action: 'redo:', keyEquivalent: 'Z') 76 | addItem(NSMenuItem.separatorItem) 77 | addItemWithTitle('Cut', action: 'cut:', keyEquivalent: 'x') 78 | addItemWithTitle('Copy', action: 'copy:', keyEquivalent: 'c') 79 | addItemWithTitle('Paste', action: 'paste:', keyEquivalent: 'v') 80 | item = addItemWithTitle('Paste and Match Style', action: 'pasteAsPlainText:', keyEquivalent: 'V') 81 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 82 | addItemWithTitle('Delete', action: 'delete:', keyEquivalent: '') 83 | addItemWithTitle('Select All', action: 'selectAll:', keyEquivalent: 'a') 84 | addItem(NSMenuItem.separatorItem) 85 | addItem(find_menu) 86 | addItem(spelling_and_grammar_menu) 87 | addItem(substitutions_menu) 88 | addItem(transformations_menu) 89 | addItem(speech_menu) 90 | end 91 | 92 | fontMenu = createMenu('Font') do 93 | addItemWithTitle('Show Fonts', action: 'orderFrontFontPanel:', keyEquivalent: 't') 94 | addItemWithTitle('Bold', action: 'addFontTrait:', keyEquivalent: 'b') 95 | addItemWithTitle('Italic', action: 'addFontTrait:', keyEquivalent: 'i') 96 | addItemWithTitle('Underline', action: 'underline:', keyEquivalent: 'u') 97 | addItem(NSMenuItem.separatorItem) 98 | addItemWithTitle('Bigger', action: 'modifyFont:', keyEquivalent: '+') 99 | addItemWithTitle('Smaller', action: 'modifyFont:', keyEquivalent: '-') 100 | end 101 | 102 | textMenu = createMenu('Text') do 103 | addItemWithTitle('Align Left', action: 'alignLeft:', keyEquivalent: '{') 104 | addItemWithTitle('Center', action: 'alignCenter:', keyEquivalent: '|') 105 | addItemWithTitle('Justify', action: 'alignJustified:', keyEquivalent: '') 106 | addItemWithTitle('Align Right', action: 'alignRight:', keyEquivalent: '}') 107 | addItem(NSMenuItem.separatorItem) 108 | addItemWithTitle('Show Ruler', action: 'toggleRuler:', keyEquivalent: '') 109 | item = addItemWithTitle('Copy Ruler', action: 'copyRuler:', keyEquivalent: 'c') 110 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSControlKeyMask 111 | item = addItemWithTitle('Paste Ruler', action: 'pasteRuler:', keyEquivalent: 'v') 112 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSControlKeyMask 113 | end 114 | 115 | addMenu('Format') do 116 | addItem fontMenu 117 | addItem textMenu 118 | end 119 | 120 | addMenu('View') do 121 | item = addItemWithTitle('Show Toolbar', action: 'toggleToolbarShown:', keyEquivalent: 't') 122 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 123 | addItemWithTitle('Customize Toolbar…', action: 'runToolbarCustomizationPalette:', keyEquivalent: '') 124 | end 125 | 126 | NSApp.windowsMenu = addMenu('Window') do 127 | addItemWithTitle('Minimize', action: 'performMiniaturize:', keyEquivalent: 'm') 128 | addItemWithTitle('Zoom', action: 'performZoom:', keyEquivalent: '') 129 | addItem(NSMenuItem.separatorItem) 130 | addItemWithTitle('Bring All To Front', action: 'arrangeInFront:', keyEquivalent: '') 131 | end.menu 132 | 133 | NSApp.helpMenu = addMenu('Help') do 134 | addItemWithTitle("#{appName} Help", action: 'showHelp:', keyEquivalent: '?') 135 | end.menu 136 | 137 | NSApp.mainMenu = @mainMenu 138 | end 139 | 140 | private 141 | 142 | def addMenu(title, &b) 143 | item = createMenu(title, &b) 144 | @mainMenu.addItem item 145 | item 146 | end 147 | 148 | def createMenu(title, &b) 149 | menu = NSMenu.alloc.initWithTitle(title) 150 | menu.instance_eval(&b) if b 151 | item = NSMenuItem.alloc.initWithTitle(title, action: nil, keyEquivalent: '') 152 | item.submenu = menu 153 | item 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /samples/reddit/app/osx/reddit_controller.rb: -------------------------------------------------------------------------------- 1 | class RedditController 2 | def initialize 3 | @data = [] 4 | 5 | scroll_view = NSScrollView.alloc.initWithFrame(NSMakeRect(0, 0, 480, 322)) 6 | scroll_view.autoresizingMask = NSViewMinXMargin|NSViewMinYMargin|NSViewWidthSizable|NSViewHeightSizable 7 | scroll_view.hasVerticalScroller = true 8 | app.window.contentView.addSubview(scroll_view) 9 | 10 | @table_view = NSTableView.alloc.init 11 | column_title = NSTableColumn.alloc.initWithIdentifier("text") 12 | column_title.editable = false 13 | column_title.headerCell.setTitle("Text") 14 | column_title.width = 480 15 | @table_view.addTableColumn(column_title) 16 | 17 | @table_view.delegate = self 18 | @table_view.dataSource = self 19 | @table_view.autoresizingMask = NSViewMinXMargin|NSViewMaxXMargin|NSViewMinYMargin|NSViewMaxYMargin 20 | @table_view.target = self 21 | 22 | scroll_view.setDocumentView(@table_view) 23 | end 24 | 25 | def data=(data) 26 | @data = data 27 | @table_view.reloadData 28 | end 29 | 30 | def app 31 | NSApp.delegate 32 | end 33 | 34 | def numberOfRowsInTableView(aTableView) 35 | @data.size 36 | end 37 | 38 | def tableView(aTableView, 39 | objectValueForTableColumn: aTableColumn, 40 | row: rowIndex) 41 | @data[rowIndex].title 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /samples/reddit/app/post.rb: -------------------------------------------------------------------------------- 1 | class Post 2 | def initialize(post = {}) 3 | @post = post 4 | end 5 | 6 | def title 7 | @post.fetch('title', "No title") 8 | end 9 | 10 | def thumbnail 11 | thumbnail = @post.fetch('thumbnail', nil) 12 | return nil unless thumbnail.start_with?('http') 13 | thumbnail 14 | end 15 | 16 | def subreddit 17 | @post.fetch('subreddit', nil) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /samples/reddit/app/post_row.rb: -------------------------------------------------------------------------------- 1 | class PostRow < UI::ListRow 2 | def initialize 3 | self.align_items = :flex_start 4 | self.flex_direction = :row 5 | 6 | self.add_child(thumbnail) 7 | self.add_child(content) 8 | content.add_child(subreddit) 9 | content.add_child(title) 10 | end 11 | 12 | def update(post) 13 | title.text = post.title 14 | subreddit.text = "/r/#{post.subreddit}" 15 | 16 | if post.thumbnail 17 | Net.get(post.thumbnail) do |response| 18 | thumbnail.source = response.body.dataUsingEncoding(NSUTF8StringEncoding) 19 | end 20 | end 21 | end 22 | 23 | protected 24 | 25 | def thumbnail 26 | @thumbnail ||= build_thumbnail 27 | end 28 | 29 | def build_thumbnail 30 | image = UI::Image.new 31 | image.width = 32 32 | image.height = 32 33 | image.margin = [5, 15, 5, 5] 34 | image 35 | end 36 | 37 | def subreddit 38 | @subreddit ||= build_subreddit 39 | end 40 | 41 | def build_subreddit 42 | label = UI::Label.new 43 | label.flex = 1 44 | label.margin = [0, 0, 5, 0] 45 | label.font = {name: "Helvetica", size: 14, trait: :bold} 46 | label 47 | end 48 | 49 | def title 50 | @title ||= build_title 51 | end 52 | 53 | def build_title 54 | label = UI::Label.new 55 | label.flex = 1 56 | label.font = {name: "Helvetica", size: 12} 57 | label 58 | end 59 | 60 | def content 61 | @content ||= build_content 62 | end 63 | 64 | def build_content 65 | view = UI::View.new 66 | view.flex = 1 67 | view.margin = [5, 5, 15, 5] 68 | view 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /samples/reddit/app/posts_list.rb: -------------------------------------------------------------------------------- 1 | class PostsList < UI::List 2 | def initialize 3 | super 4 | self.flex = 1 5 | self.render_row do 6 | PostRow 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/reddit/app/posts_screen.rb: -------------------------------------------------------------------------------- 1 | class PostsScreen < UI::Screen 2 | def on_load 3 | self.navigation.title = "Reddit" 4 | 5 | self.input.on(:change) { |text| fetch_posts(text) } 6 | self.view.add_child(self.input) 7 | self.view.add_child(self.list) 8 | self.view.update_layout 9 | 10 | fetch_posts("cats") 11 | end 12 | 13 | protected 14 | 15 | def fetch_posts(query) 16 | return if query.length < 3 17 | RedditFetcher.fetch_posts(query) do |posts| 18 | self.list.data_source = posts 19 | end 20 | end 21 | 22 | def input 23 | @input ||= build_input 24 | end 25 | 26 | def build_input 27 | input = UI::TextInput.new 28 | input.height = 50 29 | input.justify_content = :flex_end 30 | input.margin = 5 31 | input 32 | end 33 | 34 | def list 35 | @list ||= PostsList.new 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /samples/reddit/app/reddit_fetcher.rb: -------------------------------------------------------------------------------- 1 | class RedditFetcher 2 | def self.fetch_posts(query, &block) 3 | Net.get("https://www.reddit.com/search.json?q=#{query}") do |response| 4 | if data = response.body['data'] 5 | if children = data['children'] 6 | posts = children.map do |t| 7 | Post.new(t['data']) 8 | end 9 | end 10 | end 11 | block.call(posts || []) if block 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /samples/reddit/config/android.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake android:config' to see complete project settings. 5 | app.name = 'Reddit' 6 | app.vm_debug_logs = true 7 | app.permissions = ["android.permission.INTERNET"] 8 | end 9 | -------------------------------------------------------------------------------- /samples/reddit/config/ios.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake ios:config' to see complete project settings. 5 | app.name = 'Reddit' 6 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } 7 | end 8 | -------------------------------------------------------------------------------- /samples/reddit/config/osx.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake ios:config' to see complete project settings. 5 | app.name = 'Reddit' 6 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } 7 | end 8 | -------------------------------------------------------------------------------- /samples/reddit/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/reddit/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /samples/reddit/resources/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/reddit/resources/Default-667h@2x.png -------------------------------------------------------------------------------- /samples/reddit/resources/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/reddit/resources/Default-736h@3x.png -------------------------------------------------------------------------------- /samples/ui_demo/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'motion-flow', path: '../../' 5 | # Add your dependencies here: 6 | -------------------------------------------------------------------------------- /samples/ui_demo/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | motion-flow (0.1.9) 5 | motion-gradle 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | motion-gradle (2.1.0) 11 | rake (10.5.0) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | motion-flow! 18 | rake 19 | 20 | BUNDLED WITH 21 | 1.16.0 22 | -------------------------------------------------------------------------------- /samples/ui_demo/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require '../../lib/motion-flow.rb' 4 | 5 | # This file is empty on purpose, you can edit it to add your own tasks. 6 | # Use `rake -T' to see the list of all tasks. 7 | # For iOS specific settings, refer to config/ios.rb. 8 | # For Android specific settings, refer to config/android.rb. 9 | -------------------------------------------------------------------------------- /samples/ui_demo/app/android/main_activity.rb: -------------------------------------------------------------------------------- 1 | class MainActivity < Android::Support::V7::App::AppCompatActivity 2 | def onCreate(savedInstanceState) 3 | super 4 | UI.context = self 5 | 6 | main_screen = WelcomeScreen.new 7 | navigation = UI::Navigation.new(main_screen) 8 | flow_app = UI::Application.new(navigation, self) 9 | flow_app.start 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /samples/ui_demo/app/ios/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | attr_accessor :window #needed atm 3 | def application(application, didFinishLaunchingWithOptions:launchOptions) 4 | main_screen = WelcomeScreen.new 5 | navigation = UI::Navigation.new(main_screen) 6 | flow_app = UI::Application.new(navigation, self) 7 | flow_app.start 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/ui_demo/app/welcome_screen.rb: -------------------------------------------------------------------------------- 1 | class WelcomeScreen < UI::Screen 2 | 3 | def on_show 4 | self.navigation.hide_bar 5 | end 6 | 7 | def on_load 8 | $screen = self 9 | 10 | background = UI::View.new 11 | background.flex = 1 12 | background.margin = 25 13 | background.background_color = :white 14 | self.view.add_child(background) 15 | 16 | label = UI::Label.new 17 | #label.height = 50 18 | label.margin = [10, 10, 5, 10] 19 | label.text = "It is a period of civil war. Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire." 20 | label.font = { :name => 'Starjedi', :size => 18.0 } 21 | label.background_color = :red 22 | label.color = :white 23 | label.text_alignment = :center 24 | background.add_child(label) 25 | $label = label 26 | 27 | button = UI::Button.new 28 | button.height = 50 29 | button.margin = [0, 5] 30 | button.title = "Submit" 31 | button.background_color = :blue 32 | button.color = :black 33 | #button.border_width = 2.0 34 | #button.border_color = :black 35 | #button.border_radius = 3.0 36 | background.add_child(button) 37 | 38 | text_field = UI::TextInput.new 39 | text_field.height = 50 40 | text_field.margin = [10, 5] 41 | text_field.text = "A textfield" 42 | text_field.background_color = :green 43 | text_field.color = :black 44 | #text_field.on(:change) { |text| p text } 45 | #text_field.on(:blur) { p 'blur'} 46 | #text_field.on(:focus) { p 'focus' } 47 | background.add_child(text_field) 48 | 49 | logo = UI::Image.new 50 | logo.source = "rubymotion-logo.png" 51 | logo.height = 100 52 | logo.margin = [10, 5] 53 | logo.resize_mode = :contain 54 | background.add_child(logo) 55 | 56 | # label.height = 200 57 | # label.animate(delay: 1.0, duration: 0.25) do 58 | # p "animation ended" 59 | # end 60 | 61 | # list = UI::List.new 62 | # list.render_row do |data| 63 | # label = UI::Label.new 64 | # label.text = data 65 | # # label.flex = 1 66 | # # label.margin = 5 67 | # label.height = 100 68 | # label.width = 100 69 | # label.background_color = :green 70 | # label 71 | # end 72 | # list.data_source = ["laurent", "mark", "watson"] 73 | # list.height = 150 74 | # list.margin = 5 75 | # background.add_child(list) 76 | # 77 | # Task.after 2.0 do 78 | # list.data_source = ["laurent", "mark", "watson"].reverse 79 | # end 80 | 81 | self.view.update_layout 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /samples/ui_demo/config/android.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake android:config' to see complete project settings. 5 | app.name = 'uiapp' 6 | end 7 | -------------------------------------------------------------------------------- /samples/ui_demo/config/ios.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake ios:config' to see complete project settings. 5 | app.name = 'uiapp' 6 | end 7 | -------------------------------------------------------------------------------- /samples/ui_demo/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/ui_demo/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /samples/ui_demo/resources/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/ui_demo/resources/Default-667h@2x.png -------------------------------------------------------------------------------- /samples/ui_demo/resources/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/ui_demo/resources/Default-736h@3x.png -------------------------------------------------------------------------------- /samples/ui_demo/resources/Starjedi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/ui_demo/resources/Starjedi.ttf -------------------------------------------------------------------------------- /samples/ui_demo/resources/rubymotion-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/samples/ui_demo/resources/rubymotion-logo.png -------------------------------------------------------------------------------- /template/flow/files/.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /template/flow/files/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'motion-flow' 5 | # Add your dependencies here: 6 | -------------------------------------------------------------------------------- /template/flow/files/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | begin 4 | require 'bundler' 5 | rescue LoadError 6 | raise "Could not load the bundler gem. Install it with `gem install bundler`." 7 | end 8 | Bundler.require 9 | 10 | # This file is empty on purpose, you can edit it to add your own tasks. 11 | # Use `rake -T' to see the list of all tasks. 12 | # For iOS specific settings, refer to config/ios.rb. 13 | # For OS X specific settings, refer to config/osx.rb. 14 | # For Android specific settings, refer to config/android.rb. 15 | -------------------------------------------------------------------------------- /template/flow/files/app/android/main_activity.rb: -------------------------------------------------------------------------------- 1 | class MainActivity < Android::App::Activity 2 | def onCreate(savedInstanceState) 3 | Store.context = self 4 | super 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /template/flow/files/app/ios/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | rootViewController = UIViewController.alloc.init 4 | rootViewController.title = 'testios' 5 | rootViewController.view.backgroundColor = UIColor.whiteColor 6 | 7 | navigationController = UINavigationController.alloc.initWithRootViewController(rootViewController) 8 | 9 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 10 | @window.rootViewController = navigationController 11 | @window.makeKeyAndVisible 12 | 13 | true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /template/flow/files/config/android.rb.erb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | begin 4 | require 'bundler' 5 | Bundler.require 6 | rescue LoadError 7 | end 8 | 9 | Motion::Project::App.setup do |app| 10 | # Use `rake android:config' to see complete project settings. 11 | app.name = '<%= name %>' 12 | end 13 | -------------------------------------------------------------------------------- /template/flow/files/config/ios.rb.erb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | begin 4 | require 'bundler' 5 | Bundler.require 6 | rescue LoadError 7 | end 8 | 9 | Motion::Project::App.setup do |app| 10 | # Use `rake ios:config' to see complete project settings. 11 | app.name = '<%= name %>' 12 | end 13 | -------------------------------------------------------------------------------- /template/flow/files/config/osx.rb.erb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | begin 4 | require 'bundler' 5 | Bundler.require 6 | rescue LoadError 7 | end 8 | 9 | Motion::Project::App.setup do |app| 10 | # Use `rake ios:config' to see complete project settings. 11 | app.name = '<%= name %>' 12 | end 13 | -------------------------------------------------------------------------------- /template/flow/files/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/template/flow/files/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /template/flow/files/resources/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/template/flow/files/resources/Default-667h@2x.png -------------------------------------------------------------------------------- /template/flow/files/resources/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipByte/Flow/ea2b52d8c14609fc26df7c661139e90b9600b218/template/flow/files/resources/Default-736h@3x.png -------------------------------------------------------------------------------- /test/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'thin' 5 | gem 'sinatra' 6 | -------------------------------------------------------------------------------- /test/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | daemons (1.2.5) 5 | eventmachine (1.2.5) 6 | mustermann (1.0.1) 7 | rack (2.0.3) 8 | rack-protection (2.0.0) 9 | rack 10 | rake (12.3.0) 11 | sinatra (2.0.0) 12 | mustermann (~> 1.0) 13 | rack (~> 2.0) 14 | rack-protection (= 2.0.0) 15 | tilt (~> 2.0) 16 | thin (1.7.2) 17 | daemons (~> 1.0, >= 1.0.9) 18 | eventmachine (~> 1.0, >= 1.0.4) 19 | rack (>= 1, < 3) 20 | tilt (2.0.8) 21 | 22 | PLATFORMS 23 | ruby 24 | 25 | DEPENDENCIES 26 | rake 27 | sinatra 28 | thin 29 | 30 | BUNDLED WITH 31 | 1.16.0 32 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Run the tests 4 | 5 | * gem install sinatra 6 | * Start the local http server : `ruby server.rb` 7 | 8 | ### iOS 9 | 10 | * rake ios:spec 11 | 12 | ### Android 13 | 14 | * rake android:spec 15 | -------------------------------------------------------------------------------- /test/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require '../lib/motion-flow.rb' 4 | 5 | # This file is empty on purpose, you can edit it to add your own tasks. 6 | # Use `rake -T' to see the list of all tasks. 7 | # For iOS specific settings, refer to config/ios.rb. 8 | # For Android specific settings, refer to config/android.rb. 9 | -------------------------------------------------------------------------------- /test/app/android/main_activity.rb: -------------------------------------------------------------------------------- 1 | class MainActivity < Android::App::Activity 2 | def onCreate(savedInstanceState) 3 | Store.context = self 4 | Net.context = self 5 | super 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/app/ios/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | rootViewController = UIViewController.alloc.init 4 | rootViewController.title = 'testios' 5 | rootViewController.view.backgroundColor = UIColor.whiteColor 6 | 7 | navigationController = UINavigationController.alloc.initWithRootViewController(rootViewController) 8 | 9 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 10 | @window.rootViewController = navigationController 11 | @window.makeKeyAndVisible 12 | 13 | true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/app/osx/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def applicationDidFinishLaunching(notification) 3 | @mainWindow = NSWindow.alloc.initWithContentRect([[240, 180], [480, 360]], 4 | styleMask: NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask, 5 | backing: NSBackingStoreBuffered, 6 | defer: false) 7 | @mainWindow.title = NSBundle.mainBundle.infoDictionary['CFBundleName'] 8 | @mainWindow.orderFrontRegardless 9 | end 10 | end -------------------------------------------------------------------------------- /test/config/android.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake android:config' to see complete project settings. 5 | app.name = 'test' 6 | app.archs = ['x86'] 7 | app.api_version = '23' 8 | app.vm_debug_logs = true 9 | app.permissions = ["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE", "android.permission.ACCESS_FINE_LOCATION"] 10 | end 11 | -------------------------------------------------------------------------------- /test/config/ios.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake ios:config' to see complete project settings. 5 | app.name = 'test' 6 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } 7 | app.info_plist['NSLocationWhenInUseUsageDescription'] = "Required to test location services" 8 | app.info_plist['NSLocationAlwaysUsageDescription'] = "Required to test location services" 9 | end 10 | -------------------------------------------------------------------------------- /test/config/osx.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Motion::Project::App.setup do |app| 4 | # Use `rake ios:config' to see complete project settings. 5 | app.name = 'test' 6 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } 7 | end 8 | -------------------------------------------------------------------------------- /test/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require 'thin' 4 | Thin::Logging.debug = true 5 | Thin::Logging.trace = true 6 | 7 | helpers do 8 | def restricted_area 9 | headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' 10 | halt 401, "Not authorized\n" 11 | end 12 | 13 | def http_protect! 14 | return if http_authorized? 15 | restricted_area 16 | end 17 | 18 | def token_protect! 19 | return if token_authorized? 20 | restricted_area 21 | end 22 | 23 | def token_authorized? 24 | request.env.fetch('HTTP_AUTHORIZATION', nil) && 25 | 'rubymotion' == request.env['HTTP_AUTHORIZATION'].match(/Token token="(.*)"/).captures.first 26 | end 27 | 28 | def http_authorized? 29 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 30 | @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == ['username', 'admin'] 31 | end 32 | 33 | def payload_request 34 | body = request.body.read 35 | [ 36 | 200, 37 | { 38 | "Content-Type" => "application/json", 39 | "X-Request-Method" => request.env['REQUEST_METHOD'] 40 | }, 41 | { 42 | "args" => params, 43 | "data" => body, 44 | "json" => JSON.load(body) 45 | }.to_json 46 | ] 47 | end 48 | end 49 | 50 | get('/token_auth_protected') do 51 | token_protect! 52 | "Welcome" 53 | end 54 | 55 | get('/basic_auth_protected') do 56 | http_protect! 57 | "Welcome" 58 | end 59 | 60 | get('/') do 61 | [ 62 | 200, 63 | { 64 | "Content-Type" => "application/json", 65 | "X-Request-Method" => request.env['REQUEST_METHOD'] 66 | }, 67 | { 68 | "args" => params 69 | }.to_json 70 | ] 71 | end 72 | 73 | post('/') do 74 | payload_request 75 | end 76 | 77 | patch('/') do 78 | payload_request 79 | end 80 | 81 | delete('/') do 82 | payload_request 83 | end 84 | 85 | put('/') do 86 | payload_request 87 | end 88 | 89 | options('/') do 90 | [ 91 | 200, 92 | { 93 | "X-Request-Method" => request.env['REQUEST_METHOD'] 94 | }, 95 | nil 96 | ] 97 | end 98 | 99 | head('/') do 100 | [ 101 | 200, 102 | { 103 | "X-Request-Method" => request.env['REQUEST_METHOD'] 104 | }, 105 | nil 106 | ] 107 | end 108 | 109 | get('/txt') do 110 | [ 111 | 200, 112 | {"Content-Type" => "text/plain"}, 113 | "User: #{params['user']}" 114 | ] 115 | end 116 | 117 | post('/form') do 118 | [ 119 | 200, 120 | {"Content-Type" => "application/json"}, 121 | 122 | { 123 | "args" => params, 124 | "data" => request.body.read, 125 | "json" => params 126 | }.to_json 127 | ] 128 | end 129 | -------------------------------------------------------------------------------- /test/spec/base64_spec.rb: -------------------------------------------------------------------------------- 1 | describe Base64 do 2 | context "#decode" do 3 | before do 4 | @subject = Base64.decode("eHg=") 5 | end 6 | 7 | it "returns a base64 encoded string" do 8 | @subject.should == "xx" 9 | end 10 | end 11 | 12 | context "#encode" do 13 | before do 14 | @subject = Base64.encode("xx") 15 | end 16 | 17 | it "returns a base64 encoded string" do 18 | @subject.should == "eHg=" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/spec/digest_spec.rb: -------------------------------------------------------------------------------- 1 | digests = [ 2 | [Digest::MD5, '5eb63bbbe01eeed093cb22bb8f5acdc3'], 3 | [Digest::SHA1, '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'], 4 | [Digest::SHA224, '2f05477fc24bb4faefd86517156dafdecec45b8ad3cf2522a563582b'], 5 | [Digest::SHA256, 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'], 6 | [Digest::SHA384, 'fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd'], 7 | [Digest::SHA512, '309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f'] 8 | ] 9 | 10 | digests.each do |klass, hello_world_md| 11 | describe "#{klass}" do 12 | it "can digest strings directly using .digest" do 13 | klass.digest('hello world').should == hello_world_md 14 | end 15 | 16 | it "can be instantiated using .new" do 17 | digest = klass.new 18 | digest.class.should == klass 19 | end 20 | 21 | it "can digest strings sequentially using #update and #digest" do 22 | digest = klass.new 23 | digest.update('hello') 24 | digest.update(' ') 25 | digest.update('world') 26 | digest.digest.should == hello_world_md 27 | end 28 | 29 | it "can be reset using #reset" do 30 | digest = klass.new 31 | digest.update('iaudnaidunfaisdfnasfjweawer') 32 | digest.reset 33 | digest.update('hello world') 34 | digest.digest.should == hello_world_md 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/spec/helpers/android/constants.rb: -------------------------------------------------------------------------------- 1 | HTTP_SERVER = "http://10.0.2.2:4567" 2 | -------------------------------------------------------------------------------- /test/spec/helpers/cocoa/constants.rb: -------------------------------------------------------------------------------- 1 | HTTP_SERVER = "http://localhost:4567" 2 | -------------------------------------------------------------------------------- /test/spec/json_spec.rb: -------------------------------------------------------------------------------- 1 | describe "JSON" do 2 | it "can serialize empty arrays" do 3 | txt = [].to_json 4 | txt.should == '[]' 5 | JSON.load(txt).should == [] 6 | end 7 | 8 | it "can serialize complex arrays" do 9 | ary = [1, 'two', {'three' => 3}, true, false, [5, 6.1, [7.2, 8]], 9, 10] 10 | txt = ary.to_json 11 | txt.class.should == String 12 | 13 | ary2 = JSON.load(txt) 14 | ary2.class.should == Array 15 | ary2.should == ary 16 | end 17 | 18 | it "can serialize empty hashes" do 19 | txt = {}.to_json 20 | txt.should == '{}' 21 | JSON.load(txt).should == {} 22 | end 23 | 24 | it "can serialize complex hashes" do 25 | hash = { 26 | 'one' => 1, 27 | 'two' => 2, 28 | 'three' => { 'three' => 3 }, 29 | 'four' => true, 30 | 'five' => false, 31 | 'six' => 6.1, 32 | 'seven' => [7.1, 7.2, 7.3], 33 | 'eight' => [8, 8, 8, [8, [8, 8.8, 8]], 8, 8] 34 | } 35 | txt = hash.to_json 36 | txt.class.should == String 37 | 38 | hash2 = JSON.load(txt) 39 | hash2.class.should == Hash 40 | hash2.should == hash 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/spec/net/authorization_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Authorization do 2 | context "Invalid params" do 3 | it "expects valid token or basic params" do 4 | Proc.new { Net::Authorization.new }.should.raise? RuntimeError 5 | end 6 | end 7 | 8 | context "Basic authorization" do 9 | before do 10 | @subject = Net::Authorization.new(username: "foo", password: "bar") 11 | end 12 | 13 | it ".to_s" do 14 | @subject.to_s.should == "Basic Zm9vOmJhcg==" 15 | end 16 | 17 | it ".username" do 18 | @subject.username.should == "foo" 19 | end 20 | 21 | it ".password" do 22 | @subject.password.should == "bar" 23 | end 24 | 25 | it ".basic?" do 26 | @subject.basic?.should == true 27 | end 28 | 29 | it ".token?" do 30 | @subject.token?.should == false 31 | end 32 | end 33 | 34 | context "Token authorization" do 35 | before do 36 | @subject = Net::Authorization.new(token: "xxxx") 37 | end 38 | 39 | it ".to_s" do 40 | @subject.to_s.should == "Token token=\"xxxx\"" 41 | end 42 | 43 | it ".basic?" do 44 | @subject.basic?.should == false 45 | end 46 | 47 | it ".token" do 48 | @subject.token.should == "xxxx" 49 | end 50 | 51 | it ".token?" do 52 | @subject.token?.should == true 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/spec/net/config_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Config do 2 | it "can set connect_timeout" do 3 | Net::Config.connect_timeout = 10 4 | Net::Config.connect_timeout.should == 10 5 | end 6 | 7 | it "has a default connect_timeout" do 8 | Net::Config.connect_timeout = nil 9 | Net::Config.connect_timeout.should == 30 10 | end 11 | 12 | it "can set read_timeout" do 13 | Net::Config.read_timeout = 10 14 | Net::Config.read_timeout.should == 10 15 | end 16 | 17 | it "has a default read_timeout" do 18 | Net::Config.read_timeout = nil 19 | Net::Config.read_timeout.should == 604800 20 | end 21 | 22 | it "can set user_agent" do 23 | Net::Config.user_agent = "Some User Agent" 24 | Net::Config.user_agent.should == "Some User Agent" 25 | end 26 | 27 | it "has a default user_agent" do 28 | Net::Config.user_agent = nil 29 | Net::Config.user_agent.should == Net::Config::USER_AGENT 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/spec/net/expectation_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Expectation do 2 | before do 3 | @url = "http://www.example.com" 4 | @subject = Net::Expectation.new(@url) 5 | end 6 | 7 | after do 8 | Net::Expectation.clear 9 | end 10 | 11 | describe ".new" do 12 | it "sets url" do 13 | @subject.url.should == @url 14 | end 15 | end 16 | 17 | describe ".all" do 18 | context "when @expectations nil" do 19 | it "returns empty array" do 20 | Net::Expectation.all.should == [] 21 | end 22 | end 23 | end 24 | 25 | describe ".clear" do 26 | it "clears all" do 27 | Net::Expectation.all << Net::Expectation.new('www') 28 | Net::Expectation.all.count.should == 1 29 | Net::Expectation.clear 30 | Net::Expectation.all.should == [] 31 | end 32 | end 33 | 34 | describe "#and_return" do 35 | before do 36 | @response = Net::Response.new 37 | end 38 | 39 | it "sets response" do 40 | @subject.and_return(@response) 41 | @subject.response.should == @response 42 | end 43 | end 44 | 45 | describe "#matches?" do 46 | before do 47 | @request = Net::Request.new(@url) 48 | end 49 | 50 | it "should match identic string url" do 51 | @subject.matches?(@request).should == true 52 | end 53 | end 54 | 55 | describe "#response" do 56 | before do 57 | @response = Net::Response.new 58 | @subject.and_return(Net::Response.new) 59 | end 60 | 61 | it "marks the response as mocked" do 62 | @subject.response.mock.should == true 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/spec/net/header_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Header do 2 | it "has a a field and a value" do 3 | header = Net::Header.new('Content-Type', 'application/json') 4 | header.field.should == 'Content-Type' 5 | header.value.should == 'application/json' 6 | end 7 | 8 | it "rewrites shorthands" do 9 | header = Net::Header.new(:content_type, :json) 10 | header.field.should == 'Content-Type' 11 | header.value.should == 'application/json' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/spec/net/response_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Response do 2 | before do 3 | @subject = Net::Response.new({ 4 | status_code: 200, 5 | status_message: :ok, 6 | mime_type: "application/json", 7 | body: '' 8 | }) 9 | end 10 | 11 | it ".status" do 12 | @subject.status.should == 200 13 | end 14 | 15 | it ".status_message" do 16 | @subject.status_message.should == :ok 17 | end 18 | 19 | it ".mime_type" do 20 | @subject.mime_type.should == "application/json" 21 | end 22 | 23 | it ".body" do 24 | @subject.body.should == "" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/spec/net/session_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net::Session do 2 | before do 3 | @subject = Net::Session.build("www.example.com") do 4 | header(:content_type, :json) 5 | authorize(token: "xxxx") 6 | end 7 | end 8 | 9 | it ".headers" do 10 | @subject.headers.should == {"Content-Type" => "application/json"} 11 | end 12 | 13 | it ".authorization" do 14 | @subject.authorization.should.is_a? Net::Authorization 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/spec/net_spec.rb: -------------------------------------------------------------------------------- 1 | describe Net do 2 | before do 3 | @response = nil 4 | end 5 | 6 | # describe ".reachable?" do 7 | # before do 8 | # @reachable = false 9 | # end 10 | 11 | # # FIXME : Sometimes, this spec will be failed with "LocalJumpError: no block given" 12 | # # it "tracks network reachability state" do 13 | # # Net.reachable?("www.google.com") do |reachable| 14 | # # sleep 0.1 15 | # # @reachable = reachable 16 | # # resume 17 | # # end 18 | 19 | # # wait do 20 | # # @reachable.should == true 21 | # # end 22 | # # end 23 | # end 24 | 25 | describe ".stub" do 26 | before do 27 | @url = "http://unkown_domain.test" 28 | @request = Net::Request.new(@url) 29 | @expected_response = Net::Response.new 30 | end 31 | 32 | it "doesnt hit network" do 33 | Net.stub(@url).and_return(@expected_response) 34 | Net.get(@url) do |response| 35 | response.should == @expected_response 36 | end 37 | end 38 | end 39 | 40 | describe ".get" do 41 | it "can pass Token based HTTP auth" do 42 | session = Net.build(HTTP_SERVER) do 43 | authorize(token: 'rubymotion') 44 | end 45 | session.get('/token_auth_protected') do |response| 46 | @response = response 47 | resume 48 | end 49 | 50 | wait do 51 | @response.body.to_s.should == "Welcome" 52 | end 53 | end 54 | 55 | it "can pass Basic HTTP auth" do 56 | session = Net::Session.build(HTTP_SERVER) do 57 | authorize(username: 'username', password: 'admin') 58 | end 59 | session.get('/basic_auth_protected') do |response| 60 | @response = response 61 | resume 62 | end 63 | 64 | wait do 65 | @response.body.to_s.should == "Welcome" 66 | end 67 | end 68 | 69 | it "has a correct TXT response" do 70 | Net.get("#{HTTP_SERVER}/txt?user=1") do |response| 71 | @response = response 72 | resume 73 | end 74 | 75 | wait do 76 | @response.body.to_s.should =~ /User: 1/ 77 | @response.status.should == 200 78 | @response.status_message.should == "HTTP/1.1 200 OK" 79 | end 80 | end 81 | end 82 | 83 | describe ".post" do 84 | it "has a correct JSON response" do 85 | options = { 86 | body: {user: 1}, 87 | headers: { 88 | 'Content-Type' => 'application/json' 89 | } 90 | } 91 | Net.post("#{HTTP_SERVER}?test=1", options) do |response| 92 | @response = response 93 | resume 94 | end 95 | 96 | wait do 97 | @response.body['args']['test'].should == "1" 98 | @response.body['json']['user'].should == 1 99 | @response.status.should == 200 100 | @response.headers['X-Request-Method'].should == "POST" 101 | @response.status_message.should == "HTTP/1.1 200 OK" 102 | end 103 | end 104 | 105 | it "can post form url encoded body" do 106 | options = { 107 | body: "user=1" 108 | } 109 | Net.post("#{HTTP_SERVER}/form", options) do |response| 110 | @response = response 111 | resume 112 | end 113 | 114 | wait do 115 | @response.body['data'].should == "user=1" 116 | end 117 | end 118 | end 119 | 120 | describe ".put" do 121 | it "has a correct JSON response" do 122 | options = { 123 | body: {user: 1}, 124 | headers: { 125 | 'Content-Type' => 'application/json' 126 | } 127 | } 128 | Net.put("#{HTTP_SERVER}?test=1", options) do |response| 129 | @response = response 130 | resume 131 | end 132 | 133 | wait do 134 | @response.body['args']['test'].should == "1" 135 | @response.body['json']['user'].should == 1 136 | @response.status.should == 200 137 | @response.headers['X-Request-Method'].should == "PUT" 138 | @response.status_message.should == "HTTP/1.1 200 OK" 139 | end 140 | end 141 | end 142 | 143 | describe ".patch" do 144 | it "has a correct JSON response" do 145 | options = { 146 | body: {user: 1}, 147 | headers: { 148 | 'Content-Type' => 'application/json' 149 | } 150 | } 151 | Net.patch("#{HTTP_SERVER}?test=1", options) do |response| 152 | @response = response 153 | resume 154 | end 155 | 156 | wait do 157 | @response.body['args']['test'].should == "1" 158 | @response.body['json']['user'].should == 1 159 | @response.status.should == 200 160 | @response.headers['X-Request-Method'].should == "PATCH" 161 | @response.status_message.should == "HTTP/1.1 200 OK" 162 | end 163 | end 164 | end 165 | 166 | describe ".delete" do 167 | it "has a correct JSON response" do 168 | options = { 169 | body: {user: 1}, 170 | headers: { 171 | 'Content-Type' => 'application/json' 172 | } 173 | } 174 | Net.delete("#{HTTP_SERVER}?test=1", options) do |response| 175 | @response = response 176 | resume 177 | end 178 | 179 | wait do 180 | @response.body['args']['test'].should == "1" 181 | @response.body['json']['user'].should == 1 182 | @response.status.should == 200 183 | @response.headers['X-Request-Method'].should == "DELETE" 184 | @response.status_message.should == "HTTP/1.1 200 OK" 185 | end 186 | end 187 | end 188 | 189 | describe ".head" do 190 | it "has a correct response" do 191 | Net.head(HTTP_SERVER) do |response| 192 | @response = response 193 | resume 194 | end 195 | 196 | wait do 197 | @response.headers['X-Request-Method'].should == "HEAD" 198 | end 199 | end 200 | end 201 | 202 | describe ".options" do 203 | it "has a correct response" do 204 | Net.options(HTTP_SERVER) do |response| 205 | @response = response 206 | resume 207 | end 208 | 209 | wait do 210 | @response.headers['X-Request-Method'].should == "OPTIONS" 211 | end 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /test/spec/store_spec.rb: -------------------------------------------------------------------------------- 1 | describe Store do 2 | before do 3 | Store['string'] = 1 4 | Store[:symbol] = 1 5 | Store['hash'] = {'test' => 1} 6 | Store['array'] = [1, 2, 3] 7 | Store['complex'] = [1, 2, {'test' => 1}, "A", [{'test' => 2}]] 8 | end 9 | 10 | describe "hash methods" do 11 | it "access a string key" do 12 | Store['string'].should == 1 13 | end 14 | 15 | it "converts symbols keys to strings" do 16 | Store['symbol'].should == 1 17 | end 18 | 19 | it "serializes hash" do 20 | Store['hash'].should == {'test' => 1} 21 | end 22 | 23 | it "serializes array" do 24 | Store['array'].should == [1, 2, 3] 25 | end 26 | 27 | it "serializes complex structures" do 28 | Store['complex'].should == [1, 2, {'test' => 1}, "A", [{'test' => 2}]] 29 | end 30 | end 31 | 32 | describe ".all" do 33 | it "returns all stored values" do 34 | dict = Store.all 35 | dict['string'].should == 1 36 | dict['symbol'].should == 1 37 | dict['hash'].should == {'test' => 1} 38 | dict['array'].should == [1, 2, 3] 39 | dict['complex'].should == [1, 2, {'test' => 1}, "A", [{'test' => 2}]] 40 | end 41 | end 42 | 43 | describe ".delete" do 44 | it "deletes a string key" do 45 | Store.delete('string') 46 | Store['string'].should == nil 47 | end 48 | 49 | it "deletes a symbol key" do 50 | Store.delete(:symbol) 51 | Store[:symbol].should == nil 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/spec/task_spec.rb: -------------------------------------------------------------------------------- 1 | class ProxyTestClass 2 | attr_accessor :proof 3 | end 4 | 5 | describe Task do 6 | before do 7 | @proxy = ProxyTestClass.new 8 | end 9 | 10 | describe '.after' do 11 | it 'schedules and executes timers' do 12 | @proxy.proof = false 13 | Task.after 0.5 do 14 | @proxy.proof = true 15 | end 16 | wait 1 do 17 | @proxy.proof.should == true 18 | end 19 | end 20 | end 21 | 22 | describe '.every' do 23 | it 'runs callbacks repeatedly' do 24 | @proxy.proof = 0 25 | @timer = Task.every 0.5 do 26 | @proxy.proof = @proxy.proof + 1 27 | @timer.stop if @proxy.proof > 2 28 | end 29 | wait 1.1 do 30 | @proxy.proof.should >= 2 31 | end 32 | end 33 | end 34 | 35 | describe '.stop' do 36 | it 'cancels timers' do 37 | @proxy.proof = true 38 | timer = Task.after 10.0 do 39 | @proxy.proof = false 40 | end 41 | timer.stop 42 | @proxy.proof.should == true 43 | end 44 | 45 | it 'cancels periodic timers' do 46 | @proxy.proof = true 47 | timer = Task.every 10.0 do 48 | @proxy.proof = false 49 | end 50 | timer.stop 51 | @proxy.proof.should == true 52 | end 53 | end 54 | 55 | describe '.main' do 56 | it 'runs on the main thread' do 57 | @proxy.proof = false 58 | Task.main do 59 | @proxy.proof = Task.main? 60 | end 61 | 62 | wait 1 do 63 | @proxy.proof.should == true 64 | end 65 | end 66 | end 67 | 68 | describe '.schedule' do 69 | it 'waits for completion' do 70 | @proxy.proof = 0 71 | 72 | queue = Task.queue 73 | queue.schedule { @proxy.proof += 1 } 74 | queue.schedule { @proxy.proof += 1 } 75 | queue.wait 76 | 77 | wait 0.1 do 78 | @proxy.proof.should == 2 79 | end 80 | end 81 | end 82 | 83 | describe '.background' do 84 | it 'runs on the background thread' do 85 | @proxy.proof = true 86 | Task.background do 87 | @proxy.proof = Task.main? 88 | end 89 | 90 | wait 1 do 91 | @proxy.proof.should == false 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/spec/ui/color_spec.rb: -------------------------------------------------------------------------------- 1 | describe UI::Color do 2 | context "#() protocol conversion" do 3 | it "should convert a String with a #" do 4 | UI::Color("#00F").to_a.should == [0, 0, 255, 255] 5 | end 6 | 7 | it "should convert a String without a #" do 8 | UI::Color("00F").to_a.should == [0, 0, 255, 255] 9 | end 10 | 11 | it "should convert a String without 6 digits" do 12 | UI::Color("FF8A19").to_a.should == [255, 138, 25, 255] 13 | end 14 | 15 | it "should convert a String without 8 digits (alpha component)" do 16 | UI::Color("88FF8A19").to_a.should == [255, 138, 25, 136] 17 | end 18 | 19 | it "should convert a 3 values Array to rgba with default alpha = 255" do 20 | UI::Color([0, 0, 1]).to_a.should == [0, 0, 1, 255] 21 | end 22 | 23 | it "should convert a 4 values Array" do 24 | UI::Color([0, 0, 1, 255]).to_a.should == [0, 0, 1, 255] 25 | end 26 | 27 | it "should convert native platform Color object" do 28 | if defined?(UIColor) 29 | UI::Color(UIColor.blueColor).to_a.should == [0, 0, 255, 255] 30 | else 31 | UI::Color(Android::Graphics::Color.argb(255, 0, 0, 255)).to_a.should == [0, 0, 255, 255] 32 | end 33 | end 34 | 35 | it "should convert a Symbol" do 36 | UI::Color(:blue).to_a.should == [0, 0, 255, 255] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/spec/ui/label.rb: -------------------------------------------------------------------------------- 1 | describe UI::Label do 2 | describe "#measure" do 3 | context "when text is nil" do 4 | it "returns [0, 0]" do 5 | label = UI::Label.new 6 | label.text = nil 7 | w = h = 500.0 8 | label.measure(w,h).should == [0,0] 9 | end 10 | end 11 | 12 | context "when text is empty" do 13 | it "returns [0, 0]" do 14 | label = UI::Label.new 15 | label.text = "" 16 | w = h = 500.0 17 | label.measure(w,h).should == [0,0] 18 | end 19 | end 20 | 21 | context "when text is not empty" do 22 | it "returns the correct size" do 23 | label = UI::Label.new 24 | label.text = "Hello World" 25 | w = h = 500.0 26 | label.measure(w,h).should == [w, 21] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/spec/ui/view.rb: -------------------------------------------------------------------------------- 1 | describe UI::View do 2 | context "#hidden=(hidden)" do 3 | it "should set size to [0, 0]" do 4 | view = UI::View.new 5 | view.width = 100 6 | view.height = 100 7 | view.hidden = true 8 | view.width.should == 0 9 | view.height.should == 0 10 | end 11 | 12 | it "should restore previous size" do 13 | view = UI::View.new 14 | view.width = 100 15 | view.height = 100 16 | view.hidden = true 17 | view.hidden = false 18 | view.width.should == 100 19 | view.height.should == 100 20 | end 21 | 22 | it "restores the size of a view never shown" do 23 | view = UI::View.new 24 | view.hidden = true 25 | view.width = 100 26 | view.height = 100 27 | view.hidden = false 28 | view.width.should == 100 29 | view.height.should == 100 30 | end 31 | end 32 | end 33 | --------------------------------------------------------------------------------