ProMotion 0.7.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +1 -1
  3. data/ProMotion.gemspec +2 -0
  4. data/README.md +51 -449
  5. data/Rakefile +11 -1
  6. data/app/screens/basic_screen.rb +0 -24
  7. data/lib/ProMotion/cocoatouch/{NavigationController.rb → navigation_controller.rb} +0 -0
  8. data/lib/ProMotion/cocoatouch/split_view_controller.rb +29 -0
  9. data/lib/ProMotion/cocoatouch/tab_bar_controller.rb +43 -0
  10. data/lib/ProMotion/cocoatouch/{TableViewCell.rb → table_view_cell.rb} +0 -1
  11. data/lib/ProMotion/cocoatouch/{TableViewController.rb → table_view_controller.rb} +0 -0
  12. data/lib/ProMotion/cocoatouch/{ViewController.rb → view_controller.rb} +6 -1
  13. data/lib/ProMotion/{screen_helpers → containers}/split_screen.rb +0 -2
  14. data/lib/ProMotion/containers/tabs.rb +95 -0
  15. data/lib/ProMotion/delegate/delegate.rb +2 -13
  16. data/lib/ProMotion/delegate/{delegate_helper.rb → delegate_module.rb} +29 -16
  17. data/lib/ProMotion/delegate/delegate_notifications.rb +17 -8
  18. data/lib/ProMotion/delegate/delegate_parent.rb +11 -0
  19. data/lib/ProMotion/extensions/conversions.rb +20 -0
  20. data/lib/ProMotion/{helpers/logger.rb → logger.rb} +0 -0
  21. data/lib/ProMotion/map/map_screen.rb +6 -0
  22. data/lib/ProMotion/map/map_screen_annotation.rb +56 -0
  23. data/lib/ProMotion/map/map_screen_module.rb +207 -0
  24. data/lib/ProMotion/{push_notifications → push_notification}/push_notification.rb +13 -6
  25. data/lib/ProMotion/{screens → screen}/screen.rb +1 -1
  26. data/lib/ProMotion/{screens/_screen_module.rb → screen/screen_module.rb} +76 -51
  27. data/lib/ProMotion/{screen_helpers → screen}/screen_navigation.rb +8 -12
  28. data/lib/ProMotion/{screens/_tables → table/cell}/table_view_cell_module.rb +18 -27
  29. data/lib/ProMotion/table/data/table_data.rb +98 -0
  30. data/lib/ProMotion/table/extensions/indexable.rb +9 -0
  31. data/lib/ProMotion/table/extensions/refreshable.rb +45 -0
  32. data/lib/ProMotion/table/extensions/searchable.rb +61 -0
  33. data/lib/ProMotion/table/grouped_table.rb +9 -0
  34. data/lib/ProMotion/table/grouped_table_screen.rb +6 -0
  35. data/lib/ProMotion/table/table.rb +312 -0
  36. data/lib/ProMotion/table/table_screen.rb +6 -0
  37. data/lib/ProMotion/{screens/_compatibility → thirdparty}/formotion_screen.rb +18 -5
  38. data/lib/ProMotion/version.rb +1 -1
  39. data/lib/ProMotion/view/styling.rb +137 -0
  40. data/lib/ProMotion/web/web_screen.rb +6 -0
  41. data/lib/ProMotion/web/web_screen_module.rb +155 -0
  42. data/resources/WebScreen.html +6 -0
  43. data/resources/test.jpeg +0 -0
  44. data/spec/functional/func_map_screen_spec.rb +105 -0
  45. data/spec/functional/func_screen_spec.rb +54 -7
  46. data/spec/functional/func_searchable_table_spec.rb +1 -1
  47. data/spec/functional/func_tab_bar_spec.rb +78 -0
  48. data/spec/functional/func_table_screen_spec.rb +61 -14
  49. data/spec/functional/func_web_screen_spec.rb +51 -0
  50. data/spec/helpers/map_screen.rb +51 -0
  51. data/spec/helpers/present_screen.rb +26 -0
  52. data/spec/helpers/tab_screen.rb +4 -0
  53. data/spec/helpers/table_screen.rb +12 -3
  54. data/spec/helpers/table_screen_formotion.rb +25 -0
  55. data/spec/helpers/table_screen_indexable.rb +13 -0
  56. data/spec/helpers/test_delegate.rb +28 -0
  57. data/spec/helpers/web_screen.rb +22 -0
  58. data/spec/unit/delegate_spec.rb +53 -4
  59. data/spec/unit/map_spec.rb +48 -0
  60. data/spec/unit/screen_helpers_spec.rb +24 -16
  61. data/spec/unit/screen_spec.rb +22 -18
  62. data/spec/unit/split_screen_in_tab_bar_spec.rb +2 -2
  63. data/spec/unit/split_screen_open_screen_spec.rb +15 -10
  64. data/spec/unit/split_screen_spec.rb +2 -2
  65. data/spec/unit/tab_spec.rb +41 -0
  66. data/spec/unit/tables/formotion_screen_spec.rb +16 -0
  67. data/spec/unit/tables/table_indexable_spec.rb +12 -0
  68. data/spec/unit/tables/table_module_spec.rb +24 -9
  69. data/spec/unit/tables/table_screen_spec.rb +1 -1
  70. data/spec/unit/tables/table_view_cell_spec.rb +9 -8
  71. data/spec/unit/view_helper_spec.rb +2 -2
  72. data/spec/unit/web_spec.rb +106 -0
  73. metadata +96 -35
  74. data/lib/ProMotion/cocoatouch/SplitViewController.rb +0 -23
  75. data/lib/ProMotion/helpers/console.rb +0 -29
  76. data/lib/ProMotion/helpers/measure_helper.rb +0 -20
  77. data/lib/ProMotion/helpers/system_helper.rb +0 -29
  78. data/lib/ProMotion/helpers/view_helper.rb +0 -82
  79. data/lib/ProMotion/screen_helpers/screen_elements.rb +0 -36
  80. data/lib/ProMotion/screen_helpers/screen_tabs.rb +0 -95
  81. data/lib/ProMotion/screens/_table_screen_module.rb +0 -47
  82. data/lib/ProMotion/screens/_tables/_refreshable_table.rb +0 -45
  83. data/lib/ProMotion/screens/_tables/_searchable_table.rb +0 -60
  84. data/lib/ProMotion/screens/_tables/_sectioned_table.rb +0 -5
  85. data/lib/ProMotion/screens/_tables/_table.rb +0 -169
  86. data/lib/ProMotion/screens/_tables/grouped_table.rb +0 -16
  87. data/lib/ProMotion/screens/_tables/plain_table.rb +0 -17
  88. data/lib/ProMotion/screens/_tables/table_data.rb +0 -175
  89. data/lib/ProMotion/screens/behaves_like_screen.rb +0 -10
  90. data/lib/ProMotion/screens/table_screen.rb +0 -16
  91. data/spec/unit/ios_version_spec.rb +0 -28
@@ -0,0 +1,6 @@
1
+ module ProMotion
2
+ class TableScreen < TableViewController
3
+ include ProMotion::ScreenModule
4
+ include ProMotion::Table
5
+ end
6
+ end
@@ -2,7 +2,7 @@ module ProMotion
2
2
  if defined?(Formotion) && defined?(Formotion::FormController)
3
3
  class FormotionScreen < Formotion::FormController
4
4
  include ProMotion::ScreenModule
5
-
5
+
6
6
  def self.new(args = {})
7
7
  s = self.alloc.initWithStyle(UITableViewStyleGrouped)
8
8
  s.on_create(args) if s.respond_to?(:on_create)
@@ -15,7 +15,9 @@ module ProMotion
15
15
  PM.logger.error "PM::FormotionScreen requires a `table_data` method or form: to be passed into `new`."
16
16
  end
17
17
 
18
+ t = s.title # Formotion kills the title when you request tableView.
18
19
  s.tableView.allowsSelectionDuringEditing = true
20
+ s.title = t
19
21
 
20
22
  s
21
23
  end
@@ -26,6 +28,15 @@ module ProMotion
26
28
  self.form.controller = self
27
29
  self.tableView.reloadData
28
30
  end
31
+
32
+ def screen_setup
33
+ self.title = self.class.send(:get_title)
34
+ end
35
+
36
+ def loadView
37
+ super
38
+ self.send(:on_load) if self.respond_to?(:on_load)
39
+ end
29
40
 
30
41
  def viewDidLoad
31
42
  super
@@ -34,21 +45,23 @@ module ProMotion
34
45
 
35
46
  def viewWillAppear(animated)
36
47
  super
37
- self.view_will_appear(animated) if self.respond_to?(:view_will_appear)
48
+ self.view_will_appear(animated) if self.respond_to?("view_will_appear:")
38
49
  end
39
50
 
40
51
  def viewDidAppear(animated)
41
52
  super
42
- self.view_did_appear(animated) if self.respond_to?(:view_did_appear)
53
+ self.view_did_appear(animated) if self.respond_to?("view_did_appear:")
43
54
  end
44
55
 
45
56
  def viewWillDisappear(animated)
46
- self.view_will_disappear(animated) if self.respond_to?(:view_will_disappear)
57
+ self.view_will_disappear(animated) if self.respond_to?("view_will_disappear:")
47
58
  super
48
59
  end
49
60
 
50
61
  def viewDidDisappear(animated)
51
- self.view_did_disappear(animated) if self.respond_to?(:view_did_disappear)
62
+ if self.respond_to?("view_did_disappear:")
63
+ self.view_did_disappear(animated)
64
+ end
52
65
  super
53
66
  end
54
67
 
@@ -1,3 +1,3 @@
1
1
  module ProMotion
2
- VERSION = "0.7.8" unless defined?(ProMotion::VERSION)
2
+ VERSION = "1.0.0" unless defined?(ProMotion::VERSION)
3
3
  end
