forked from githubdulong/Script
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScriptDude.scriptable
More file actions
12 lines (11 loc) · 14.1 KB
/
Copy pathScriptDude.scriptable
File metadata and controls
12 lines (11 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
{
"always_run_in_app" : false,
"icon" : {
"color" : "deep-brown",
"glyph" : "user-astronaut"
},
"name" : "ScriptDude",
"script" : "\/\/ This script was downloaded using ScriptDude.\n\/\/ Do not remove these lines, if you want to benefit from automatic updates.\n\/\/ source: https:\/\/scriptdu.de\/script.js; docs: https:\/\/scriptdu.de\/; hash: 655675326;\n\n\/\/ Variables used by Scriptable.\n\/\/ These must be at the very top of the file. Do not edit.\n\/\/ icon-color: deep-gray; icon-glyph: magic;\n\/\/ This script was downloaded using ScriptDude.\n\/\/ Do not remove these lines, if you want to benefit from automatic updates.\n\/\/ source: https:\/\/scriptdu.de\/script.js; docs: https:\/\/scriptdu.de\/; hash: -1503760114;\n\nclass ScriptDude {\n \n constructor() {\n try {\n this.fileManager = FileManager.iCloud()\n } catch(e) {\n this.fileManager = FileManager.local()\n }\n this.documentsDirectory = this.fileManager.documentsDirectory()\n this.updateableScripts = [];\n this.uptodateScripts = [];\n this.table = new UITable();\n }\n \n ensureCorrectScriptNaming() {\n const scriptName = 'ScriptDude';\n if(Script.name() != scriptName) {\n let alert = new Alert();\n alert.title = 'Wrong script name';\n alert.message = `In order to work properly, this script needs to be named \"${scriptName}\". Please rename the script by clicking on its title and run it again.`;\n alert.addCancelAction(\"Okay\");\n alert.presentAlert();\n Script.complete();\n throw \"Wrong script name\";\n }\n }\n \n makeUrlUpdateable(url) {\n \/\/ Strip revision from gist\n if(url.startsWith(\"https:\/\/gist.githubusercontent.com\/\")) {\n let parts = url.split('\/');\n if(parts.length == 8) {\n url = parts.filter((el, i) => i != 6).join('\/');\n }\n }\n return url;\n }\n\n hashCode(input) {\n return Array.from(input).reduce((accumulator, currentChar) => Math.imul(31, accumulator) + currentChar.charCodeAt(0), 0)\n }\n \n render() {\n this.table.removeAllRows();\n this.getPackageUI().map(row => this.table.addRow(row));\n this.table.reload();\n }\n \n getPackageUI() {\n let rows = []; \n if(this.updateableScripts.length) {\n rows.push(...this.getPackageUISection(\"Updates\", this.updateableScripts));\n }\n if(this.uptodateScripts.length) {\n rows.push(...this.getPackageUISection(\"Installed\", this.uptodateScripts));\n }\n let manualInstall = new UITableRow();\n let manualInstallButton = manualInstall.addButton(\"Install from URL\");\n manualInstallButton.onTap = () => {\n this.getInstallationUI();\n }\n rows.push(manualInstall);\n let scriptablesLink = new UITableRow();\n let scriptablesLinkButton = scriptablesLink.addButton(\"Browse scriptables.net\");\n scriptablesLinkButton.onTap = () => {\n Safari.open('https:\/\/scriptables.net\/');\n }\n rows.push(scriptablesLink);\n return rows;\n }\n \n getPackageUISection(title, scripts) {\n let rows = [];\n let header = new UITableRow();\n let text = header.addText(title);\n text.titleFont = Font.headline();\n rows.push(header, ...scripts.map(this.getPackageUIRow.bind(this)), new UITableRow());\n return rows;\n }\n\n mergeHeaders(codeOld, codeNew)\n {\n let icon = null, color = null;\n \n if (!!codeOld)\n {\n let lines = codeOld.split(\"\\n\", 20);\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex];\n let match = line.match(\/icon-color: ([\\w\\-]*);\/);\n if (match)\n color = match[1];\n match = line.match(\/icon-glyph: ([\\w\\-]*);\/);\n if (match)\n icon = match[1];\n if (icon != null && color != null)\n break;\n }\n }\n\n let scriptableHeader = \"\"; \/\/ detect Scriptable header\n if (codeNew.trim().startsWith('\/\/ Variables used by Scriptable'))\n {\n let lines = codeNew.split(\"\\n\"); \/\/ HINT: to improve speed, we could use just the first 20 lines of the code -> code.split(\"\\n\", 20);\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n let line = lines[lineIndex];\n if (line.indexOf('\/\/ Variables used by Scriptable.') != -1 || line.indexOf('\/\/ These must be at the very top of the file') != -1)\n {\n scriptableHeader += line + \"\\n\";\n codeNew = codeNew.substr(line.length + 1); \/\/ Remove scriptable header from original code\n }\n else if (line.match(\/\\\/\\\/( [\\w\\-]*: [\\w\\-]*;)+\/)) \/\/ Find \"key: value;\" list used by scriptable\n {\n codeNew = codeNew.substr(line.length + 1); \/\/ Remove scriptable header from original code\n let match = line.match(\/icon-color: ([\\w\\-]*);\/);\n if (match)\n line = line.replace(match[1], color);\n match = line.match(\/icon-glyph: ([\\w\\-]*);\/);\n if (match)\n line = line.replace(match[1], icon);\n\n scriptableHeader += line + \"\\n\";\n }\n else\n {\n break;\n }\n }\n }\n else\n {\n scriptableHeader = `\/\/ Variables used by Scriptable.\\n\/\/ These must be at the very top of the file. Do not edit.\\n\/\/ icon-color: ${color || 'blue'}; icon-glyph: ${icon || 'circle'};\\n`;\n }\n return [scriptableHeader, codeNew];\n }\n\n getPackageUIRow(script) {\n const iconWidth = 60;\n let row = new UITableRow();\n let text = row.addText(script.name, script.source.split('\/')[2]);\n text.subtitleFont = Font.systemFont(10)\n if(script.updateAvailable) {\n let updateButton = row.addButton(\"Update\");\n updateButton.titleColor = Color.red()\n updateButton.rightAligned();\n updateButton.onTap = () => {\n this.updateScript(script);\n }\n updateButton.widthWeight = iconWidth;\n }\n let documentationButton = row.addButton(\"Docs\");\n documentationButton.rightAligned();\n documentationButton.onTap = () => {\n Safari.open(script.docs)\n }\n documentationButton.widthWeight = iconWidth;\n let width = Device.screenSize().width - (script.updateAvailable ? iconWidth*2 : iconWidth);\n text.widthWeight = width;\n return row;\n }\n \n async installScript(name, sourceUrl, documentationUrl, icon, color, showMessage) {\n sourceUrl = this.makeUrlUpdateable(sourceUrl);\n let filePath = this.fileManager.joinPath(this.documentsDirectory, name + '.js');\n if(this.fileManager.fileExists(filePath)) {\n let error = new Alert();\n error.title = `A script with the name ${name} does already exist.`;\n error.presentAlert();\n return;\n }\n if(false != showMessage) {\n let warning = new Alert();\n warning.title = \"Warning\";\n warning.message = `Scriptable scripts can access sensitive data on your device. Make sure to check downloaded scripts before running them for the first time. Do you want to continue downloading \"${name}\" from \"${sourceUrl}\"?`;\n warning.addAction(\"Continue\");\n warning.addCancelAction(\"Cancel\");\n let result = await warning.presentAlert();\n if(-1 == result) {\n return;\n }\n }\n let req = new Request(sourceUrl);\n let codeOriginal = await req.loadString();\n let hash = this.hashCode(codeOriginal);\n let [scriptableHeader, code] = this.mergeHeaders(null, codeOriginal);\n\n let codeToStore = Data.fromString(`${scriptableHeader}\/\/ This script was downloaded using ScriptDude.\\n\/\/ Do not remove these lines, if you want to benefit from automatic updates.\\n\/\/ source: ${sourceUrl}; docs: ${documentationUrl}; hash: ${hash};\\n\\n${code}`);\n this.fileManager.write(filePath, codeToStore);\n this.showLoadingIndicator();\n this.updateScriptsData().then(() => { this.render() });\n }\n \n async getInstallationUI() {\n let install = new Alert()\n install.title = \"Install\"\n install.message = \"ScriptDude makes downloading and updating Scriptable scripts easy.\";\n install.addTextField(\"Script Name\")\n install.addTextField(\"Source URL\")\n install.addTextField(\"Documentation URL\")\n install.addAction(\"Install\")\n install.addCancelAction(\"Cancel\")\n let result = await install.presentAlert()\n if(0 == result) {\n let name = install.textFieldValue(0)\n let source = install.textFieldValue(1)\n let documentation = install.textFieldValue(2)\n await this.installScript(name, source, documentation)\n }\n }\n \n async run()\n {\n if(config.runsInWidget) {\n await this.updateScriptsData();\n Script.setWidget(this.getWidget())\n } else {\n this.ensureCorrectScriptNaming();\n this.showLoadingIndicator();\n await this.updateScriptsData();\n this.render();\n this.table.present(true);\n this.checkForInstallationRequestFromWeb();\n }\n }\n \n getWidget() {\n let list = new ListWidget();\n let header = list.addText(\"🧑🚀 ScriptDude\".toUpperCase());\n header.font = Font.mediumSystemFont(13);\n list.addSpacer();\n let number = list.addText(this.updateableScripts.length+\"\");\n number.font = Font.largeTitle();\n number.rightAlignText();\n let title = list.addText(\"Updates\".toUpperCase());\n title.font = Font.mediumSystemFont(13);\n title.rightAlignText();\n list.refreshAfterDate = new Date(Date.now() + 60*60*1000);\n return list;\n }\n \n checkForInstallationRequestFromWeb() {\n try {\n let data = args.queryParameters;\n if(data.name && data.source && data.docs) {\n this.installScript(data.name, data.source, data.docs, data.icon, data.color, true);\n }\n } catch(e) {\n \/\/ Input malformed \n }\n }\n \n showLoadingIndicator() {\n let row = new UITableRow();\n row.addText(\"Loading\", \"Please be patient, ScriptDude is collecting information about your scripts and checks for available updates.\");\n row.height = 100;\n this.table.removeAllRows();\n this.table.addRow(row);\n this.table.reload();\n }\n \n updateScript(script)\n {\n let [scriptableHeader, code] = this.mergeHeaders(script.content, script.updatePayload.code);\n let codeToStore = Data.fromString(`${scriptableHeader}\/\/ This script was downloaded using ScriptDude.\\n\/\/ Do not remove these lines, if you want to benefit from automatic updates.\\n\/\/ source: ${script.source}; docs: ${script.docs}; hash: ${script.updatePayload.hash};\\n\\n${code}`);\n this.fileManager.write(script.path, codeToStore);\n this.showLoadingIndicator();\n this.updateScriptsData().then(() => { this.render() })\n }\n \n async updateScriptsData() {\n let files = this.fileManager.listContents(this.documentsDirectory)\n let managedScripts = files\n \/\/ Convert to full paths\n .map(fileName => this.fileManager.joinPath(this.documentsDirectory, fileName))\n \/\/ Remove directories\n .filter(filePath => !this.fileManager.isDirectory(filePath))\n \/\/ Add file name and content metadata\n .map(filePath => {\n return {\n path: filePath,\n name: this.fileManager.fileName(filePath),\n content: this.fileManager.read(filePath).toRawString()\n };\n })\n \/\/ Filter for scripts that show up in Scriptable\n .filter(file => file.content && file.content.trimLeft().startsWith(\"\/\/ Variables used by Scriptable.\"))\n \/\/ Add source and origin metadata\n .map(file => {\n let potentialScriptData = file.content\n .split(\"\\n\", 50) \/\/ Scan first max. 50 lines\n .filter(line => line.length != 0) \/\/ Skip empty lines\n .filter(line => line.indexOf('\/\/') != -1) \/\/ Take only comments\n .map(line => line.substr(2).trim()) \/\/ Remove comment slashes\n .filter(line => \n line.indexOf('source:') != -1 \n && line.indexOf('hash:') != -1\n && line.indexOf('docs:') != -1\n );\n if(!!potentialScriptData && potentialScriptData.length > 0) {\n let customMetadata = potentialScriptData[0]\n .split(';')\n .map(keyValue => keyValue.split(':').map(text => text.trim()))\n .filter(keyValue => keyValue[0].length)\n .reduce((dict, addable) => { \n dict[addable.shift()] = addable.join(':');\n return dict;\n }, {});\n if(!!customMetadata['source'] \n && !!customMetadata['hash']) {\n file.source = this.makeUrlUpdateable(customMetadata['source']);\n file.hash = customMetadata['hash'];\n file.docs = customMetadata['docs'] || '';\n }\n }\n return file;\n })\n \/\/ Filter for scripts managed by Scriptstore\n .filter(file => !!file.source && !!file.hash);\n \n managedScripts = await Promise.all(managedScripts.map(async (script) => {\n let req = new Request(script.source);\n let code = await req.loadString();\n let hash = this.hashCode(code);\n script.updateAvailable = hash != script.hash;\n script.updatePayload = {\n hash: hash,\n code: code\n }\n return script;\n }));\n managedScripts = managedScripts.sort((a, b) => a.name < b.name ? -1 : 1);\n this.updateableScripts = managedScripts.filter(script => script.updateAvailable);\n this.uptodateScripts = managedScripts.filter(script => !script.updateAvailable);\n }\n \n }\n \n await new ScriptDude().run();\n Script.complete();",
"share_sheet_inputs" : [
]
}