├── .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 | [](https://travis-ci.org/HipByte/Flow)
2 |
3 | # Flow
4 |
5 |
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 |
--------------------------------------------------------------------------------