-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathPhotos.scriptable
15 lines (15 loc) · 24.7 KB
/
Photos.scriptable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"always_run_in_app" : false,
"icon" : {
"color" : "cyan",
"glyph" : "images"
},
"name" : "Photos",
"script" : "\/**\n *\n * @version 1.1.0\n * @author Honye\n *\/\n\n\/**\n * @param {object} options\n * @param {string} [options.title]\n * @param {string} [options.message]\n * @param {Array<{ title: string; [key: string]: any }>} options.options\n * @param {boolean} [options.showCancel = true]\n * @param {string} [options.cancelText = 'Cancel']\n *\/\nconst presentSheet = async (options) => {\n options = {\n showCancel: true,\n cancelText: 'Cancel',\n ...options\n };\n const alert = new Alert();\n if (options.title) {\n alert.title = options.title;\n }\n if (options.message) {\n alert.message = options.message;\n }\n if (!options.options) {\n throw new Error('The \"options\" property of the parameter cannot be empty')\n }\n for (const option of options.options) {\n alert.addAction(option.title);\n }\n if (options.showCancel) {\n alert.addCancelAction(options.cancelText);\n }\n const value = await alert.presentSheet();\n return {\n value,\n option: options.options[value]\n }\n};\n\n\/**\n * 多语言国际化\n * @param {{[language: string]: string} | [en:string, zh:string]} langs\n *\/\nconst i18n = (langs) => {\n const language = Device.language();\n if (Array.isArray(langs)) {\n langs = {\n en: langs[0],\n zh: langs[1],\n others: langs[0]\n };\n } else {\n langs.others = langs.others || langs.en;\n }\n return langs[language] || langs.others\n};\n\n\/\/ Variables used by Scriptable.\n\nconst ALERTS_AS_SHEETS = false;\nconst fm = FileManager.local();\nconst CACHE_FOLDER = 'cache\/nobg';\nconst cachePath = fm.joinPath(fm.documentsDirectory(), CACHE_FOLDER);\nconst deviceId = `${Device.model()}_${Device.name()}`.replace(\/[^a-zA-Z0-9\\-_]\/, '').toLowerCase();\n\nconst generateSlices = async function ({ caller = 'none' }) {\n const opts = { caller };\n\n const appearance = (await isUsingDarkAppearance()) ? 'dark' : 'light';\n const altAppearance = appearance === 'dark' ? 'light' : 'dark';\n\n if (!fm.fileExists(cachePath)) {\n fm.createDirectory(cachePath, true);\n }\n\n let message;\n\n message = i18n([\n 'To change background make sure you have a screenshot of you home screen. Go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot.',\n '设置背景前先确认已有主屏截图。返回主屏打开编辑模式,滑动到最右边后截图'\n ]);\n const options = [\n i18n(['Pick Screenshot', '选择截图']),\n i18n(['Exit to Take Screenshot', '退出去截屏'])\n ];\n let resp = await presentAlert(message, options, ALERTS_AS_SHEETS);\n if (resp === 1) return false\n\n \/\/ Get screenshot and determine phone size.\n const wallpaper = await Photos.fromLibrary();\n const height = wallpaper.size.height;\n let suffix = '';\n\n \/\/ Check for iPhone 12 Mini here:\n if (height === 2436) {\n \/\/ We'll save everything in the config, to keep things centralized\n const cfg = await loadConfig();\n if (cfg['phone-model'] === undefined) {\n \/\/ Doesn't exist, ask them which phone they want to generate for,\n \/\/ the mini or the others?\n message = i18n(['What model of iPhone do you have?', '确认手机机型']);\n const options = ['iPhone 12 mini', 'iPhone 11 Pro, XS, or X'];\n resp = await presentAlert(message, options, ALERTS_AS_SHEETS);\n \/\/ 0 represents iPhone Mini and 1 others.\n cfg['phone-model'] = resp;\n await saveConfig(cfg); \/\/ Save the config\n if (resp === 0) {\n suffix = '_mini';\n }\n } else {\n \/\/ Config already contains iPhone model, use it from cfg\n if (cfg['phone-model']) {\n suffix = '_mini';\n }\n }\n }\n\n const phone = phoneSizes[height + suffix];\n if (!phone) {\n message = i18n([\n \"It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image.\",\n '看似你选的图不是屏幕截图或不支持你的机型,尝试使用其他截图'\n ]);\n await presentAlert(message, [i18n(['OK', '好的'])], ALERTS_AS_SHEETS);\n return false\n }\n\n \/** @type {('small'|'medium'|'large')[]} *\/\n const families = ['small', 'medium', 'large'];\n\n \/\/ generate crop rects for all sizes\n for (let i = 0; i < families.length; i++) {\n const widgetSize = families[i];\n\n const crops = widgetPositions[widgetSize].map(item => {\n const position = item.value;\n\n const crop = { pos: position, w: 0, h: 0, x: 0, y: 0 };\n crop.w = phone[widgetSize].w;\n crop.h = phone[widgetSize].h;\n crop.x = phone.left;\n\n const pos = position.split('-');\n\n crop.y = phone[pos[0]];\n\n if (widgetSize === 'large' && pos[0] === 'bottom') {\n crop.y = phone.middle;\n }\n\n if (pos.length > 1) {\n crop.x = phone[pos[1]];\n }\n\n return crop\n });\n\n for (let c = 0; c < crops.length; c++) {\n const crop = crops[c];\n const imgCrop = cropImage(wallpaper, new Rect(crop.x, crop.y, crop.w, crop.h));\n\n const imgName = `${deviceId}-${appearance}-${widgetSize}-${crop.pos}.jpg`;\n const imgPath = fm.joinPath(cachePath, imgName);\n\n if (fm.fileExists(imgPath)) {\n try { fm.remove(imgPath); } catch (e) { }\n }\n fm.writeImage(imgPath, imgCrop);\n }\n }\n\n if (opts.caller !== 'self') {\n message = i18n([\n `Slices saved for ${appearance} mode. You can switch to ${altAppearance} mode and run this again to also generate slices.`,\n `已经保存${appearance === 'dark' ? '夜间' : '白天'}透明背景。你可以切换到${altAppearance === 'dark' ? '夜间' : '白天'}模式后再次运行`\n ]);\n } else {\n message = i18n(['Slices saved.', '已保存']);\n }\n await presentAlert(message, [i18n(['Ok', '好的'])], ALERTS_AS_SHEETS);\n\n return true\n};\n\/\/ ------------------------------------------------\n\/**\n * @param {string} instanceName\n * @param {boolean} reset\n *\/\nconst getSliceForWidget = async function (instanceName, reset = false) {\n const appearance = (await isUsingDarkAppearance()) ? 'dark' : 'light';\n let position = reset ? null : await getConfig(instanceName);\n if (!position) {\n log(`Background for \"${instanceName}\" is not yet set.`);\n\n \/\/ check if slices exists\n const testImage = fm.joinPath(cachePath, `${deviceId}-${appearance}-medium-top.jpg`);\n let readyToChoose = false;\n if (!fm.fileExists(testImage)) {\n \/\/ need to generate slices\n \/\/ FIXME\n readyToChoose = await generateSlices({ caller: instanceName || 'self' });\n } else {\n readyToChoose = true;\n }\n\n \/\/ now set the\n let backgrounChosen;\n if (readyToChoose) {\n backgrounChosen = await chooseBackgroundSlice(instanceName);\n }\n\n if (backgrounChosen) {\n const cfg = await loadConfig();\n position = cfg[instanceName];\n } else {\n return null\n }\n }\n const imgPath = fm.joinPath(cachePath, `${deviceId}-${appearance}-${position}.jpg`);\n if (!fm.fileExists(imgPath)) {\n log(`Slice does not exists - ${deviceId}-${appearance}-${position}.jpg`);\n return null\n }\n\n const image = fm.readImage(imgPath);\n return image\n};\n\/\/ ------------------------------------------------\nconst transparent = getSliceForWidget;\n\/\/ ------------------------------------------------\nconst chooseBackgroundSlice = async function (name) {\n \/\/ Prompt for widget size and position.\n let message = i18n(['What is the size of the widget?', '组件尺寸']);\n \/** @type {{label:string;value:'small'|'medium'|'large'}[]} *\/\n const sizes = [\n { label: i18n(['Small', '小号']), value: 'small' },\n { label: i18n(['Medium', '中号']), value: 'medium' },\n { label: i18n(['Large', '大号']), value: 'large' },\n { label: i18n(['Cancel', '取消']), value: 'cancel' }\n ];\n const size = await presentAlert(message, sizes, ALERTS_AS_SHEETS);\n if (size === 3) return false\n const widgetSize = sizes[size].value;\n\n message = i18n(['Where will it be placed on?', '组件位置']);\n const positions = widgetPositions[widgetSize];\n positions.push(i18n(['Cancel', '取消']));\n const resp = await presentAlert(message, positions, ALERTS_AS_SHEETS);\n\n if (resp === positions.length - 1) return false\n const position = positions[resp].value;\n\n const cfg = await loadConfig();\n cfg[name] = `${widgetSize}-${position}`;\n\n await saveConfig(cfg);\n await presentAlert(i18n(['Background saved.', '已保存']), [i18n(['Ok', '好'])], ALERTS_AS_SHEETS);\n return true\n};\n\/**\n * @param {string} instance\n * @returns {Promise<string|undefined>}\n *\/\nconst getConfig = async (instance) => {\n try {\n const conf = await loadConfig();\n return conf[instance]\n } catch (err) {}\n};\n\n\/\/ -- [helpers] -----------------------------------\n\/\/ ------------------------------------------------\n\/**\n * @returns {Promise<Record<string, string>>}\n *\/\nasync function loadConfig () {\n const configPath = fm.joinPath(cachePath, 'widget-positions.json');\n if (!fm.fileExists(configPath)) {\n await fm.writeString(configPath, '{}');\n return {}\n } else {\n const strConf = fm.readString(configPath);\n const cfg = JSON.parse(strConf);\n return cfg\n }\n}\n\nconst hasConfig = async () => {\n try {\n const conf = await loadConfig();\n return conf\n } catch (err) {\n return false\n }\n};\n\/\/ ------------------------------------------------\n\/**\n * @param {Record<string, string>} cfg\n *\/\nasync function saveConfig (cfg) {\n const configPath = fm.joinPath(cachePath, 'widget-positions.json');\n await fm.writeString(configPath, JSON.stringify(cfg));\n return cfg\n}\n\/\/ ------------------------------------------------\nasync function presentAlert (\n prompt = '',\n items = ['OK'],\n asSheet = false\n) {\n const alert = new Alert();\n alert.message = prompt;\n\n for (let n = 0; n < items.length; n++) {\n const item = items[n];\n if (typeof item === 'string') {\n alert.addAction(item);\n } else {\n alert.addAction(item.label);\n }\n }\n const resp = asSheet\n ? await alert.presentSheet()\n : await alert.presentAlert();\n return resp\n}\n\/\/ ------------------------------------------------\n\/**\n * @type {Record<'small'|'medium'|'large', {label:string;value:string}[]>}\n *\/\nconst widgetPositions = {\n small: [\n { label: i18n(['Top Left', '左上']), value: 'top-left' },\n { label: i18n(['Top Right', '右上']), value: 'top-right' },\n { label: i18n(['Middle Left', '左中']), value: 'middle-left' },\n { label: i18n(['Middle Right', '右中']), value: 'middle-right' },\n { label: i18n(['Bottom Left', '左下']), value: 'bottom-left' },\n { label: i18n(['Bottom Right', '右下']), value: 'bottom-right' }\n ],\n medium: [\n { label: i18n(['Top', '上方']), value: 'top' },\n { label: i18n(['Middle', '中部']), value: 'middle' },\n { label: i18n(['Bottom', '下方']), value: 'bottom' }\n ],\n large: [\n { label: i18n(['Top', '上方']), value: 'top' },\n { label: i18n(['Bottom', '下方']), value: 'bottom' }\n ]\n};\n\/\/ ------------------------------------------------\n\/**\n * @type {Record<string|number, {\n * models: string[];\n * small: { w: number; h: number };\n * medium: { w: number; h: number };\n * large: { w: number; h: number };\n * left:number; right:number; top:number; middle:number; bottom:number;\n * }>}\n *\/\nconst phoneSizes = {\n 2796: {\n models: ['14 Pro Max'],\n small: { w: 510, h: 510 },\n medium: { w: 1092, h: 510 },\n large: { w: 1092, h: 1146 },\n left: 99,\n right: 681,\n top: 282,\n middle: 918,\n bottom: 1554\n },\n\n 2556: {\n models: ['14 Pro'],\n small: { w: 474, h: 474 },\n medium: { w: 1014, h: 474 },\n large: { w: 1014, h: 1062 },\n left: 82,\n right: 622,\n top: 270,\n middle: 858,\n bottom: 1446\n },\n\n 2778: {\n models: ['12 Pro Max', '13 Pro Max', '14 Plus'],\n small: { w: 510, h: 510 },\n medium: { w: 1092, h: 510 },\n large: { w: 1092, h: 1146 },\n left: 96,\n right: 678,\n top: 246,\n middle: 882,\n bottom: 1518\n },\n\n 2532: {\n models: ['12', '12 Pro', '13', '14'],\n small: { w: 474, h: 474 },\n medium: { w: 1014, h: 474 },\n large: { w: 1014, h: 1062 },\n left: 78,\n right: 618,\n top: 231,\n middle: 819,\n bottom: 1407\n },\n\n 2688: {\n models: ['Xs Max', '11 Pro Max'],\n small: { w: 507, h: 507 },\n medium: { w: 1080, h: 507 },\n large: { w: 1080, h: 1137 },\n left: 81,\n right: 654,\n top: 228,\n middle: 858,\n bottom: 1488\n },\n\n 1792: {\n models: ['11', 'Xr'],\n small: { w: 338, h: 338 },\n medium: { w: 720, h: 338 },\n large: { w: 720, h: 758 },\n left: 54,\n right: 436,\n top: 160,\n middle: 580,\n bottom: 1000\n },\n\n 2436: {\n models: ['X', 'Xs', '11 Pro'],\n small: { w: 465, h: 465 },\n medium: { w: 987, h: 465 },\n large: { w: 987, h: 1035 },\n left: 69,\n right: 591,\n top: 213,\n middle: 783,\n bottom: 1353\n },\n\n '2436_mini': {\n models: ['12 Mini'],\n small: { w: 465, h: 465 },\n medium: { w: 987, h: 465 },\n large: { w: 987, h: 1035 },\n left: 69,\n right: 591,\n top: 231,\n middle: 801,\n bottom: 1371\n },\n\n 2208: {\n models: ['6+', '6s+', '7+', '8+'],\n small: { w: 471, h: 471 },\n medium: { w: 1044, h: 471 },\n large: { w: 1044, h: 1071 },\n left: 99,\n right: 672,\n top: 114,\n middle: 696,\n bottom: 1278\n },\n\n 1334: {\n models: ['6', '6s', '7', '8'],\n small: { w: 296, h: 296 },\n medium: { w: 642, h: 296 },\n large: { w: 642, h: 648 },\n left: 54,\n right: 400,\n top: 60,\n middle: 412,\n bottom: 764\n },\n\n 1136: {\n models: ['5', '5s', '5c', 'SE'],\n small: { w: 282, h: 282 },\n medium: { w: 584, h: 282 },\n large: { w: 584, h: 622 },\n left: 30,\n right: 332,\n top: 59,\n middle: 399,\n bottom: 399\n }\n};\n\/\/ ------------------------------------------------\nfunction cropImage (img, rect) {\n const draw = new DrawContext();\n draw.size = new Size(rect.width, rect.height);\n draw.drawImageAtPoint(img, new Point(-rect.x, -rect.y));\n return draw.getImage()\n}\n\/\/ ------------------------------------------------\nasync function isUsingDarkAppearance () {\n return !(Color.dynamic(Color.white(), Color.black()).red)\n}\n\nconst localFile = FileManager.local();\nconst APP_ROOT = localFile.joinPath(localFile.documentsDirectory(), Script.name());\nconst PHOTOS_DIR = localFile.joinPath(APP_ROOT, 'photos');\n\nconst main = async () => {\n if (!localFile.fileExists(PHOTOS_DIR)) {\n localFile.createDirectory(PHOTOS_DIR, true);\n }\n\n if (config.runsInActionExtension) {\n choosePhotos();\n return\n }\n\n if (config.runsInApp) {\n const {\n option: { key } = {}\n } = await presentSheet({\n options: [\n {\n title: i18n(['Preview', '预览']),\n key: 'preview'\n },\n {\n title: i18n(['Photos', '查看图片']),\n key: 'photos'\n },\n {\n title: i18n(['Transparent background', '透明背景']),\n key: 'transparentBg'\n }\n ],\n cancelText: i18n(['Cancel', '取消'])\n });\n if (key === 'preview') {\n const widget = await createWidget();\n widget.presentSmall();\n Script.complete();\n return\n }\n if (key === 'photos') {\n presentAlbums();\n return\n }\n if (key === 'transparentBg') {\n if (await hasConfig()) {\n const { option } = await presentSheet({\n options: [\n {\n title: i18n(['Update widget size and position', '修改组件尺寸和位置']),\n value: 'update'\n },\n {\n title: i18n(['Update wallpaper screenshot', '更新壁纸截图']),\n value: 'reset'\n }\n ],\n cancelText: i18n(['Cancel', '取消'])\n });\n if (option) {\n if (option.value === 'update') {\n await transparent(Script.name(), true);\n } else {\n await generateSlices({ caller: Script.name() });\n }\n }\n } else {\n await transparent(Script.name(), true);\n }\n return\n }\n }\n\n if (config.runsInWidget) {\n const widget = await createWidget();\n Script.setWidget(widget);\n Script.complete();\n }\n};\n\n\/** 通过分享菜单选择照片 *\/\nconst choosePhotos = async () => {\n const albums = getAlbums();\n let album;\n const { option } = await presentSheet({\n message: i18n([\n 'Choose Album',\n '选择相册'\n ]),\n options: [\n ...albums.map((name) => ({ title: name, type: 'album' })),\n {\n title: i18n(['New Album', '新建相册']),\n type: 'new'\n }\n ]\n });\n if (option) {\n if (option.type === 'album') {\n album = option.title;\n }\n if (option.type === 'new') {\n album = await createAlbum();\n }\n }\n const albumDir = localFile.joinPath(PHOTOS_DIR, album);\n\n const filePaths = args.fileURLs;\n const images = args.images;\n if (filePaths && filePaths.length) { \/\/ 图片文件分享\n for (const filePath of filePaths) {\n const filename = localFile.fileName(filePath, true);\n const copyPath = localFile.joinPath(albumDir, filename);\n try {\n localFile.copy(filePath, copyPath);\n } catch (e) {\n await alert(e.message);\n }\n }\n } else if (images && images.length) { \/\/ 图片分享\n for (const image of images) {\n const filePath = localFile.joinPath(albumDir, `${Date.now()}.jpg`);\n localFile.writeImage(filePath, image);\n }\n }\n\n presentPhotos(album);\n};\n\n\/**\n * @param {string} album\n *\/\nconst _choosePhoto = async (album) => {\n const {\n option: { key } = {}\n } = await presentSheet({\n options: [\n {\n title: i18n(['Camera', '拍照']),\n key: 'camera'\n },\n {\n title: i18n(['Albums', '相册']),\n key: 'album'\n }\n ]\n });\n const image = await (async () => {\n if (key === 'camera') {\n return await Photos.fromCamera()\n }\n if (key === 'album') {\n return await Photos.fromLibrary()\n }\n })();\n const filename = `${Date.now().toString()}.jpg`;\n const albumDir = localFile.joinPath(PHOTOS_DIR, album);\n const filePath = localFile.joinPath(albumDir, filename);\n localFile.writeImage(filePath, image);\n return filePath\n};\n\nconst getAlbums = () => {\n const albums = localFile.listContents(PHOTOS_DIR)\n .filter((name) => localFile.isDirectory(localFile.joinPath(PHOTOS_DIR, name)));\n return albums\n};\n\n\/** 添加相册 *\/\nconst createAlbum = async () => {\n const alert = new Alert();\n alert.title = i18n(['New Album', '新建相册']);\n alert.addTextField(i18n(['Input the album name', '输入相册名']));\n alert.addAction(i18n(['Save', '保存']));\n alert.addCancelAction(i18n(['Cancel', '取消']));\n const index = await alert.presentAlert();\n if (index === 0) {\n const name = alert.textFieldValue(0);\n localFile.createDirectory(\n localFile.joinPath(PHOTOS_DIR, name),\n true\n );\n return { name }\n }\n};\n\n\/**\n * @param {string} album\n *\/\nconst getPhotos = (album) => {\n const dir = localFile.joinPath(PHOTOS_DIR, album);\n return localFile.listContents(dir)\n .map((filename) => {\n const albumDir = localFile.joinPath(PHOTOS_DIR, album);\n return localFile.joinPath(albumDir, filename)\n })\n};\n\nconst createWidget = async () => {\n let [album] = (args.widgetParameter || '').split(',').map(str => str.trim());\n const widget = new ListWidget();\n if (!album) {\n const albums = getAlbums();\n if (albums.length > 0) {\n album = albums[0];\n } else {\n widget.addText(i18n(['Go to APP set photos', '请先去 APP 选择照片']));\n return widget\n }\n }\n const photos = getPhotos(album);\n const length = photos.length;\n if (length > 0) {\n if (await getConfig(Script.name())) {\n widget.backgroundImage = await transparent(Script.name());\n }\n widget.setPadding(0, 0, 0, 0);\n const index = Math.floor(Math.random() * length);\n const image = localFile.readImage(photos[index]);\n const imageStack = widget.addStack();\n imageStack.layoutVertically();\n imageStack.addStack().addSpacer();\n imageStack.addSpacer();\n imageStack.backgroundImage = image;\n } else {\n widget.addText(i18n([`Album \"${album}\" is empty`, `相册\"${album}\"是空的`]));\n }\n return widget\n};\n\n\/** 展示相册列表 *\/\nconst presentAlbums = () => {\n const albums = localFile.listContents(PHOTOS_DIR)\n .filter((name) => localFile.isDirectory(localFile.joinPath(PHOTOS_DIR, name)));\n const table = new UITable();\n const head = new UITableRow();\n table.addRow(head);\n head.isHeader = true;\n head.addText(i18n(['Albums', '相册']));\n \/\/ 添加相册\n const cellNew = head.addButton(i18n(['New Album', '新建相册']));\n cellNew.rightAligned();\n cellNew.onTap = async () => {\n const alert = new Alert();\n alert.title = i18n(['New Album', '新建相册']);\n alert.addTextField(i18n(['Input the album name', '输入相册名']));\n alert.addAction(i18n(['Save', '保存']));\n alert.addCancelAction(i18n(['Cancel', '取消']));\n const index = await alert.presentAlert();\n if (index === 0) {\n const name = alert.textFieldValue(0);\n localFile.createDirectory(\n localFile.joinPath(PHOTOS_DIR, name),\n true\n );\n addRow(name);\n table.reload();\n }\n };\n const addRow = (album) => {\n const row = new UITableRow();\n table.addRow(row);\n const count = localFile.listContents(\n localFile.joinPath(PHOTOS_DIR, album)\n ).length;\n const cellName = row.addText(album, `${count} photos`);\n cellName.subtitleColor = new Color('#888888');\n const cellView = row.addButton(i18n(['View', '查看']));\n cellView.onTap = () => presentPhotos(album);\n const cellDelete = row.addButton(i18n(['Delete', '删除']));\n cellDelete.onTap = async () => {\n const alert = new Alert();\n alert.message = i18n([`Are you sure delete \"${album}\"?`, `确定删除\"${album}\"吗?`]);\n alert.addAction(i18n(['Delete', '删除']));\n alert.addCancelAction(i18n(['Cancel', '取消']));\n const value = await alert.presentAlert();\n if (value === 0) {\n localFile.remove(localFile.joinPath(PHOTOS_DIR, album));\n table.removeRow(row);\n table.reload();\n }\n };\n };\n for (const [, album] of albums.entries()) {\n addRow(album);\n }\n table.present();\n};\n\n\/**\n * 展示相册照片\n * @param {string}\n *\/\nconst presentPhotos = (album) => {\n const photos = getPhotos(album);\n const table = new UITable();\n const head = new UITableRow();\n table.addRow(head);\n head.isHeader = true;\n head.addText(i18n(['Photos', '照片']));\n const cellChoose = head.addButton(i18n(['Choose photos', '选择图片']));\n cellChoose.rightAligned();\n cellChoose.onTap = async () => {\n const filePath = await _choosePhoto(album);\n addRow(filePath);\n table.reload();\n };\n\n const addRow = (filePath) => {\n const row = new UITableRow();\n table.addRow(row);\n const image = Image.fromFile(filePath);\n const cellImage = row.addImage(image);\n cellImage.widthWeight = 4;\n const dfm = new DateFormatter();\n dfm.dateFormat = 'yy-MM-dd HH:mm:ss';\n const cellName = row.addText(\n localFile.fileName(filePath, true),\n dfm.string(localFile.modificationDate(filePath))\n );\n cellName.widthWeight = 10;\n cellName.titleFont = Font.systemFont(14);\n cellName.subtitleFont = Font.lightSystemFont(10);\n const buttonPreview = row.addButton(i18n(['Preview', '查看大图']));\n buttonPreview.widthWeight = 6;\n buttonPreview.rightAligned();\n buttonPreview.onTap = () => {\n QuickLook.present(image, true);\n };\n const buttonDelete = row.addButton(i18n(['Delete', '删除']));\n buttonDelete.widthWeight = 4;\n buttonDelete.rightAligned();\n buttonDelete.onTap = () => {\n localFile.remove(filePath);\n table.removeRow(row);\n table.reload();\n };\n };\n\n for (const filePath of photos) {\n addRow(filePath);\n }\n QuickLook.present(table);\n};\n\nconst alert = (message, title = '') => {\n const alertIns = new Alert();\n alertIns.title = title;\n alertIns.message = String(message);\n return alertIns.present()\n};\n\nawait main();\n",
"share_sheet_inputs" : [
"file-url",
"url",
"image",
"plain-text"
]
}