@saltcorn/mobile-app 1.1.1-beta.1 → 1.1.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "1.1.1-beta.1",
4
+ "version": "1.1.1-beta.2",
5
5
  "description": "Saltcorn mobile app for Android and iOS",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -0,0 +1,81 @@
1
+ import UIKit
2
+ import Capacitor
3
+ import SendIntent
4
+
5
+
6
+ @UIApplicationMain
7
+ class AppDelegate: UIResponder, UIApplicationDelegate {
8
+
9
+ var window: UIWindow?
10
+ let store = ShareStore.store
11
+
12
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13
+ // Override point for customization after application launch.
14
+ return true
15
+ }
16
+
17
+ func applicationWillResignActive(_ application: UIApplication) {
18
+ // 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.
19
+ // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
20
+ }
21
+
22
+ func applicationDidEnterBackground(_ application: UIApplication) {
23
+ // 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.
24
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
25
+ }
26
+
27
+ func applicationWillEnterForeground(_ application: UIApplication) {
28
+ // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
29
+ }
30
+
31
+ func applicationDidBecomeActive(_ application: UIApplication) {
32
+ // 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.
33
+ }
34
+
35
+ func applicationWillTerminate(_ application: UIApplication) {
36
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
37
+ }
38
+
39
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
40
+ var success = true
41
+ if CAPBridge.handleOpenUrl(url, options) {
42
+ success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
43
+ }
44
+
45
+ guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
46
+ let params = components.queryItems else {
47
+ return false
48
+ }
49
+ let titles = params.filter { $0.name == "title" }
50
+ let descriptions = params.filter { $0.name == "description" }
51
+ let types = params.filter { $0.name == "type" }
52
+ let urls = params.filter { $0.name == "url" }
53
+
54
+ store.shareItems.removeAll()
55
+
56
+ if(titles.count > 0){
57
+ for index in 0...titles.count-1 {
58
+ var shareItem: JSObject = JSObject()
59
+ shareItem["title"] = titles[index].value!
60
+ shareItem["description"] = descriptions[index].value!
61
+ shareItem["type"] = types[index].value!
62
+ shareItem["url"] = urls[index].value!
63
+ store.shareItems.append(shareItem)
64
+ }
65
+ }
66
+
67
+ store.processed = false
68
+ let nc = NotificationCenter.default
69
+ nc.post(name: Notification.Name("triggerSendIntent"), object: nil )
70
+
71
+ return success
72
+ }
73
+
74
+ func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
75
+ // Called when the app was launched with an activity, including Universal Links.
76
+ // Feel free to add additional processing here, but if you want the App API to support
77
+ // tracking app url opens, make sure to keep this call
78
+ return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
79
+ }
80
+
81
+ }
@@ -0,0 +1,33 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSExtension</key>
6
+ <dict>
7
+ <key>NSExtensionAttributes</key>
8
+ <dict>
9
+ <key>NSExtensionActivationRule</key>
10
+ <dict>
11
+ <key>NSExtensionActivationSupportsFileWithMaxCount</key>
12
+ <integer>5</integer>
13
+ <key>NSExtensionActivationSupportsImageWithMaxCount</key>
14
+ <integer>5</integer>
15
+ <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
16
+ <integer>5</integer>
17
+ <key>NSExtensionActivationSupportsText</key>
18
+ <true/>
19
+ <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
20
+ <integer>1</integer>
21
+ <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
22
+ <integer>1</integer>
23
+ <key>NSExtensionActivationUsesStrictMatching</key>
24
+ <false/>
25
+ </dict>
26
+ </dict>
27
+ <key>NSExtensionMainStoryboard</key>
28
+ <string>MainInterface</string>
29
+ <key>NSExtensionPointIdentifier</key>
30
+ <string>com.apple.share-services</string>
31
+ </dict>
32
+ </dict>
33
+ </plist>
@@ -0,0 +1,193 @@
1
+ //
2
+ // ShareViewController.swift
3
+ // mindlib
4
+ //
5
+ // Created by Carsten Klaffke on 05.07.20.
6
+ //
7
+
8
+ import MobileCoreServices
9
+ import Social
10
+ import UIKit
11
+
12
+ class ShareItem {
13
+
14
+ public var title: String?
15
+ public var type: String?
16
+ public var url: String?
17
+ }
18
+
19
+ class ShareViewController: UIViewController {
20
+
21
+ private var shareItems: [ShareItem] = []
22
+
23
+ override public func viewDidAppear(_ animated: Bool) {
24
+ super.viewDidAppear(animated)
25
+ self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
26
+ }
27
+
28
+ private func sendData() {
29
+ let queryItems = shareItems.map {
30
+ [
31
+ URLQueryItem(
32
+ name: "title",
33
+ value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
34
+ URLQueryItem(name: "description", value: ""),
35
+ URLQueryItem(
36
+ name: "type",
37
+ value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
38
+ URLQueryItem(
39
+ name: "url",
40
+ value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
41
+ ]
42
+ }.flatMap({ $0 })
43
+ var urlComps = URLComponents(string: "scappscheme://")!
44
+ urlComps.queryItems = queryItems
45
+ openURL(urlComps.url!)
46
+ }
47
+
48
+ fileprivate func createSharedFileUrl(_ url: URL?) -> String {
49
+ let fileManager = FileManager.default
50
+
51
+ let copyFileUrl =
52
+ fileManager.containerURL(forSecurityApplicationGroupIdentifier: "YOUR_APP_GROUP_ID")!
53
+ .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + url!
54
+ .lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
55
+ try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
56
+
57
+ return copyFileUrl
58
+ }
59
+
60
+ func saveScreenshot(_ image: UIImage, _ index: Int) -> String {
61
+ let fileManager = FileManager.default
62
+
63
+ let copyFileUrl =
64
+ fileManager.containerURL(forSecurityApplicationGroupIdentifier: "YOUR_APP_GROUP_ID")!
65
+ .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
66
+ + "/screenshot_\(index).png"
67
+ do {
68
+ try image.pngData()?.write(to: URL(string: copyFileUrl)!)
69
+ return copyFileUrl
70
+ } catch {
71
+ print(error.localizedDescription)
72
+ return ""
73
+ }
74
+ }
75
+
76
+ fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
77
+ async throws -> ShareItem
78
+ {
79
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
80
+ let url = results as! URL?
81
+ let shareItem: ShareItem = ShareItem()
82
+
83
+ if url!.isFileURL {
84
+ shareItem.title = url!.lastPathComponent
85
+ shareItem.type = "application/" + url!.pathExtension.lowercased()
86
+ shareItem.url = createSharedFileUrl(url)
87
+ } else {
88
+ shareItem.title = url!.absoluteString
89
+ shareItem.url = url!.absoluteString
90
+ shareItem.type = "text/plain"
91
+ }
92
+
93
+ return shareItem
94
+ }
95
+
96
+ fileprivate func handleTypeText(_ attachment: NSItemProvider)
97
+ async throws -> ShareItem
98
+ {
99
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
100
+ let shareItem: ShareItem = ShareItem()
101
+ let text = results as! String
102
+ shareItem.title = text
103
+ shareItem.type = "text/plain"
104
+ return shareItem
105
+ }
106
+
107
+ fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
108
+ async throws -> ShareItem
109
+ {
110
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
111
+ let shareItem: ShareItem = ShareItem()
112
+
113
+ let url = results as! URL?
114
+ shareItem.title = url!.lastPathComponent
115
+ shareItem.type = "video/" + url!.pathExtension.lowercased()
116
+ shareItem.url = createSharedFileUrl(url)
117
+ return shareItem
118
+ }
119
+
120
+ fileprivate func handleTypeImage(_ attachment: NSItemProvider, _ index: Int)
121
+ async throws -> ShareItem
122
+ {
123
+ let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
124
+
125
+ let shareItem: ShareItem = ShareItem()
126
+ switch data {
127
+ case let image as UIImage:
128
+ shareItem.title = "screenshot_\(index)"
129
+ shareItem.type = "image/png"
130
+ shareItem.url = self.saveScreenshot(image, index)
131
+ case let url as URL:
132
+ shareItem.title = url.lastPathComponent
133
+ shareItem.type = "image/" + url.pathExtension.lowercased()
134
+ shareItem.url = self.createSharedFileUrl(url)
135
+ default:
136
+ print("Unexpected image data:", type(of: data))
137
+ }
138
+ return shareItem
139
+ }
140
+
141
+ override public func viewDidLoad() {
142
+ super.viewDidLoad()
143
+
144
+ shareItems.removeAll()
145
+
146
+ let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
147
+ Task {
148
+ try await withThrowingTaskGroup(
149
+ of: ShareItem.self,
150
+ body: { taskGroup in
151
+
152
+ for (index, attachment) in extensionItem.attachments!.enumerated() {
153
+ if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
154
+ taskGroup.addTask {
155
+ return try await self.handleTypeUrl(attachment)
156
+ }
157
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
158
+ taskGroup.addTask {
159
+ return try await self.handleTypeText(attachment)
160
+ }
161
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
162
+ taskGroup.addTask {
163
+ return try await self.handleTypeMovie(attachment)
164
+ }
165
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
166
+ taskGroup.addTask {
167
+ return try await self.handleTypeImage(attachment, index)
168
+ }
169
+ }
170
+ }
171
+
172
+ for try await item in taskGroup {
173
+ self.shareItems.append(item)
174
+ }
175
+ })
176
+
177
+ self.sendData()
178
+
179
+ }
180
+ }
181
+
182
+ @objc func openURL(_ url: URL) {
183
+ var responder: UIResponder? = self
184
+ while responder != nil {
185
+ if let application = responder as? UIApplication {
186
+ application.open(url, options: [:], completionHandler: nil)
187
+ return
188
+ }
189
+ responder = responder?.next
190
+ }
191
+ }
192
+
193
+ }
@@ -4,6 +4,7 @@ import { apiCall } from "./api";
4
4
  import { Camera, CameraResultType } from "@capacitor/camera";
5
5
  import { Geolocation } from "@capacitor/geolocation";
6
6
  import { ScreenOrientation } from "@capacitor/screen-orientation";
7
+ import { SendIntent } from "send-intent";
7
8
 
8
9
  const orientationChangeListeners = new Set();
9
10
 
@@ -80,13 +81,13 @@ export function clearTopAlerts() {
80
81
  }
81
82
 
82
83
  export function errorAlert(error) {
84
+ console.error(error);
83
85
  showAlerts([
84
86
  {
85
87
  type: "error",
86
88
  msg: error.message ? error.message : "An error occured.",
87
89
  },
88
90
  ]);
89
- console.error(error);
90
91
  }
91
92
 
92
93
  // TODO combine with loadEncodedFile
@@ -187,3 +188,34 @@ export function registerScreenOrientationListener(name, listener) {
187
188
  export async function getScreenOrientation() {
188
189
  return await ScreenOrientation.orientation();
189
190
  }
191
+
192
+ export async function sendIntentCallback() {
193
+ console.log("sendIntentCallback");
194
+ try {
195
+ const received = await SendIntent.checkSendIntentReceived();
196
+ console.log("received: ", received);
197
+ if (received) {
198
+ const response = await apiCall({
199
+ method: "POST",
200
+ path: "/notifications/share-handler",
201
+ body: received,
202
+ });
203
+ console.log("Share data sent to server.");
204
+ console.log(response);
205
+ if (response.data.error) {
206
+ errorAlert(response.data.error);
207
+ } else {
208
+ showAlerts([
209
+ {
210
+ type: "success",
211
+ msg:
212
+ "Shared: " +
213
+ (received.title || received.text || received.url || ""),
214
+ },
215
+ ]);
216
+ }
217
+ }
218
+ } catch (error) {
219
+ console.log("Error in sendIntentCallback: ", error);
220
+ }
221
+ }
@@ -228,7 +228,7 @@ export function isHtmlFile() {
228
228
  return iframe.getAttribute("is-html-file") === "true";
229
229
  }
230
230
 
231
- async function reload() {
231
+ export async function reload() {
232
232
  const currentRoute = currentLocation();
233
233
  if (!currentRoute) await gotoEntryView();
234
234
  await handleRoute(currentRoute, currentQuery(true));
@@ -336,12 +336,13 @@ export async function replaceIframeInnerContent(content) {
336
336
  }
337
337
 
338
338
  export function splitPathQuery(url) {
339
- let path = url;
340
- let query = undefined;
341
- const queryStart = url.indexOf("?");
342
- if (queryStart > 0) {
343
- path = url.substring(0, queryStart);
344
- query = url.substring(queryStart);
345
- }
346
- return { path, query };
339
+ if (url === "/") return { path: "/", query: undefined, hash: undefined };
340
+ const urlObj =
341
+ url.startsWith("http://") || url.startsWith("https://")
342
+ ? new URL(url)
343
+ : new URL(`http://${url}`);
344
+ const path = url.split("?")[0];
345
+ const query = urlObj.search?.substring(1);
346
+ const hash = urlObj.hash?.substring(1);
347
+ return { path, query, hash };
347
348
  }
package/src/init.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  gotoEntryView,
23
23
  addRoute,
24
24
  } from "./helpers/navigation.js";
25
+ import { sendIntentCallback } from "./helpers/common.js";
25
26
 
26
27
  import i18next from "i18next";
27
28
  import i18nextSprintfPostProcessor from "i18next-sprintf-postprocessor";
@@ -319,6 +320,7 @@ export async function init({
319
320
 
320
321
  state.mobileConfig.networkState = await Network.getStatus();
321
322
  Network.addListener("networkStatusChange", networkChangeCallback);
323
+
322
324
  const networkDisabled = state.mobileConfig.networkState === "none";
323
325
  const jwt = state.mobileConfig.jwt;
324
326
  const alerts = [];
@@ -365,6 +367,11 @@ export async function init({
365
367
  });
366
368
  }
367
369
  }
370
+ if (state.mobileConfig.allowOfflineMode) {
371
+ await sendIntentCallback();
372
+ window.addEventListener("sendIntentReceived", sendIntentCallback);
373
+ }
374
+
368
375
  let page = null;
369
376
  if (!lastLocation) {
370
377
  addRoute({ route: entryPoint, query: undefined });
@@ -311,10 +311,10 @@ function invalidate_pagings(currentQuery) {
311
311
  async function set_state_fields(kvs, disablePjax, e) {
312
312
  try {
313
313
  showLoadSpinner();
314
- let newhref = get_current_state_url(e);
314
+ const current = get_current_state_url(e);
315
315
  let queryParams = [];
316
316
  const { path, query } =
317
- parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
317
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(current);
318
318
  let currentQuery = query || {};
319
319
  if (Object.keys(kvs).some((k) => !is_paging_param(k))) {
320
320
  currentQuery = invalidate_pagings(currentQuery);
@@ -333,6 +333,8 @@ async function set_state_fields(kvs, disablePjax, e) {
333
333
  if (disablePjax)
334
334
  await parent.saltcorn.mobileApp.navigation.handleRoute(path, queryStr);
335
335
  else await pjax_to(path, queryStr, e);
336
+ } catch (error) {
337
+ parent.saltcorn.mobileApp.common.errorAlert(error);
336
338
  } finally {
337
339
  removeLoadSpinner();
338
340
  }
@@ -376,32 +378,29 @@ async function pjax_to(href, query, e) {
376
378
  }
377
379
  }
378
380
 
379
- async function set_state_field(key, value) {
381
+ async function set_state_field(key, value, e) {
380
382
  try {
381
383
  showLoadSpinner();
382
- const query = updateQueryStringParameter(
383
- parent.saltcorn.mobileApp.navigation.currentQuery(),
384
- key,
385
- value
386
- );
387
- await parent.saltcorn.mobileApp.navigation.handleRoute(
388
- parent.saltcorn.mobileApp.navigation.currentLocation(),
389
- query
390
- );
384
+ const newhref = get_current_state_url(e);
385
+ const { path, query } =
386
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
387
+ await pjax_to(path, updateQueryStringParameter(query, key, value), e);
388
+ } catch (error) {
389
+ parent.saltcorn.mobileApp.common.errorAlert(error);
391
390
  } finally {
392
391
  removeLoadSpinner();
393
392
  }
394
393
  }
395
394
 
396
- async function unset_state_field(key) {
395
+ async function unset_state_field(key, e) {
397
396
  try {
398
397
  showLoadSpinner();
399
- const href = parent.saltcorn.mobileApp.navigation.currentLocation();
400
- const query = removeQueryStringParameter(
401
- parent.saltcorn.mobileApp.navigation.currentLocation(),
402
- key
403
- );
404
- await parent.saltcorn.mobileApp.navigation.handleRoute(href, query);
398
+ const newhref = get_current_state_url(e);
399
+ const { path, query } =
400
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
401
+ await pjax_to(path, removeQueryStringParameter(query, key), e);
402
+ } catch (error) {
403
+ parent.saltcorn.mobileApp.common.errorAlert(error);
405
404
  } finally {
406
405
  removeLoadSpinner();
407
406
  }
@@ -623,50 +622,58 @@ function openInAppBrowser(url, domId) {
623
622
  }
624
623
  }
625
624
 
626
- async function select_id(id) {
625
+ async function select_id(id, e) {
627
626
  try {
628
627
  showLoadSpinner();
629
- const newQuery = updateQueryStringParameter(
630
- parent.saltcorn.mobileApp.navigation.currentQuery(),
631
- "id",
632
- id
633
- );
634
- await parent.handleRoute(
635
- parent.saltcorn.mobileApp.navigation.currentLocation(),
636
- newQuery
637
- );
628
+ const newhref = get_current_state_url(e);
629
+ const { path, query } =
630
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
631
+ await pjax_to(path, updateQueryStringParameter(query, "id", id), e);
632
+ } catch (error) {
633
+ parent.saltcorn.mobileApp.common.errorAlert(error);
638
634
  } finally {
639
635
  removeLoadSpinner();
640
636
  }
641
637
  }
642
638
 
643
- async function check_state_field(that) {
639
+ async function check_state_field(that, e) {
644
640
  try {
645
641
  showLoadSpinner();
642
+ const newhref = get_current_state_url(e);
643
+ const { path, query } =
644
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
645
+ const checked = that.checked;
646
646
  const name = that.name;
647
- const newQuery = that.checked
648
- ? updateQueryStringParameter(
649
- parent.saltcorn.mobileApp.navigation.currentQuery(),
650
- name,
651
- that.value
652
- )
653
- : removeQueryStringParameter(name);
654
- await parent.saltcorn.mobileApp.navigation.handleRoute(
655
- parent.saltcorn.mobileApp.navigation.currentLocation(),
656
- newQuery
657
- );
647
+ const value = encodeURIComponent(that.value);
648
+ const newQuery = checked
649
+ ? updateQueryStringParameter(query, name, value)
650
+ : removeQueryStringParameter(query, name);
651
+ await pjax_to(path, newQuery, e);
652
+ } catch (error) {
653
+ parent.saltcorn.mobileApp.common.errorAlert(error);
658
654
  } finally {
659
655
  removeLoadSpinner();
660
656
  }
661
657
  }
662
658
 
663
- async function clear_state() {
659
+ async function clear_state(omit_fields_str, e) {
664
660
  try {
665
661
  showLoadSpinner();
666
- await parent.saltcorn.mobileApp.navigation.handleRoute(
667
- parent.saltcorn.mobileApp.navigation.currentLocation(),
668
- undefined
669
- );
662
+ const newhref = get_current_state_url(e);
663
+ const { path, query, hash } =
664
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(newhref);
665
+ let newQuery = "";
666
+ if (omit_fields_str) {
667
+ const omit_fields = omit_fields_str.split(",");
668
+ const params = new URLSearchParams(query);
669
+ for (const f of omit_fields) {
670
+ if (params.get(f))
671
+ updateQueryStringParameter(newQuery, f, params.get(f));
672
+ }
673
+ }
674
+ await pjax_to(path, newQuery, e);
675
+ } catch (error) {
676
+ parent.saltcorn.mobileApp.common.errorAlert(error);
670
677
  } finally {
671
678
  removeLoadSpinner();
672
679
  }