Skip to content

Instantly share code, notes, and snippets.

@pghant
Created July 27, 2025 23:43
Show Gist options
  • Select an option

  • Save pghant/1a60d83a04a087c3cc99c4342770b25b to your computer and use it in GitHub Desktop.

Select an option

Save pghant/1a60d83a04a087c3cc99c4342770b25b to your computer and use it in GitHub Desktop.
Clash Upgrades Widget
{
"always_run_in_app" : false,
"icon" : {
"color" : "yellow",
"glyph" : "gavel"
},
"name" : "Clash Upgrades",
"script" : "const DATA_MAP_URL = 'https:\/\/gist.githubusercontent.com\/pghant\/0717bb1e0e4e0d1373e90bdb3057d9dd\/raw\/dc943f912a7d30cf559349b415c0b4d21c3cad0e\/cocMapping.json';\nconst DATA_MAPPING = await (new Request(DATA_MAP_URL)).loadJSON();\nconst TABLE_WIDTH = {\n COL_1: 140,\n COL_2: 60,\n COL_3: 110\n}\n\nfunction createTable(widget, headerContent, tableContent, colWidths) {\n const fontSize = 12;\n\n const headerRow = widget.addStack();\n headerRow.layoutHorizontally();\n headerContent.forEach((cellContent, idx) => {\n const cell = headerRow.addStack();\n const text = cell.addText(cellContent);\n text.font = Font.mediumSystemFont(fontSize);\n cell.size = new Size(colWidths[idx], 0);\n });\n\n for (const rowContent of tableContent) {\n const row = widget.addStack();\n row.layoutHorizontally();\n rowContent.forEach((cellContent, idx) => {\n const cell = row.addStack();\n if (cellContent.type === 'text') {\n const text = cell.addText(cellContent.data);\n text.font = Font.regularSystemFont(fontSize);\n } else if (cellContent.type === 'date') {\n const date = cell.addDate(cellContent.data);\n date.font = Font.regularSystemFont(fontSize);\n date.applyRelativeStyle();\n date.centerAlignText();\n if (cellContent.data < new Date()) {\n date.textColor = Color.red();\n }\n }\n cell.size = new Size(colWidths[idx], 0);\n });\n }\n}\n\nfunction getCurrentUpgradeRows(upgradeData, timestamp) {\n upgradeData.sort((a, b) => a.timer > b.timer);\n const currentUpgrades = [];\n for (const upgrade of upgradeData) {\n currentUpgrades.push([\n {data: upgrade.name, type: 'text'},\n {data: `${upgrade.level} → ${upgrade.level + 1}`, type: 'text'}, \/\/ TODO: Fix for supercharging once we know max levels\n {data: new Date((timestamp + upgrade.timer) * 1000), type: 'date'}\n ]);\n }\n return currentUpgrades;\n}\n\n\/**\n * Parses Clash of Clans export JSON data. Finds and maps in progress upgrades. Returns an array of upgrades including\n * name, time in seconds remaining, and current level\n *\/\nfunction findInProgress(importedData, type) {\n let inProgress = [];\n if (!!importedData[type]) {\n inProgress = importedData[type]\n .filter(e => !!e.timer)\n .map(e => {\n const elementData = DATA_MAPPING.find(dm => dm.dataId === e.data);\n return {\n name: elementData.name,\n timer: e.timer,\n level: e.lvl,\n supercharge: e.supercharge\n }\n });\n }\n if (type === 'buildings') {\n \/\/ check for in progress crafted defenses\n const craftedDefenseData = importedData[type].find(d => d.data === 1000097);\n if (!!craftedDefenseData) {\n craftedDefenseData.types.forEach(craftedType => {\n craftedType.modules.forEach(craftedTypeModule => {\n if (!!craftedTypeModule.timer) {\n inProgress.push({\n name: 'Crafted Defense', \/\/ TODO: Add individual crafted defense modules to lookup\n timer: craftedTypeModule.timer,\n level: craftedTypeModule.lvl\n });\n }\n });\n });\n }\n }\n\n return inProgress;\n}\n\nasync function getInProgressUpgrades() {\n let importedData;\n const userTagParam = args.widgetParameter ?? 'QCRPYJ8VJ';\n const fm = FileManager.local();\n const documentDirectory = fm.documentsDirectory();\n const clashUpgradeDirectory = fm.joinPath(documentDirectory, 'ClashUpgrades');\n if (!fm.isDirectory(clashUpgradeDirectory)) {\n fm.createDirectory(clashUpgradeDirectory);\n } \n const upgradeFileName = fm.joinPath(clashUpgradeDirectory, `${userTagParam}.json`);\n console.log(upgradeFileName);\n try {\n importedData = JSON.parse(Pasteboard.paste());\n const userTag = importedData.tag.replace('#', '');\n console.log('user tag from export ' + userTag);\n if (userTagParam !== userTag) {\n console.error('User tag mismatch');\n throw new Error('User tag mismatch');\n }\n \/\/ save to new file with the user tag that is passed in\n fm.writeString(upgradeFileName, JSON.stringify(importedData));\n } catch {\n \/\/ check for existing file and pull that instead\n if (fm.fileExists(upgradeFileName)) {\n importedData = JSON.parse(fm.readString(upgradeFileName));\n } else {\n throw new Error('Invalid data');\n }\n }\n\n return {\n inProgress: [\n ...findInProgress(importedData, 'buildings'),\n ...findInProgress(importedData, 'traps'),\n ...findInProgress(importedData, 'heroes'),\n ...findInProgress(importedData, 'units'),\n ...findInProgress(importedData, 'spells'),\n ...findInProgress(importedData, 'siege_machines'),\n ...findInProgress(importedData, 'pets')\n ],\n timestamp: importedData.timestamp\n };\n}\n\nasync function getWidget() {\n const upgrades = await getInProgressUpgrades();\n const widget = new ListWidget();\n const title = widget.addText(`Clash Upgrades - #${args.widgetParameter}`);\n title.centerAlignText();\n title.font = Font.title3();\n widget.addSpacer(8);\n createTable(\n widget,\n ['Name', 'Level', 'Complete'],\n getCurrentUpgradeRows(upgrades.inProgress, upgrades.timestamp),\n [TABLE_WIDTH.COL_1, TABLE_WIDTH.COL_2, TABLE_WIDTH.COL_3]\n );\n\n return widget;\n}\n\ntry {\n const widget = await getWidget();\n if (config.runsInWidget) {\n Script.setWidget(widget);\n } else {\n widget.presentLarge();\n }\n} catch (error) {\n console.error(error);\n const widget = new ListWidget();\n const title = widget.addText('Invalid data for Clash Upgrades');\n title.centerAlignText();\n title.font = Font.title2();\n if (config.runsInWidget) {\n Script.setWidget(widget);\n } else {\n widget.presentLarge();\n }\n}\n\nScript.complete();",
"share_sheet_inputs" : [
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment