Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 98 additions & 5 deletions src/lib/fileList.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const filesTree = {};
const pendingScans = new Set();
const events = {
"add-file": [],
"push-file": [],
"remove-file": [],
"add-folder": [],
"remove-folder": [],
Expand Down Expand Up @@ -125,7 +126,7 @@ export default function files(dir) {
}

/**
* @typedef {'add-file'|'remove-file'|'add-folder'|'remove-folder'|'refresh'} FileListEvent
* @typedef {'add-file'|'push-file'|'remove-file'|'add-folder'|'remove-folder'|'refresh'} FileListEvent
*/

/**
Expand Down Expand Up @@ -227,7 +228,7 @@ export async function addRoot({ url, name }) {

const tree = await Tree.createRoot(url, name);
filesTree[url] = tree;
trackScan(getAllFiles(tree));
trackScan(getAllFiles(tree, null, { indexContent: false }));
emit("add-folder", tree);
} catch (error) {
// ignore
Expand All @@ -251,10 +252,14 @@ function onRemoveFolder({ url }) {
* @param {Tree} parent - An array to store files
* @param {Tree} [root] - Root path
*/
async function getAllFiles(parent, root) {
async function getAllFiles(parent, root, options = {}) {
root = root || parent.root;
if (!parent.children || !root.isConnected) return;

if (supportsNativeWorkspace(root.url)) {
return getAllFilesNative(parent, root, options);
}

try {
const entries = await fsOperation(parent.url).lsDir();
const promises = [];
Expand All @@ -276,11 +281,98 @@ async function getAllFiles(parent, root) {
// why not outside? because parent may be removed
if (!root.isConnected) return;
parent.children.length = 0;
getAllFiles(parent);
getAllFiles(parent, root, options);
}, 3000);
}
}

function supportsNativeWorkspace(url = "") {
return (
typeof sdcard !== "undefined" &&
typeof sdcard.workspaceScan === "function" &&
(/^file:/.test(url) || /^content:/.test(url))
);
}

async function getAllFilesNative(parent, root, options = {}) {
const id = `scan-${Date.now()}-${Math.random().toString(36).slice(2)}`;

return new Promise((resolve, reject) => {
let settled = false;

const finish = (fn, value) => {
if (settled) return;
settled = true;
fn(value);
};

const cancelIfDisconnected = () => {
if (root.isConnected) return false;
try {
sdcard.workspaceCancel(id);
} catch (_) {
// ignore cancellation failures
}
finish(resolve);
return true;
};

sdcard.workspaceScan(
{
id,
rootUrl: parent.url,
title: parent.name,
excludeFolders: settings.value.excludeFolders,
showHiddenFiles: !!settings.value.fileBrowser?.showHiddenFiles,
defaultEncoding: settings.value.defaultFileEncoding,
indexContent: !!options.indexContent,
},
(event) => {
if (cancelIfDisconnected()) return;
switch (event?.type || event?.action) {
case "batch":
addNativeEntries(root, event.entries || []);
break;
case "done":
finish(resolve);
break;
case "error":
finish(reject, new Error(event.error || "Native scan failed"));
break;
}
},
(error) => {
finish(reject, error);
},
);
});
}

function addNativeEntries(root, entries) {
for (const item of entries) {
const parentUrl = item.parentUrl || item.parent;
const parentTree =
parentUrl === root.url ? root : getTree([root], parentUrl);
if (!parentTree?.children) continue;
if (parentTree.children.find(({ url }) => url === item.url)) continue;

const file = new Tree(
item.name,
item.url,
item.isDirectory,
item.mime || item.type,
item.size,
item.modifiedDate,
);
parentTree.children.push(file);

if (!file.children) {
emit("push-file", file);
emit("add-file", file);
}
}
}

/**
* Emit an event
* @param {string} event
Expand All @@ -307,7 +399,7 @@ function trackScan(scan) {
async function createChildTree(parent, item, root) {
if (!root.isConnected) return;
const { name, url, isDirectory, mime, type, size, modifiedDate } = item;
const exists = parent.children.findIndex(({ value }) => value === url);
const exists = parent.children.findIndex((child) => child.url === url);
if (exists > -1) {
return;
}
Expand Down Expand Up @@ -344,6 +436,7 @@ async function createChildTree(parent, item, root) {
}

emit("push-file", file);
emit("add-file", file);
}

export class Tree {
Expand Down
49 changes: 49 additions & 0 deletions src/plugins/sdcard/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ interface DocumentFile {
uri: string;
}

interface WorkspaceFileEntry {
rootUrl: string;
parent: string;
parentUrl: string;
name: string;
path: string;
url: string;
uri: string;
mime?: string;
type?: string;
isDirectory: boolean;
isFile: boolean;
size: number;
modifiedDate: number;
}

type WorkspaceEvent =
| { id: string; type: 'status'; action: 'status'; state: string; message: string; progress: number }
| { id: string; type: 'batch'; action: 'batch'; entries: WorkspaceFileEntry[] }
| { id: string; type: 'search-result'; action: 'search-result'; data: any }
| { id: string; type: 'replace-result'; action: 'replace-result'; file: WorkspaceFileEntry; text: string }
| { id: string; type: 'progress'; action: 'progress'; data: number }
| { id: string; type: 'done' | 'done-searching' | 'done-replacing' | 'error'; action: string; [key: string]: any };

interface SDcard {
/**
* Copy file/directory to given destination
Expand Down Expand Up @@ -257,6 +281,31 @@ interface SDcard {
): {
unwatch: () => void;
};
workspaceScan(
options: any,
onEvent: (event: WorkspaceEvent) => void,
onFail: (err: any) => void,
): void;
workspaceSearch(
options: any,
onEvent: (event: WorkspaceEvent) => void,
onFail: (err: any) => void,
): void;
workspaceCancel(
id: string,
onSuccess?: (res: 'OK') => void,
onFail?: (err: any) => void,
): void;
workspaceMarkDirty(
urls: string[],
onSuccess?: (res: 'OK') => void,
onFail?: (err: any) => void,
): void;
workspaceClear(
roots: string[],
onSuccess?: (res: 'OK') => void,
onFail?: (err: any) => void,
): void;
}

declare var sdcard: SDcard;
3 changes: 2 additions & 1 deletion src/plugins/sdcard/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<framework src="androidx.documentfile:documentfile:1.0.1" />

<source-file src="src/android/SDcard.java" target-dir="src/com/foxdebug/sdcard"/>
<source-file src="src/android/WorkspaceIndex.java" target-dir="src/com/foxdebug/sdcard"/>
<config-file target="AndroidManifest.xml" parent="/manifest"></config-file>
</platform>
</plugin>
</plugin>
27 changes: 27 additions & 0 deletions src/plugins/sdcard/src/android/SDcard.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ public class SDcard extends CordovaPlugin {
private DocumentFile originalRootFile;
private CallbackContext activityResultCallback;
private HashMap<String, MyFileObserver> fileObservers = new HashMap();
private WorkspaceIndex workspaceIndex;

public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
this.REQUEST_CODE = this.ACCESS_INTENT;
this.context = cordova.getContext();
this.activity = cordova.getActivity();
this.contentResolver = this.context.getContentResolver();
this.workspaceIndex = new WorkspaceIndex(this.context);
this.storageManager = (StorageManager) this.activity.getSystemService(
Context.STORAGE_SERVICE
);
Expand Down Expand Up @@ -153,6 +156,30 @@ public boolean execute(
case "unwatch file":
unwatchFile(arg1);
break;
case "workspace scan":
workspaceIndex.scan(
args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0),
callback
);
break;
case "workspace search":
workspaceIndex.search(
args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0),
callback
);
break;
case "workspace cancel":
workspaceIndex.cancel(arg1);
callback.success("OK");
break;
case "workspace mark dirty":
workspaceIndex.markDirty(args.optJSONArray(0));
callback.success("OK");
break;
case "workspace clear":
workspaceIndex.clear(args.optJSONArray(0));
callback.success("OK");
break;
default:
return false;
}
Expand Down
Loading