Skip to content

Commit

Permalink
Merge pull request #2160 from IDEMSInternational/feat/asset-download
Browse files Browse the repository at this point in the history
Feat: share assets; save assets to device
  • Loading branch information
esmeetewinkel authored Dec 15, 2023
2 parents f767084 + 5549cd2 commit 20234b0
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 29 deletions.
1 change: 1 addition & 0 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ android {

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-file-opener')
implementation project(':capacitor-community-firebase-crashlytics')
implementation project(':capacitor-firebase-authentication')
implementation project(':capacitor-firebase-performance')
Expand Down
6 changes: 5 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<!-- usesCleartextTraffic is required by capacitor-blob-writer -->
<!-- requestLegacyExternalStorage is required to save files to "Documents" folder on Android 10 -->

<activity
android:exported="true"
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/assets/capacitor.plugins.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
[
{
"pkg": "@capacitor-community/file-opener",
"classpath": "com.ryltsov.alex.plugins.file.opener.FileOpenerPlugin"
},
{
"pkg": "@capacitor-community/firebase-crashlytics",
"classpath": "com.getcapacitor.community.firebasecrashlytics.FirebaseCrashlyticsPlugin"
Expand Down
3 changes: 3 additions & 0 deletions android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

include ':capacitor-community-file-opener'
project(':capacitor-community-file-opener').projectDir = new File('../node_modules/@capacitor-community/file-opener/android')

include ':capacitor-community-firebase-crashlytics'
project(':capacitor-community-firebase-crashlytics').projectDir = new File('../node_modules/@capacitor-community/firebase-crashlytics/android')

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@angular/platform-browser": "~15.0.4",
"@angular/platform-browser-dynamic": "~15.0.4",
"@angular/router": "~15.0.4",
"@capacitor-community/file-opener": "^1.0.5",
"@capacitor-community/firebase-crashlytics": "^3.0.0",
"@capacitor-firebase/authentication": "^5.1.0",
"@capacitor-firebase/performance": "^5.1.0",
Expand All @@ -65,6 +66,7 @@
"@ngx-matomo/tracker": "^1.3.3",
"@sentry/angular": "^7.21.1",
"@supabase/supabase-js": "^2.13.1",
"@types/file-saver": "^2.0.7",
"bootstrap-datepicker": "^1.10.0",
"capacitor-blob-writer": "^1.1.14",
"clone": "^2.1.2",
Expand All @@ -78,6 +80,7 @@
"document-register-element": "^1.14.10",
"dompurify": "^2.3.6",
"extract-math": "^1.2.3",
"file-saver": "^2.0.5",
"firebase": "^9.8.1",
"globalthis": "^1.0.2",
"howler": "^2.2.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/data-models/flowTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,11 @@ export namespace FlowTypes {
"go_to",
"go_to_url",
"google_auth",
"open_external",
"pop_up",
"process_template",
"reset_app",
"save_to_device",
"set_field",
"set_item",
"set_items",
Expand Down
5 changes: 4 additions & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { IAppConfig } from "./shared/model";
import { TaskService } from "./shared/services/task/task.service";
import { AppUpdateService } from "./shared/services/app-update/app-update.service";
import { RemoteAssetService } from "./shared/services/remote-asset/remote-asset.service";
import { FileManagerService } from "./shared/services/file-manager/file-manager.service";
import { AsyncServiceBase } from "./shared/services/asyncService.base";
import { SyncServiceBase } from "./shared/services/syncService.base";
import { SeoService } from "./shared/services/seo/seo.service";
Expand Down Expand Up @@ -94,7 +95,8 @@ export class AppComponent {
private serverService: ServerService,
private appUpdateService: AppUpdateService,
private remoteAssetService: RemoteAssetService,
private shareService: ShareService
private shareService: ShareService,
private fileManagerService: FileManagerService
) {
this.initializeApp();
}
Expand Down Expand Up @@ -214,6 +216,7 @@ export class AppComponent {
this.seoService,
this.feedbackService,
this.shareService,
this.fileManagerService,
],
deferred: [this.analyticsService],
implicit: [
Expand Down
121 changes: 112 additions & 9 deletions src/app/shared/services/file-manager/file-manager.service.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,146 @@
import { Injectable } from "@angular/core";
import { Directory, Encoding, Filesystem } from "@capacitor/filesystem";
import { FileOpener } from "@capacitor-community/file-opener";
import { Capacitor } from "@capacitor/core";
import write_blob from "capacitor-blob-writer";
import { saveAs } from "file-saver";
import { SyncServiceBase } from "../syncService.base";
import { environment } from "src/environments/environment";
import { IAssetContents } from "src/app/data";
import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry";
import { TemplateAssetService } from "../../components/template/services/template-asset.service";
import { ErrorHandlerService } from "../error-handler/error-handler.service";

@Injectable({
providedIn: "root",
})
export class FileManagerService extends SyncServiceBase {
cacheName: string;

constructor() {
constructor(
private errorHandler: ErrorHandlerService,
private templateActionRegistry: TemplateActionRegistry,
private templateAssetService: TemplateAssetService
) {
super("FileManager");
this.initialise();
}

private initialise() {
this.cacheName = environment.deploymentConfig.name;
this.registerTemplateActionHandlers();
}

private registerTemplateActionHandlers() {
this.templateActionRegistry.register({
// Native: save file to Downloads folder in external device storage and open;
// Web: download file
save_to_device: async ({ args }) => {
await this.downloadTemplateAsset({ relativePath: args[0] });
},
// Native: save file to app's internal cache on device and open it externally to the app
// (file is not permanently saved and is not accessible through external storage);
// Web: open file in new tab, or download if not viewable in browser
open_external: async ({ args }) => {
await this.openTemplateAsset(args[0]);
},
});
}

/**
* Save a file to the local filesystem (native only)
* @returns the local filesystem path to the saved file
* Save an asset file to device storage.
* On native devices, save file to native storage and optionally trigger prompt to open with native apps.
* On web, prompt standard browser download.
* @param options.relativePath The relative path to an asset, in standard authoring syntax
* @param options.open On native devices, open the file after download. Default: true
*/
async saveFile(blob: Blob, relativePath: string) {
private async downloadTemplateAsset(options: {
relativePath: string;
open?: boolean;
directory?: keyof typeof Directory;
subdirectory?: string;
}) {
const {
relativePath,
open = true,
// To save to user's general Download folder, requires saving to to the "ExternalStorage" directory, with subdirectory "Download".
//However this requires additional permissions on Android versions <= 10, which are considered "dangerous"
directory = "Documents",
subdirectory,
} = options;
await this.templateAssetService.ready();
const blob = (await this.templateAssetService.fetchAsset(relativePath, "blob")) as Blob;
try {
if (Capacitor.isNativePlatform()) {
const { localFilepath } = await this.saveFile({
data: blob,
targetPath: relativePath,
directory,
subdirectory,
});
if (open) FileOpener.open({ filePath: localFilepath, openWithDefault: false });
} else {
const filename = relativePath.split("/").pop();
saveAs(blob, filename);
}
} catch (err) {
this.errorHandler.handleError(err);
}
}

/**
* Open an asset file externally.
* On native devices, download the file to external storage and prompt to open with native apps.
* On web, open the asset file in a new browser tab (or prompt download if non-viewable)
* @param relativePath
*/
private async openTemplateAsset(relativePath: string) {
if (Capacitor.isNativePlatform()) {
// Save to "Cache" directory, which may be deleted in cases of low memory. See https://capacitorjs.com/docs/apis/filesystem#directory
this.downloadTemplateAsset({
relativePath,
open: true,
directory: "Cache",
subdirectory: "",
});
} else {
await this.templateAssetService.ready();
const fileUrl = this.templateAssetService.getTranslatedAssetPath(relativePath);
window.open(fileUrl, "_blank");
}
}

/**
* Save a file to the local filesystem (native only),
* @param options.directory the name of the directory in which to save the file.
* E.g. the permenent "Data" directory (default) or the temporary "Cache" (see https://capacitorjs.com/docs/apis/filesystem#directory)
* @param options.subdirectory Additional folder path to be added between "directory" and target path. The app deployment name is always added.
* @returns the local filesystem path to the saved file, in both "file://*" format and usable src format
*/
async saveFile(options: {
data: Blob;
targetPath: string;
directory?: keyof typeof Directory;
subdirectory?: string;
}) {
const { data, targetPath, directory = "Data", subdirectory = "" } = options;
const path = (subdirectory ? subdirectory + "/" : "") + `${this.cacheName}/${targetPath}`;
// Docs for write_blob are here: https://github.com/diachedelic/capacitor-blob-writer#readme
const src = await write_blob({
directory: Directory.Data,
path: `${this.cacheName}/${relativePath}`,
blob,
const localFilepath = await write_blob({
directory: Directory[directory],
path,
blob: data,
fast_mode: true,
recursive: true,
on_fallback(error) {
console.error(error);
},
});
return Capacitor.convertFileSrc(src);
return { localFilepath, src: Capacitor.convertFileSrc(localFilepath) };
}

public async deleteFile(localFilepath: string) {
return await Filesystem.deleteFile({ path: localFilepath });
}

/**
Expand Down
15 changes: 3 additions & 12 deletions src/app/shared/services/remote-asset/remote-asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TemplateAssetService } from "../../components/template/services/templat
import { AsyncServiceBase } from "../asyncService.base";
import { IAssetEntry } from "packages/data-models/deployment.model";
import { DynamicDataService } from "../dynamic-data/dynamic-data.service";
import { arrayToHashmap } from "../../utils";
import { arrayToHashmap, convertBlobToBase64 } from "../../utils";

const CORE_ASSET_PACK_NAME = "core_assets";

Expand Down Expand Up @@ -257,7 +257,7 @@ export class RemoteAssetService extends AsyncServiceBase {
complete: async () => {
console.log(`[REMOTE ASSETS] File ${fileIndex + 1} of ${totalFiles} downloaded to cache`);
if (data) {
const filesystemPath = await this.fileManagerService.saveFile(data, assetEntry.id);
await this.fileManagerService.saveFile({ data, targetPath: assetEntry.id });
await this.updateAssetContents(assetEntry);
}
progress$.next(progress);
Expand Down Expand Up @@ -343,7 +343,7 @@ export class RemoteAssetService extends AsyncServiceBase {
},
complete: async () => {
if (responseType === "base64") {
data = await this.convertBlobToBase64(data as Blob);
data = await convertBlobToBase64(data as Blob);
}
updates$.next({ progress: 100, data, subscription });
updates$.complete();
Expand Down Expand Up @@ -426,13 +426,4 @@ export class RemoteAssetService extends AsyncServiceBase {
private getSupabaseFilepath(relativePath: string) {
return `${this.folderName}/${relativePath}`;
}

private convertBlobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
}
}
Loading

0 comments on commit 20234b0

Please sign in to comment.