@@ -0,0 +1,137 @@
1
+ module ProMotion
2
+ module Styling
3
+ include Conversions
4
+
5
+ def set_attributes(element, args = {})
6
+ args.each { |k, v| set_attribute(element, k, v) }
7
+ element
8
+ end
9
+
10
+ def set_attribute(element, k, v)
11
+ return element unless element
12
+
13
+ if v.is_a?(Hash) && element.respond_to?(k)
14
+ sub_element = element.send(k)
15
+ set_attributes(sub_element, v) if sub_element
16
+ elsif element.respond_to?("#{k}=")
17
+ element.send("#{k}=", v)
18
+ elsif v.is_a?(Array) && element.respond_to?("#{k}") && element.method("#{k}").arity == v.length
19
+ element.send("#{k}", *v)
20
+ else
21
+ # Doesn't respond. Check if snake case.
22
+ if k.to_s.include?("_")
23
+ set_attribute(element, objective_c_method_name(k), v)
24
+ end
25
+ end
26
+ element
27
+ end
28
+
29
+ def set_easy_attributes(parent, element, args={})
30
+ attributes = {}
31
+
32
+ if args[:resize]
33
+ attributes[:autoresizingMask] = UIViewAutoresizingNone
34
+ args[:resize].each { |r| attributes[:autoresizingMask] |= map_resize_symbol(r) }
35
+ end
36
+
37
+ if [:left, :top, :width, :height].select{ |a| args[a] && args[a] != :auto }.length == 4
38
+ attributes[:frame] = CGRectMake(args[:left], args[:top], args[:width], args[:height])
39
+ end
40
+
41
+ set_attributes element, attributes
42
+ element
43
+ end
44
+
45
+ def content_height(view)
46
+ height = 0
47
+ view.subviews.each do |sub_view|
48
+ next if sub_view.isHidden
49
+ y = sub_view.frame.origin.y
50
+ h = sub_view.frame.size.height
51
+ if (y + h) > height
52
+ height = y + h
53
+ end
54
+ end
55
+ height
56
+ end
57
+
58
+ def closest_parent(type, this_view = nil)
59
+ # iterate up the view hierarchy to find the parent element of "type" containing this view
60
+ this_view ||= view_or_self.superview
61
+ while this_view != nil do
62
+ return this_view if this_view.is_a? type
63
+ this_view = this_view.superview
64
+ end
65
+ nil
66
+ end
67
+
68
+ def add(element, attrs = {})
69
+ add_to view_or_self, element, attrs
70
+ end
71
+ alias :add_element :add
72
+ alias :add_view :add
73
+
74
+ def remove(element)
75
+ element.removeFromSuperview
76
+ element = nil
77
+ end
78
+ alias :remove_element :remove
79
+ alias :remove_view :remove
80
+
81
+ def add_to(parent_element, element, attrs = {})
82
+ parent_element.addSubview element
83
+ if attrs && attrs.length > 0
84
+ set_attributes(element, attrs)
85
+ set_easy_attributes(parent_element, element, attrs)
86
+ end
87
+ element
88
+ end
89
+
90
+ def view_or_self
91
+ self.respond_to?(:view) ? self.view : self
92
+ end
93
+
94
+ # These three color methods are stolen from BubbleWrap.
95
+ def rgb_color(r,g,b)
96
+ rgba_color(r,g,b,1)
97
+ end
98
+
99
+ def rgba_color(r,g,b,a)
100
+ r,g,b = [r,g,b].map { |i| i / 255.0}
101
+ UIColor.colorWithRed(r, green: g, blue:b, alpha:a)
102
+ end
103
+
104
+ def hex_color(str)
105
+ hex_color = str.gsub("#", "")
106
+ case hex_color.size
107
+ when 3
108
+ colors = hex_color.scan(%r{[0-9A-Fa-f]}).map{ |el| (el * 2).to_i(16) }
109
+ when 6
110
+ colors = hex_color.scan(%r<[0-9A-Fa-f]{2}>).map{ |el| el.to_i(16) }
111
+ else
112
+ raise ArgumentError
113
+ end
114
+
115
+ if colors.size == 3
116
+ rgb_color(colors[0], colors[1], colors[2])
117
+ else
118
+ raise ArgumentError
119
+ end
120
+ end
121
+
122
+ protected
123
+
124
+ def map_resize_symbol(symbol)
125
+ @_resize_symbols ||= {
126
+ left: UIViewAutoresizingFlexibleLeftMargin,
127
+ right: UIViewAutoresizingFlexibleRightMargin,
128
+ top: UIViewAutoresizingFlexibleTopMargin,
129
+ bottom: UIViewAutoresizingFlexibleBottomMargin,
130
+ width: UIViewAutoresizingFlexibleWidth,
131
+ height: UIViewAutoresizingFlexibleHeight
132
+ }
133
+ @_resize_symbols[symbol] || symbol
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,6 @@
1
+ module ProMotion
2
+ class WebScreen < ViewController
3
+ include ProMotion::ScreenModule
4
+ include ProMotion::WebScreenModule
5
+ end
6
+ end
@@ -0,0 +1,155 @@
1
+ module ProMotion
2
+ module WebScreenModule
3
+
4
+ attr_accessor :webview, :external_links, :detector_types
5
+
6
+ def screen_setup
7
+ check_content_data
8
+ self.external_links ||= false
9
+ end
10
+
11
+ def on_init
12
+
13
+ self.detector_types ||= UIDataDetectorTypeNone
14
+ if self.detector_types.is_a? Array
15
+ detectors = UIDataDetectorTypeNone
16
+ self.detector_types.each { |dt| detectors |= map_detector_symbol(dt) }
17
+ self.detector_types = detectors
18
+ end
19
+
20
+ self.webview ||= add UIWebView.new, {
21
+ frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height),
22
+ resize: [ :width, :height ],
23
+ delegate: self,
24
+ data_detector_types: self.detector_types
25
+ }
26
+
27
+ set_initial_content
28
+ end
29
+
30
+ def web
31
+ self.webview
32
+ end
33
+
34
+ def set_initial_content
35
+ return unless self.respond_to?(:content)
36
+ content.is_a?(NSURL) ? open_url(content) : set_content(content)
37
+ end
38
+
39
+ def set_content(content)
40
+ content_path = File.join(NSBundle.mainBundle.resourcePath, content)
41
+
42
+ if File.exists? content_path
43
+ content_string = File.read content_path
44
+ content_base_url = NSURL.fileURLWithPath NSBundle.mainBundle.resourcePath
45
+
46
+ self.web.loadHTMLString(convert_retina_images(content_string), baseURL:content_base_url)
47
+ else
48
+ # We assume the user wants to load an arbitrary string into the web view
49
+ self.web.loadHTMLString(content, baseURL:nil)
50
+ end
51
+ end
52
+
53
+ def open_url(url)
54
+ request = NSURLRequest.requestWithURL(
55
+ url.is_a?(NSURL) ? url : NSURL.URLWithString(url)
56
+ )
57
+ web.loadRequest request
58
+ end
59
+
60
+ def convert_retina_images(content)
61
+ #Convert images over to retina if the images exist.
62
+ if UIScreen.mainScreen.bounds.respondsToSelector('displayLinkWithTarget:selector:') && UIScreen.mainScreen.bounds.scale == 2.0 # Thanks BubbleWrap! https://github.com/rubymotion/BubbleWrap/blob/master/motion/core/device/ios/screen.rb#L9
63
+ content.gsub!(/src=['"](.*?)\.(jpg|gif|png)['"]/) do |img|
64
+ if File.exists?(File.join(NSBundle.mainBundle.resourcePath, "#{$1}@2x.#{$2}"))
65
+ # Create a UIImage to get the width and height of hte @2x image
66
+ tmp_image = UIImage.imageNamed("/#{$1}@2x.#{$2}")
67
+ new_width = tmp_image.size.width / 2
68
+ new_height = tmp_image.size.height / 2
69
+
70
+ img = "src=\"#{$1}@2x.#{$2}\" width=\"#{new_width}\" height=\"#{new_height}\""
71
+ end
72
+ end
73
+ end
74
+ content
75
+ end
76
+
77
+ def check_content_data
78
+ PM.logger.error "Missing #content method in WebScreen #{self.class.to_s}." unless self.respond_to?(:content)
79
+ end
80
+
81
+ def html
82
+ self.webview.stringByEvaluatingJavaScriptFromString("document.documentElement.outerHTML")
83
+ end
84
+
85
+ def evaluate(js)
86
+ self.webview.stringByEvaluatingJavaScriptFromString(js)
87
+ end
88
+
89
+ def current_url
90
+ evaluate('document.URL')
91
+ end
92
+
93
+ # Navigation
94
+ def can_go_back; web.canGoBack; end
95
+ def can_go_forward; web.canGoForward; end
96
+ def back; web.goBack if can_go_back; end
97
+ def forward; web.goForward if can_go_forward; end
98
+ def refresh; web.reload; end
99
+ def stop; web.stopLoading; end
100
+ alias :reload :refresh
101
+
102
+ def open_in_chrome(inRequest)
103
+ # Add pod 'OpenInChrome' to your Rakefile if you want links to open in chrom for users.
104
+ chrome_controller = OpenInChromeController.sharedInstance
105
+ return open_in_safari(inRequest) unless chrome_controller.isChromeInstalled
106
+ chrome_controller.open_in_chrome(inRequest.URL)
107
+ end
108
+
109
+ def open_in_safari(inRequest)
110
+ #Open UIWebView delegate links in Safari.
111
+ UIApplication.sharedApplication.openURL(inRequest.URL)
112
+ end
113
+
114
+ #UIWebViewDelegate Methods - Camelcase
115
+ def webView(inWeb, shouldStartLoadWithRequest:inRequest, navigationType:inType)
116
+ if self.external_links == true && inType == UIWebViewNavigationTypeLinkClicked
117
+ if defined?(OpenInChromeController)
118
+ open_in_chrome inRequest
119
+ else
120
+ open_in_safari inRequest
121
+ end
122
+ return false #don't allow the web view to load the link.
123
+ end
124
+
125
+ load_request_enable = true #return true on default for local file loading.
126
+ load_request_enable = !!on_request(inRequest, inType) if self.respond_to?(:on_request)
127
+ load_request_enable
128
+ end
129
+
130
+ def webViewDidStartLoad(webView)
131
+ load_started if self.respond_to?(:load_started)
132
+ end
133
+
134
+ def webViewDidFinishLoad(webView)
135
+ load_finished if self.respond_to?(:load_finished)
136
+ end
137
+
138
+ def webView(webView, didFailLoadWithError:error)
139
+ load_failed(error) if self.respond_to?("load_failed:")
140
+ end
141
+
142
+ protected
143
+
144
+ def map_detector_symbol(symbol)
145
+ {
146
+ phone: UIDataDetectorTypePhoneNumber,
147
+ link: UIDataDetectorTypeLink,
148
+ address: UIDataDetectorTypeAddress,
149
+ event: UIDataDetectorTypeCalendarEvent,
150
+ all: UIDataDetectorTypeAll
151
+ }[symbol] || UIDataDetectorTypeNone
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,6 @@
1
+ <html>
2
+ <head></head>
3
+ <body>
4
+ <h1>Test</h1>
5
+ </body>
6
+ </html>
Binary file
@@ -0,0 +1,105 @@
1
+ describe "ProMotion::TestMapScreen functionality" do
2
+ tests PM::TestMapScreen
3
+
4
+ # Override controller to properly instantiate
5
+ def controller
6
+ rotate_device to: :portrait, button: :bottom
7
+ @map ||= TestMapScreen.new(nav_bar: true)
8
+ @map.will_appear
9
+ @map.navigation_controller
10
+ end
11
+
12
+ after do
13
+ @map = nil
14
+ end
15
+
16
+ it "should have a navigation bar" do
17
+ @map.navigationController.should.be.kind_of(UINavigationController)
18
+ end
19
+
20
+ it "should have the map properly centered" do
21
+ center_coordinate = @map.center
22
+ center_coordinate.latitude.should.be.close 35.090648651123, 0.001
23
+ center_coordinate.longitude.should.be.close -82.965972900391, 0.001
24
+ end
25
+
26
+ it "should move the map center" do
27
+ @map.center = {latitude: 35.07496, longitude: -82.95916, animated: true}
28
+
29
+ wait 0.75 do
30
+ center_coordinate = @map.center
31
+ center_coordinate.latitude.should.be.close 35.07496, 0.001
32
+ center_coordinate.longitude.should.be.close -82.95916, 0.001
33
+ end
34
+ end
35
+
36
+ it "should select an annotation" do
37
+ @map.selected_annotations.should == nil
38
+ @map.select_annotation @map.annotations.first
39
+ wait 0.75 do
40
+ @map.selected_annotations.count.should == 1
41
+ end
42
+ end
43
+
44
+ it "should select an annotation by index" do
45
+ @map.selected_annotations.should == nil
46
+ @map.select_annotation_at 2
47
+ wait 0.75 do
48
+ @map.selected_annotations.count.should == 1
49
+ @map.selected_annotations[0].should == @map.promotion_annotation_data[2]
50
+ end
51
+ end
52
+
53
+ it "should select another annotation and check that the title is correct" do
54
+ @map.selected_annotations.should == nil
55
+ @map.select_annotation @map.annotations[1]
56
+ wait 0.75 do
57
+ @map.selected_annotations.count.should == 1
58
+ end
59
+
60
+ @map.selected_annotations.first.title.should == "Turtleback Falls"
61
+ @map.selected_annotations.first.subtitle.should == "Nantahala National Forest"
62
+
63
+ end
64
+
65
+ it "should deselect selected annotations" do
66
+ @map.select_annotation @map.annotations.last
67
+ wait 0.75 do
68
+ # @map.selected_annotations.count.should == 1
69
+ end
70
+
71
+ @map.deselect_annotations
72
+ wait 0.75 do
73
+ @map.selected_annotations.should == nil
74
+ end
75
+ end
76
+
77
+ it "should add an annotation and be able to zoom immediately" do
78
+ ann = {
79
+ longitude: -82.966093558105,
80
+ latitude: 35.092520895652,
81
+ title: "Something Else"
82
+ }
83
+ @map.annotations.count.should == 5
84
+ @map.add_annotation ann
85
+ @map.annotations.count.should == 6
86
+ @map.set_region @map.region(coordinate: @map.annotations.last.coordinate, span: [0.05, 0.05])
87
+ @map.select_annotation @map.annotations.last
88
+ end
89
+
90
+ it "should be able to overwrite all annotations" do
91
+ anns = [{
92
+ longitude: -122.029620,
93
+ latitude: 37.331789,
94
+ title: "My Cool Pin"
95
+ },{
96
+ longitude: -80.8498118 ,
97
+ latitude: 35.2187218,
98
+ title: "My Cool Pin"
99
+ }]
100
+ @map.annotations.count.should == 5
101
+ @map.add_annotations anns
102
+ @map.annotations.count.should == 2
103
+ end
104
+
105
+ end