-
Notifications
You must be signed in to change notification settings - Fork 33
/
Copy pathImport-Script.scriptable
13 lines (13 loc) · 13.4 KB
/
Import-Script.scriptable
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"always_run_in_app" : false,
"icon" : {
"color" : "blue",
"glyph" : "download"
},
"name" : "Import-Script",
"script" : "\n\/* -----------------------------------------------\nScript : Import-Script.js\nAuthor : [email protected]\nVersion : 1.5.2\nDescription :\n A script to download and import files into the\n Scriptable folder. Includes a mini repo file \n browser for github repos.\n\nSupported Sites\n* github.com\n* gist.github.com\n* pastebin.com\n* hastebin.com\n* raw code from the clipboard\n\nChangelog:\nv1.5.2 - (fix) detection errors on last version\nv1.5.1 - (fix) unrecognized github file urls\nv1.5.0 - (new) ability to accept urls via the \n queryString argument. This is to allow\n creating scriptable:\/\/\/ links on webpages\n to download scripts\nv1.4.0 - (new) option to use local storage instead\n of iCloud for users who don't have \n iCloud enabled\nv1.3.0 - (new) hastebin.com support\nv1.2.0 - (update)renamed to Import-Script.js\n - (fix) script names with spaces are saved \n with URL Encoding\nv1.1.1 - fix gist error introduced in v1.1\nv1.1.0 - support for gists with multiple files\nv1.0.0 - Initial releast\n----------------------------------------------- *\/\n\n\/\/ detect is icloud is used\nconst USE_ICLOUD = module.filename.includes('Documents\/iCloud~')\n\nlet url;\nlet data;\n\n\/\/ if there are no urls passed via the share sheet\n\/\/ get text from the clipboard\nif (args.urls.length > 0) {\n input = args.urls[0]\n} else if (args.queryParameters.url) {\n input = args.queryParameters.url\n} else {\n input = Pasteboard.paste()\n}\n\n\n\/\/await presentAlert(`[${input}]`)\n\n\/\/ exit if there's no input\nif (!input) {\n log('nothing to work with')\n return\n}\n\nlog(`input: ${input}`)\n\n\/\/ identify if the input is one of the supported\n\/\/ websites. if not, then it might be raw code.\n\/\/ ask the user about it\nvar urlType = getUrlType(input)\nlog(urlType)\n\/\/await presentAlert(JSON.stringify(urlType))\nif (!urlType) {\n let resp = await presentAlert('Unable to identify urls from the input. Is it already the actual code?', [\"Yes\",\"No\"])\n if (resp==0) {\n urlType = {name:'code'}\n } else {\n await presentAlert('Unsupported input.')\n return\n }\n}\n\n\/\/ store the information into a common structure\nswitch (urlType.type) {\n case 'repo': \n data = await pickFileFromRepo(input, '')\n break;\n case 'repo-folder':\n data = await pickFileFromRepo(urlType.slices[urlType.repoIndex], urlType.slices[urlType.pathIndex])\n break;\n case 'repo-file': \n data = await getRepoFileDetails(urlType.slices[urlType.repoIndex],urlType.slices[urlType.pathIndex])\n break;\n case 'raw': \n var slices = input.match(urlType.regex)\n data = {\n source: 'rawurl',\n name: decodeURIComponent(`${urlType.slices[urlType.nameIndex]}${urlType.extension}`),\n download_url: input\n }\n break;\n case 'gist': \n data = await pickFileFromGist(urlType.slices[urlType.idIndex])\n break;\n case 'pastebin': \n data = {\n source: 'pastebin',\n name: `${urlType.slices[urlType.nameIndex]}${urlType.extension}`,\n download_url: input.replace('.com','.com\/raw')\n }\n break;\n case 'code':\n data = {\n source: 'raw',\n name: 'Untitled.js',\n code: input\n } \n break;\n default:\n}\n\nlog('data')\nlog(data)\n\nif (data) {\n let importedFile = await importScript(data)\n if (importedFile) {\n await presentAlert(`Imported ${importedFile}`,[\"OK\"])\n }\n return\n}\n\n\/\/------------------------------------------------\nfunction getUrlType(url) {\n const typeMatchers = [\n {name: 'gh-repo', \n regex: \/^https:\\\/\\\/github.com\\\/[^\\s\\\/]+\\\/[^\\s\\\/]+\\\/?$\/,\n type: 'repo',\n repoIndex: 0\n },\n {name: 'gh-repo-folder', \n regex: \/^(https:\\\/\\\/github.com\\\/[^\\s\\\/]+\\\/[^\\s\\\/]+)\\\/tree\\\/[^\\s\\\/]+(\\\/[^\\s]+\\\/?)$\/ ,\n type: 'repo-folder',\n repoIndex: 1,\n pathIndex: 2\n },\n {name: 'gh-repo-file-noblob', \n regex: \/^(https:\\\/\\\/github.com\\\/[^\\s\\\/]+\\\/[^\\s\\\/]+)\\\/(?!blob)([^\\s]+\\.[a-zA-Z\\d]+)$\/,\n type: 'repo-file',\n repoIndex: 1,\n pathIndex: 2\n },\n {name: 'gh-repo-file', \n regex: \/^(https:\\\/\\\/github.com\\\/[^\\s\\\/]+\\\/[^\\s\\\/]+)\\\/blob\\\/[^\\s\\\/]+(\\\/[^\\s]+)$\/,\n type: 'repo-file',\n repoIndex: 1,\n pathIndex: 2\n },\n {name: 'gh-repo-raw', \n regex: \/^https:\\\/\\\/raw\\.githubusercontent\\.com(\\\/[^\\\/\\s]+)+\\\/([^\\s]+)\/,\n type: 'raw',\n nameIndex: 2\n },\n {name: 'gh-gist', \n regex: \/^(https:\\\/\\\/gist\\.github.com\\\/)([^\\\/]+)\\\/([a-z0-9]+)$\/,\n type: 'gist',\n idIndex: 3\n },\n {name: 'gh-gist-raw', \n regex: \/^https:\\\/\\\/gist\\.githubusercontent\\.com\\\/[^\\\/]+\\\/[^\\\/]+\\\/raw\\\/[^\\\/]+\\\/(.+)$\/,\n type: 'raw',\n nameIndex: 1\n },\n {name: 'pastebin-raw', \n regex: \/^https:\\\/\\\/pastebin\\.com\\\/raw\\\/([a-zA-Z\\d]+)$\/,\n type: 'raw',\n nameIndex: 1,\n extension: '.js'\n },\n {name: 'pastebin', \n regex: \/^https:\\\/\\\/pastebin\\.com\\\/(?!raw)([a-zA-Z\\d]+)\/,\n type: 'pastebin',\n nameIndex: 1,\n extension: '.js'\n },\n {name: 'hastebin', \n regex: \/^https:\\\/\\\/hastebin\\.com\\\/([a-z]+\\.[a-z]+)$\/,\n type: 'pastebin',\n nameIndex: 1\n },\n {name: 'hastebin-raw', \n regex: \/^https:\\\/\\\/hastebin\\.com\\\/raw\\\/([a-z]+\\.[a-z]+)$\/,\n type: 'raw',\n nameIndex: 1\n }\n ]\n let types = typeMatchers.filter( matcher => {\n return matcher.regex.test(url)\n })\n\n var type;\n if (types.length) {\n type = types[0]\n type['slices'] = url.match(type.regex)\n if (!type.hasOwnProperty('extension')) {\n type.extension = ''\n }\n }\n\n return type\n}\n\/\/------------------------------------------------\nasync function pickFileFromRepo(url, path) {\n\n log('fn:pickFileFromRepo')\n log(`url = ${url}`)\n log(`path = ${path}`)\n\n url = url.replace(\/\\\/$\/,'')\n const apiUrl = url.replace('\/github.com\/',\n 'api.github.com\/repos\/')\n\n log(`apiURL=${apiUrl}`)\n\n let req = new Request(apiUrl)\n try {\n var data = await req.loadJSON()\n } catch (e) {\n await presentAlert(\"Unable to fetch repo information. Likely due to api limits\", [\"OK\"])\n return null\n }\n \n let contents_url = data.contents_url\n log(`contents_url = ${contents_url}`)\n\n \/\/ get contents\n contents_url = contents_url.replace('{+path}',path)\n req = new Request(contents_url)\n try {\n var contents = await req.loadJSON()\n } catch (e) {\n await presentAlert(\"Unable to fetch repo information. Likely due to api limits\", [\"OK\"])\n return null\n }\n \n log(contents.map(c=>c.name).join(\"\\n\"))\n\n let table = new UITable()\n let list = []\n\n \/\/ add a .. entry if path is passed\n if (path) {\n list.push({\n name: '..',\n type: 'dir',\n path: '..'\n })\n }\n\n list.push(contents)\n list = list.flat().sort( (a,b) => {\n if (a.type==b.type) {\n if (a.name.toLowerCase() < b.name.toLowerCase()) {\n return -1\n } else if (a.name.toLowerCase() > b.name.toLowerCase()) {\n return 1\n }\n } else {\n if (a.type == 'dir' ) {\n return -1\n } else if (b.type == 'dir' ) {\n return 1\n }\n }\n\n return 0\n })\n \n let selected;\n list.forEach( content => {\n const row = new UITableRow()\n\n let name = content.name\n let display_name = content.type == 'dir' ? `${name}\/` : name\n if (name=='..') display_name = name\n \n let icon = content.type=='dir'?(name=='..'?'arrow.left':'folder'):'doc'\n let sfIcon = SFSymbol.named(`${icon}.circle`)\n sfIcon.applyFont(Font.systemFont(25))\n let img = sfIcon.image\n let iconCell = row.addImage(img)\n iconCell.widthWeight = 10\n iconCell.centerAligned()\n\n let nameCell = row.addText(display_name)\n nameCell.widthWeight = 90\n\n row.onSelect = (index) => {\n selected = list[index]\n }\n\n table.addRow(row)\n })\n\n let resp = await table.present()\n\n if (!selected) return null\n\n log(selected.name)\n \n if (selected.type == 'dir') {\n if (selected.name == '..') {\n const lastPath = path.split('\/').reverse().slice(1).reverse().join('\/')\n selected = await pickFileFromRepo(url, lastPath)\n } else {\n selected = await pickFileFromRepo(url, selected.path)\n }\n }\n\n if (selected) {\n return {\n source: 'repo',\n name: selected.name,\n download_url: selected.download_url\n }\n } \n return null\n}\n\/\/------------------------------------------------\nasync function getRepoFileDetails(repoUrl, path) {\n\n repoUrl = repoUrl.replace(\/\\\/$\/,'')\n path = path.replace(\/^\\\/\/,'')\n\n log(`repo ${repoUrl}`)\n log(`path ${path}`)\n path = path.replace(\/blob\\\/[^\\\/]+\/,'')\n log(`path ${path}`)\n\n let apiUrl = repoUrl.replace('\/github.com\/',`api.github.com\/repos\/`)\n apiUrl = `${apiUrl}\/contents\/${path}`\n const req = new Request(apiUrl)\n try {\n var resp = await req.loadJSON()\n log(resp)\n if (resp.message) {\n await presentAlert(resp.message)\n return null\n }\n \n } catch(e) {\n log(e.message)\n await presentAlert(`Unable to fetch repo information - ${e.message}`, [\"OK\"])\n return null\n }\n\n const data = {\n source: 'repo',\n name: resp.name,\n path: resp.path,\n download_url: resp.download_url\n }\n return data\n\n}\n\/\/------------------------------------------------\nasync function pickFileFromGist(gistId) {\n let apiUrl = `https:\/\/api.github.com\/gists\/${gistId}` \n log(apiUrl)\n const req = new Request(apiUrl)\n\n try {\n var gist = await req.loadJSON()\n } catch(e) {\n await presentAlert(\"Unable to fetch repo information. Likely due to api limits\", [\"OK\"])\n return null\n }\n\n let filenames = Object.keys(gist.files)\n log(filenames)\n \/\/ don't show browser if just one file\n if (filenames.length == 1) {\n let file = gist.files[filenames[0]]\n log(file)\n return {\n source: 'gist',\n name: file.filename,\n download_url: file.raw_url\n } \n }\n\n let selected;\n\n let table = new UITable()\n filenames = filenames.sort()\n filenames.forEach( filename => {\n const row = new UITableRow()\n\n let sfIcon = SFSymbol.named(`doc.circle`)\n sfIcon.applyFont(Font.systemFont(25))\n let img = sfIcon.image\n let iconCell = row.addImage(img)\n iconCell.widthWeight = 10\n iconCell.centerAligned()\n\n let nameCell = row.addText(filename)\n nameCell.widthWeight = 90\n\n row.onSelect = (index) => {\n selected = filenames[index]\n }\n\n table.addRow(row)\n })\n\n await table.present()\n\n if (!selected) return null \n\n if (selected) {\n let file = gist.files[selected]\n return {\n source: 'gist',\n name: file.filename,\n download_url: file.raw_url\n }\n } \n\n\n}\n\/\/------------------------------------------------\nasync function importScript(data) {\n \n var fm = USE_ICLOUD ? FileManager.iCloud() :\n FileManager.local()\n \n log(`fn:importScript`)\n log(data.source)\n log(data.name)\n\n var code;\n var name = data.name\n\n if (data.source == 'raw' ) {\n code = data.code\n } else {\n let url = data.download_url\n let resp = await presentAlert(`Download ${name}?`,[\"Yes\",\"No\"])\n if (resp==0) {\n log(`downloading from ${url}`)\n const req = new Request(url)\n code = await req.loadString()\n } \n }\n\n if (code) {\n\n let filename = name\n let fileExists = true\n\n while(fileExists) {\n filename = await presentPrompt(\"Save as\", filename)\n log(filename)\n\n if (filename) {\n let savePath = fm.joinPath(fm.documentsDirectory(), filename)\n fileExists = fm.fileExists(savePath)\n log(fileExists)\n\n if (fileExists) {\n let resp = await presentAlert('File exists. Overwrite?',[\"Yes\",\"No\",\"Cancel\"])\n if (resp==2) {\n fileExists = false\n filename = null\n } else {\n fileExists = resp == 1\n }\n }\n } else {\n fileExists = false\n }\n }\n\n if (filename) {\n log(`saving ${filename}`)\n const path = fm.joinPath(fm.documentsDirectory(), filename)\n fm.writeString(path, code)\n return filename\n }\n } \n return null\n}\n\/\/------------------------------------------------\nasync function presentAlert(prompt,items = [\"OK\"],asSheet) \n{\n\n let alert = new Alert()\n alert.message = prompt\n \n for (const item of items) {\n alert.addAction(item)\n }\n let resp = asSheet ? \n await alert.presentSheet() : \n await alert.presentAlert()\n log(resp)\n return resp\n}\n\/\/------------------------------------------------\nasync function presentPrompt(prompt,defaultText) \n{\n let alert = new Alert()\n alert.message = prompt\n\n alert.addTextField(\"\",defaultText)\n \n var buttons = [\"OK\", \"Cancel\"]\n for (const button of buttons) {\n alert.addAction(button)\n }\n let resp = await alert.presentAlert()\n if (resp==0) {\n return alert.textFieldValue(0)\n }\n return null\n}\n",
"share_sheet_inputs" : [
"plain-text",
"url"
]
}