shmmeee
Well-Known Member
Got ChatGPT to code me a widget for my phone that shows the top five thread titles on the CCFC forum. So in theory I don’t have to come here and get sucked in and can just watch for a thread about a new signing. It works as a piece of software it does not work as a way to stop my coming on SBT too much. Thought I’d share.
iOS you need to download an app called Scriptable then create a new script, past the below in, save it with a name, and then add the scriptable widget to your Home Screen and choose run script and select the one you made.
Here’s what it looks like:
Here’s the script:
iOS you need to download an app called Scriptable then create a new script, past the below in, save it with a name, and then add the scriptable widget to your Home Screen and choose run script and select the one you made.
Here’s what it looks like:

Here’s the script:
Code:
// SkyBlueTalk — CCFC Top Threads (polished)
// Medium/Small widget. Auto dark/light, club sky-blue gradient.
// Taps: header → forum, each row → thread.
// ── CONFIG ─────────────────────────────────────────────────────────────────────
const FEED_URL = "https://www.skybluestalk.co.uk/forums/coventry-city-general-chat.7/index.rss";
const MAX_ITEMS = 5; // keep 3–6 for Medium, 2–4 for Small
const TITLE_LINE_LIMIT = 1; // 1 = tidy, 2 = roomier
const SHOW_REL_TIME = true; // show “· 1h” after title
const TRIM_PREFIXES = [
/^match\s*thread[:\-\s]*/i,
/^pre[-\s]?match[:\-\s]*/i,
/^post[-\s]?match[:\-\s]*/i,
/^transfer(s)?[:\-\s]*/i,
/^\[\s*rumou?r\s*\]\s*/i
];
// Colors
const SKY = Color.dynamic(new Color("#69b9ff"), new Color("#2b86d1"));
const DEEP = Color.dynamic(new Color("#1a6db3"), new Color("#154e80"));
const TEXT = Color.dynamic(Color.black(), Color.white());
const SUBTLE = Color.dynamic(new Color("#334155"), new Color("#b0c4d9"));
// ───────────────────────────────────────────────────────────────────────────────
async function fetchRSS(url){
const req = new Request(url);
req.headers = { "User-Agent": "ScriptableWidget/1.1 (+iOS)" };
return await req.loadString();
}
function parseRSS(xml){
const items = [];
const blocks = xml.match(/<item>[\s\S]*?<\/item>/g) || [];
for (let i=0; i<Math.min(blocks.length, MAX_ITEMS); i++){
const b = blocks[i];
const t = textOf(b, "title");
items.push({
title: prettifyTitle(t),
link: textOf(b, "link"),
date: new Date(textOf(b, "pubDate") || Date.now())
});
}
return items;
}
function textOf(block, tag){
const m = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, "i").exec(block);
return (m ? m[1] : "").replace(/<!\[CDATA\[|\]\]>/g,"").trim();
}
function prettifyTitle(t){
let s = t.replace(/\s+/g," ").trim();
for (const rx of TRIM_PREFIXES) s = s.replace(rx,"");
// collapse bracketed prefixes like [Tickets], (H), etc. at start
s = s.replace(/^(\[.*?\]|\(.*?\))\s+/g,"");
return s;
}
function timeAgo(d){
const secs = Math.max(1, (Date.now() - d.getTime())/1000);
if (secs < 60) return `${Math.floor(secs)}s`;
const mins = secs/60;
if (mins < 60) return `${Math.floor(mins)}m`;
const hrs = mins/60;
if (hrs < 24) return `${Math.floor(hrs)}h`;
const days = hrs/24;
return `${Math.floor(days)}d`;
}
function header(w){
const h = w.addStack();
h.layoutHorizontally();
h.centerAlignContent();
h.url = "https://www.skybluestalk.co.uk/forums/coventry-city-general-chat.7/";
const badge = SFSymbol.named("soccerball");
const img = h.addImage(badge.image);
img.imageSize = new Size(14,14);
img.tintColor = TEXT;
h.addSpacer(6);
const t = h.addText("SkyBlueTalk — CCFC");
t.font = Font.boldSystemFont(12);
t.textColor = TEXT;
h.addSpacer();
const refresh = h.addText(new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"}));
refresh.font = Font.systemFont(10);
refresh.textColor = SUBTLE;
}
function row(w, item){
const s = w.addStack();
s.layoutHorizontally();
s.url = item.link;
// bullet
const dot = s.addText("•");
dot.font = Font.boldSystemFont(13);
dot.textColor = TEXT;
s.addSpacer(6);
// title
const t = s.addText(item.title);
t.font = Font.systemFont(13);
t.textColor = TEXT;
t.lineLimit = TITLE_LINE_LIMIT;
if (SHOW_REL_TIME){
s.addSpacer(6);
const ago = s.addText(`· ${timeAgo(item.date)}`);
ago.font = Font.systemFont(11);
ago.textColor = SUBTLE;
}
}
function makeWidget(items){
const w = new ListWidget();
const grad = new LinearGradient();
grad.colors = [SKY, DEEP];
grad.locations = [0, 1];
grad.startPoint = new Point(0, 0);
grad.endPoint = new Point(1, 1);
w.backgroundGradient = grad;
w.setPadding(10, 12, 10, 12);
header(w);
w.addSpacer(6);
items.forEach((it, idx) => {
row(w, it);
if (idx < items.length-1) w.addSpacer(4);
});
return w;
}
async function run(){
try{
const xml = await fetchRSS(FEED_URL);
const items = parseRSS(xml);
const widget = makeWidget(items);
if (config.runsInWidget) Script.setWidget(widget);
else await widget.presentMedium();
} catch (e){
const w = new ListWidget();
w.addText("SkyBlueTalk — error").font = Font.boldSystemFont(12);
w.addText(String(e)).font = Font.systemFont(10);
Script.setWidget(w);
}
Script.complete();
}
await run();