Last active
August 16, 2021 06:23
-
-
Save ashfinal/30504de6dfddeaa5f3a35fb86f6a32f9 to your computer and use it in GitHub Desktop.
Slim file manager that allows you to manipulate hidden files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Variables used by Scriptable. | |
| // These must be at the very top of the file. Do not edit. | |
| // icon-color: gray; icon-glyph: folder-open; | |
| const APPTABLE = new UITable() | |
| const FM = FileManager.local() | |
| const OFFSET = 2 | |
| const VOLICON = ["๐ก", "๐", "๐", "๐", "๐"] | |
| var ACTIONROW | |
| var REGISTER | |
| var TIMER = new Timer() | |
| var CURINSTA | |
| var HEADCONTAINER = [] | |
| var INFOCACHE = {} | |
| var LOCATIONROW | |
| var OVOLIDX | |
| var ROWSCONTAINER = [] | |
| var SELECTED | |
| var VOLINDEX = 0 | |
| var VOLUMES = {} | |
| var MSHEADROW | |
| var MSMODE = false | |
| var MSREGISTER = [] | |
| var MSARRAY | |
| var LASTUTI | |
| class FilesTree { | |
| constructor(root, flavour=sbtm) { | |
| this.root = root | |
| this.chain = [root] | |
| this.cursor = 0 | |
| this._nodes | |
| this._location | |
| this._flavour = flavour | |
| this.structure = (() => { | |
| let pkg = {} | |
| pkg[root] = {} | |
| let nodes = FM.listContents(root) | |
| this.cacheBasicInfo(root, nodes) | |
| nodes.forEach((e) => pkg[root][e] = {}) | |
| return pkg | |
| })() | |
| } | |
| get nodes() { | |
| return Object.keys( | |
| this.getNode(this.structure) | |
| ).sort(this._flavour) | |
| } | |
| get location() { | |
| return this.chain.reduce( | |
| (l, r) => FM.joinPath(l, r) | |
| ) | |
| } | |
| set flavour(value) { | |
| this._flavour = value | |
| } | |
| getNode(tree, start=0) { | |
| if (start == this.cursor) { | |
| return tree[this.chain[start]] | |
| } else { | |
| let ntree = tree[this.chain[start]] | |
| start++ | |
| return this.getNode(ntree, start) | |
| } | |
| } | |
| addNodes(tree, nodes, start=0) { | |
| if (start == this.cursor) { | |
| nodes.forEach( | |
| (e) => tree[this.chain[start]][e] = {} | |
| ) | |
| } else { | |
| let ntree = tree[this.chain[start]] | |
| start++ | |
| this.addNodes(ntree, nodes, start) | |
| } | |
| } | |
| removeNodes(tree, nodes, start=0) { | |
| if (start == this.cursor) { | |
| nodes.forEach( | |
| (e) => delete tree[this.chain[start]][e] | |
| ) | |
| } else { | |
| let ntree = tree[this.chain[start]] | |
| start++ | |
| this.removeNodes(ntree, nodes, start) | |
| } | |
| } | |
| renameNode(tree, name, newname, start=0) { | |
| if (start == this.cursor) { | |
| let clone = JSON.stringify( | |
| tree[this.chain[start]][name] | |
| ) | |
| delete tree[this.chain[start]][name] | |
| tree[this.chain[start]][newname] = JSON.parse(clone) | |
| } else { | |
| let ntree = tree[this.chain[start]] | |
| start++ | |
| this.renameNode(ntree, name, newname, start) | |
| } | |
| } | |
| cacheBasicInfo(path, nodes) { | |
| nodes.forEach(function(e) { | |
| let npath = FM.joinPath(path, e) | |
| if (INFOCACHE[npath] == undefined) { | |
| INFOCACHE[npath] = {} | |
| let type = FM.isDirectory(npath) | |
| let mtime = FM.modificationDate(npath) | |
| let size = FM.fileSize(npath) | |
| INFOCACHE[npath]["type"] = type | |
| INFOCACHE[npath]["mtime"] = mtime | |
| INFOCACHE[npath]["size"] = size | |
| } | |
| }) | |
| } | |
| cacheAllSubNodes(path, dirname) { | |
| let npath = FM.joinPath(path, dirname) | |
| let nodes = FM.listContents(npath) | |
| this.cacheBasicInfo(npath, nodes) | |
| INFOCACHE[npath]["count"] = nodes.length | |
| nodes.forEach((e) => { | |
| let subpath = FM.joinPath(npath, e) | |
| if (FM.isDirectory(subpath)) { | |
| this.cacheAllSubNodes(npath, e) | |
| } | |
| }) | |
| } | |
| goHome() { | |
| this.chain = [this.root] | |
| this.cursor = 0 | |
| } | |
| travelDownward(dirname) { | |
| this.chain.push(dirname) | |
| this.cursor++ | |
| let subnodes = this.getNode(this.structure) | |
| if (Object.values(subnodes).length == 0) { | |
| let nodes = FM.listContents(this.location) | |
| this.cacheBasicInfo(this.location, nodes) | |
| INFOCACHE[this.location]["count"] = nodes.length | |
| this.addNodes(this.structure, nodes) | |
| } | |
| } | |
| travelUpward() { | |
| if (this.chain.length > 1 && this.cursor > 0) { | |
| this.chain.pop() | |
| this.cursor -= 1 | |
| } | |
| } | |
| createDirectory(dirname) { | |
| let path = FM.joinPath(this.location, dirname) | |
| try { | |
| FM.createDirectory(path) | |
| this.cacheBasicInfo(this.location, [dirname]) | |
| if (INFOCACHE[this.location]) { | |
| INFOCACHE[this.location]["count"] += 1 | |
| } | |
| this.addNodes(this.structure, [dirname]) | |
| } catch (err) { | |
| let notify = new Notification() | |
| notify.title = `Can't create directory: ${dirname}` | |
| notify.body = err.message | |
| notify.schedule() | |
| } | |
| } | |
| createFile(filename) { | |
| let path = FM.joinPath(this.location, filename) | |
| try { | |
| FM.writeString(path, "") | |
| this.cacheBasicInfo(this.location, [filename]) | |
| if (INFOCACHE[this.location]) { | |
| INFOCACHE[this.location]["count"] += 1 | |
| } | |
| this.addNodes(this.structure, [filename]) | |
| } catch (err) { | |
| let notify = new Notification() | |
| notify.title = `Can't create file: ${filename}` | |
| notify.body = err.message | |
| notify.schedule() | |
| } | |
| } | |
| remove(names) { | |
| let failed = [] | |
| let curl = (" " + this.location).slice(1) | |
| names.forEach((name) => { | |
| let path = FM.joinPath(curl, name) | |
| try { | |
| FM.remove(path) | |
| if (FM.isDirectory(path)) { | |
| let items = Object.keys(INFOCACHE) | |
| items.forEach((e) => { | |
| if (e.startsWith(path)) { | |
| delete INFOCACHE[e] | |
| } | |
| }) | |
| } else { | |
| delete INFOCACHE[path] | |
| } | |
| if (INFOCACHE[curl]) { | |
| INFOCACHE[curl]["count"] -= 1 | |
| } | |
| } catch (err) { | |
| logError(err.message) | |
| failed.push(name) | |
| } | |
| }) | |
| let succeed = names.filter((e) => !failed.includes(e)) | |
| this.removeNodes(this.structure, succeed) | |
| if (failed.length) { | |
| let notify = new Notification() | |
| let count = failed.length | |
| let msg = failed.slice(0, 3).join(", ") + | |
| (count >= 3 ? " ... " : "") | |
| notify.title = `Can't delete ${count} items` | |
| notify.body = `${msg}\nSee the log for more detail.` | |
| notify.schedule() | |
| } | |
| } | |
| rename(name, newname) { | |
| if (name == newname) { | |
| return | |
| } | |
| let path = FM.joinPath(this.location, name) | |
| let npath = FM.joinPath(this.location, newname) | |
| try { | |
| FM.move(path, npath) | |
| if (FM.isDirectory(path)) { | |
| INFOCACHE[npath] = INFOCACHE[path] | |
| let items = Object.keys(INFOCACHE) | |
| items.forEach((e) => { | |
| if (e.startsWith(path)) delete INFOCACHE[e] | |
| }) | |
| } else { | |
| INFOCACHE[npath] = INFOCACHE[path] | |
| delete INFOCACHE[path] | |
| } | |
| this.renameNode(this.structure, name, newname) | |
| } catch (err) { | |
| let notify = new Notification() | |
| notify.title = `Can't rename: ${name}` | |
| notify.body = err.message | |
| notify.schedule() | |
| } | |
| } | |
| copy() { | |
| let tasks = MSREGISTER.length ? MSREGISTER : [REGISTER] | |
| if (!tasks.length) return | |
| let failed = [] | |
| let curl = (" " + this.location).slice(1) | |
| tasks.forEach((task) => { | |
| let name = FM.fileName(task, true) | |
| let path = FM.joinPath(curl, name) | |
| try { | |
| FM.copy(task, path) | |
| INFOCACHE[path] = INFOCACHE[task] | |
| if (INFOCACHE[curl]) { | |
| INFOCACHE[curl]["count"] += 1 | |
| } | |
| } catch (err) { | |
| logError(err.message) | |
| failed.push(task) | |
| } | |
| }) | |
| let succeed = tasks.filter((e) => !failed.includes(e)) | |
| let snames = succeed.map((e) => FM.fileName(e, true)) | |
| this.addNodes(this.structure, snames) | |
| let fnames = failed.map((e) => FM.fileName(e, true)) | |
| if (fnames.length) { | |
| let notify = new Notification() | |
| let count = fnames.length | |
| let msg = fnames.slice(0, 3).join(", ") + | |
| (count >= 3 ? " ... " : "") | |
| notify.title = `Can't copy ${count} items` | |
| notify.body = `${msg}\nSee the log for more detail.` | |
| notify.schedule() | |
| } | |
| } | |
| } | |
| function format(number) { | |
| let units = ["KB", "MB", "GB"] | |
| let idx = 0 | |
| let size = number | |
| while (size > 1024) { | |
| size /= 1024 | |
| idx++ | |
| } | |
| return size.toFixed(2) + units[idx] | |
| } | |
| function sbtm(a, b) { | |
| let patha = FM.joinPath(CURINSTA.location, a) | |
| let pathb = FM.joinPath(CURINSTA.location, b) | |
| let typea = INFOCACHE[patha]["type"] | |
| let typeb = INFOCACHE[pathb]["type"] | |
| let mtimea = INFOCACHE[patha]["mtime"] | |
| let mtimeb = INFOCACHE[pathb]["mtime"] | |
| // Sort by type and mtime | |
| return (typeb - typea) || (mtimeb - mtimea) | |
| } | |
| function createActionRow() { | |
| let row = new UITableRow() | |
| let back = row.addButton("๐") | |
| back.centerAligned() | |
| back.onTap = () => { | |
| APPTABLE.removeRow(ACTIONROW) | |
| ROWSCONTAINER.slice(SELECTED + 1 - OFFSET) | |
| .forEach((e) => APPTABLE.removeRow(e[0])) | |
| ROWSCONTAINER.slice(SELECTED - OFFSET) | |
| . forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| let copy = row.addButton("๐") | |
| copy.centerAligned() | |
| copy.onTap = () => { | |
| let name = CURINSTA.nodes[SELECTED-OFFSET] | |
| let path = FM.joinPath(CURINSTA.location, name) | |
| REGISTER = path | |
| APPTABLE.removeAllRows() | |
| renderTable() | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| TIMER.invalidate() | |
| TIMER.timeInterval = 60 * 1000 | |
| function callback() { | |
| if (SELECTED != undefined || MSMODE) { | |
| TIMER.invalidate() | |
| TIMER.timeInterval = 5000 | |
| TIMER.schedule(callback) | |
| } else { | |
| APPTABLE.removeAllRows() | |
| REGISTER = undefined | |
| APPTABLE.addRow(HEADCONTAINER[0]) | |
| APPTABLE.addRow(LOCATIONROW) | |
| ROWSCONTAINER.forEach((e) => { | |
| APPTABLE.addRow(e[0]) | |
| }) | |
| APPTABLE.reload() | |
| } | |
| } | |
| TIMER.schedule(callback) | |
| } | |
| let remove = row.addButton("๐งน") | |
| remove.centerAligned() | |
| remove.onTap = async () => { | |
| let name = CURINSTA.nodes[SELECTED - OFFSET] | |
| let alert = new Alert() | |
| alert.title = "Really want to delete?" | |
| alert.message = name | |
| alert.addDestructiveAction("Sure") | |
| alert.addCancelAction("Cancel") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| CURINSTA.remove([name]) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| } | |
| let rename = row.addButton("๐") | |
| rename.centerAligned() | |
| rename.onTap = async () => { | |
| let name = CURINSTA.nodes[SELECTED-OFFSET] | |
| let alert = new Alert() | |
| alert.title = "Rename this file/directory:" | |
| alert.message = name | |
| alert.addTextField(name, name) | |
| alert.addDestructiveAction("OK") | |
| alert.addCancelAction("Cancel") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| let text = alert.textFieldValue(0).trim() | |
| if (text && text != name) { | |
| CURINSTA.rename(name, text) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| } | |
| } | |
| let info = row.addButton("๐ฉบ") | |
| info.centerAligned() | |
| info.onTap = async () => { | |
| let curnodes = [].concat(CURINSTA.nodes) | |
| let name = curnodes[SELECTED-OFFSET] | |
| let path = FM.joinPath(CURINSTA.location, name) | |
| let df = new DateFormatter() | |
| df.dateFormat = "yyyy-MM-dd HH:mm:ss" | |
| let ctime = FM.creationDate(path) | |
| INFOCACHE[path]["ctime"] = ctime | |
| let mtime = INFOCACHE[path]["mtime"] | |
| let fctime = df.string(new Date(ctime)) | |
| let fmtime = df.string(new Date(mtime)) | |
| let type = INFOCACHE[path]["type"] | |
| let suffix = undefined | |
| if (type) { | |
| CURINSTA.cacheAllSubNodes(CURINSTA.location, name) | |
| let idx = SELECTED - OFFSET | |
| ROWSCONTAINER[idx][0] = createBaseRow(curnodes[idx]) | |
| ROWSCONTAINER[idx][1] = createAltRow(idx) | |
| let items = Object.keys(INFOCACHE) | |
| let filecount = 0 | |
| let dircount = 0 | |
| let size = 0 | |
| for (let v of items) { | |
| if (v == path) continue | |
| if (v.startsWith(path)) { | |
| if (INFOCACHE[v]["type"]) { | |
| dircount++ | |
| } else { | |
| filecount++ | |
| size += INFOCACHE[v]["size"] | |
| } | |
| } | |
| } | |
| suffix = `Contains ${dircount} directories, ${filecount} files.\nTotal size: ${format(size)}` | |
| } else { | |
| let uti = INFOCACHE[path]["uti"] | |
| if (!uti) { | |
| uti = FM.getUTI(path) | |
| INFOCACHE[path]["uti"] = uti | |
| } | |
| suffix = `UTI: ${uti}` | |
| } | |
| let msg = `Created: ${fctime}\nModified: ${fmtime}\n` | |
| msg += suffix | |
| let alert = new Alert() | |
| alert.title = name | |
| alert.message = msg | |
| alert.addAction("Copy") | |
| alert.addAction("OK") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| Pasteboard.copy(msg) | |
| } | |
| } | |
| return row | |
| } | |
| function createHeadRow() { | |
| let row = new UITableRow() | |
| row.isHeader = true | |
| row.dismissOnSelect = false | |
| let top = row.addButton("๐") | |
| top.onTap = () => { | |
| if (SELECTED != undefined) return | |
| CURINSTA.goHome() | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| for(let [k, v] of Object.entries(VOLUMES)) { | |
| let vol = row.addButton(VOLICON[k]) | |
| vol.onTap = () => { | |
| if (k == OVOLIDX) return | |
| VOLINDEX = k | |
| CURINSTA = VOLUMES[VOLINDEX] | |
| OVOLIDX = new Number(k) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| } | |
| let mount = row.addButton("๐ช") | |
| mount.onTap = async () => { | |
| if (SELECTED != undefined) return | |
| let root = await DocumentPicker.openFolder() | |
| if (!root) return | |
| let count = Object.keys(VOLUMES).length | |
| if (count >= VOLICON.length) { | |
| let err = new Notification() | |
| err.title = "Too many tabs!" | |
| err.body = "If you need to load this directory, please relaunch the app." | |
| err.schedule() | |
| return | |
| } | |
| let ninsta = new FilesTree(root) | |
| VOLINDEX++ | |
| VOLUMES[VOLINDEX] = ninsta | |
| CURINSTA = VOLUMES[VOLINDEX] | |
| OVOLIDX = new Number(VOLINDEX) | |
| APPTABLE.removeAllRows() | |
| HEADCONTAINER[0] = createHeadRow() | |
| HEADCONTAINER[1] = createAltHeadRow() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| let newfile = row.addButton("๐") | |
| newfile.onTap = async () => { | |
| if (SELECTED != undefined) return | |
| let alert = new Alert() | |
| alert.title = "Create new file:" | |
| alert.addTextField("") | |
| alert.addDestructiveAction("OK") | |
| alert.addCancelAction("Cancel") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| let text = alert.textFieldValue(0).trim() | |
| if (text) { | |
| CURINSTA.createFile(text) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| } | |
| } | |
| let newdir = row.addButton("๐") | |
| newdir.onTap = async () => { | |
| if (SELECTED != undefined) return | |
| let alert = new Alert() | |
| alert.title = "Create new directory:" | |
| alert.addTextField("") | |
| alert.addDestructiveAction("OK") | |
| alert.addCancelAction("Cancel") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| let text = alert.textFieldValue(0).trim() | |
| if (text) { | |
| CURINSTA.createDirectory(text) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| } | |
| } | |
| let ms = row.addButton("โ๏ธ") | |
| ms.onTap = () => { | |
| if (SELECTED != undefined) return | |
| if (ROWSCONTAINER.length == 0) return | |
| MSMODE = true | |
| MSREGISTER = [] | |
| MSARRAY = new Uint8Array(ROWSCONTAINER.length).fill(0) | |
| APPTABLE.removeAllRows() | |
| if (MSHEADROW == undefined) { | |
| MSHEADROW = createMSHeadRow() | |
| } | |
| APPTABLE.addRow(MSHEADROW) | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| fillMSRows() | |
| ROWSCONTAINER.forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| } | |
| return row | |
| } | |
| function createAltHeadRow() { | |
| let nrow = createHeadRow() | |
| let paste = nrow.addButton("๐ฅ") | |
| paste.onTap = () => { | |
| if (SELECTED != undefined) return | |
| APPTABLE.removeAllRows() | |
| CURINSTA.copy() | |
| MSREGISTER = [] | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| return nrow | |
| } | |
| function createLocationRow() { | |
| let row = new UITableRow() | |
| row.dismissOnSelect = false | |
| row.backgroundColor = new Color("#F2F2F2", 1) | |
| row.height = 20 | |
| let parts = CURINSTA.location.split("/") | |
| let last2 = parts.slice(parts.length - 2, parts.length) | |
| let prefix | |
| if (MSMODE) { | |
| prefix = "โ " + String(MSARRAY.reduce((a, b) => a + b)) + " " | |
| } else { | |
| prefix = CURINSTA.location != CURINSTA.root ? "โง " : "โบ " | |
| } | |
| let text = row.addText(prefix + last2.join("/")) | |
| text.titleFont = Font.thinMonospacedSystemFont(14) | |
| if (!MSMODE) { | |
| row.onSelect = (number) => { | |
| if (SELECTED != undefined) return | |
| CURINSTA.travelUpward() | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| } | |
| return row | |
| } | |
| function createBaseRow(name) { | |
| let row = new UITableRow() | |
| row.dismissOnSelect = false | |
| let path = FM.joinPath(CURINSTA.location, name) | |
| let type = INFOCACHE[path]["type"] | |
| let icon = row.addText(type ? "๐" : "๐") | |
| icon.widthWeight = 10 | |
| let size = INFOCACHE[path]["size"] | |
| let fsize = (size != undefined) ? format(size) : "? KB" | |
| let mtime = INFOCACHE[path]["mtime"] | |
| let df = new DateFormatter() | |
| df.dateFormat = "yyyy-MM-dd HH:mm:ss" | |
| let fmtime = df.string(new Date(mtime)) | |
| let count = INFOCACHE[path]["count"] | |
| let fcount = (count != undefined) ? `${count} items` : "? items" | |
| let subt = type ? `${fcount} ${fmtime}` : `${fsize} ${fmtime}` | |
| let tcell = row.addText(name, subt) | |
| tcell.widthWeight = 80 | |
| tcell.titleFont = Font.systemFont(16) | |
| tcell.titleColor = Color.darkGray() | |
| tcell.subtitleFont = Font.systemFont(12) | |
| tcell.subtitleColor = Color.lightGray() | |
| if (MSMODE) { | |
| let ccell = row.addText("โ") | |
| ccell.widthWeight = 10 | |
| ccell.centerAligned() | |
| } | |
| if (!MSMODE) { | |
| row.onSelect = (number) => { | |
| if (SELECTED != undefined) return | |
| let idx = number - OFFSET | |
| if (ROWSCONTAINER[idx][1] == undefined) { | |
| let nrow = createAltRow(idx) | |
| ROWSCONTAINER[idx][1] = nrow | |
| } | |
| ROWSCONTAINER.slice(idx) | |
| .forEach((e) => APPTABLE.removeRow(e[0])) | |
| APPTABLE.addRow(ROWSCONTAINER[idx][1]) | |
| ROWSCONTAINER.slice(idx + 1) | |
| .forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| SELECTED = number | |
| } | |
| } else { | |
| row.onSelect = (number) => { | |
| APPTABLE.removeAllRows() | |
| let idx = number - OFFSET | |
| MSARRAY[idx] = Number(!Boolean(MSARRAY[idx])) | |
| let name = CURINSTA.nodes[idx] | |
| let path = FM.joinPath(CURINSTA.location, name) | |
| if (!INFOCACHE[path]["type"]) { | |
| let uti = INFOCACHE[path]["uti"] | |
| if (!uti) { | |
| uti = FM.getUTI(path) | |
| INFOCACHE[path]["uti"] = uti | |
| } | |
| if (MSARRAY[idx]) LASTUTI = uti | |
| } | |
| APPTABLE.addRow(MSHEADROW) | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| ROWSCONTAINER.forEach( | |
| (e, i) => APPTABLE.addRow(e[MSARRAY[i]]) | |
| ) | |
| APPTABLE.reload() | |
| } | |
| } | |
| return row | |
| } | |
| function fillRows() { | |
| CURINSTA.nodes.forEach(function(e) { | |
| let rowholder = new Array(2) | |
| let row = createBaseRow(e) | |
| rowholder[0] = row | |
| ROWSCONTAINER.push(rowholder) | |
| }) | |
| } | |
| function createAltRow(idx) { | |
| let curnodes = [].concat(CURINSTA.nodes) | |
| let nrow = createBaseRow(curnodes[idx]) | |
| let path = FM.joinPath(CURINSTA.location, curnodes[idx]) | |
| let back = nrow.addButton("๐") | |
| back.widthWeight = 15 | |
| back.onTap = () => { | |
| APPTABLE.removeRow(ROWSCONTAINER[idx][1]) | |
| ROWSCONTAINER.slice(idx + 1) | |
| .forEach((e) => APPTABLE.removeRow(e[0])) | |
| ROWSCONTAINER.slice(idx) | |
| .forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| if (CURINSTA.location != CURINSTA.root) { | |
| let up = nrow.addButton("๐บ") | |
| up.widthWeight = 15 | |
| up.onTap = () => { | |
| CURINSTA.travelUpward() | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| } | |
| let type = INFOCACHE[path]["type"] | |
| if (type) { | |
| let down = nrow.addButton("๐ป") | |
| down.widthWeight = 15 | |
| down.onTap = () => { | |
| let dirname = curnodes[idx] | |
| CURINSTA.travelDownward(dirname) | |
| APPTABLE.removeAllRows() | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| } | |
| let more = nrow.addButton("๐ก") | |
| more.widthWeight = 15 | |
| more.onTap = () => { | |
| if (ACTIONROW == undefined) ACTIONROW = createActionRow() | |
| APPTABLE.removeRow(ROWSCONTAINER[idx][1]) | |
| ROWSCONTAINER.slice(idx + 1) | |
| .forEach((e) => APPTABLE.removeRow(e[0])) | |
| APPTABLE.addRow(ACTIONROW) | |
| ROWSCONTAINER.slice(idx + 1) | |
| .forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| } | |
| if (!type) { | |
| let share = nrow.addButton("๐ง") | |
| share.widthWeight = 15 | |
| share.onTap = () => { | |
| let fname = curnodes[idx] | |
| let fpath = FM.joinPath(CURINSTA.location, fname) | |
| QuickLook.present(fpath, false) | |
| APPTABLE.removeRow(ROWSCONTAINER[idx][1]) | |
| ROWSCONTAINER.slice(idx + 1) | |
| .forEach((e) => APPTABLE.removeRow(e[0])) | |
| ROWSCONTAINER.slice(idx) | |
| .forEach((e) => APPTABLE.addRow(e[0])) | |
| APPTABLE.reload() | |
| SELECTED = undefined | |
| } | |
| } | |
| return nrow | |
| } | |
| function createMSHeadRow() { | |
| let row = new UITableRow() | |
| row.isHeader = true | |
| row.dismissOnSelect = false | |
| let all = row.addButton("๐ธ") | |
| all.centerAligned() | |
| all.onTap = () => { | |
| if (MSARRAY.every((e) => e)) return | |
| APPTABLE.removeAllRows() | |
| MSARRAY.fill(1) | |
| APPTABLE.addRow(MSHEADROW) | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| ROWSCONTAINER.forEach((e) => APPTABLE.addRow(e[1])) | |
| APPTABLE.reload() | |
| } | |
| let same = row.addButton("๐งฒ") | |
| same.centerAligned() | |
| same.onTap = () => { | |
| if (MSARRAY.every((e) => e)) return | |
| if (!LASTUTI) return | |
| APPTABLE.removeAllRows() | |
| let curl = (" " + CURINSTA.location).slice(1) | |
| CURINSTA.nodes.forEach((e, i) => { | |
| let path = FM.joinPath(curl, e) | |
| if (!INFOCACHE[path]["type"]) { | |
| let uti = INFOCACHE[path]["uti"] | |
| if (!uti) { | |
| uti = FM.getUTI(path) | |
| INFOCACHE[path]["uti"] = uti | |
| } | |
| if (uti == LASTUTI) MSARRAY[i] = 1 | |
| } | |
| }) | |
| APPTABLE.addRow(MSHEADROW) | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| ROWSCONTAINER.forEach((e, i) => APPTABLE.addRow(e[MSARRAY[i]])) | |
| APPTABLE.reload() | |
| } | |
| let reverse = row.addButton("โ๏ธ") | |
| reverse.centerAligned() | |
| reverse.onTap = () => { | |
| APPTABLE.removeAllRows() | |
| MSARRAY.forEach( | |
| (e, i) => MSARRAY[i] = Number(!Boolean(e)) | |
| ) | |
| APPTABLE.addRow(MSHEADROW) | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| ROWSCONTAINER.forEach( | |
| (e, i) => APPTABLE.addRow(e[MSARRAY[i]]) | |
| ) | |
| APPTABLE.reload() | |
| } | |
| let copy = row.addButton("๐") | |
| copy.centerAligned() | |
| copy.onTap = () => { | |
| if (MSARRAY.every((e) => !e)) return | |
| let curl = (" " + CURINSTA.location).slice(1) | |
| CURINSTA.nodes.forEach((e, i) => { | |
| if (MSARRAY[i]) { | |
| let path = FM.joinPath(curl, e) | |
| MSREGISTER.push(path) | |
| } | |
| }) | |
| APPTABLE.removeAllRows() | |
| MSMODE = false | |
| LASTUTI = undefined | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| let del = row.addButton("๐งน") | |
| del.centerAligned() | |
| del.onTap = async () => { | |
| if (MSARRAY.every((e) => !e)) return | |
| let names = CURINSTA.nodes.filter((e, i) => MSARRAY[i]) | |
| let alert = new Alert() | |
| let count = names.length | |
| alert.title = `Really want to delete ${count} items?` | |
| alert.message = names.slice(0, 3).join(", ") + | |
| (count >= 3 ? " ... " : "") | |
| alert.addDestructiveAction("Sure") | |
| alert.addCancelAction("Cancel") | |
| let resp = await alert.present() | |
| if (resp == 0) { | |
| CURINSTA.remove(names) | |
| APPTABLE.removeAllRows() | |
| MSMODE = false | |
| MSREGISTER = [] | |
| LASTUTI = undefined | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| } | |
| let end = row.addButton("๐") | |
| end.centerAligned() | |
| end.onTap = () => { | |
| APPTABLE.removeAllRows() | |
| MSMODE = false | |
| MSREGISTER = [] | |
| LASTUTI = undefined | |
| ROWSCONTAINER = [] | |
| renderTable() | |
| APPTABLE.reload() | |
| } | |
| return row | |
| } | |
| function fillMSRows() { | |
| CURINSTA.nodes.forEach(function(e, i) { | |
| let nrow = createBaseRow(e) | |
| ROWSCONTAINER[i][0].onSelect = nrow.onSelect | |
| ROWSCONTAINER[i][1] = nrow | |
| }) | |
| } | |
| function renderTable() { | |
| if (REGISTER || MSREGISTER.length) { | |
| if (HEADCONTAINER[1] == undefined) { | |
| HEADCONTAINER[1] = createAltHeadRow() | |
| } | |
| APPTABLE.addRow(HEADCONTAINER[1]) | |
| } else { | |
| if (HEADCONTAINER[0] == undefined) { | |
| HEADCONTAINER[0] = createHeadRow() | |
| } | |
| APPTABLE.addRow(HEADCONTAINER[0]) | |
| } | |
| LOCATIONROW = createLocationRow() | |
| APPTABLE.addRow(LOCATIONROW) | |
| if (ROWSCONTAINER.length == 0) fillRows() | |
| ROWSCONTAINER.forEach((e) => APPTABLE.addRow(e[0])) | |
| } | |
| let home = FileManager.iCloud().documentsDirectory() | |
| let scriptable = new FilesTree(home) | |
| VOLUMES[VOLINDEX] = scriptable | |
| CURINSTA = VOLUMES[VOLINDEX] | |
| OVOLIDX = new Number(VOLINDEX) | |
| renderTable() | |
| await APPTABLE.present(true) | |
| // vim:set et sw=2 ts=2: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The script is meant to run in Scriptable app under iOS.