diff --git a/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme b/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme
new file mode 100644
index 0000000..d708c4b
--- /dev/null
+++ b/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme b/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme
new file mode 100644
index 0000000..08986b0
--- /dev/null
+++ b/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/App/upload/Base.lproj/MainInterface.storyboard b/ios/App/upload/Base.lproj/MainInterface.storyboard
new file mode 100644
index 0000000..286a508
--- /dev/null
+++ b/ios/App/upload/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/App/upload/Info.plist b/ios/App/upload/Info.plist
new file mode 100644
index 0000000..4b1f7e7
--- /dev/null
+++ b/ios/App/upload/Info.plist
@@ -0,0 +1,18 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+ TRUEPREDICATE
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.share-services
+
+
+
diff --git a/ios/App/upload/ShareViewController.swift b/ios/App/upload/ShareViewController.swift
new file mode 100644
index 0000000..7c1d68b
--- /dev/null
+++ b/ios/App/upload/ShareViewController.swift
@@ -0,0 +1,192 @@
+//
+// ShareViewController.swift
+// upload
+//
+// Created by Lucio Lelii on 10/03/23.
+//
+
+import UIKit
+import Social
+import MobileCoreServices
+import Foundation.NSURLSession
+
+class ShareItem {
+ public var title: String?
+ public var type: String?
+ public var url: String?
+ public var webPath: String?
+}
+
+class ShareViewController: UIViewController {
+ private var shareItems: [ShareItem] = []
+ override public func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ print("did appear called");
+ self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
+ }
+
+ override public func viewDidLoad() {
+ super.viewDidLoad()
+ shareItems.removeAll()
+ let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
+ Task {
+ try await withThrowingTaskGroup(
+ of: ShareItem.self,
+ body: { taskGroup in
+ for attachment in extensionItem.attachments! {
+ if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
+ taskGroup.addTask {
+ return try await self.handleTypeUrl(attachment)
+ }
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
+ taskGroup.addTask {
+ return try await self.handleTypeText(attachment)
+ }
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
+ taskGroup.addTask {
+ return try await self.handleTypeMovie(attachment)
+ }
+ } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
+ taskGroup.addTask {
+ return try await self.handleTypeImage(attachment)
+ }
+ }
+ }
+ for try await item in taskGroup {
+ self.shareItems.append(item)
+ }
+ })
+ self.sendData()
+ }
+ }
+
+ private func sendData() {
+ let queryItems = shareItems.map {
+ [
+ URLQueryItem(
+ name: "title",
+ value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
+ URLQueryItem(name: "description", value: ""),
+ URLQueryItem(
+ name: "type",
+ value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
+ URLQueryItem(
+ name: "url",
+ value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
+ URLQueryItem(
+ name: "webPath",
+ value: $0.webPath?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")
+ ]
+ }.flatMap({ $0 })
+ var urlComps = URLComponents(string: "d4sworkspace://share-input")!
+ urlComps.queryItems = queryItems
+ var opened = openURL(urlComps.url!)
+ print("sending data %s",opened);
+ }
+
+ fileprivate func createSharedFileUrl(_ url: URL?) -> String {
+ let fileManager = FileManager.default
+ print("share url: " + url!.absoluteString)
+ let copyFileUrl =
+ fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.gcube.workspace")!
+ .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + url!
+ .lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
+ try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
+
+ return copyFileUrl
+ }
+
+ func saveScreenshot(_ image: UIImage) -> String {
+ let fileManager = FileManager.default
+
+ let copyFileUrl =
+ fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.gcube.workspace")!
+ .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
+ + "/screenshot.png"
+ do {
+ try image.pngData()?.write(to: URL(string: copyFileUrl)!)
+ return copyFileUrl
+ } catch {
+ print(error.localizedDescription)
+ return ""
+ }
+ }
+
+ fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
+ async throws -> ShareItem
+ {
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
+ let url = results as! URL?
+ let shareItem: ShareItem = ShareItem()
+
+ if url!.isFileURL {
+ shareItem.title = url!.lastPathComponent
+ shareItem.type = "application/" + url!.pathExtension.lowercased()
+ shareItem.url = createSharedFileUrl(url)
+ shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
+ } else {
+ shareItem.title = url!.absoluteString
+ shareItem.url = url!.absoluteString
+ shareItem.webPath = url!.absoluteString
+ shareItem.type = "text/plain"
+ }
+ return shareItem
+ }
+
+ fileprivate func handleTypeText(_ attachment: NSItemProvider)
+ async throws -> ShareItem
+ {
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
+ let shareItem: ShareItem = ShareItem()
+ let text = results as! String
+ shareItem.title = text
+ shareItem.type = "text/plain"
+ return shareItem
+ }
+
+ fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
+ async throws -> ShareItem
+ {
+ let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
+ let shareItem: ShareItem = ShareItem()
+ let url = results as! URL?
+ shareItem.title = url!.lastPathComponent
+ shareItem.type = "video/" + url!.pathExtension.lowercased()
+ shareItem.url = createSharedFileUrl(url)
+ shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
+ return shareItem
+ }
+
+ fileprivate func handleTypeImage(_ attachment: NSItemProvider)
+ async throws -> ShareItem
+ {
+ let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
+ let shareItem: ShareItem = ShareItem()
+ switch data {
+ case let image as UIImage:
+ shareItem.title = "screenshot"
+ shareItem.type = "image/png"
+ shareItem.url = self.saveScreenshot(image)
+ shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
+ case let url as URL:
+ shareItem.title = url.lastPathComponent
+ shareItem.type = "image/" + url.pathExtension.lowercased()
+ shareItem.url = self.createSharedFileUrl(url)
+ shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
+ default:
+ print("Unexpected image data:", type(of: data))
+ }
+ return shareItem
+ }
+
+ @objc func openURL(_ url: URL) -> Bool {
+ var responder: UIResponder? = self
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ return application.perform(#selector(openURL(_:)), with: url) != nil
+ }
+ responder = responder?.next
+ }
+ return false
+ }
+}
diff --git a/src/app/_helper/intent-init.factory.ts b/src/app/_helper/intent-init.factory.ts
new file mode 100644
index 0000000..5aa4c46
--- /dev/null
+++ b/src/app/_helper/intent-init.factory.ts
@@ -0,0 +1,15 @@
+import { ShareExtension } from "capacitor-share-extension";
+import { D4sIntentService } from "../d4s-intent.service";
+
+export function initializeIntent(d4sIntent: D4sIntentService) {
+
+ return () => {
+ window.addEventListener('sendIntentReceived', () => {
+ console.log("event fired");
+ d4sIntent.checkIntent();
+ });
+ d4sIntent.checkIntent();
+ }
+
+}
+
diff --git a/src/app/_helper/utils.ts b/src/app/_helper/utils.ts
new file mode 100644
index 0000000..c31b073
--- /dev/null
+++ b/src/app/_helper/utils.ts
@@ -0,0 +1,43 @@
+import { AlertController } from "@ionic/angular";
+
+/**
+ * Format bytes as human-readable text.
+ *
+ * @param bytes Number of bytes.
+ * @param si True to use metric (SI) units, aka powers of 1000. False to use
+ * binary (IEC), aka powers of 1024.
+ * @param dp Number of decimal places to display.
+ *
+ * @return Formatted string.
+ */
+export function humanFileSize(bytes:number, si=true, dp=1) {
+ const thresh = si ? 1000 : 1024;
+
+ if (Math.abs(bytes) < thresh) {
+ return bytes + ' B';
+ }
+
+ const units = si
+ ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+ : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ let u = -1;
+ const r = 10**dp;
+
+ do {
+ bytes /= thresh;
+ ++u;
+ } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
+
+
+ return bytes.toFixed(dp) + ' ' + units[u];
+ }
+
+
+ export async function presentConnectionAlert(err: string, alertCtrl: AlertController){
+ const alert = await alertCtrl.create({
+ header: 'Connection ERROR',
+ message: err,
+ buttons: ['OK'],
+ });
+ await alert.present();
+ }
\ No newline at end of file
diff --git a/src/app/d4s-intent.service.spec.ts b/src/app/d4s-intent.service.spec.ts
new file mode 100644
index 0000000..9bd3c23
--- /dev/null
+++ b/src/app/d4s-intent.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { D4sIntentService } from './d4s-intent.service';
+
+describe('D4sIntentService', () => {
+ let service: D4sIntentService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(D4sIntentService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/d4s-intent.service.ts b/src/app/d4s-intent.service.ts
new file mode 100644
index 0000000..c26bc71
--- /dev/null
+++ b/src/app/d4s-intent.service.ts
@@ -0,0 +1,61 @@
+import { Injectable } from '@angular/core';
+import { ModalController, ModalOptions, ToastController } from '@ionic/angular';
+import { ShareExtension } from 'capacitor-share-extension';
+import { EventService } from './event.service';
+import { UploadFile } from './model/actions/upload-file';
+import { StoragehubService } from './storagehub.service';
+import { UploaderInfoService } from './uploader-info.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class D4sIntentService {
+
+ constructor(private modalCtrl: ModalController,
+ private toastController: ToastController,
+ private storagehub: StoragehubService,
+ private uploaderInfo: UploaderInfoService,
+ private event: EventService) { }
+
+
+ async checkIntent() {
+ try {
+ const result: any = await ShareExtension.checkSendIntentReceived();
+ /* sample result::
+ { payload: [
+ {
+ "type":"image%2Fjpg",
+ "description":"",
+ "title":"IMG_0002.JPG",
+ // url contains a full, platform-specific file URL that can be read later using the Filsystem API.
+ "url":"file%3A%2F%2F%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
+ // webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering.
+ "webPath":"capacitor%3A%2F%2Flocalhost%2F_capacitor_file_%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
+ }]
+ }
+ */
+ if (result && result.payload && result.payload.length) {
+ console.log('Intent received: ', JSON.stringify(result.payload[0]));
+ var modalOptions: ModalOptions = new UploadFile().getModalOptions(this.storagehub, this.uploaderInfo, result.payload[0],
+ (id : string) =>{
+ this.event.ReloadEvent.emit(id);
+ ShareExtension.finish();
+ }, (text: string) => this.presentToast(text));
+ let modal = await this.modalCtrl.create(modalOptions);
+ await modal.present();
+ } else {
+ console.log("nothing received");
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ async presentToast(text: string) {
+ const toast = await this.toastController.create({
+ message: text,
+ duration: 1500
+ });
+ await toast.present();
+ }
+}
diff --git a/src/app/event.service.spec.ts b/src/app/event.service.spec.ts
new file mode 100644
index 0000000..df6b168
--- /dev/null
+++ b/src/app/event.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { EventService } from './event.service';
+
+describe('EventService', () => {
+ let service: EventService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(EventService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/event.service.ts b/src/app/event.service.ts
new file mode 100644
index 0000000..a32e5bd
--- /dev/null
+++ b/src/app/event.service.ts
@@ -0,0 +1,13 @@
+import { EventEmitter, Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EventService {
+
+ ReloadEvent = new EventEmitter();
+
+ constructor() { }
+
+
+}
diff --git a/src/app/model/actions/open-file.ts b/src/app/model/actions/open-file.ts
new file mode 100644
index 0000000..fb70f6c
--- /dev/null
+++ b/src/app/model/actions/open-file.ts
@@ -0,0 +1,99 @@
+import { AlertController, LoadingController } from "@ionic/angular";
+import { WSItem } from "../ws-item";
+import { StoragehubService } from "src/app/storagehub.service";
+import { Directory, Filesystem } from "@capacitor/filesystem";
+import { FileOpener } from "@awesome-cordova-plugins/file-opener/ngx";
+import { App } from '@capacitor/app';
+import { isPlatform } from '@ionic/angular';
+import { presentConnectionAlert } from "src/app/_helper/utils";
+
+
+export class OpenFile {
+
+ pluginListener: any;
+
+ item: WSItem;
+
+ constructor(item: WSItem) {
+ this.item = item;
+ if (isPlatform("android")) {
+ App.addListener('resume', () => {
+ Filesystem.deleteFile(
+ {
+ path: item.getTitle(),
+ directory: Directory.Documents
+ }).then(() => console.log("file deleted"));
+ this.pluginListener?.remove();
+ }).then((handler) => this.pluginListener = handler);
+ }
+ }
+
+ async open(storageHub: StoragehubService, fileOpener: FileOpener, loadingCtrl: LoadingController, alertCtrl: AlertController) {
+ const size = this.item.item.content?.size;
+ if (!size || size / 1000000 > 20) {
+ this.presentAlert(alertCtrl);
+ return;
+ }
+
+ this.showLoading(loadingCtrl);
+ storageHub.downloadFile(this.item.item.id).then(obs => obs.subscribe({
+ next: async blob => {
+ const base64 = await this.blobToBase64(blob) as string;
+ const fileWrited = await Filesystem.writeFile({
+ path: this.item.getTitle(),
+ data: base64,
+ directory: Directory.Documents
+ });
+
+ const mimeType = this.item.item.content?.mimeType;
+ fileOpener.open(fileWrited.uri, mimeType ? mimeType : "application/octet-stream").then(() => {
+ loadingCtrl.dismiss();
+ if (isPlatform("ios"))
+ Filesystem.deleteFile(
+ {
+ path: this.item.getTitle(),
+ directory: Directory.Documents
+ });
+
+ })
+
+ },
+ error: (err) => {
+ loadingCtrl.dismiss();
+ this.pluginListener?.remove();
+ presentConnectionAlert(err, alertCtrl);
+ }
+ }))
+ }
+
+
+ async presentAlert(alertCtrl: AlertController) {
+ const alert = await alertCtrl.create({
+ header: 'Alert',
+ message: 'File too big to be opeend here!',
+ buttons: ['OK'],
+ });
+
+ await alert.present();
+ }
+
+ async showLoading(loadingCtrl: LoadingController) {
+ const loading = await loadingCtrl.create({
+ message: 'Loading...',
+ duration: 20000,
+ spinner: 'circles',
+ });
+
+ loading.present();
+ return loading;
+ }
+
+
+ blobToBase64(blob: Blob) {
+ return new Promise((resolve, _) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsDataURL(blob);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/app/model/actions/upload-file.ts b/src/app/model/actions/upload-file.ts
new file mode 100644
index 0000000..a527cd4
--- /dev/null
+++ b/src/app/model/actions/upload-file.ts
@@ -0,0 +1,57 @@
+import { ModalOptions } from "@ionic/angular";
+import { StoragehubService } from "src/app/storagehub.service";
+import { WsViewerComponent } from "src/app/ws-viewer/ws-viewer.component";
+import { WSItem } from "../ws-item";
+import { Filesystem } from "@capacitor/filesystem";
+import { UploaderInfoService } from "src/app/uploader-info.service";
+
+export class UploadFile {
+
+
+
+ getModalOptions(storagehub: StoragehubService, uploaderInfo: UploaderInfoService, data: any, reload: Function, notify: Function): ModalOptions {
+ return {
+ component: WsViewerComponent,
+ componentProps: {
+ finishLabel: "Upload here",
+ title: "Select destination folder",
+ notClickableIds: [],
+ notSelectableIds: [],
+ onSelected: (destinationItem: WSItem) => {
+ const itemId = destinationItem.item.id;
+ uploaderInfo.uploadStarted(itemId, data.title);
+ this.actionHandler(destinationItem, data, storagehub).then( obs => obs.subscribe({
+ next: () => {},
+ error: err => uploaderInfo.uploadFinished(itemId, data.title),
+ complete: () => {
+ notify(`uploaded file ${data.title}`);
+ uploaderInfo.uploadFinished(itemId, data.title),
+ reload(itemId);
+ },
+
+ }
+ ));
+ }
+ }
+ };
+ }
+
+
+ async actionHandler(destinationItem: WSItem, result: any, storagehub: StoragehubService) {
+ const pathDecodedWebPath = decodeURIComponent(result.webPath);
+
+ const file = await fetch(pathDecodedWebPath)
+ .then(res => res.blob()) // Gets the response and returns it as a blob
+ .then(blob => { return new File([blob], result.title)});
+
+ console.log("before uploading file ");
+ return storagehub.uploadFile(destinationItem.item.id, result.title, file);
+ }
+
+ getName(): string {
+ return "UploadFile";
+ }
+ getActionType(): string | undefined {
+ return undefined;
+ }
+}
\ No newline at end of file
diff --git a/src/app/uploader-info.service.spec.ts b/src/app/uploader-info.service.spec.ts
new file mode 100644
index 0000000..9a32a38
--- /dev/null
+++ b/src/app/uploader-info.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UploaderInfoService } from './uploader-info.service';
+
+describe('UploaderInfoService', () => {
+ let service: UploaderInfoService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(UploaderInfoService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/uploader-info.service.ts b/src/app/uploader-info.service.ts
new file mode 100644
index 0000000..0dd2086
--- /dev/null
+++ b/src/app/uploader-info.service.ts
@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UploaderInfoService {
+
+ myMap = new Map();
+
+ constructor() {
+ }
+
+ uploadStarted(itemId: string, fileName: string) {
+ var values = this.myMap.get(itemId);
+ if (!values)
+ values = [fileName];
+ else
+ values.push(fileName);
+ this.myMap.set(itemId, values);
+ }
+
+ uploadFinished(itemId: string , fileName: string) {
+ var values = this.myMap.get(itemId);
+ var newValues: string[] = [];
+ values?.filter(val => val!= fileName).forEach(val => newValues?.push(val))
+ if (newValues)
+ if (newValues.length>0)
+ this.myMap.set(itemId, newValues);
+ else this.myMap.delete(itemId);
+ }
+
+ getUnderUpload(itemId: string): string[]{
+ var values = this.myMap.get(itemId);
+ //console.log(`get upload called with array ${values}`);
+ return values? values : [];
+ }
+
+}