Code:
// == Scriptable Widget Code ==
// @name U-Labs Forum Widget
// @desc Zeigt die neuesten Beiträge aus dem U-Labs-Forum an.
// @version 1.0
// @autor Integer
// @icon newspaper.fill
const forumUrl = "https://u-labs.de/forum/";
const isDarkMode = Device.isUsingDarkAppearance();
const backgroundColor = isDarkMode ? new Color("#1E1E1E") : Color.white();
const textColor = isDarkMode ? Color.white() : new Color("#333333");
const metaTextColor = isDarkMode ? Color.gray() : new Color("#666666");
const altRowColor = isDarkMode ? new Color("#3C3C3E") : new Color("#E0E0E0");
const evenRowColor = isDarkMode ? new Color("#2C2C2C") : new Color("#F5F5F5");
async function fetchForumData() {
try {
const request = new Request(forumUrl);
return await request.loadString();
} catch (error) {
displayError("Fehler beim Laden der Daten");
return null;
}
}
function displayError(message) {
const widget = new ListWidget();
widget.backgroundColor = backgroundColor;
const errorText = widget.addText(message);
errorText.textColor = textColor;
errorText.font = Font.mediumSystemFont(14);
Script.setWidget(widget);
Script.complete();
}
function decodeHtmlEntities(text) {
const entities = {
""": '"',
"&": '&',
"<": '<',
">": '>',
"'": "'"
};
return text.replace(/&[^;]+;/g, match => entities[match] || match);
}
function parseForumPosts(html) {
const postRegex = /<tr data-thread-id="(\d+)"[^>]*>[\s\S]*?ulo-post-title[^>]*href="([^"]+)"[^>]*>([^<]+)[\s\S]*?projecthead">([^<]+)[\s\S]*?ulo-post-time[^>]*data-timestamp="(\d+)"[^>]*>([^<]+)<\/span>/g;
const posts = [];
let match;
while ((match = postRegex.exec(html)) !== null) {
const [_, threadId, url, title, author, timestamp, timeAgo] = match;
posts.push({
threadId,
url: `https://u-labs.de/forum/${url}`,
title: decodeHtmlEntities(title.trim()),
author: decodeHtmlEntities(author.trim()),
timestamp,
timeAgo: decodeHtmlEntities(timeAgo.trim())
});
}
return posts;
}
function getClickedPosts() {
let clickedPosts = [];
const fileManager = FileManager.local();
const filePath = fileManager.joinPath(fileManager.documentsDirectory(), "clickedPosts.json");
try {
const fileData = fileManager.readString(filePath);
clickedPosts = fileData ? JSON.parse(fileData) : [];
} catch (error) {
log("Fehler beim Laden der geklickten Beiträge:", error);
}
return clickedPosts;
}
function saveClickedPost(threadId) {
const clickedPosts = getClickedPosts();
if (!clickedPosts.includes(threadId)) {
clickedPosts.push(threadId);
const fileManager = FileManager.local();
const filePath = fileManager.joinPath(fileManager.documentsDirectory(), "clickedPosts.json");
try {
fileManager.writeString(filePath, JSON.stringify(clickedPosts));
} catch (error) {
log("Fehler beim Speichern der geklickten Beiträge:", error);
}
}
}
function addMetaStack(metaStack, author, timeAgo) {
const authorIcon = SFSymbol.named("person.fill");
const authorImage = metaStack.addImage(authorIcon.image);
authorImage.tintColor = metaTextColor;
authorImage.imageSize = new Size(12, 12);
const authorText = metaStack.addText(` ${author} ·`);
authorText.font = Font.systemFont(12);
authorText.textColor = metaTextColor;
const timeIcon = SFSymbol.named("clock.fill");
const timeImage = metaStack.addImage(timeIcon.image);
timeImage.tintColor = metaTextColor;
timeImage.imageSize = new Size(12, 12);
const timeText = metaStack.addText(` ${timeAgo}`);
timeText.font = Font.systemFont(12);
timeText.textColor = metaTextColor;
metaStack.centerAlignContent();
}
async function createWidget(size) {
const htmlData = await fetchForumData();
if (!htmlData) return;
const posts = parseForumPosts(htmlData);
const widget = new ListWidget();
widget.backgroundColor = backgroundColor;
widget.setPadding(8, 8, 8, 8);
const headerStack = widget.addStack();
headerStack.layoutHorizontally();
headerStack.centerAlignContent();
headerStack.spacing = 8;
const headerIcon = SFSymbol.named("newspaper.fill");
const headerIconImage = headerStack.addImage(headerIcon.image);
headerIconImage.tintColor = Color.blue();
headerIconImage.imageSize = new Size(20, 20);
const headerText = headerStack.addText("Neuste Beiträge");
headerText.font = Font.boldSystemFont(16);
headerText.textColor = textColor;
widget.addSpacer(12);
const maxPosts = size === "small" ? 1 : size === "medium" ? 2 : 5;
posts.slice(0, maxPosts).forEach((post, index) => {
const postBackground = widget.addStack();
postBackground.layoutVertically();
postBackground.setPadding(8, 8, 8, 8);
postBackground.backgroundColor = getClickedPosts().includes(post.threadId) ? new Color("#A0A0A0") : (index % 2 === 0 ? altRowColor : evenRowColor);
postBackground.cornerRadius = 8;
postBackground.size = new Size(widget.size ? widget.size.width - 32 : 300, 0);
const postStack = postBackground.addStack();
postStack.layoutVertically();
const title = postStack.addText(post.title);
title.font = size === "small" ? Font.boldSystemFont(12) : Font.boldSystemFont(14);
title.textColor = textColor;
title.lineLimit = 1;
title.url = post.url;
title.onTap = () => {
saveClickedPost(post.threadId);
Script.complete();
};
if (size !== "small") {
const metaStack = postStack.addStack();
metaStack.layoutHorizontally();
metaStack.spacing = 4;
addMetaStack(metaStack, post.author, post.timeAgo);
}
widget.addSpacer(8);
});
return widget;
}
const widgetSize = config.widgetFamily || "large";
const widget = await createWidget(widgetSize);
if (!config.runsInWidget) widget.presentLarge();
Script.setWidget(widget);
Script.complete();