Last active
October 10, 2022 09:10
-
-
Save morioprog/c2cde4738678f10e561832ea14fd181b to your computer and use it in GitHub Desktop.
競技プログラミングのコンテスト予定を表示するiOSウィジェット(導入方法:https://blog.morio.dev/2021/01/contest_schedule_widget/)
This file contains 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
// 初期設定 | |
// 1. CLIST(`https://clist.by/`)にログイン | |
// 2. `https://clist.by/api/v1/doc/`の上部の「show my api-key」を選択 | |
// 3. 出てきたモーダル下部の「Param query」を下のCLIST_APIに貼り付け | |
const CLIST_API = "/?username=*****&api_key=****************************************"; | |
// contestIds : 表示するコンテストサイトのリスト (CLISTにおけるコンテストサイトのID) | |
// > `https://clist.by/api/v1/doc/#!/resource/resource_list/`から検索できる | |
// * codeforces.com : 1 | |
// * atcoder.jp : 93 | |
// * yukicoder.me : 109 | |
// * topcoder.com : 12 | |
const contestIds = [1, 93, 109, 12]; | |
// -------------------------------------------------------------- | |
const apiTemplate = ({resource_id}) => `https://clist.by/api/v1/contest${CLIST_API}&resource__id=${resource_id}&start__gt=${encodeURIComponent(new Date().toISOString())}`; | |
const FILE_MANAGER = FileManager.local(); // or FileManager.iCloud() | |
const LASTUPD_FILENAME = "contest_shedule_last_update.txt"; | |
const JSON_FILENAME = "contest_schedule.json"; | |
function saveFile(filename, str) { | |
const path = FILE_MANAGER.joinPath(FILE_MANAGER.documentsDirectory(), filename); | |
FILE_MANAGER.writeString(path, str); | |
} | |
function loadFile(filename) { | |
const path = FILE_MANAGER.joinPath(FILE_MANAGER.documentsDirectory(), filename); | |
if (FILE_MANAGER.fileExists(path)) return FILE_MANAGER.readString(path); | |
return ""; | |
} | |
function loadContests() { | |
const str = loadFile(JSON_FILENAME); | |
let contests = str === "" ? [] : JSON.parse(str); | |
// jsonから読み込んだときにすでに始まってるものがあれば弾く | |
contests = contests.filter(contest => new Date(contest.start + '+0000') >= new Date()); | |
return contests; | |
} | |
function date2str(date) { | |
const dateStr = `${date.getMonth() + 1}/${date.getDate()}`; | |
const timeStr = `${date.getHours()}:${('0' + date.getMinutes()).slice(-2)}`; | |
return `${dateStr} ${timeStr}`; | |
} | |
// maxLength : 最大何件のコンテストを表示するか (Mediumなら3, Largeなら9にするとちょうどいい) | |
async function fetchContestSchedule(maxLength) { | |
let lastUpd = loadFile(LASTUPD_FILENAME); | |
let contests = []; | |
// 1時間ごとに更新 | |
if (lastUpd === "" || (new Date() - new Date(lastUpd)) >= (1000 * 60 * 60)) { | |
// 初期設定をしていない | |
if (~CLIST_API.indexOf('*')) { | |
return [false, lastUpd, "Error: ソースコード上部の初期設定を行ってください", contests]; | |
} | |
try { | |
for (const id of contestIds) { | |
const req = new Request(apiTemplate({resource_id: id})); | |
const contestJson = await req.loadJSON(); | |
contests = contests.concat(await contestJson.objects); | |
} | |
} catch (err) { | |
// とりあえずjsonから読み込む | |
contests = loadContests(); | |
contests.sort((a, b) => a.start === b.start ? a.event > b.event : a.start > b.start); | |
return [false, lastUpd, err.toString(), contests.slice(0, maxLength)]; | |
} | |
lastUpd = new Date().toISOString(); | |
saveFile(JSON_FILENAME, JSON.stringify(contests)); | |
saveFile(LASTUPD_FILENAME, lastUpd); | |
} else { | |
// jsonから読み込む | |
contests = loadContests(); | |
} | |
contests.sort((a, b) => a.start === b.start ? a.event > b.event : a.start > b.start); | |
return [true, lastUpd, "Success", contests.slice(0, maxLength)]; | |
} | |
async function createWidget(maxLength) { | |
const wg = new ListWidget(); | |
const [valid, lastUpd, err, contests] = await fetchContestSchedule(maxLength=maxLength); | |
console.log(err); | |
// Background color | |
const gradient = new LinearGradient(); | |
gradient.colors = [ | |
new Color("141414"), | |
new Color("13233f"), | |
]; | |
gradient.locations = [ | |
0.0, | |
1.0, | |
]; | |
wg.backgroundGradient = gradient; | |
// Header | |
wg.addSpacer(4); | |
const widgetHeader = wg.addStack(); | |
const iconStack = widgetHeader.addStack(); | |
iconStack.addSpacer(6); | |
const iconElement = iconStack.addText(valid ? "AC" : "WA"); | |
iconStack.addSpacer(6); | |
iconElement.textColor = new Color("fefffe"); | |
iconElement.font = new Font("GillSans", 16); // Lato Fontがないため... | |
iconStack.backgroundColor = new Color(valid ? "5cb85c" : "efad4e"); | |
iconStack.cornerRadius = 4; | |
widgetHeader.addSpacer(8); | |
const titleElement = widgetHeader.addText("Contest Schedule"); | |
titleElement.font = new Font("Avenir-Medium", 16); | |
titleElement.textColor = new Color("fefffe"); | |
titleElement.textOpacity = 0.7; | |
wg.addSpacer(8); | |
// Upcoming Contests | |
if (contests.length === 0) { | |
wg.addSpacer(); | |
if (valid) { | |
const noContest = wg.addText("No Upcoming Contest :("); | |
noContest.font = Font.boldRoundedSystemFont(22); | |
noContest.centerAlignText(); | |
} else { | |
const errRow = wg.addText(err); | |
errRow.textSize = 10; | |
errRow.textColor = Color.red(); | |
} | |
} else { | |
let isFirst = true; | |
for (const contest of contests) { | |
if (!isFirst) wg.addSpacer(4); | |
isFirst = false; | |
const start = new Date(contest.start + '+0000'); | |
const contestHeader = wg.addText(`${date2str(start)} / ${contest.resource.name}`); | |
contestHeader.font = Font.regularRoundedSystemFont(8); | |
contestHeader.textColor = new Color("fefffe"); | |
contestHeader.url = contest.href; | |
const contestBody = wg.addText(`${contest.event}`); | |
contestBody.font = Font.boldRoundedSystemFont(12); | |
contestBody.textColor = new Color("fefffe"); | |
contestBody.url = contest.href; | |
} | |
} | |
wg.addSpacer(); | |
// Last Update & Error | |
const widgetFooter = wg.addStack(); | |
if (!valid && contests.length !== 0) { | |
const errCell = widgetFooter.addText(err); | |
errCell.font = new Font("Avenir-Medium", 8); | |
errCell.textOpacity = 0.8; | |
errCell.textColor = Color.red(); | |
} | |
widgetFooter.addSpacer(); | |
const updCell = widgetFooter.addText(`Last Update: ${lastUpd === "" ? "-" : date2str(new Date(lastUpd))}`); | |
// const updCell = widgetFooter.addText(`Last Update: ${lastUpd === "" ? "-" : date2str(new Date(lastUpd))} [${date2str(new Date())}]`); | |
updCell.font = new Font("Avenir-Medium", 8); | |
updCell.textColor = new Color("fefffe"); | |
updCell.textOpacity = 0.7; | |
return wg; | |
} | |
(async function() { | |
if (config.runsInWidget) { | |
let maxLength = 3; | |
if (config.widgetFamily === "large") maxLength = 9; | |
const wg = await createWidget(maxLength=maxLength); | |
Script.setWidget(wg); | |
} else { | |
// for debugging | |
const wg = await createWidget(maxLength=9); | |
wg.presentLarge(); | |
} | |
Script.complete(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
(僕みたいに、コンテストID編集して、すぐに更新したい人は、53行目に1 || って入れればいけます)
すぐにアップデートできるようの変数がcontestIdのすぐそこにあるか、lastContestIdを覚えて、編集されていると時間に関係なく更新してくれると便利だなと思いました。