├── .gitignore
├── 01-gcm-web-push-notifications
├── .gitignore
├── README.md
├── dist
│ └── .gitignore
├── images
│ ├── icon@128.png
│ ├── icon@144.png
│ ├── icon@152.png
│ └── icon@192.png
├── index.html
├── manifest.json
├── service-worker.js
├── src
│ └── app.js
└── sync-gateway-config.json
├── 02-gcm-webhook
├── README.md
├── articles.json
├── main.go
├── profiles.json
├── sync-gateway-config.json
└── view.json
├── 03-ios-share-extension
├── Playgrounds
│ ├── .gitignore
│ ├── REST_GET_channels.playground
│ │ ├── Contents.swift
│ │ ├── Sources
│ │ │ └── SupportCode.swift
│ │ ├── contents.xcplayground
│ │ ├── playground.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── timeline.xctimeline
│ └── REST_POST_document.playground
│ │ ├── Contents.swift
│ │ ├── Sources
│ │ └── SupportCode.swift
│ │ ├── contents.xcplayground
│ │ ├── playground.xcworkspace
│ │ └── contents.xcworkspacedata
│ │ └── timeline.xctimeline
├── README.md
├── TeamPicks
│ ├── .gitignore
│ ├── Share Track
│ │ ├── Info.plist
│ │ ├── MainInterface.storyboard
│ │ ├── ShareViewController.swift
│ │ └── TeamTableViewController.swift
│ ├── TeamPicks.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ ├── TeamPicks
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.xib
│ │ │ └── Main.storyboard
│ │ ├── Images.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ └── ViewController.swift
│ └── TeamPicksTests
│ │ ├── Info.plist
│ │ └── TeamPicksTests.swift
└── sync-gateway-config.json
├── 04-ios-sync-progress-indicator
├── .gitignore
├── CityExplorer
│ ├── .gitignore
│ ├── CityExplorer.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcuserdata
│ │ │ └── jamesnocentini.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ ├── CityExplorer.xcworkspace
│ │ └── contents.xcworkspacedata
│ ├── CityExplorer
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.xib
│ │ │ └── Main.storyboard
│ │ ├── Images.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── ViewController.swift
│ │ └── bridging-header.h
│ ├── CityExplorerTests
│ │ ├── CityExplorerTests.swift
│ │ └── Info.plist
│ ├── Podfile
│ └── Podfile.lock
├── README.md
├── package.json
├── requestRx.js
├── sync-gateway-config.json
└── sync.js
├── 05-android-recycler-view-animations
├── CityExplorer
│ ├── .gitignore
│ ├── CityExplorer.iml
│ ├── app
│ │ ├── .gitignore
│ │ ├── app.iml
│ │ ├── build.gradle
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ ├── androidTest
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── couchbase
│ │ │ │ └── cityexplorer
│ │ │ │ └── ApplicationTest.java
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── couchbase
│ │ │ │ └── cityexplorer
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── PlacesAdapter.java
│ │ │ │ └── model
│ │ │ │ └── Place.java
│ │ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ ├── ic_add_white_24dp.png
│ │ │ └── ic_delete_white_24dp.png
│ │ │ ├── drawable-mdpi
│ │ │ ├── ic_add_white_24dp.png
│ │ │ └── ic_delete_white_24dp.png
│ │ │ ├── drawable-xhdpi
│ │ │ ├── ic_add_white_24dp.png
│ │ │ └── ic_delete_white_24dp.png
│ │ │ ├── drawable-xxhdpi
│ │ │ ├── ic_add_white_24dp.png
│ │ │ └── ic_delete_white_24dp.png
│ │ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ └── row_places.xml
│ │ │ ├── menu
│ │ │ └── menu_main.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ │ └── values
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
└── README.md
├── 06-channels-users-roles
├── README.md
└── sync-gateway-config.json
├── 07-deploy-digital-ocean
├── Dockerfile
└── README.md
├── 08-signup-and-login
├── .gitignore
├── README.md
├── SmartHome
│ ├── .gitignore
│ ├── SmartHome.iml
│ ├── app
│ │ ├── .gitignore
│ │ ├── app.iml
│ │ ├── build.gradle
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ ├── androidTest
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── couchbase
│ │ │ │ └── smarthome
│ │ │ │ └── ApplicationTest.java
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── couchbase
│ │ │ │ └── smarthome
│ │ │ │ ├── Login.java
│ │ │ │ ├── SignUp.java
│ │ │ │ └── WelcomeActivity.java
│ │ │ └── res
│ │ │ ├── layout
│ │ │ ├── activity_login.xml
│ │ │ ├── activity_sign_up.xml
│ │ │ └── activity_welcome.xml
│ │ │ ├── menu
│ │ │ ├── menu_login.xml
│ │ │ ├── menu_sign_up.xml
│ │ │ └── menu_welcome.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-v21
│ │ │ └── styles.xml
│ │ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ │ └── values
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
├── server.js
└── sync-gateway-config.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | .idea
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/README.md:
--------------------------------------------------------------------------------
1 | # Couchbase by Example: GCM Push Notifications
2 |
3 | Push Notifications are great to engage your users effectively and prompt to notify them of important events.
4 |
5 | In this tutorial, you’ll learn how to use the Push API. Web Push Notifications enable websites and web apps on Android to receive Push Notifications just like Native application do.
6 |
7 | ## Refresher on GCM
8 |
9 | To send Push Notifications with the GCM service, we make a request to the GCM servers providing the message to send and the address to send it to (which application instance running on what device).
10 |
11 | The address in this case is called a registration ID. Google Cloud Messaging 3.0 introduced the concept of instance ID (a.k.a Identity for Robots). Instance ID is a new way to identify Android, iOS and Web apps and can be used for GCM but other use cases too. You can read more about them on Google’s developer site:
12 |
13 | > https://developers.google.com/instance-id/
14 |
15 | When the user installs the application on the device, an instance ID will be automatically generated. Since we’re using instance IDs in the scope of GCM, we will refer to it as the **registration token**.
16 |
17 | With a simple call to the Web Push JavaScript API, you get back the registration token in the same way you would on Android and iOS.
18 |
19 | > http://www.w3.org/TR/push-api/
20 |
21 | When we send a push notification to this registration token, Chrome will display the notification like so.
22 |
23 | ![][image-1]
24 |
25 | ## Architecture considerations
26 |
27 | We could imagine a simple news application to notify users when a new article is published through Push Notifications. The Push Notification is a visual clue to let the user know that new content is available but it could also be for the application itself to pull the new articles from the server:
28 |
29 | ![][image-2]
30 |
31 | The reason we’re using the Web Push API instead of a continuous pull replication with Sync Gateway to check for new articles is that it can work even when the browser is closed. Compared to a web socket or long polling, which will only be kept alive as long as the browser and web page is kept open.
32 |
33 | Create a new directory called `gcm-web-push-notifications`.
34 |
35 | ## Creating a Google API project
36 |
37 | Open the Google Developer Console and log into your account:
38 |
39 | > https://console.developers.google.com
40 |
41 | Create a new Project called `Timely News`:
42 |
43 | ![][image-3]
44 |
45 | Once the new project appears in the list, click on it and copy down the project number, you will need it throughout this tutorial.
46 |
47 | The Google Developer Console lists all the available APIs. To use a particular one, you must first activate it. Open the `APIs & auth > APIs` panel and enable the `Cloud Messaging for Android` api:
48 |
49 | ![][image-4]
50 |
51 | Finally, you will need a Server API key to test Push Notifications are working:
52 |
53 | ![][image-5]
54 |
55 | Copy down the Server API key as well.
56 |
57 | ## Get started with Browserify
58 |
59 | In this section, you will learn how to use browserify to bundle external dependencies such as PouchDB in your application. It’s a great way to get a project started quickly. In the `gcm-web-push-notifications` folder, run the npm command to install all the dependencies:
60 |
61 | $ npm install browserify watchify pouchdb pouchdb-find --save-dev
62 |
63 | Create a new file named `index.html` with the basic HTML5 template:
64 |
65 | ![][image-6]
66 |
67 | Create a new file `app.js` in the `src` folder, all of the logic for Timely News will reside in `app.js`. You will use the `watchify` command to automatically bundle JS files into a `bundle.js` output file.
68 |
69 | Create a new folder called `dist` and run the command:
70 |
71 | $ watchify ./src/app.js -o ./dist/bundle.js
72 |
73 | In `index.html`, don’t forget to add a script tag to link the `bundle.js` output file:
74 |
75 | ![][image-7]
76 |
77 | ## Push API and Service Worker
78 |
79 | There are three components to register and receive Push Notifications. First the manifest file that contains metadata used by Chrome. Second, retrieving the registration ID using the Push API once a Service Worker has been registered. Third, implementing methods in the Service Worker to handle incoming notifications.
80 |
81 | The manifest file contains information such as the GCM sender id (i.e. your project number) and icon images to display in the notification banner. In a new file `manifest.json`, add the following:
82 |
83 | {
84 | "name": "Timely News",
85 | "short_name": "TS",
86 | "icons": [{
87 | "src": "images/icon@128.png",
88 | "sizes": "128x128"
89 | }, {
90 | "src": "images/icon@152.png",
91 | "sizes": "152x152"
92 | }, {
93 | "src": "images/icon@144.png",
94 | "sizes": "144x144"
95 | }, {
96 | "src": "images/icon@192.png",
97 | "sizes": "192x192"
98 | }],
99 | "start_url": "/index.html",
100 | "display": "standalone",
101 | "gcm_sender_id": "562982014144",
102 | "gcm_user_visible_only": true
103 | }
104 |
105 | **NOTE**: 1) Replace the `gcm_sender_id` with your project number. 2) You can find the icons here to use them in your project.
106 |
107 | In `app.js`, check for Service Worker support and prompt the user to get the permission to send Push Notifications. Notice we’re referencing a service worker file named `service-worker.js`. I’ll explain why in the next step.
108 |
109 | if ('serviceWorker' in navigator) {
110 | navigator.serviceWorker.register('./service-worker.js', {scope: './'});
111 | navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) {
112 | serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
113 | .then(function (pushSubscription) {
114 | console.log('The reg ID is :: ', pushSubscription.subscriptionId);
115 |
116 | });
117 | });
118 | }
119 |
120 | This will log the registration ID for this application instance to the console:
121 |
122 | ![][image-8]
123 |
124 | The Push API works with a Service Worker to handle incoming notifications even when the browser tab window is closed. Create a new file named `service-worker.js` with the following event listener to handle incoming notifications:
125 |
126 | self.addEventListener('push', function (event) {
127 | console.log('Received a push message', event);
128 |
129 | var notificationOptions = {
130 | body: 'The highlights of Google I/O 2015',
131 | icon: 'images/icon@192.png',
132 | tag: 'highlights-google-io-2015',
133 | data: null
134 | };
135 |
136 | if (self.registration.showNotification) {
137 | self.registration.showNotification('Timely News', notificationOptions);
138 | return;
139 | } else {
140 | new Notification('Timely News', notificationOptions);
141 | }
142 | });
143 |
144 | With the registration ID and API Key you can send a POST request to the GCM server to test it:
145 |
146 | curl --header "Authorization: key=API_KEY" \
147 | --header Content-Type:"application/json" \
148 | https://android.googleapis.com/gcm/send \
149 | -d '{"registration_ids":["REGESTRATION_ID"]}'
150 |
151 | Now you know how to use Web notifications in Chrome!
152 |
153 | ![][image-9]
154 |
155 | You can read more about the specifics of the Push API in this great blog post:
156 |
157 | > https://developers.google.com/web/updates/2015/03/push-notificatons-on-the-open-web
158 |
159 | In the next section, you will use PouchDB to create a document of type `profile` to save the registration ID. With the PouchDB Find Plugin, you will check if a profile already exists locally.
160 |
161 | ## PouchDB & PouchDB Find
162 |
163 | In `app.js`, require the PouchDB package, attach it to the global scope (necessary for the PouchDB Inspector to work).
164 |
165 | var PouchDB = require('pouchdb');
166 |
167 | // Expose PouchDB on the window object to use the
168 | // PouchDB Chrome debugger extension http://bit.ly/1L6dArH
169 | window.PouchDB = PouchDB;
170 |
171 | Create a database called timely-news:
172 |
173 | var db = new PouchDB('timely-news');
174 | PouchDB.plugin(require('pouchdb-find'));
175 |
176 | After logging the registration ID to the console, use the PouchDB Find Plugin to check if a document of type profile already exists. If it doesn’t, create one and save it:
177 |
178 | db.createIndex({index: {fields: ['type']}})
179 | .then(function() {
180 | db.find({
181 | selector: {type: 'profile'}
182 | }).then(function (res) {
183 | console.log(res);
184 | if (res.docs.length == 0) {
185 | db.post({
186 | 'type': 'profile',
187 | 'registration_ids': [pushSubscription.subscriptionId]
188 | }, function(err, res) {
189 | console.log(err, res);
190 | });
191 | }
192 | });
193 | });
194 |
195 | Use the PouchDB Inspector to view the database content right in DevTools!
196 |
197 | > https://chrome.google.com/webstore/detail/pouchdb-inspector/hbhhpaojmpfimakffndmpmpndcmonkfa?hl=en
198 |
199 | ![][image-10]
200 |
201 | In the next section, you will get Sync Gateway set up with an in-memory database (called Walrus) and the GUEST mode enabled. Then, you will use a Push replication to save the Profile document to Sync Gateway.
202 |
203 | ## Sync Gateway
204 |
205 | Download Sync Gateway and unzip the file:
206 |
207 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile
208 |
209 | You can find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `cors.json` file to the root of your project:
210 |
211 | $ cp ~/Downloads/couchbase-sync-gateway/examples/cors.json /path/to/proj/gcm-web-push-notifications/sync-gateway-config.json
212 |
213 | Start Sync Gateway and open the Admin Dashboard on `http://localhost:4985/_admin/` to keep an eye on synced documents.
214 |
215 | ![][image-11]
216 |
217 | In `app.js`, start a replication with the local "timely-news" database as source database and "http://localhost:4984/db" as the target.
218 |
219 | PouchDB.replicate('timely-news', 'http://localhost:4984/db', {
220 | live: true
221 | });
222 |
223 | Go back to the Admin Dashboard and you should see the Profile document:
224 |
225 | ![][image-12]
226 |
227 | ## Conclusion
228 |
229 | Google Cloud Messaging enables us to develop once and be able to send messages to users on Android, iOS and Chrome. The Push API and Service Worker augment the Web experience and with a native look and feel on Android it’s a great time to implement Web Push Notification on your website. PouchDB and Sync Gateway can handle the data syncing of device tokens. In the next post, you will learn how to use the Sync Gateway Web Hook api to send Push Notifications when a particular change occurrs.
230 |
231 | [image-1]: http://cl.ly/image/253b2d0L171t/Screen_Shot_2015-06-12_at_10_14_09.png
232 | [image-2]: http://cl.ly/image/1v3V1l1s1a3b/Untitled%20Diagram%20(2).png
233 | [image-3]: http://i.gyazo.com/9522e9de1362af96a06b46e3c22ee1d7.gif
234 | [image-4]: http://i.gyazo.com/bd7393a8ad275b88914d2562334dc31e.gif
235 | [image-5]: http://i.gyazo.com/b08ad6be05e6da8cc485ab0f160eebfd.gif
236 | [image-6]: http://i.gyazo.com/7d52183f54135c3c9e3f8a32e63a48d7.gif
237 | [image-7]: http://i.gyazo.com/858cab1759038b84f36000603318db4f.gif
238 | [image-8]: http://cl.ly/image/1X1B3b3b0F30/Screen%20Shot%202015-06-13%20at%2016.21.28.png
239 | [image-9]: http://i.gyazo.com/5e52aeec4355e7f4e7341065077f9090.gif
240 | [image-10]: http://cl.ly/image/0Q1t0M361a1s/Screen%20Shot%202015-06-13%20at%2016.33.05.png
241 | [image-11]: http://i.gyazo.com/37acae33dce5e3e9d4c50945f6550b51.gif
242 | [image-12]: http://cl.ly/image/3L2Z2s081o1I/Screen%20Shot%202015-06-13%20at%2016.50.18.png
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/dist/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/images/icon@128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@128.png
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/images/icon@144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@144.png
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/images/icon@152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@152.png
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/images/icon@192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@192.png
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Timely News",
3 | "short_name": "TS",
4 | "icons": [{
5 | "src": "images/icon@128.png",
6 | "sizes": "128x128"
7 | }, {
8 | "src": "images/icon@152.png",
9 | "sizes": "152x152"
10 | }, {
11 | "src": "images/icon@144.png",
12 | "sizes": "144x144"
13 | }, {
14 | "src": "images/icon@192.png",
15 | "sizes": "192x192"
16 | }],
17 | "start_url": "/index.html",
18 | "display": "standalone",
19 | "gcm_sender_id": "562982014144",
20 | "gcm_user_visible_only": true
21 | }
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/service-worker.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('push', function (event) {
2 | console.log('Received a push message', event);
3 |
4 | var notificationOptions = {
5 | body: 'The highlights of Google I/O 2015',
6 | icon: 'images/icon@192.png',
7 | tag: 'highlights-google-io-2015',
8 | data: null
9 | };
10 |
11 | if (self.registration.showNotification) {
12 | self.registration.showNotification('Timely News', notificationOptions);
13 | return;
14 | } else {
15 | new Notification('Timely News', notificationOptions);
16 | }
17 | });
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/src/app.js:
--------------------------------------------------------------------------------
1 | var PouchDB = require('pouchdb');
2 |
3 | // Expose PouchDB on the window object to use the
4 | // PouchDB Chrome debugger extension http://bit.ly/1L6dArH
5 | window.PouchDB = PouchDB;
6 |
7 |
8 | var db = new PouchDB('timely-news');
9 | PouchDB.plugin(require('pouchdb-find'));
10 |
11 | PouchDB.replicate('timely-news', 'http://localhost:4984/db', {
12 | live: true
13 | });
14 |
15 | if ('serviceWorker' in navigator) {
16 | navigator.serviceWorker.register('./service-worker.js', {scope: './'});
17 | navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) {
18 | serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
19 | .then(function (pushSubscription) {
20 | console.log('The reg ID is :: ', pushSubscription.subscriptionId);
21 |
22 | db.createIndex({index: {fields: ['type']}})
23 | .then(function() {
24 | db.find({
25 | selector: {type: 'profile'}
26 | }).then(function (res) {
27 | console.log(res);
28 | if (res.docs.length == 0) {
29 | db.post({
30 | 'type': 'profile',
31 | 'registration_ids': [pushSubscription.subscriptionId]
32 | }, function(err, res) {
33 | console.log(err, res);
34 | });
35 | }
36 | });
37 | });
38 |
39 | });
40 | });
41 | }
--------------------------------------------------------------------------------
/01-gcm-web-push-notifications/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["*"],
3 | "CORS": {
4 | "Origin":["http://localhost:8000"],
5 | "LoginOrigin":["http://localhost:8000"],
6 | "Headers":["Content-Type"],
7 | "MaxAge": 1728000
8 | },
9 | "databases": {
10 | "db": {
11 | "server": "walrus:",
12 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/02-gcm-webhook/README.md:
--------------------------------------------------------------------------------
1 | # Couchbase by Example: Sync Gateway Webhooks
2 |
3 | In the previous post, you learned how to set up Google Cloud Messaging with the Service Worker and Push API to handle notifications and used PouchDB + Sync Gateway to sync registration tokens. In this tutorial, you will focus exclusively on webhooks to dispatch the notifications to particular users.
4 |
5 | You will continue building Timely News, a news application to notify users of new articles matching their topics of interest.
6 |
7 | ## Scenarios
8 |
9 | There are different scenarios for sending a push notification:
10 |
11 | - **Group Messaging**: this concept was introduced in GCM to send notifications to up to 20 devices simultaneously. It’s very well suited for sending notifications to all devices that belong to a single user
12 | - **Up and Down**: a user updated a document and other users should be notified about it through a Push Notification
13 |
14 | ## Data Model
15 |
16 | Let’s start with the smallest document, a Profile document holding registration tokens of the user’s devices and topics of interest:
17 |
18 | {
19 | "type": "profile",
20 | "name": "Oliver",
21 | "subscription": "free", // other values "expired", "premium"
22 | "topics": ["g20", "science", "nsa", "design"],
23 | "registration_ids": ["AP91DIwQ", "AP91W9kX"]
24 | }
25 |
26 | And the Article document may have the following properties:
27 |
28 | {
29 | "type": "article",
30 | "title": "Design tools for developers",
31 | "content": "...",
32 | "topic": "design"
33 | }
34 |
35 | ## Group Messaging
36 |
37 | Imagine a scenario where a user is currently signed up on a freemium account and inputs a invite code to access the premium plan for a limited time. It would be nice to send a notification to all the user’s devices to fetch the additional content.
38 |
39 | **Brief**: Send a one-off notification to freemium users that also have an invite code to unlock other devices.
40 |
41 | Download the 1.1 release of Sync Gateway:
42 |
43 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase_Mobile
44 |
45 | You will find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `exampleconfig.json` file to the root of your project:
46 |
47 | cp ~/Downloads/couchbase-sync-gateway/examples/exampleconfig.json /path/to/proj/sync-gateway-config.json
48 |
49 | Add three users in the configuration file:
50 |
51 | {
52 | "log": ["CRUD", "HTTP+"],
53 | "databases": {
54 | "db": {
55 | "server": "walrus:",
56 | "users": {
57 | "zack": {
58 | "password": "letmein"
59 | },
60 | "ali": {
61 | "password": "letmein"
62 | },
63 | "adam": {
64 | "password": "letmein"
65 | },
66 | "GUEST": {"disabled": true}
67 | }
68 | }
69 | }
70 | }
71 |
72 | Add a web hook with the following properties in the `db` object:
73 |
74 | "event_handlers": {
75 | "document_changed": [
76 | {
77 | "handler": "webhook",
78 | "url": "http://localhost:8000/invitecode",
79 | "filter": `function(doc) {
80 | if (doc.type == "profile" && doc.invite_code) {
81 | return true;
82 | }
83 | return false;
84 | }`
85 | }
86 | ]
87 | }
88 |
89 | Start Sync Gateway:
90 |
91 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway ./sync-gateway-config.json
92 |
93 | Create a new file `main.go` to handle the webhook:
94 |
95 | func main() {
96 | http.HandleFunc("/invitecode", func(w http.ResponseWriter, r *http.Request) {
97 | log.Println("ping")
98 |
99 | })
100 |
101 | log.Fatal(http.ListenAndServe(":8000", nil))
102 | }
103 |
104 | Start the Go server:
105 |
106 | $ go run main.go
107 |
108 | Using curl, make a POST request to `:4984/db/bulk_doc` to save 3 Profile documents simultaneously:
109 |
110 | curl -H 'Content-Type: application/json' \
111 | -vX POST http://localhost:4985/db/_bulk_docs \
112 | --data @profiles.json
113 |
114 | **NOTE**: To save space on the command line, the `--data` argument specifies that the request body is in `profiles.json`.
115 |
116 | Notice that only Ali’s Profile document is POSTed to the webhook endpoint:
117 |
118 | ![][image-1]
119 |
120 | In the next section, you will configure a second web hook to notify all users when a new article that matches their interest is published.
121 |
122 | ## Up and Down
123 |
124 | Add another webhook entry that filters only documents of type `article`:
125 |
126 | {
127 | "handler": "webhook",
128 | "url": "http://localhost:8000/new_article",
129 | "filter": `function(doc) {
130 | if (doc.type == "article") {
131 | return true;
132 | }
133 | return false;
134 | }`
135 | }
136 |
137 | Add another handler in your Go server:
138 |
139 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) {
140 | log.Println("ping")
141 | })
142 |
143 | Check that the webhook is working as expected by adding an Article document:
144 |
145 | curl -H 'Content-Type: application/json' \
146 | -vX POST http://localhost:4985/db/_bulk_docs \
147 | --data @articles.json
148 |
149 | In this case, you have to do a bit more work to figure out what set of users to notify. This is a good use case for using a view to index the Profile documents and emitting the topic as the key and registrations IDs as the value for every topic in the topics array.
150 |
151 | To register a view, we can use the Sync Gateway PUT `/_design/ddocname` endpoint with the view definition in the request body:
152 |
153 | curl -H 'Content-Type: application/json' \
154 | -vX PUT http://localhost:4985/db/_design/extras \
155 | --data @view.json
156 |
157 | Notice that the article we posted above has design in it’s topic and the only user subscribed to this topic is Adam. Consequently, if you query that view with the key "design", only one (key, value) pair should return with the topic as key and device tokens as value:
158 |
159 | curl -H 'Content-Type: application/json' \
160 | -vX GET ':4985/db/_design/extras/_view/user_topics?key="design"'
161 |
162 | < HTTP/1.1 200 OK
163 | < Content-Length: 95
164 | < Content-Type: application/json
165 | * Server Couchbase Sync Gateway/1.1.0 is not blacklisted
166 | < Server: Couchbase Sync Gateway/1.1.0
167 | < Date: Wed, 17 Jun 2015 17:46:35 GMT
168 | <
169 | * Connection #0 to host left intact
170 | {"total_rows":1,"rows":[{"id":"4caa204e81b118cf23500f320e138aa8","key":"design","value":null}]}
171 |
172 | Now, you can edit handler in `main.go` to subsequently query the `user_topics` view with the key being the topic of the article:
173 |
174 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) {
175 | log.Println("ping")
176 |
177 | var data map[string]interface{}
178 | body, _ := ioutil.ReadAll(r.Body)
179 | json.Unmarshal(body, &data)
180 |
181 | topic := data["topic"].(string)
182 | log.Printf("Querying user Profiles subscribed to %s", topic)
183 |
184 | var stringUrl string = fmt.Sprintf("http://localhost:4985/db/_design/extras/_view/user_topics?key=\"%s\"", topic)
185 | res, err := http.Get(stringUrl)
186 | if err != nil {
187 | fmt.Print(err)
188 | return
189 | }
190 |
191 | if res != nil {
192 | var result map[string]interface{}
193 | body, _ = ioutil.ReadAll(res.Body)
194 | json.Unmarshal(body, &result)
195 | log.Printf("Result from the user_topics query %v", result["rows"].([]interface {}))
196 | }
197 | })
198 |
199 | Run the `bulk_doc` request again and you will see the list of device tokens to use in the logs:
200 |
201 | ![][image-2]
202 |
203 | ## Conclusion
204 |
205 | In this tutorial, you learned how to use Web Hooks in the scenario of GCM Push Notifications and used Couchbase Server Views to access additional information at Webhook Time™.
206 |
207 | [image-1]: http://i.gyazo.com/7ec3dd332f2d029af364590a4c2e3e63.gif
208 | [image-2]: http://i.gyazo.com/b8c8731e0cbdb5b11e8c35710fe3a092.gif
--------------------------------------------------------------------------------
/02-gcm-webhook/articles.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": [
3 | {
4 | "type": "article",
5 | "title": "Design tool for developers",
6 | "topic": "design"
7 | }
8 | ]
9 | }
--------------------------------------------------------------------------------
/02-gcm-webhook/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "encoding/json"
7 | "io/ioutil"
8 | "fmt"
9 | )
10 |
11 | func main() {
12 |
13 | http.HandleFunc("/invitecode", func(w http.ResponseWriter, r *http.Request) {
14 | log.Println("ping")
15 |
16 | var data map[string]interface{}
17 |
18 | body, _ := ioutil.ReadAll(r.Body)
19 | json.Unmarshal(body, &data)
20 | log.Printf("Send a notification to %s with device tokens %v", data["name"].(string),
21 | data["registration_ids"].([]interface {}))
22 |
23 | // send notification to all devices
24 |
25 | })
26 |
27 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) {
28 | log.Println("ping")
29 |
30 | var data map[string]interface{}
31 |
32 | body, _ := ioutil.ReadAll(r.Body)
33 | json.Unmarshal(body, &data)
34 |
35 | topic := data["topic"].(string)
36 | log.Printf("Querying user Profiles subscribed to %s", topic)
37 |
38 | var stringUrl string = fmt.Sprintf("http://localhost:4985/db/_design/extras/_view/user_topics?key=\"%s\"", topic)
39 | res, err := http.Get(stringUrl)
40 |
41 | if err != nil {
42 | fmt.Print(err)
43 | return
44 | }
45 |
46 | if res != nil {
47 |
48 | var result map[string]interface{}
49 |
50 | body, _ = ioutil.ReadAll(res.Body)
51 | json.Unmarshal(body, &result)
52 |
53 | log.Printf("Result from the user_topics query %v", result["rows"].([]interface {}))
54 | }
55 |
56 | })
57 |
58 | log.Fatal(http.ListenAndServe(":8000", nil))
59 | }
--------------------------------------------------------------------------------
/02-gcm-webhook/profiles.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": [
3 | {
4 | "type": "profile",
5 | "registration_ids": ["AP91SZs2"],
6 | "name": "zack"
7 | },
8 | {
9 | "type": "profile",
10 | "name": "ali",
11 | "registration_ids": ["AP91IEsF"],
12 | "invite_code": true,
13 | "topics": ["hacking"]
14 | },
15 | {
16 | "type": "profile",
17 | "name": "adam",
18 | "registration_ids": ["AP91DKsR", "AP91CNs8"],
19 | "topics": ["design", "science"]
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/02-gcm-webhook/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["CRUD", "HTTP+"],
3 | "adminInterface": "localhost:4985",
4 | "databases": {
5 | "db": {
6 | "server": "walrus:",
7 | "users": {
8 | "zack": {
9 | "password": "letmein"
10 | },
11 | "ali": {
12 | "password": "letmein"
13 | },
14 | "adam": {
15 | "password": "letmein"
16 | },
17 | "GUEST": {"disabled": true}
18 | },
19 | "event_handlers": {
20 | "document_changed": [
21 | {
22 | "handler": "webhook",
23 | "url": "http://localhost:8000/invitecode",
24 | "filter": `function(doc) {
25 | if (doc.type == "profile" && doc.invite_code) {
26 | return true;
27 | }
28 | return false;
29 | }`
30 | },
31 | {
32 | "handler": "webhook",
33 | "url": "http://localhost:8000/new_article",
34 | "filter": `function(doc) {
35 | if (doc.type == "article") {
36 | return true;
37 | }
38 | return false;
39 | }`
40 | }
41 | ]
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/02-gcm-webhook/view.json:
--------------------------------------------------------------------------------
1 | {
2 | "views": {
3 | "user_topics": {
4 | "map": "function(doc, meta) { if (doc.type == 'profile') { doc.topics.forEach(function(topic) { emit(topic, doc.registration_ids) }) } }"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Xcode ###
4 | build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | xcuserdata
14 | *.xccheckout
15 | *.moved-aside
16 | DerivedData
17 | *.xcuserstate
18 |
19 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_GET_channels.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 |
3 | import UIKit
4 | import XCPlayground
5 |
6 | // Let asynchronous code run
7 | XCPSetExecutionShouldContinueIndefinitely()
8 |
9 | var url = NSURL(string: "http://localhost:4984/db/_session")!
10 | var session = NSURLSession.sharedSession()
11 |
12 | var task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
13 |
14 | var json: Dictionary = (NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: nil) as? Dictionary)!
15 |
16 | if let userCtx = json["userCtx"] as? Dictionary {
17 | if let channels = userCtx["channels"] as? Dictionary {
18 |
19 | var array: [String] = []
20 |
21 | for (item, _) in channels {
22 | if item != "!" {
23 | array.append(item)
24 | }
25 | }
26 |
27 | }
28 | }
29 | })
30 |
31 | task.resume()
32 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_GET_channels.playground/Sources/SupportCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to SG REST API.playground.
3 | //
4 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_GET_channels.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_GET_channels.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_GET_channels.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_POST_document.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 |
3 | import UIKit
4 | import XCPlayground
5 |
6 | // Let asynchronous code run
7 | XCPSetExecutionShouldContinueIndefinitely()
8 |
9 | let url = NSURL(string: "http://localhost:4984/db/")!
10 | let session = NSURLSession.sharedSession()
11 |
12 | let request = NSMutableURLRequest(URL: url)
13 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
14 | request.HTTPMethod = "POST"
15 |
16 | let json: Dictionary = ["name": "oliver"]
17 | let data = NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions.allZeros, error: nil)
18 |
19 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in
20 | let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableLeaves, error: nil) as! Dictionary
21 | println(json)
22 |
23 | }
24 |
25 | uploadTask.resume()
26 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_POST_document.playground/Sources/SupportCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to REST_POST_document.playground.
3 | //
4 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_POST_document.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_POST_document.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/03-ios-share-extension/Playgrounds/REST_POST_document.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/03-ios-share-extension/README.md:
--------------------------------------------------------------------------------
1 | # Couchbase by Example: iOS Share Extensions
2 |
3 | An app extension lets you extend custom functionality and content beyond your app and make it available to users while they’re using other apps or the system. In this post, you will learn how to use extensions with the Couchbase Lite iOS SDK and Sync Gateway.
4 |
5 | ## Extensions Recap
6 |
7 | Action extensions are all about changing content in-place and Share extensions is about moving content from the current host application to your application or web service.
8 |
9 | Action extensions act on the current content and because of that it uses the content as the user interface (for example, a Translate extension to translate text in-place would be an Action extension).
10 |
11 | In Safari, if we tap on the action bar on the top right, it brings up the share sheet with two types of extensions:
12 |
13 | ![][image-1]
14 |
15 | The Share Extensions are on the top and the Action Extensions are on the bottom.
16 |
17 | Imagine you are building a music curation platform and would like to allow curators to pick songs while browsing the internet. This is a perfect use case for a Share extension. In this tutorial, you will build this Share extension with multiple View Controllers on screen to share the beat with a particular curation team.
18 |
19 | ## Share Extension
20 |
21 | In Xcode, create a new Single View Application project called `TeamPicks`:
22 |
23 | ![][image-2]
24 |
25 | Then, select the **File \> New \> Target** menu item and select Share Extension in the new target wizard. Name your extension `Share Track`:
26 |
27 | ![][image-3]
28 |
29 | Select the Extension Target and run it, it should popup in Safari. Use the more menu item to display your extension in the Share Sheet by default:
30 |
31 | ![][image-4]
32 |
33 | Apple’s default share extension view has a text area populated with the title of the web page on which it was invoked and a preview area on the right. You will add a configuration item below to allow the user to pick the team to share the song with. Add a new property `item` of type `SLComposeSheetConfigurationItem` and change the `configureItems` method to read:
34 |
35 | override func configurationItems() -> [AnyObject]! {
36 | self.item = SLComposeSheetConfigurationItem()
37 |
38 | self.item.title = "Team"
39 | self.item.value = "None"
40 |
41 | self.item.tapHandler = {
42 | // TBA
43 | }
44 |
45 | return [self.item]
46 | }
47 |
48 | If the user taps on a cell, the tapHandler closure is invoked and that’s where you will push a new table view controller on the `SLComposeServiceViewController`, the same way you would with UINavigationController. First, open a new file `TeamTableViewController` subclassing `UITableViewController`:
49 |
50 | ![][image-5]
51 |
52 | In `ShareViewController`, add property `teamPickerVC` of type `TeamTableViewController` and push it on the nav stack in the tapHandler:
53 |
54 | self.item.tapHandler = {
55 | self.teamPickerVC = TeamTableViewController()
56 | self.pushConfigurationViewController(self.teamPickerVC)
57 | }
58 |
59 | Run the extension and navigate to and back from the configuration view controller.
60 |
61 | ![][image-6]
62 |
63 | In the next section, you will learn how to set up Sync Gateway with basic channels so that we can display them in the table view later on.
64 |
65 | ## Sync Gateway
66 |
67 | Download Sync Gateway and unzip the file:
68 |
69 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile
70 |
71 | You can find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `cors.json` file to the root of your project:
72 |
73 | $ cp /Downloads/couchbase-sync-gateway/examples/admin_party.json /path/to/proj/sync-gateway-config.json
74 |
75 | Each team will be represented by a channel. The channel name will be the team name. Channels are a convenient way to tag documents and by giving users access to a set of channels, you can add fine grained access control. In this tutorial, you will use channels exclusively with the GUEST account (all unauthorised requests made to Sync Gateway). In a full featured application, you would likely give each user access to channels in the Sync Function.
76 |
77 | Change the configuration file to declare the list of channels, i.e. teams:
78 |
79 | {
80 | "log": ["HTTP+"],
81 | "databases": {
82 | "db": {
83 | "server": "walrus:",
84 | "users": {
85 | "GUEST": {
86 | "disabled": false,
87 | "admin_channels": ["pop", "rock", "house"]
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 | **NOTE**: It’s also possible to create channels programmatically in the Sync Function by using the **channel** command.
95 |
96 | Start Sync Gateway and open the Admin Dashboard on `http://localhost:4985/_admin/` to monitor the channels and what documents were synced to them:
97 |
98 | ![][image-7]
99 |
100 | Now you can display the list of teams in the share extensions by simply using `NSURLSession` to retrieve the channel names from Sync Gateway.
101 |
102 | # Populating TeamPickerViewController
103 |
104 | In the `ViewDidLoad` method of `ShareViewController`, make a GET request to `http://localhost:4984/db/_session`. Use the Swift Playground attached to this project for hints on how to make that request:
105 |
106 | > ./Playgrounds/REST_GET_channels
107 |
108 | Pass the result to a `teams` property (of type `[String]`) on the View Controller and replace the required `UITableViewDataSource` methods:
109 |
110 | - `tableView:numberOfSectionsInTableView:` should return 1
111 | - `tableView:numberOfRowsInSection:` should return the number of teams
112 | - `tableView:cellForRowAtIndexPath:` should set the `textLabel`’s text property to a team name for each row:
113 |
114 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
115 | var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("TeamCell") as? UITableViewCell
116 | if (cell == nil) {
117 | cell = UITableViewCell(style: .Default, reuseIdentifier: "TeamCell")
118 | }
119 |
120 | cell!.textLabel!.text = teams[indexPath.item]
121 | return cell!
122 | }
123 |
124 |
125 | Run the extension and you should see the list of teams:
126 |
127 | ![][image-8]
128 |
129 | Now you will learn how to pass back the selected team to the ShareViewController using a protocol. Above the class definition of Table View in `TeamTableViewController.swift`, add a protocol:
130 |
131 | protocol TeamViewProtocol {
132 | func sendingViewController(viewController: TeamTableViewController, sentItem: String)
133 | }
134 |
135 | In TeamTableViewController, add a delegate property of type `TeamViewProtocol?` and implement `tableView:didSelectRowAtIndexPath`:
136 |
137 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
138 | self.delegate?.sendingViewController(self, sentItem: self.teams[indexPath.item])
139 | }
140 |
141 | Implement the protocol in `ShareViewController`:
142 |
143 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) {
144 | self.item.value = sentItem
145 | self.popConfigurationViewController()
146 | }
147 |
148 | In `tapHandler` of ShareViewController, set the delegate before pushing the View Controller on the navigation stack:
149 |
150 | self.item.tapHandler = {
151 | self.teamPickerVC = TeamTableViewController()
152 | self.teamPickerVC.delegate = self
153 | self.pushConfigurationViewController(self.teamPickerVC)
154 | }
155 |
156 | Run the app and selecting a team should now return to the ShareViewController with the configuration item value updated accordingly.
157 |
158 | In the next section, you will save the document to Sync Gateway when the `didSelectPost` method is called (i.e when clicking the Post button).
159 |
160 | # Saving the Pick document
161 |
162 | The final step is to save the document to Sync Gateway when the post button is pressed. This time, you will use `NSURLSession` to make a POST request to `http://localhost:4984/db/` with the track name and team name:
163 |
164 | override func didSelectPost() {
165 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
166 |
167 | var properties = [
168 | "text": self.contentText,
169 | "team": self.item.value
170 | ]
171 |
172 | let url = NSURL(string: "http://localhost:4984/db/")!
173 | let session = NSURLSession.sharedSession()
174 |
175 | let request = NSMutableURLRequest(URL: url)
176 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
177 | request.HTTPMethod = "POST"
178 |
179 | let data = NSJSONSerialization.dataWithJSONObject(properties, options: .allZeros, error: nil)
180 |
181 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in
182 |
183 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
184 | self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
185 | }
186 |
187 | uploadTask.resume()
188 | }
189 |
190 | Check the results in the Admin Dashboard. The document should appear in the Documents tab:
191 |
192 | ![][image-9]
193 |
194 | **NOTE**: We could also use the [bulk docs][1] endpoint to save multiple documents in one API request. This is particularly useful in app extensions where there is limited time to perform network operations.
195 |
196 | ## Conclusion
197 |
198 | Share extensions are great for sharing data with your app and the Web. Setting up a Share Extension to fetch documents on-demand from Sync Gateway is a good way to give more context awareness to the user. You then have the choice to save the document back to Sync Gateway through the REST API or in the Couchbase Lite database also used by your iOS application, we will explore how to do that in the next post!
199 |
200 | [1]: http://developer.couchbase.com/mobile/develop/references/couchbase-lite/rest-api/database/post-bulk-docs/index.html
201 |
202 | [image-1]: http://cl.ly/image/1a3R200o2A1h/Screen_Shot_2015-06-13_at_22_27_31.png
203 | [image-2]: http://cl.ly/image/2S260n2l1W2c/Screen%20Shot%202015-06-17%20at%2010.54.10.png
204 | [image-3]: http://cl.ly/image/3N2R102X2Y3y/Screen%20Shot%202015-06-17%20at%2010.55.13.png
205 | [image-4]: http://i.gyazo.com/b11f135b2635fc65ec79321e9a953d03.gif
206 | [image-5]: http://i.gyazo.com/f22b1dab9393ddcfc5c5ddb96da47379.gif
207 | [image-6]: http://i.gyazo.com/4c079d89c4b87f6496c9c5aae3714885.gif
208 | [image-7]: http://i.gyazo.com/37acae33dce5e3e9d4c50945f6550b51.gif
209 | [image-8]: http://i.gyazo.com/7ce508653facb35c31ff0c1a15f1a26b.gif
210 | [image-9]: http://i.gyazo.com/21f2ad20fcdc608a7a2b174a07ccb1de.gif
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Xcode ###
4 | build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | xcuserdata
14 | *.xccheckout
15 | *.moved-aside
16 | DerivedData
17 | *.xcuserstate
18 |
19 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/Share Track/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Share Track
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | com.couchbase.TeamPicks.$(PRODUCT_NAME:rfc1034identifier)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | NSExtension
26 |
27 | NSExtensionAttributes
28 |
29 | NSExtensionActivationRule
30 | TRUEPREDICATE
31 |
32 | NSExtensionMainStoryboard
33 | MainInterface
34 | NSExtensionPointIdentifier
35 | com.apple.share-services
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/Share Track/MainInterface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/Share Track/ShareViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareViewController.swift
3 | // Share Track
4 | //
5 | // Created by James Nocentini on 17/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Social
11 |
12 | class ShareViewController: SLComposeServiceViewController, TeamViewProtocol {
13 |
14 | var item: SLComposeSheetConfigurationItem!
15 | var teamPickerVC: TeamTableViewController!
16 |
17 | override func isContentValid() -> Bool {
18 | // Do validation of contentText and/or NSExtensionContext attachments here
19 | return true
20 | }
21 |
22 | override func didSelectPost() {
23 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
24 |
25 | var properties = [
26 | "text": self.contentText,
27 | "team": self.item.value
28 | ]
29 |
30 | let url = NSURL(string: "http://localhost:4984/db/")!
31 | let session = NSURLSession.sharedSession()
32 |
33 | let request = NSMutableURLRequest(URL: url)
34 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
35 | request.HTTPMethod = "POST"
36 |
37 | let data = NSJSONSerialization.dataWithJSONObject(properties, options: .allZeros, error: nil)
38 |
39 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in
40 |
41 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
42 | self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
43 | }
44 |
45 | uploadTask.resume()
46 | }
47 |
48 | override func configurationItems() -> [AnyObject]! {
49 | self.item = SLComposeSheetConfigurationItem()
50 |
51 | self.item.title = "Team"
52 | self.item.value = "None"
53 |
54 | self.item.tapHandler = {
55 | self.teamPickerVC = TeamTableViewController()
56 | self.teamPickerVC.delegate = self
57 | self.pushConfigurationViewController(self.teamPickerVC)
58 | }
59 |
60 | return [self.item]
61 | }
62 |
63 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) {
64 | self.item.value = sentItem
65 | self.popConfigurationViewController()
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/Share Track/TeamTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamTableViewController.swift
3 | // TeamPicks
4 | //
5 | // Created by James Nocentini on 17/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol TeamViewProtocol {
12 | func sendingViewController(viewController: TeamTableViewController, sentItem: String)
13 | }
14 |
15 | class TeamTableViewController: UITableViewController {
16 |
17 | var teams: [String] = []
18 | var delegate: TeamViewProtocol?
19 |
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 |
23 | var url = NSURL(string: "http://localhost:4984/db/_session")!
24 | var session = NSURLSession.sharedSession()
25 |
26 | var task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
27 |
28 | var json: Dictionary = (NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: nil) as? Dictionary)!
29 |
30 | if let userCtx = json["userCtx"] as? Dictionary {
31 | if let channels = userCtx["channels"] as? Dictionary {
32 |
33 | var array: [String] = []
34 |
35 | for (item, _) in channels {
36 | if item != "!" {
37 | array.append(item)
38 | }
39 | }
40 |
41 | self.teams = array
42 |
43 | dispatch_async(dispatch_get_main_queue(), { () -> Void in
44 | self.tableView.reloadData()
45 | })
46 |
47 | }
48 | }
49 | })
50 |
51 | task.resume()
52 |
53 |
54 |
55 | self.clearsSelectionOnViewWillAppear = false
56 | }
57 |
58 | override func didReceiveMemoryWarning() {
59 | super.didReceiveMemoryWarning()
60 | // Dispose of any resources that can be recreated.
61 | }
62 |
63 | // MARK: - Table view data source
64 |
65 | override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
66 | // #warning Potentially incomplete method implementation.
67 | // Return the number of sections.
68 | return 1
69 | }
70 |
71 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
72 | // #warning Incomplete method implementation.
73 | // Return the number of rows in the section.
74 | return self.teams.count
75 | }
76 |
77 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
78 |
79 | var cell = tableView.dequeueReusableCellWithIdentifier("TeamCell") as? UITableViewCell
80 |
81 | if (cell == nil) {
82 | cell = UITableViewCell(style: .Default, reuseIdentifier: "TeamCell")
83 | }
84 |
85 | cell!.textLabel!.text = self.teams[indexPath.item]
86 |
87 | return cell!
88 | }
89 |
90 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
91 | self.delegate?.sendingViewController(self, sentItem: self.teams[indexPath.item])
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // TeamPicks
4 | //
5 | // Created by James Nocentini on 17/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(application: UIApplication) {
33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicks/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // TeamPicks
4 | //
5 | // Created by James Nocentini on 17/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | // Do any additional setup after loading the view, typically from a nib.
16 | }
17 |
18 | override func didReceiveMemoryWarning() {
19 | super.didReceiveMemoryWarning()
20 | // Dispose of any resources that can be recreated.
21 | }
22 |
23 |
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicksTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/03-ios-share-extension/TeamPicks/TeamPicksTests/TeamPicksTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamPicksTests.swift
3 | // TeamPicksTests
4 | //
5 | // Created by James Nocentini on 17/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XCTest
11 |
12 | class TeamPicksTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/03-ios-share-extension/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["HTTP+"],
3 | "databases": {
4 | "db": {
5 | "server": "walrus:",
6 | "users": {
7 | "GUEST": {
8 | "disabled": false,
9 | "admin_channels": ["pop", "rock", "house"]
10 | }
11 | }
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | node_modules
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Xcode ###
4 | build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | xcuserdata
14 | *.xccheckout
15 | *.moved-aside
16 | DerivedData
17 | *.xcuserstate
18 |
19 | Pods
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/xcuserdata/jamesnocentini.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/xcuserdata/jamesnocentini.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CityExplorer
4 | //
5 | // Created by James Nocentini on 21/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | func applicationWillResignActive(application: UIApplication) {
22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
24 | }
25 |
26 | func applicationDidEnterBackground(application: UIApplication) {
27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
29 | }
30 |
31 | func applicationWillEnterForeground(application: UIApplication) {
32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
33 | }
34 |
35 | func applicationDidBecomeActive(application: UIApplication) {
36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
37 | }
38 |
39 | func applicationWillTerminate(application: UIApplication) {
40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
41 | }
42 |
43 |
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // CityExplorer
4 | //
5 | // Created by James Nocentini on 21/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | var pull: CBLReplication?
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | // Do any additional setup after loading the view, typically from a nib.
18 |
19 | let manager = CBLManager.sharedInstance()
20 | let databaseExists = manager.databaseExistsNamed("cityexplorer")
21 | var database = manager.databaseNamed("cityexplorer", error: nil)
22 | if databaseExists {
23 | database?.deleteDatabase(nil)
24 | database = manager.databaseNamed("cityexplorer", error: nil)
25 | }
26 |
27 | let gateway = NSURL(string: "http://localhost:4984/db")!
28 |
29 | pull = database?.createPullReplication(gateway)
30 |
31 | let nctr = NSNotificationCenter.defaultCenter()
32 | nctr.addObserver(self, selector: "replicationProgress:", name: kCBLReplicationChangeNotification, object: pull)
33 |
34 | pull?.start()
35 | }
36 |
37 | @IBOutlet var progressView: UIProgressView!
38 |
39 | func replicationProgress(notification: NSNotification) {
40 |
41 | let active = pull?.status == .Active
42 | let completed = pull?.status == .Stopped
43 |
44 | println("Status : \(pull?.status.rawValue)")
45 | println("Changes Count: \(pull?.changesCount)")
46 | println("Completed Count: \(pull?.completedChangesCount)")
47 | println("======")
48 |
49 | if pull!.changesCount > 0 {
50 | let number = Float(pull!.completedChangesCount) / Float(pull!.changesCount)
51 | self.progressView.progress = number
52 | }
53 |
54 | }
55 |
56 | override func didReceiveMemoryWarning() {
57 | super.didReceiveMemoryWarning()
58 | // Dispose of any resources that can be recreated.
59 | }
60 |
61 |
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorer/bridging-header.h:
--------------------------------------------------------------------------------
1 | //
2 | // bridging-header.h
3 | // CityExplorer
4 | //
5 | // Created by James Nocentini on 21/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | #ifndef CityExplorer_bridging_header_h
10 | #define CityExplorer_bridging_header_h
11 |
12 | #include
13 |
14 | #endif
15 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorerTests/CityExplorerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CityExplorerTests.swift
3 | // CityExplorerTests
4 | //
5 | // Created by James Nocentini on 21/06/2015.
6 | // Copyright (c) 2015 Couchbase. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XCTest
11 |
12 | class CityExplorerTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/CityExplorerTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '6.0'
3 |
4 | target 'CityExplorer' do
5 |
6 | pod 'couchbase-lite-ios', '~> 1.1.0'
7 |
8 | end
9 |
10 | target 'CityExplorerTests' do
11 |
12 | end
13 |
14 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/CityExplorer/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - couchbase-lite-ios (1.1.0):
3 | - couchbase-lite-ios/Core (= 1.1.0)
4 | - couchbase-lite-ios/Core (1.1.0)
5 |
6 | DEPENDENCIES:
7 | - couchbase-lite-ios (~> 1.1.0)
8 |
9 | SPEC CHECKSUMS:
10 | couchbase-lite-ios: fc7be01032dc1a4c923d1a52b6734eb9b20bf072
11 |
12 | COCOAPODS: 0.37.1
13 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/README.md:
--------------------------------------------------------------------------------
1 | # Couchbase by Example: Displaying a Sync Progress Indicator
2 |
3 | `NSProgress` is an object in Foundation that represents the completion of some work. That work could be downloading a file, installing an app or something your own application is doing.
4 |
5 | The `NSProgress` exists to let you easily report progress in your application across various components both for the UI and the system. With Couchbase Mobile, data is exchanged by initiating replications and with those come change events that can inform you of the progress.
6 |
7 | In this tutorial, you'll import data from the Google Places API to Sync Gateway and replicate them to an iOS app.
8 |
9 | > https://developers.google.com/places/
10 |
11 | Along the way, you'll learn how to:
12 |
13 | - Use the Sync Gateway Admin REST API to import data from an external source.
14 | - Setup a pull replication with the iOS SDK.
15 | - Use replication change notifications to display a progress bar in the UI.
16 |
17 | Let's get started!
18 |
19 | ## Getting Started
20 |
21 | To use the Google Places API in this tutorial, you will first create a new project in the Google Developer Console and then generate a Server API Key.
22 |
23 | Open the Google Developer Console and log into your account:
24 |
25 | > https://console.developers.google.com
26 |
27 | Create a new Project called **City Explorer**:
28 |
29 | 
30 |
31 | Once the new project appears in the list, click on it and navigate to **APIs & auth > APIs** in the left navigation drawer. Enable the **Google Places API Web Service**.
32 |
33 | Once enabled, go to the **Credentials** tab in the left navigation drawer, create a new Key (Server key) and copy down the API key as you will need it throughout this tutorial.
34 |
35 | In the next section, you will use a couple libraries and the Admin REST API to sync the Places data to Sync Gateway.
36 |
37 | ## Sync Gateway
38 |
39 | Download Sync Gateway and unzip the file:
40 |
41 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile
42 |
43 | You can find the Sync Gateway binary in the **bin** folder and examples of configuration files in the **examples** folder. Copy the **basic-walrus-bucket.json** file to the root of your project:
44 |
45 | $ cp /Downloads/couchbase-sync-gateway/examples/basic-walrus-bucket.json /path/to/proj/sync-gateway-config.json
46 |
47 | Start Sync Gateway:
48 |
49 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway
50 |
51 | Open the Admin Dashboard to monitor the documents that were saved to Sync Gateway.
52 |
53 | http://localhost:4985/_admin/
54 |
55 | In the next section, you will write a small NodeJS app with the RxJS and Request modules to import the Places data to Sync Gateway.
56 |
57 | ## Places API → Sync Gateway
58 |
59 | Before you start scripting the app server, check that your API Key is working correctly, open the following url in your browser, you should see the JSON response.
60 |
61 | https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+London&key=API_KEY
62 |
63 | **NOTE**: Don’t forget to add your API Key in the URL.
64 |
65 | 
66 |
67 | To build the app server that will import the data from the Places API to Sync Gateway, you will use [RxJS](https://github.com/Reactive-Extensions/RxJS) and [Request](https://github.com/request/request). Code that deals with more than one event or asynchronous computation gets complicated quickly. RxJS makes these computations *first-class citizens* and provides a model that allows for readable and composable APIs to deal with these asynchronous computations. The Request module is the de-facto library to make http requests in NodeJS simpler than ever. Go ahead and install the dependencies:
68 |
69 | $ npm install request rx --save
70 |
71 | Copy **requestRx.js** from the [GitHub repo](https://github.com/couchbaselabs/Couchbase-by-Example/blob/master/04-ios-sync-progress-indicator/requestRx.js) to your project folder. We’re simply wrapping the Request api in RxJS constructs (flatMap, filter, subscribe...). For example, instead of using `request.get`, you will use `requestRx.get`.
72 |
73 | Create a new file called **sync.js**, require the `requestRx` and `Rx` modules. Define a couple constants:
74 |
75 | const api_key = 'AIzaSyBGRQzQ2Sy1zgIrMrbYUknd1L25idYOoII';
76 | const url = 'https://maps.googleapis.com/maps/api/place';
77 | const gateway = 'http://localhost:4985/db';
78 |
79 | **NOTE**: You will use the JavaScript ES 6 syntax (more specifically string interpolation and arrow functions) which will make your program shorter and more readable.
80 |
81 | Next, use the `requestRx` method to follow the chain of requests describe in the diagram.
82 |
83 | If you are wondering how to use Reactive Extensions, I stronly encourage you to follow [this tutorial](http://reactive-extensions.github.io/learnrx/). It will take a couple hours to complete but you will come out of it with a very clear understanding of Reactive Extensions:
84 |
85 | http://reactive-extensions.github.io/learnrx/
86 |
87 | This might be a lot to take in but the best think to do is experiment with the different operators (flatMap, zip, subscribe, fromArray):
88 |
89 | ```javascript
90 | // 1. Search for Places
91 | requestRx.get(`${url}/textsearch/json?key=${api_key}&query=restaurants+in+london`)
92 | .subscribe((res) => {
93 | var places = JSON.parse(res.body).results;
94 | var placesStream = Rx.Observable.fromArray(places);
95 |
96 | // 2. Send the Places in bulk to Sync Gateway
97 | requestRx({uri: `${gateway}/_bulk_docs`, method: 'POST', json: {docs: places}})
98 | .flatMap((docsRes) => {
99 | var docsStream = Rx.Observable.fromArray(docsRes.body);
100 |
101 | // Merge the place's photoreference with the doc id and rev
102 | return Rx.Observable.zip(placesStream, docsStream, (place, doc) => {
103 | return {
104 | id: doc.id,
105 | rev: doc.rev,
106 | ref: place.photos[0].photo_reference
107 | }
108 | });
109 | })
110 | .flatMap((doc) => {
111 |
112 | // 3. Get the binary jpg photo using the ref property (i.e. photoreference)
113 | var options = {
114 | uri: `${url}/photo?key=${api_key}&maxwidth=400&photoreference=${doc.ref}`,
115 | encoding: null
116 | };
117 | return requestRx.get(options)
118 | .flatMap((photo) => {
119 |
120 | // 4. Save the photo as an attachment on the corresponding document
121 | return requestRx({
122 | uri: `${gateway}/${doc.id}/photo?rev=${doc.rev}`,
123 | method: 'PUT',
124 | headers: {'Content-Type': 'image/jpg'},
125 | body: photo.body
126 | })
127 | })
128 | })
129 | .subscribe((res) => {
130 | });
131 | });
132 | ```
133 |
134 | 1. Get the Places that match the query `restaurants in London`. Use the ES 6 string interpolation feature in the url.
135 | 2. The `_bulk_docs` endpoint is very convenient for importing large datasets to a Sync Gateway instance. Read more about it in the [docs](http://developer.couchbase.com/mobile/develop/references/sync-gateway/rest-api/database/post-bulk-docs/index.html).
136 | 3. After saving the document, you save the photo as an attachment, you must first get the image from the Places API. Notice the `encoding` property is set to `null`. This is required by the Request module for any response body that isn’t a string. Read more about it in the [Request docs](https://github.com/request/request#user-content-requestoptions-callback).
137 | 4. You must tell Sync Gateway which document (by specifying the document id) and revision of that document (by specifying the revision number) to save this attachment on.
138 |
139 | To run your NodeJS app written with the JavaScript ES 6 syntax, you can use [Babel](https://babeljs.io/). Install it and run it with the **sync.js** file:
140 |
141 | $ npm install babel -g
142 | $ babel-node sync.js
143 |
144 | 
145 |
146 | Now that you have documents including images stored in the in-memory bucket of Sync Gateway, you will start coding the iOS app to include a progress bar managed by the replication change notification.
147 |
148 | ## iOS application
149 |
150 | In Xcode, create a new **Single View Application** called **CityExplorer**:
151 |
152 | 
153 |
154 | Close the project and install the Couchbase Lite iOS SDK via Cocoapods:
155 |
156 | $ pod init
157 | $ pod search couchbase
158 |
159 | Add the dependency to the **Podfile** in the root of the project, then run install:
160 |
161 | $ pod install
162 |
163 | Open **CityExplorer.workspace** this time and create a bridging header:
164 |
165 | 
166 |
167 | Navigate to build settings to add the bridging header:
168 |
169 | 
170 |
171 | Open `ViewController.swift` and add a property `pull` of type `CBLReplication?`. In the `viewDidLoad` method, add the following:
172 |
173 | // 1
174 | let manager = CBLManager.sharedInstance()
175 | // 2
176 | let databaseExists = manager.databaseExistsNamed("cityexplorer")
177 | var database = manager.databaseNamed("cityexplorer", error: nil)
178 | if databaseExists {
179 | database?.deleteDatabase(nil)
180 | database = manager.databaseNamed("cityexplorer", error: nil)
181 | }
182 |
183 | let gateway = NSURL(string: "http://localhost:4984/db")!
184 |
185 | // 3
186 | pull = database?.createPullReplication(gateway)
187 |
188 | let nctr = NSNotificationCenter.defaultCenter()
189 | nctr.addObserver(self, selector: "replicationProgress:", name: kCBLReplicationChangeNotification, object: pull)
190 |
191 | // 4
192 | pull?.start()
193 |
194 | A couple of things are happening above:
195 |
196 | 1. You get the shared instance of the manager.
197 | 2. With the manager instance, you delete the content of the database. This will ensure that the replication starts from scratch every time you run the app.
198 | 3. You instantiate a pull replication and register as an oberserver on the notification named `kCBLReplicationChangeNotification`.
199 | 4. You start the replication.
200 |
201 | And add the `replicationProgress` method to simply log the changesCount and completedChangesCount properties:
202 |
203 | func replicationProgress(notification: NSNotification) {
204 | println("Changes count \(pull?.changesCount)")
205 | println("Completed \(pull?.completedChangesCount)")
206 | }
207 |
208 | In the next section, you will add a progress view in the Storyboard, then will connect it to the View Controller.
209 |
210 | ## Progress Bar
211 |
212 | In the Storyboard, add a Progress View in the centre of the View:
213 |
214 | 
215 |
216 | Connect the UI handle to the controller property:
217 |
218 | 
219 |
220 | In the `replicationProgress` method, update the progressView's `progress` property accordingly:
221 |
222 | let active = pull?.status == .Active
223 | let completed = pull?.status == .Stopped
224 |
225 | println("Status : \(pull?.status.rawValue)")
226 | println("Changes Count: \(pull?.changesCount)")
227 | println("Completed Count: \(pull?.completedChangesCount)")
228 | println("======")
229 |
230 | if pull!.changesCount > 0 {
231 | let number = Float(pull!.completedChangesCount) / Float(pull!.changesCount)
232 | self.progressView.progress = number
233 | }
234 |
235 | Run the app and you should see the progress bar updating as the documents are replicated:
236 |
237 | 
238 |
239 | You can run the **sync.js** script a couple times to have more documents to pull. The Places API returns a maximum of 20 results in one response.
240 |
241 | ## Conclusion
242 |
243 | In this tutorial, you learned how to set up a project to use the Google Places API and a NodeJS program to import data as documents and attachments in Sync Gateway. You also used the `NSNotification` api on iOS to register for the `kCBLReplicationChangeNotification` notification and update the progress view in your iOS application.
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "04-ios-sync-progress-indicator",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "requestRx.js",
6 | "dependencies": {
7 | "rx": "~2.5.3",
8 | "request": "~2.58.0"
9 | },
10 | "devDependencies": {},
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "author": "",
15 | "license": "ISC"
16 | }
17 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/requestRx.js:
--------------------------------------------------------------------------------
1 | const rx = require('rx');
2 | const request = require('request');
3 |
4 | let wrapMethodInRx = (method) => {
5 | return function (...args) {
6 | return rx.Observable.create((subj) => {
7 | // Push the callback as the last parameter
8 | args.push((err, resp, body) => {
9 | if (err) {
10 | subj.onError(err);
11 | return;
12 | }
13 |
14 | if (resp.statusCode >= 400) {
15 | subj.onError(new Error(`Request failed: ${resp.statusCode}\n${body}`));
16 | return;
17 | }
18 |
19 | subj.onNext({response: resp, body: body});
20 | subj.onCompleted();
21 | });
22 |
23 | try {
24 | method(...args);
25 | } catch (e) {
26 | subj.onError(e);
27 | }
28 |
29 | return rx.Disposable.empty;
30 | })
31 | }
32 | };
33 |
34 | let requestRx = wrapMethodInRx(request);
35 | requestRx.get = wrapMethodInRx(request.get);
36 | requestRx.post = wrapMethodInRx(request.post);
37 | requestRx.patch = wrapMethodInRx(request.patch);
38 | requestRx.put = wrapMethodInRx(request.put);
39 | requestRx.del = wrapMethodInRx(request.del);
40 |
41 | requestRx.pipe = (url, stream) => {
42 | return rx.Observable.create((subj) => {
43 | try {
44 | request.get(url).pipe(stream)
45 | .on('error', (err) => subj.onError(err))
46 | .on('end', () => { subj.onNext(true); subj.onCompleted(); });
47 | } catch (e) {
48 | subj.onError(e);
49 | }
50 | });
51 | };
52 |
53 | module.exports = requestRx;
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["*"],
3 | "databases": {
4 | "db": {
5 | "server": "walrus:",
6 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } }
7 | }
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/04-ios-sync-progress-indicator/sync.js:
--------------------------------------------------------------------------------
1 | var requestRx = require('./requestRx.js');
2 | var Rx = require('rx');
3 |
4 | const api_key = 'AIzaSyD4e6ZUIc9G2AxKansIUKa0enFzWZy5h8w';
5 | const url = 'https://maps.googleapis.com/maps/api/place';
6 | const gateway = 'http://localhost:4985/db';
7 |
8 | // 1. Search for Places
9 | requestRx.get(`${url}/textsearch/json?key=${api_key}&query=restaurants+in+london`)
10 | .subscribe((res) => {
11 | var places = JSON.parse(res.body).results;
12 | var placesStream = Rx.Observable.fromArray(places);
13 |
14 | // 2. Send the Places in bulk to Sync Gateway
15 | requestRx({uri: `${gateway}/_bulk_docs`, method: 'POST', json: {docs: places}})
16 | .flatMap((docsRes) => {
17 | var docsStream = Rx.Observable.fromArray(docsRes.body);
18 |
19 | // Merge the place's photoreference with the doc id and rev
20 | return Rx.Observable.zip(placesStream, docsStream, (place, doc) => {
21 | return {
22 | id: doc.id,
23 | rev: doc.rev,
24 | ref: place.photos[0].photo_reference
25 | }
26 | });
27 | })
28 | .flatMap((doc) => {
29 |
30 | // 3. Get the binary jpg photo using the ref property (i.e. photoreference)
31 | var options = {
32 | uri: `${url}/photo?key=${api_key}&maxwidth=400&photoreference=${doc.ref}`,
33 | encoding: null
34 | };
35 | return requestRx.get(options)
36 | .flatMap((photo) => {
37 |
38 | // 4. Save the photo as an attachment on the corresponding document
39 | return requestRx({
40 | uri: `${gateway}/${doc.id}/photo?rev=${doc.rev}`,
41 | method: 'PUT',
42 | headers: {'Content-Type': 'image/jpg'},
43 | body: photo.body
44 | })
45 | })
46 | })
47 | .subscribe((res) => {
48 | });
49 | });
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | /.idea/libraries
5 | .DS_Store
6 | /build
7 | /captures
8 | # Created by https://www.gitignore.io
9 |
10 | ### Android ###
11 | # Built application files
12 | *.apk
13 | *.ap_
14 |
15 | # Files for the Dalvik VM
16 | *.dex
17 |
18 | # Java class files
19 | *.class
20 |
21 | # Generated files
22 | bin/
23 | gen/
24 |
25 | # Gradle files
26 | .gradle/
27 | build/
28 | /*/build/
29 |
30 | # Local configuration file (sdk path, etc)
31 | local.properties
32 |
33 | # Proguard folder generated by Eclipse
34 | proguard/
35 |
36 | # Log Files
37 | *.log
38 |
39 | ### Android Patch ###
40 | gen-external-apklibs
41 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/CityExplorer.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | generateDebugAndroidTestSources
19 | generateDebugSources
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 22
5 | buildToolsVersion "22.0.1"
6 |
7 | defaultConfig {
8 | applicationId "com.couchbase.cityexplorer"
9 | minSdkVersion 14
10 | targetSdkVersion 22
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 |
21 | // workaround for "duplicate files during packaging of APK" issue
22 | // see https://groups.google.com/d/msg/adt-dev/bl5Rc4Szpzg/wC8cylTWuIEJ
23 | packagingOptions {
24 | exclude 'META-INF/ASL2.0'
25 | exclude 'META-INF/LICENSE'
26 | exclude 'META-INF/NOTICE'
27 | }
28 | }
29 |
30 | dependencies {
31 | compile fileTree(dir: 'libs', include: ['*.jar'])
32 | compile 'com.couchbase.lite:couchbase-lite-android:1.1.0'
33 | compile 'com.android.support:recyclerview-v7:22.2.0'
34 | compile 'com.android.support:design:22.2.0'
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/jamesnocentini/Developer/adt-bundle/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/androidTest/java/com/couchbase/cityexplorer/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.cityexplorer;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.cityexplorer;
2 |
3 | import android.support.v7.app.AppCompatActivity;
4 | import android.os.Bundle;
5 | import android.support.v7.widget.LinearLayoutManager;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.util.Log;
8 | import android.view.Menu;
9 | import android.view.MenuItem;
10 |
11 | import com.couchbase.cityexplorer.model.Place;
12 | import com.couchbase.lite.CouchbaseLiteException;
13 | import com.couchbase.lite.Database;
14 | import com.couchbase.lite.Emitter;
15 | import com.couchbase.lite.LiveQuery;
16 | import com.couchbase.lite.Manager;
17 | import com.couchbase.lite.Mapper;
18 | import com.couchbase.lite.Query;
19 | import com.couchbase.lite.QueryEnumerator;
20 | import com.couchbase.lite.QueryRow;
21 | import com.couchbase.lite.View;
22 | import com.couchbase.lite.android.AndroidContext;
23 | import com.couchbase.lite.replicator.Replication;
24 | import com.couchbase.lite.support.LazyJsonObject;
25 |
26 | import java.io.IOException;
27 | import java.net.MalformedURLException;
28 | import java.net.URL;
29 | import java.util.ArrayList;
30 | import java.util.HashMap;
31 | import java.util.Iterator;
32 | import java.util.LinkedHashMap;
33 | import java.util.List;
34 | import java.util.Map;
35 |
36 | public class MainActivity extends AppCompatActivity {
37 |
38 | private static final String PLACES_VIEW = "getPlaces";
39 |
40 | private Database database;
41 | private List currentRows;
42 | private PlacesAdapter adapter;
43 | private RecyclerView recyclerView;
44 |
45 | @Override
46 | protected void onCreate(Bundle savedInstanceState) {
47 | super.onCreate(savedInstanceState);
48 | setContentView(R.layout.activity_main);
49 |
50 | try {
51 | // replace with the IP to use
52 | URL url = new URL("http://192.168.1.218:4984/db");
53 |
54 | Manager manager = new Manager(new AndroidContext(getApplicationContext()), Manager.DEFAULT_OPTIONS);
55 |
56 | database = manager.getExistingDatabase("cityexplorer");
57 | if (database != null) {
58 | database.delete();
59 | }
60 | database = manager.getDatabase("cityexplorer");
61 | registerViews();
62 |
63 | Replication pull = database.createPullReplication(url);
64 | pull.setContinuous(true);
65 | pull.start();
66 | } catch (MalformedURLException e) {
67 | e.printStackTrace();
68 | } catch (CouchbaseLiteException e) {
69 | e.printStackTrace();
70 | } catch (IOException e) {
71 | e.printStackTrace();
72 | }
73 |
74 | recyclerView = (RecyclerView) findViewById(R.id.list);
75 | recyclerView.setLayoutManager(new LinearLayoutManager(this));
76 |
77 | adapter = new PlacesAdapter(this, new ArrayList(), database);
78 | recyclerView.setAdapter(adapter);
79 |
80 | final Query queryPlaces = database.getView(PLACES_VIEW).createQuery();
81 | database.addChangeListener(new Database.ChangeListener() {
82 | @Override
83 | public void changed(Database.ChangeEvent event) {
84 | if (event.isExternal()) {
85 | QueryEnumerator rows = null;
86 | try {
87 | rows = queryPlaces.run();
88 | } catch (CouchbaseLiteException e) {
89 | e.printStackTrace();
90 | }
91 | List places = new ArrayList<>();
92 | for (Iterator it = rows; it.hasNext(); ) {
93 | QueryRow row = it.next();
94 |
95 | Log.d("", row.getValue().toString());
96 | Map properties = database.getDocument(row.getDocumentId()).getProperties();
97 | places.add(new Place((LazyJsonObject) row.getValue()));
98 | }
99 |
100 | adapter.dataSet = places;
101 |
102 | runOnUiThread(new Runnable() {
103 | @Override
104 | public void run() {
105 | recyclerView.getAdapter().notifyDataSetChanged();
106 | }
107 | });
108 | }
109 | }
110 | });
111 | }
112 |
113 | public void deletePlace(android.view.View view) {
114 | Log.d("", "delete me");
115 | adapter.dataSet.remove(2);
116 | try {
117 | database.getExistingDocument(adapter.dataSet.get(2).getId()).delete();
118 | } catch (CouchbaseLiteException e) {
119 | e.printStackTrace();
120 | }
121 | adapter.notifyItemRemoved(2);
122 | }
123 |
124 | private void registerViews() {
125 | View placesView = database.getView(PLACES_VIEW);
126 | placesView.setMap(new Mapper() {
127 | @Override
128 | public void map(Map document, Emitter emitter) {
129 | emitter.emit(document.get("_id"), document);
130 | }
131 | }, "1");
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/PlacesAdapter.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.cityexplorer;
2 |
3 | import android.content.Context;
4 | import android.graphics.drawable.Drawable;
5 | import android.support.v7.widget.RecyclerView;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.ImageView;
10 | import android.widget.LinearLayout;
11 | import android.widget.TextView;
12 |
13 | import com.couchbase.cityexplorer.model.Place;
14 | import com.couchbase.lite.Attachment;
15 | import com.couchbase.lite.CouchbaseLiteException;
16 | import com.couchbase.lite.Database;
17 | import com.couchbase.lite.Document;
18 |
19 | import java.io.InputStream;
20 | import java.util.List;
21 |
22 | public class PlacesAdapter extends RecyclerView.Adapter {
23 |
24 | Context context;
25 | List dataSet;
26 | Database database;
27 |
28 | public PlacesAdapter(Context context, List dataSet, Database database) {
29 | this.context = context;
30 | this.dataSet = dataSet;
31 | this.database = database;
32 | }
33 |
34 | @Override
35 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
36 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_places, parent, false);
37 | return new ViewHolder(view);
38 | }
39 |
40 | @Override
41 | public void onBindViewHolder(ViewHolder holder, int position) {
42 | Place place = dataSet.get(position);
43 |
44 | holder.restaurantName.setText(place.getName());
45 | holder.restaurantText.setText(place.getAddress());
46 |
47 | Document document = database.getDocument(place.getId());
48 | Attachment attachment = document.getCurrentRevision().getAttachment("photo");
49 | if (attachment != null) {
50 | InputStream is = null;
51 | try {
52 | is = attachment.getContent();
53 | } catch (CouchbaseLiteException e) {
54 | e.printStackTrace();
55 | }
56 | Drawable drawable = Drawable.createFromStream(is, "photo");
57 | holder.restaurantImage.setImageDrawable(drawable);
58 | }
59 | }
60 |
61 | @Override
62 | public int getItemCount() {
63 | return dataSet.size();
64 | }
65 |
66 | public class ViewHolder extends RecyclerView.ViewHolder{
67 | public TextView restaurantName;
68 | public TextView restaurantText;
69 | public ImageView restaurantImage;
70 |
71 | public ViewHolder(View itemView) {
72 | super(itemView);
73 | restaurantName = (TextView) itemView.findViewById(R.id.restaurantName);
74 | restaurantText = (TextView) itemView.findViewById(R.id.restaurantText);
75 | restaurantImage = (ImageView) itemView.findViewById(R.id.restaurantImage);
76 | }
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/model/Place.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.cityexplorer.model;
2 |
3 | import com.couchbase.lite.support.LazyJsonObject;
4 |
5 | public class Place {
6 | private LazyJsonObject mLazy;
7 |
8 | public Place(LazyJsonObject lazyJsonObject) {
9 | mLazy = lazyJsonObject;
10 | }
11 |
12 | public String getName() {
13 | return (String) mLazy.get("name");
14 | }
15 |
16 | public String getId() {
17 | return (String) mLazy.get("_id");
18 | }
19 |
20 | public String getAddress() {
21 | return (String) mLazy.get("formatted_address");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
15 |
16 |
24 |
25 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/layout/row_places.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
26 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CityExplorer
3 | MainActivity
4 |
5 | Hello world!
6 | Settings
7 |
8 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:1.2.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 23 14:35:58 BST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
7 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/05-android-recycler-view-animations/CityExplorer/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/06-channels-users-roles/README.md:
--------------------------------------------------------------------------------
1 | # Couchbase by Example: Channel, Users, Roles
2 |
3 | In `04-ios-sync-progress-indicator`, you learnt how to use RxJS and the request module to import documents into a Sync Gateway database from the Google Places API. To keep it simple, you enabled the GUEST user with access to all channels. In this tutorial, you will configure the Sync Function to allow authenticated users to post reviews.
4 |
5 | The Sync Function validates document contents, and authorizes write access to documents by channel, user, and role.
6 |
7 | In total, there will be 3 types of roles in the application:
8 |
9 | - **level-1**: users with the **level-1** role can post reviews but they must be accepted by users with the **level-3** role (i.e. moderators) to be public.
10 | - **level-2**: users can post reviews without validation needed from moderators. This means they can post a comment without requiring an approval.
11 | - **level-3**: users can approve reviews or reject them.
12 |
13 | ## Download Sync Gateway
14 |
15 | Download Sync Gateway and unzip the file:
16 |
17 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile
18 |
19 | You will find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `users-role.json` file to the root of your project:
20 |
21 | ```bash
22 | cp ~/Downloads/couchbase-sync-gateway/examples/users-roles.json /path/to/proj/sync-gateway-config.json
23 | ```
24 |
25 | In the next section, you will update the configuration file to create users and roles.
26 |
27 | ## Channels, Users and Roles
28 |
29 | In `sync-gateway-config.json`, update the db object to read:
30 |
31 | ```javascript
32 | {
33 | "log": ["*"],
34 | "databases": {
35 | "db": {
36 | "server": "walrus:",
37 | "users": {
38 | "jens": {
39 | "admin_roles": ["level-1"],
40 | "password": "letmein"
41 | },
42 | "andy": {
43 | "admin_roles": ["level-2"],
44 | "password": "letmein"
45 | },
46 | "william": {
47 | "admin_roles": [],
48 | "password": "letmein"
49 | },
50 | "traun": {
51 | "admin_roles": ["level-3"],
52 | "password": "letmein"
53 | }
54 | },
55 | "roles": {
56 | "level-1": {},
57 | "level-2": {},
58 | "level-3": {}
59 | },
60 | }
61 | }
62 | }
63 | ```
64 |
65 | A couple of things are happening above:
66 |
67 | 1. You create the user `jens` with the `level-1` role.
68 | 2. You create the user `andy` with the `level-2` role.
69 | 3. You create the user `william` without any role.
70 | 4. You create the user `traun` with the `level-3` role.
71 | 5. You define the 3 roles. Just like users, roles must be explicitly created on the Admin REST API or in the config file.
72 |
73 | **Note on creating roles**
74 |
75 | The easiest way to create roles is in the configuration file as you did above.
76 |
77 | Another way to create roles is through the admin REST API. Provided that you expose an endpoint to create those roles from the application, you can create roles dynamically by sending a request to your app server (blue arrows) which will create the role and send back a 201 Created if it was successful (green arrows).
78 |
79 | 
80 |
81 | In the next section, you will add the Sync Function to handle write and read operations for the three different types of documents (`restaurant`, `review`, `profile`).
82 |
83 | ## Sync Function
84 |
85 | Roles and users can both be granted access to channels. Users can be granted roles, and inherit any channel access for those roles.
86 |
87 | Channel access determines a user’s read security. Write security can also be based on channels (using requireAccess), but can also be based on user/role (requireUser and requireRole), or document content (using throw).
88 |
89 | Read and write access to documents are independent. In fact write access is entirely governed by your sync function: unless the sync function rejects the revision, a client can modify any document. All the require* functions act as validators but also write access APIs.
90 |
91 | It's very common to see sync function creating lots and lots of channels. This is absolutely fine. However, it can get cumbersome to assign each user in turn to a channel. Instead you can use a role!
92 |
93 | Let this sink in one more time, users can be granted roles and inherit any channel access for those roles.
94 |
95 | This means you can grant a user access to multiple channels by simply assigning a role. This is very powerful and can greatly simplify the data model in your application.
96 |
97 | With roles, you don't need to assign every single user to a channel. You simply grant the role access to the channel and assign the users to the role.
98 |
99 | With that in mind, replace the sync function in `sync-gateway-config.json`:
100 |
101 | ```javascript
102 | function(doc, oldDoc) {
103 | if (doc.type == "restaurant"){
104 | channel(doc.restaurant_id);
105 | } else if (doc.type == "review") {
106 | switch(doc.role) {
107 | case "level-1": // Step 1
108 | requireRole(doc.role);
109 | var channelname = doc.owner + "-in-review";
110 | channel(channelname);
111 | access(doc.owner, channelname);
112 | access("role:level-3", channelname);
113 | break;
114 | case "level-2": // Step 2
115 | requireRole(doc.role);
116 | channel(doc.restaurant_id);
117 | break;
118 | case "level-3": // Step 3
119 | requireRole(doc.role);
120 | channel(doc.restaurant_id);
121 | break;
122 | }
123 | } else if (doc.type == "profile") {
124 | requireRole("level-3");
125 | role(doc.name, "role:" + doc.role);
126 | }
127 | }
128 | ```
129 |
130 | Here's what's happening:
131 |
132 | 1. Users with the **level-1** role have write access because you call the `channel` function. Then grant that user and the **level-3** access to this channel. This is where the power of roles really shines. By granting a role access, you are granting all the users with that role access to the channel. The call to `requireRole` will grant the write permission.
133 | 2. Documents of type `review` created by a **level-2** role: the document should go in the same channel as the restaurant it belongs to. The call to `requireRole` will grant the write permission.
134 | 3. Documents of type `review` created by a **level-3** role: the document should go in the channel for that restaurant. **level-3** users also have read access to all the `{user_name}-in-review` channels. They can approve/reject the pending reviews of other users.
135 |
136 | Start Sync Gateway with the updated configuration file:
137 |
138 | ```bash
139 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway /path/to/proj/sync-gateway-config.json
140 | ```
141 |
142 | In this example, you are utilising the 3 main features of roles:
143 |
144 | - Granting a role access to a channel and indirectly to all the users with that role.
145 | - Granting write permission using a requireRole.
146 | - Assigning a role to a user.
147 |
148 | Now you can test the Sync Function behaves as expected with the following HTTP requests.
149 |
150 | **Scenario 1**
151 |
152 | Documents of type `review` created by a **level-1** user: the document should go in the `{user_name}-in-review` channel and the users with the **level-3** role should have access to this channel too.
153 |
154 | Login as the user `jens`:
155 |
156 | ```bash
157 | curl -vX POST -H 'Content-Type: application/json' \
158 | :4984/db/_session \
159 | -d '{"name": "jens", "password": "letmein"}'
160 |
161 | > POST /db/_session HTTP/1.1
162 | > User-Agent: curl/7.37.1
163 | > Host: :4984
164 | > Accept: */*
165 | > Content-Type: application/json
166 | > Content-Length: 39
167 | >
168 | * upload completely sent off: 39 out of 39 bytes
169 | < HTTP/1.1 200 OK
170 | < Content-Length: 103
171 | < Content-Type: application/json
172 | * Server Couchbase Sync Gateway/1.1.0 is not blacklisted
173 | < Server: Couchbase Sync Gateway/1.1.0
174 | < Set-Cookie: SyncGatewaySession=6c52b8cd2c706d55e97d9606058c0abd90a5d200; Path=/db/; Expires=Tue, 07 Jul 2015 08:23:03 UTC
175 | < Date: Mon, 06 Jul 2015 08:23:03 GMT
176 | <
177 | * Connection #0 to host left intact
178 | {"authentication_handlers":["default","cookie"],"ok":true,"userCtx":{"channels":{"!":1},"name":"jens"}}⏎
179 | ```
180 |
181 | Save a new document of type `review` (substitute the token with the one returned in the `Set-Cookie` header above):
182 |
183 | ```bash
184 | curl -vX POST -H 'Content-Type: application/json' \
185 | --cookie 'SyncGatewaySession=d007ceb561f0111512c128040c32c02ea9d90234' \
186 | :4984/db/ \
187 | -d '{"type": "review", "role": "level-1", "owner": "jens"}'
188 | ```
189 |
190 | - Check that user `jens` has access to the channel `jens-in-review` and the comment document is in there.
191 |
192 | 
193 |
194 | - Check that user `traun` has access to channel `jens-in-review`.
195 |
196 | 
197 |
198 | You can also view the channels this document belongs to and roles/users that have access to it in the `Documents` tab:
199 |
200 | 
201 |
202 | **Scenario 2**
203 |
204 | Granting write access using a role.
205 |
206 | Login as `andy` and replace the token with the one you got back from the login request.
207 |
208 | ```bash
209 | curl -vX POST -H 'Content-Type: application/json' \
210 | --cookie 'SyncGatewaySession=6e7ce145ae53c83de436b47ae37d8d94beebebea' \
211 | :4984/db/ \
212 | -d '{"type": "review", "role": "level-2", "owner": "andy", "restaurant_id": "123"}'
213 | ```
214 |
215 | - Check that the comment was added to the restaurant channel (named `123` in this example).
216 |
217 | 
218 |
219 | **Scenario 3**
220 |
221 | Assigning a role to a user.
222 |
223 | Login as `traun` and replace the token with the one you got back from the login request.
224 |
225 | ```bash
226 | curl -vX POST -H 'Content-Type: application/json' \
227 | --cookie 'SyncGatewaySession=3a5c5a67ff67643f8ade175363c65354584429e9' \
228 | :4984/db/ \
229 | -d '{"type": "profile", "name": "william", "role": "level-3"}'
230 | ```
231 |
232 | - Check that `william` has role `level-3`.
233 | - Check that `william` has access to the `jens-in-review` channel.
234 |
235 | 
236 |
237 | ## Conclusion
238 |
239 | In this tutorial, you learnt how to use channels and requireRole to dynamically validate and perform write operations. You also assigned multiple channels at once to multiple users using the role API.
--------------------------------------------------------------------------------
/06-channels-users-roles/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["*"],
3 | "databases": {
4 | "db": {
5 | "server": "walrus:",
6 | "users": {
7 | "jens": {
8 | "admin_roles": ["level-1"],
9 | "password": "letmein"
10 | },
11 | "andy": {
12 | "admin_roles": ["level-2"],
13 | "password": "letmein"
14 | },
15 | "william": {
16 | "admin_roles": [],
17 | "password": "letmein"
18 | },
19 | "traun": {
20 | "admin_roles": ["level-3"],
21 | "password": "letmein"
22 | }
23 | },
24 | "roles": {
25 | "level-1": {},
26 | "level-2": {},
27 | "level-3": {}
28 | },
29 | "sync": `
30 | function(doc, oldDoc) {
31 | if (doc.type == "restaurant"){
32 | channel(doc.restaurant_id);
33 | } else if (doc.type == "review") {
34 | switch(doc.role) {
35 | case "level-1": // Step 1
36 | requireRole(doc.role);
37 | var channelname = doc.owner + "-in-review";
38 | channel(channelname);
39 |
40 | access(doc.owner, channelname);
41 | access("role:level-3", channelname);
42 |
43 | break;
44 | case "level-2": // Step 2
45 | requireRole(doc.role);
46 | channel(doc.restaurant_id);
47 |
48 | break;
49 | case "level-3": // Step 3
50 | requireRole(doc.role);
51 | channel(doc.restaurant_id);
52 |
53 | break;
54 | }
55 | } else if (doc.type == "profile") {
56 | requireRole("level-3");
57 | role(doc.name, "role:" + doc.role);
58 | }
59 | }
60 | `
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/07-deploy-digital-ocean/Dockerfile:
--------------------------------------------------------------------------------
1 | # Set the base image to Ubuntu
2 | FROM ubuntu
3 |
4 | # Install Node.js and other dependencies
5 | RUN apt-get update && \
6 | apt-get -y install curl && \
7 | curl -sL https://deb.nodesource.com/setup | sudo bash - && \
8 | apt-get -y install python build-essential nodejs
9 |
10 | # Install nodemon
11 | RUN npm install -g babel
12 |
13 | # Provides cached layer for node_modules
14 | ADD package.json /tmp/package.json
15 | RUN cd /tmp && npm install
16 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/
17 |
18 | # Define working directory
19 | WORKDIR /src
20 | ADD . /src
--------------------------------------------------------------------------------
/07-deploy-digital-ocean/README.md:
--------------------------------------------------------------------------------
1 | ## Couchbase by Example: Deploy Digital Ocean
2 |
3 | So far, you have been running Sync Gateway and perhaps Couchbase Server locally to follow the tutorials. Now it's time to learn how to easily deploy both applications and perhaps an App Server sitting alongside Sync Gateway to a PaaS.
4 |
5 | You will use the Docker images for Sync Gateway and [Couchbase Server](https://registry.hub.docker.com/u/couchbase/server/) available in the Docker registry.
6 |
7 | Then, you will create a `Dockerfile` for a NodeJS app that imports data from the Google Places API to Sync Gateway. The NodeJS app is taken from the `04-ios-sync-progress-indicator` tutorial.
8 |
9 | ## Why Docker?
10 |
11 | If you've ever done any application development and deployment then you know how difficult it can be to ensure that your development and production servers are the same or at least similar enough that it is not causing any major issue.
12 |
13 | With docker, you can build a container that houses everything you need to configure your application dependencies and services.
14 |
15 | This container can then be shared and run on any server or computer without having to do a whole bunch of setup and configuration.
16 |
17 | Create a new Digital Ocean Droplet
18 |
19 | > https://cloud.digitalocean.com/droplets/new
20 |
21 | Pick 2GB for the RAM:
22 |
23 | 
24 |
25 | Choose Ubuntu for the distribution and on the Applications tab, select Docker. Create the Droplet and connect to it via SSH.
26 |
27 | ## Installing Sync Gateway and Couchbase Server
28 |
29 | In the command line, install Couchbase Server with the following:
30 |
31 | ```
32 | docker run -d --name db -p 8091-8094:8091-8094 -p 11210:11210 couchbase
33 | ```
34 |
35 | Open the getting started wizard in Chrome on port `http://:8091`:
36 |
37 | 
38 |
39 | After completing the wizard, navigate to the `Data Buckets` tab to see the created bucket (you will configure Sync Gateway to connect to the `default` bucket):
40 |
41 | 
42 |
43 | Copy the necessary files from the `04-ios-sync-progress` tutorial to this project:
44 |
45 | ```
46 | git clone git@github.com:couchbaselabs/Couchbase-by-Example.git
47 | cd couchbase-by-example/04-ios-sync-progress-indicator
48 | cp requestRx.js sync-gateway-config.json sync.js package.json ./../07-deploy-digital-ocean/
49 | ```
50 |
51 | Create a new copy of the config file to set it up with Couchbase Server:
52 |
53 | ```
54 | cd 07-deploy-digital-ocean/
55 | cp sync-gateway-config.json production-sync-gateway-config.json
56 | ```
57 |
58 | Update `production-sync-gateway-config.json` with the server IP and bucket name:
59 |
60 | ```javascript
61 | {
62 | "log": ["*"],
63 | "databases": {
64 | "db": {
65 | "server": "http://46.101.14.135:8091/",
66 | "bucket": "default",
67 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } }
68 | }
69 | }
70 | }
71 | ```
72 |
73 | Push the files to a github repository.
74 |
75 | Specify the url to the production config file in the docker command to run the Sync Gateway container:
76 |
77 | ```bash
78 | $ docker run -d -p 4984:4984 -p 4985:4985 couchbase/sync-gateway http://git.io/vq25r
79 | ```
80 |
81 | **NOTE**: You ran `docker run` but this time specified the `-d` flag. It tells Docker to run the container and put it in the background, to daemonize it.
82 |
83 | Run the `docker ps` command to check that both containers are running:
84 |
85 | ```
86 | root@MyApp:~# docker ps
87 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
88 | ca7d4358941a couchbase/sync-gateway "/usr/local/bin/sync 2 minutes ago Up 2 minutes 0.0.0.0:4984-4985->4984-4985/tcp focused_bell
89 | c9411d002831 couchbase/server "couchbase-start cou 52 minutes ago Up 52 minutes 8092/tcp, 11207/tcp, 11210-11211/tcp, 0.0.0.0:8091->8091/tcp, 18091-18092/tcp grave_feynman
90 | ```
91 |
92 | Use the `docker logs` command specifying the container id to print the STDOUT to your console.
93 |
94 | **TIP**: Use the `-f` flag to follow the logs.
95 |
96 | In the next section, you will write a simple Dockerfile to deploy the NodeJS application to the same Droplet.
97 |
98 | ## Deploying an App Server
99 |
100 | Open a new file named `Dockerfile` and paste the following:
101 |
102 | ```
103 | # Set the base image to Ubuntu
104 | FROM ubuntu
105 |
106 | # Install Node.js and other dependencies
107 | RUN apt-get update && \
108 | apt-get -y install curl && \
109 | curl -sL https://deb.nodesource.com/setup | sudo bash - && \
110 | apt-get -y install python build-essential nodejs
111 |
112 | # Install nodemon
113 | RUN npm install -g babel
114 |
115 | # Provides cached layer for node_modules
116 | ADD package.json /tmp/package.json
117 | RUN cd /tmp && npm install
118 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/
119 |
120 | # Define working directory
121 | WORKDIR /src
122 | ADD . /src
123 | ```
124 |
125 | - Ubuntu base image pulled from Docker Hub
126 | - Install Node.js and dependencies using apt-get
127 | - Install babel to run `sync.js` because it's written in ES6.
128 | - Run npm install in a temporary directory and copy to src (for caching node_modules)
129 | - Copy the application source from the host directory to src within the container
130 |
131 | Build a Docker image using the Dockerfile:
132 |
133 | ```
134 | docker build -t myapp .
135 | ```
136 |
137 | Create a container for the custom image:
138 | ```
139 | docker run -it myapp /bin/bash
140 | ```
141 |
142 | **NOTE**: The `-i` flag is used to keep STDIN open and `-t` to allocate a pseudo-TTY.
143 |
144 | Notice the command line prompt in the container. From there you can run the import script:
145 |
146 | ```
147 | babel-node sync.js
148 | ```
149 |
150 | Use `Ctrl + D` to exit the container.
151 |
152 | You can push the Dockerfile to the GitHub repo and pull the changes in the droplet. Then run the same commands to build and run the image.
153 |
154 | ## Conclusion
155 |
156 | Yay! Now you know how to use the docker images to deploy Sync Gateway and Couchbase Server and creating your own images for other applications running alongside them.
157 |
158 | ## Extra: Accessing the Admin Dashboard
159 |
160 | The Admin Dashboard is available on `http://localhost:4985/_admin/` and only accessible on the internal network where Sync Gateway is running.
161 |
162 | However, you can use SSH tunnelling to create a connection with the droplet. This currently doesn't work with Sync Gateway instances running in Docker containers.
163 |
164 | If you already have a Docker instance running Sync Gateway, you can stop it with:
165 |
166 | ```
167 | docker stop
168 | ```
169 |
170 | Install Sync Gateway with wget:
171 |
172 | ```
173 | wget http://packages.couchbase.com/releases/couchbase-sync-gateway/1.0.0/couchbase-sync-gateway-community_1.0.0_x86_64.deb
174 | sudo dpkg -i couchbase-sync-gateway-community_1.0.0_x86_64.deb
175 | ```
176 |
177 | By default it will be installed to **/opt/couchbase-sync-gateway/bin/sync_gateway**.
178 |
179 | Start it with the command:
180 |
181 | ```
182 | $ /opt/couchbase-sync-gateway/bin/sync_gateway
183 | ```
184 |
185 | ### Creating an SSH tunnel
186 |
187 | Open a terminal on your host machine and enter the following command where is the public ip address of the droplet:
188 |
189 | ```
190 | ssh -ND 8080 root@
191 | ```
192 |
193 | **Explanation**
194 |
195 | - N: hides the output from the SSH connection. It is optional. If you wish to use the SSH connection to run commands on the server as you normally would, remove the N switch.
196 | - D: 8080 creates a dynamic port, in this case 8080, on your local computer. This is how your browser, or other software, will connect to the tunnel.
197 |
198 | ### Using as a browser proxy
199 |
200 | 1. Open `System Preferences > Network > Advanced > Proxies`
201 | 2. Check the checkbox next to SOCKS Proxy
202 | 3. Under SOCKS Proxy Server, enter `localhost` and `8080` as the port. Leave all the other fields blank.
203 | 
204 | 4. Click OK and Apply.
205 |
206 | Open `http://localhost:4985/_admin` in Chrome and you should see the Admin Dashboard.
207 |
208 | 
209 |
210 | **Reference**
211 |
212 | > https://whatbox.ca/wiki/SSH_Tunneling
213 |
--------------------------------------------------------------------------------
/08-signup-and-login/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | /.idea/libraries
5 | .DS_Store
6 | /build
7 | /captures
8 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/SmartHome.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | generateDebugAndroidTestSources
19 | generateDebugSources
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 22
5 | buildToolsVersion "22.0.1"
6 |
7 | defaultConfig {
8 | applicationId "com.couchbase.smarthome"
9 | minSdkVersion 7
10 | targetSdkVersion 22
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(dir: 'libs', include: ['*.jar'])
24 |
25 | compile 'com.android.support:design:22.2.1'
26 | compile 'com.squareup.okhttp:okhttp:2.3.0'
27 | }
28 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/jamesnocentini/Developer/adt-bundle/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/androidTest/java/com/couchbase/smarthome/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.smarthome;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/Login.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.smarthome;
2 |
3 | import android.app.Activity;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.os.Bundle;
6 | import android.view.Menu;
7 | import android.view.MenuItem;
8 | import android.view.View;
9 | import android.widget.EditText;
10 | import android.widget.Toast;
11 |
12 | import com.squareup.okhttp.Call;
13 | import com.squareup.okhttp.Callback;
14 | import com.squareup.okhttp.MediaType;
15 | import com.squareup.okhttp.OkHttpClient;
16 | import com.squareup.okhttp.Request;
17 | import com.squareup.okhttp.RequestBody;
18 | import com.squareup.okhttp.Response;
19 |
20 | import java.io.IOException;
21 |
22 | public class Login extends Activity {
23 |
24 | NetworkHelper networkHelper = new NetworkHelper();
25 |
26 | EditText nameInput;
27 | EditText passwordInput;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.activity_login);
33 |
34 | nameInput = (EditText) findViewById(R.id.nameInput);
35 | passwordInput = (EditText) findViewById(R.id.passwordInput);
36 | }
37 |
38 | @Override
39 | public boolean onCreateOptionsMenu(Menu menu) {
40 | // Inflate the menu; this adds items to the action bar if it is present.
41 | getMenuInflater().inflate(R.menu.menu_login, menu);
42 | return true;
43 | }
44 |
45 | @Override
46 | public boolean onOptionsItemSelected(MenuItem item) {
47 | // Handle action bar item clicks here. The action bar will
48 | // automatically handle clicks on the Home/Up button, so long
49 | // as you specify a parent activity in AndroidManifest.xml.
50 | int id = item.getItemId();
51 |
52 | //noinspection SimplifiableIfStatement
53 | if (id == R.id.action_settings) {
54 | return true;
55 | }
56 |
57 | return super.onOptionsItemSelected(item);
58 | }
59 |
60 | public void login(View view) {
61 | String json = "{\"name\": \"" + nameInput.getText() + "\", \"password\":\"" + passwordInput.getText() + "\"}";
62 | networkHelper.post("http://10.0.3.2:8000/smarthome/_session", json, new Callback() {
63 | @Override
64 | public void onFailure(Request request, IOException e) {
65 |
66 | }
67 |
68 | @Override
69 | public void onResponse(Response response) throws IOException {
70 | String responseStr = response.body().string();
71 | final String messageText = "Status code : " + response.code() +
72 | "\n" +
73 | "Response body : " + responseStr;
74 | runOnUiThread(new Runnable() {
75 | @Override
76 | public void run() {
77 | Toast.makeText(getApplicationContext(), messageText, Toast.LENGTH_LONG).show();
78 | }
79 | });
80 | }
81 | });
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/SignUp.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.smarthome;
2 |
3 | import android.app.Activity;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.os.Bundle;
6 | import android.view.Menu;
7 | import android.view.MenuItem;
8 | import android.view.View;
9 | import android.widget.EditText;
10 | import android.widget.Toast;
11 |
12 | import com.squareup.okhttp.Call;
13 | import com.squareup.okhttp.Callback;
14 | import com.squareup.okhttp.MediaType;
15 | import com.squareup.okhttp.OkHttpClient;
16 | import com.squareup.okhttp.Request;
17 | import com.squareup.okhttp.RequestBody;
18 | import com.squareup.okhttp.Response;
19 |
20 | import java.io.IOException;
21 |
22 | public class SignUp extends Activity {
23 |
24 | NetworkHelper networkHelper = new NetworkHelper();
25 |
26 | EditText nameInput;
27 | EditText passwordInput;
28 | EditText confirmPasswordInput;
29 |
30 | @Override
31 | protected void onCreate(Bundle savedInstanceState) {
32 | super.onCreate(savedInstanceState);
33 | setContentView(R.layout.activity_sign_up);
34 |
35 | nameInput = (EditText) findViewById(R.id.nameInput);
36 | passwordInput = (EditText) findViewById(R.id.passwordInput);
37 | confirmPasswordInput = (EditText) findViewById(R.id.confirmPasswordInput);
38 | }
39 |
40 | @Override
41 | public boolean onCreateOptionsMenu(Menu menu) {
42 | // Inflate the menu; this adds items to the action bar if it is present.
43 | getMenuInflater().inflate(R.menu.menu_sign_up, menu);
44 | return true;
45 | }
46 |
47 | @Override
48 | public boolean onOptionsItemSelected(MenuItem item) {
49 | // Handle action bar item clicks here. The action bar will
50 | // automatically handle clicks on the Home/Up button, so long
51 | // as you specify a parent activity in AndroidManifest.xml.
52 | int id = item.getItemId();
53 |
54 | //noinspection SimplifiableIfStatement
55 | if (id == R.id.action_settings) {
56 | return true;
57 | }
58 |
59 | return super.onOptionsItemSelected(item);
60 | }
61 |
62 | public void signup(View view) {
63 | if (!passwordInput.getText().toString().equals(confirmPasswordInput.getText().toString())) {
64 | Toast.makeText(getApplicationContext(), "The passwords do not match", Toast.LENGTH_LONG).show();
65 | } else {
66 | String json = "{\"name\": \"" + nameInput.getText() + "\", \"password\":\"" + passwordInput.getText() + "\"}";
67 | networkHelper.post("http://10.0.3.2:8000/signup", json, new Callback() {
68 | @Override
69 | public void onFailure(Request request, IOException e) {
70 |
71 | }
72 |
73 | @Override
74 | public void onResponse(Response response) throws IOException {
75 | String responseStr = response.body().string();
76 | final String messageText = "Status code : " + response.code() +
77 | "\n" +
78 | "Response body : " + responseStr;
79 | runOnUiThread(new Runnable() {
80 | @Override
81 | public void run() {
82 | Toast.makeText(getApplicationContext(), messageText, Toast.LENGTH_LONG).show();
83 | }
84 | });
85 | }
86 | });
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/WelcomeActivity.java:
--------------------------------------------------------------------------------
1 | package com.couchbase.smarthome;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.view.Menu;
7 | import android.view.MenuItem;
8 | import android.view.View;
9 |
10 | public class WelcomeActivity extends Activity {
11 |
12 | @Override
13 | protected void onCreate(Bundle savedInstanceState) {
14 | super.onCreate(savedInstanceState);
15 | setContentView(R.layout.activity_welcome);
16 | }
17 |
18 | @Override
19 | public boolean onCreateOptionsMenu(Menu menu) {
20 | // Inflate the menu; this adds items to the action bar if it is present.
21 | getMenuInflater().inflate(R.menu.menu_welcome, menu);
22 | return true;
23 | }
24 |
25 | @Override
26 | public boolean onOptionsItemSelected(MenuItem item) {
27 | // Handle action bar item clicks here. The action bar will
28 | // automatically handle clicks on the Home/Up button, so long
29 | // as you specify a parent activity in AndroidManifest.xml.
30 | int id = item.getItemId();
31 |
32 | //noinspection SimplifiableIfStatement
33 | if (id == R.id.action_settings) {
34 | return true;
35 | }
36 |
37 | return super.onOptionsItemSelected(item);
38 | }
39 |
40 | public void openLoginActivity(View view) {
41 | Intent intent = new Intent(this, Login.class);
42 | startActivity(intent);
43 | }
44 |
45 | public void openSignUpActivity(View view) {
46 | Intent intent = new Intent(this, SignUp.class);
47 | startActivity(intent);
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 | x
12 | x
13 | x
14 | x
15 | x
16 |
17 | x
18 | x
19 | x
20 | x
21 | x
22 |
23 | x
24 | x
25 | x
26 |
27 | x
28 | x
29 | x
30 | x
31 | x
32 | x
33 |
34 | x
35 | x
36 | x
37 |
38 | x
39 | x
40 | x
41 | x
42 | x
43 | x
44 |
45 | x
46 | x
47 | x
48 | x
49 | x
50 | x
51 | x
52 |
53 | x
54 |
55 |
56 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/layout/activity_sign_up.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
16 |
17 |
22 |
23 |
26 |
27 |
32 |
33 |
34 |
37 |
38 |
43 |
44 |
45 |
48 |
49 |
54 |
55 |
56 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/layout/activity_welcome.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
16 |
17 |
24 |
25 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/menu/menu_login.xml:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/menu/menu_sign_up.xml:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/menu/menu_welcome.xml:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/08-signup-and-login/SmartHome/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/08-signup-and-login/SmartHome/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/08-signup-and-login/SmartHome/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/08-signup-and-login/SmartHome/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SmartHome
3 |
4 | Hello world!
5 | Settings
6 | SignUp
7 | Login
8 | MainActivity
9 |
10 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:1.2.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/08-signup-and-login/SmartHome/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jul 29 15:27:24 CEST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
7 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/08-signup-and-login/SmartHome/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/08-signup-and-login/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 | , bodyParser = require('body-parser')
3 | , request = require('request').defaults({json: true})
4 | , httpProxy = require('http-proxy');
5 |
6 | // 1
7 | var app = express();
8 | app.use('/signup', bodyParser.json());
9 |
10 | // 2
11 | app.post('/signup', function (req, res) {
12 | console.log('its signup time');
13 |
14 | var json = req.body;
15 | var options = {
16 | url: 'http://0.0.0.0:4985/smarthome/_user/',
17 | method: 'POST',
18 | body: json
19 | };
20 |
21 | request(options, function(error, response) {
22 | res.writeHead(response.statusCode);
23 | res.end();
24 | });
25 | });
26 |
27 | // 3
28 | app.all('*', function(req, res) {
29 | var url = 'http://0.0.0.0:4984' + req.url;
30 | req.pipe(request(url)).pipe(res);
31 | });
32 |
33 | // 4
34 | var server = app.listen(8000, function () {
35 | var host = server.address().address;
36 | var port = server.address().port;
37 |
38 | console.log('App listening at http://%s:%s', host, port);
39 | });
--------------------------------------------------------------------------------
/08-signup-and-login/sync-gateway-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": ["*"],
3 | "databases": {
4 | "db": {
5 | "server": "walrus:",
6 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } }
7 | }
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ This repo is obsolete. It was developed using a version of Couchbase Lite that reached end of life years ago.
2 |
3 | # Couchbase by Example
4 |
5 | _Couchbase by Example_ is a hands-on introduction to Couchbase Server, Sync Gateway and Couchbase Lite. Each tutorial aims to cover the new APIs on a specific platform (iOS, Android, .NET, Web). In each folder, you will find the source code and a markdown file accompanying the sample application to build it from scratch or just walkthrough the important code snippets.
6 |
7 | # What will it include
8 |
9 | The series will cover topics such as:
10 |
11 | - New Couchbase Lite & Sync Gateway features
12 | - Handling Push Notifications
13 | - PouchDB compatibility with Sync Gateway
14 | - P2P applications
15 | - Couchbase Lite & Share Extensions on iOS
16 | - Couchbase Lite & Android Lollipop APIs
17 | - more...
18 |
19 | # Your suggestions
20 |
21 | Let us know what you would like to read about. Create a [new issue][1] and start the discussion! If we don't have plans for it and reckon it's cool for the community we will definitely have a crack at it.
22 |
23 | # Sharing and contributing
24 |
25 | Share the posts you found the most helpful for learning new ways of doing things. Tweet with the hashtag [CouchbaseByExample](https://twitter.com/search?q=%23CouchbaseByExample&src=typd)
26 |
27 | [1]: https://github.com/couchbaselabs/Couchbase-by-Example/issues/new
28 |
--------------------------------------------------------------------------------