diff --git a/website/news/app.js b/website/news/app.js new file mode 100644 index 0000000..712bcec --- /dev/null +++ b/website/news/app.js @@ -0,0 +1,280 @@ +/* app.js - fetch only when Refresh is pressed */ + +(function () { + function $(id) { return document.getElementById(id); } + + function parseQuery() { + var out = {}; + var qs = window.location.search || ""; + if (qs.indexOf("?") === 0) qs = qs.substring(1); + if (!qs) return out; + + var parts = qs.split("&"); + for (var i = 0; i < parts.length; i++) { + var kv = parts[i].split("="); + var k = decodeURIComponent(kv[0] || "").trim(); + var v = decodeURIComponent(kv.slice(1).join("=") || "").trim(); + if (k) out[k] = v; + } + return out; + } + + function setQuery(params) { + var keys = []; + for (var k in params) { + if (params.hasOwnProperty(k) && params[k] !== "" && params[k] != null) { + keys.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k])); + } + } + var qs = keys.length ? ("?" + keys.join("&")) : ""; + if (window.history && window.history.replaceState) { + window.history.replaceState(null, "", window.location.pathname + qs); + } + } + + function findById(arr, id, fallback) { + for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return arr[i]; + return fallback || null; + } + + function buildFeedUrl(feed, region) { + var base; + if (feed.type === "top") { + base = "https://news.google.com/rss"; + } else if (feed.type === "topic" && feed.topic) { + base = "https://news.google.com/rss/headlines/section/topic/" + encodeURIComponent(feed.topic); + } else { + base = "https://news.google.com/rss"; + } + + var q = []; + q.push("hl=" + encodeURIComponent(region.hl)); + q.push("gl=" + encodeURIComponent(region.gl)); + q.push("ceid=" + encodeURIComponent(region.ceid)); + return base + "?" + q.join("&"); + } + + function fetchText(url, cb) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.timeout = 15000; + + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status >= 200 && xhr.status < 300) return cb(null, xhr.responseText); + cb(new Error("HTTP " + xhr.status + " while fetching feed")); + }; + + xhr.ontimeout = function () { + cb(new Error("Timed out while fetching feed")); + }; + + xhr.onerror = function () { + cb(new Error("Network error while fetching feed")); + }; + + xhr.send(null); + } + + function safeText(s) { + if (s == null) return ""; + return String(s); + } + + function escapeHtml(s) { + s = safeText(s); + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function escapeAttr(s) { + return escapeHtml(s); + } + + function stripHtml(s) { + return safeText(s).replace(/<[^>]*>/g, ""); + } + + function parseRss(xmlText) { + var items = []; + var doc = null; + + if (window.DOMParser) { + try { + doc = new DOMParser().parseFromString(xmlText, "text/xml"); + } catch (e) { + doc = null; + } + } + if (!doc || !doc.getElementsByTagName) return items; + + var nodes = doc.getElementsByTagName("item"); + for (var i = 0; i < nodes.length; i++) { + var it = nodes[i]; + + function get(tag) { + var n = it.getElementsByTagName(tag); + if (!n || !n[0] || !n[0].textContent) return ""; + return n[0].textContent; + } + + var title = get("title"); + var link = get("link"); + var pubDate = get("pubDate"); + var desc = get("description"); + + var source = ""; + if (title.indexOf(" - ") !== -1) { + var parts = title.split(" - "); + source = parts[parts.length - 1]; + } + + items.push({ + title: title, + link: link, + pubDate: pubDate, + source: source, + description: desc + }); + } + return items; + } + + function showError(msg, show) { + var el = $("error"); + if (!el) return; + if (show) { + el.style.display = "block"; + el.textContent = msg; + } else { + el.style.display = "none"; + el.textContent = ""; + } + } + + function renderItems(items) { + var host = $("items"); + host.innerHTML = ""; + + if (!items || !items.length) { + host.innerHTML = + "
No items." + + "
Press Refresh to load.
"; + return; + } + + var html = []; + for (var i = 0; i < items.length; i++) { + var it = items[i]; + + html.push("
"); + html.push( + "" + + escapeHtml(it.title) + + "" + ); + + var meta = []; + if (it.source) meta.push(escapeHtml(it.source)); + if (it.pubDate) meta.push(escapeHtml(it.pubDate)); + if (meta.length) html.push("
" + meta.join(" · ") + "
"); + + var snippet = stripHtml(safeText(it.description)).replace(/\s+/g, " ").trim(); + if (snippet.length > 220) snippet = snippet.substring(0, 220) + "..."; + if (snippet) html.push("
" + escapeHtml(snippet) + "
"); + + html.push("
"); + } + + host.innerHTML = html.join(""); + } + + function buildSelectOptions(selectEl, arr, selectedId) { + selectEl.innerHTML = ""; + for (var i = 0; i < arr.length; i++) { + var o = document.createElement("option"); + o.value = arr[i].id; + o.textContent = arr[i].label; + if (arr[i].id === selectedId) o.selected = true; + selectEl.appendChild(o); + } + } + + function getCurrentSelection() { + var feeds = window.NEWS_FEEDS || []; + var regions = window.NEWS_REGIONS || []; + + var feedId = $("feedSelect").value; + var regionId = $("regionSelect").value; + + var feed = findById(feeds, feedId, feeds[0] || null); + var region = findById(regions, regionId, regions[0] || null); + + return { feed: feed, region: region }; + } + + function syncUrlToSelection() { + setQuery({ feed: $("feedSelect").value, region: $("regionSelect").value }); + } + + function requestNews() { + var sel = getCurrentSelection(); + if (!sel.feed || !sel.region) { + showError("feeds.js is missing NEWS_FEEDS or NEWS_REGIONS.", true); + return; + } + + syncUrlToSelection(); + showError("", false); + + var feedUrl = buildFeedUrl(sel.feed, sel.region); + + var proxyBase = window.NEWS_PROXY_BASE || ""; + var fetchUrl = proxyBase ? (proxyBase + encodeURIComponent(feedUrl)) : feedUrl; + + fetchText(fetchUrl, function (err, txt) { + if (err) { + showError( + "Could not load feed. If this is CORS, set NEWS_PROXY_BASE in feeds.js. Details: " + err.message, + true + ); + renderItems([]); + return; + } + renderItems(parseRss(txt)); + }); + } + + window.addEventListener("DOMContentLoaded", function () { + var feeds = window.NEWS_FEEDS || []; + var regions = window.NEWS_REGIONS || []; + + if (!feeds.length || !regions.length) { + showError("No feeds/regions configured. Check feeds.js.", true); + renderItems([]); + return; + } + + var q = parseQuery(); + var feedId = q.feed || window.NEWS_DEFAULT_FEED_ID || feeds[0].id; + var regionId = q.region || window.NEWS_DEFAULT_REGION_ID || regions[0].id; + + buildSelectOptions($("feedSelect"), feeds, feedId); + buildSelectOptions($("regionSelect"), regions, regionId); + + // Keep URL updated when selecting, but DO NOT request. + $("feedSelect").addEventListener("change", syncUrlToSelection); + $("regionSelect").addEventListener("change", syncUrlToSelection); + + $("refreshBtn").addEventListener("click", requestNews); + + // No auto-request on load. + renderItems([]); + syncUrlToSelection(); + }); + })(); + \ No newline at end of file diff --git a/website/news/feeds.js b/website/news/feeds.js new file mode 100644 index 0000000..1be32b3 --- /dev/null +++ b/website/news/feeds.js @@ -0,0 +1,116 @@ +/* feeds.js + Configure feeds + regions here. +*/ + +(function () { + // CORS NOTE: + // Browsers usually cannot fetch https://news.google.com/rss... directly due to CORS. + // Default uses allorigins (raw mode). You can swap to your own proxy. + // + // Examples: + // "" (no proxy; only works if YOUR server adds CORS headers or proxies it) + // "https://api.allorigins.win/raw?url=" + // "https://r.jina.ai/http://YOUR_HTTP_PROXY/" (if you host a proxy) + // + window.NEWS_PROXY_BASE = "https://api.allorigins.win/raw?url="; + + // Feed list (adding entries here automatically adds them to the dropdown) + // type: + // - "top" -> https://news.google.com/rss?... (Top headlines) + // - "topic"-> https://news.google.com/rss/headlines/section/topic/{TOPIC}?... (Topic feeds) + window.NEWS_FEEDS = [ + { id: "top", label: "Top", type: "top" }, + { id: "world", label: "World", type: "topic", topic: "WORLD" }, + { id: "nation", label: "Nation", type: "topic", topic: "NATION" }, + { id: "business", label: "Business", type: "topic", topic: "BUSINESS" }, + { id: "technology", label: "Technology", type: "topic", topic: "TECHNOLOGY" }, + { id: "entertainment", label: "Entertainment", type: "topic", topic: "ENTERTAINMENT" }, + { id: "sports", label: "Sports", type: "topic", topic: "SPORTS" }, + { id: "science", label: "Science", type: "topic", topic: "SCIENCE" }, + { id: "health", label: "Health", type: "topic", topic: "HEALTH" } + ]; + + // Regions (gl/hl/ceid). + // Add more entries and they show up automatically. + window.NEWS_REGIONS = [ + // ---- North America / default English ---- + { id: "US_EN", label: "United States (English)", hl: "en-US", gl: "US", ceid: "US:en" }, + { id: "US_ES", label: "United States (Spanish)", hl: "es-US", gl: "US", ceid: "US:es-419" }, + { id: "CA_EN", label: "Canada (English)", hl: "en-CA", gl: "CA", ceid: "CA:en" }, + { id: "CA_FR", label: "Canada (French)", hl: "fr-CA", gl: "CA", ceid: "CA:fr" }, + + // ---- UK / Ireland ---- + { id: "GB_EN", label: "United Kingdom (English)", hl: "en-GB", gl: "GB", ceid: "GB:en" }, + { id: "IE_EN", label: "Ireland (English)", hl: "en-IE", gl: "IE", ceid: "IE:en" }, + + // ---- Europe (supported Google News editions) ---- + { id: "AT_DE", label: "Austria (Deutsch)", hl: "de-AT", gl: "AT", ceid: "AT:de" }, + + { id: "BE_FR", label: "Belgium (Français)", hl: "fr-BE", gl: "BE", ceid: "BE:fr" }, + { id: "BE_NL", label: "Belgium (Nederlands)", hl: "nl-BE", gl: "BE", ceid: "BE:nl" }, + + { id: "BG_BG", label: "Bulgaria (Български)", hl: "bg-BG", gl: "BG", ceid: "BG:bg" }, + + { id: "CH_DE", label: "Switzerland (Deutsch)", hl: "de-CH", gl: "CH", ceid: "CH:de" }, + { id: "CH_FR", label: "Switzerland (Français)", hl: "fr-CH", gl: "CH", ceid: "CH:fr" }, + + { id: "CZ_CS", label: "Czechia (Čeština)", hl: "cs-CZ", gl: "CZ", ceid: "CZ:cs" }, + + { id: "DE_DE", label: "Germany (Deutsch)", hl: "de-DE", gl: "DE", ceid: "DE:de" }, + + { id: "ES_ES", label: "Spain (Español)", hl: "es-ES", gl: "ES", ceid: "ES:es" }, + + { id: "FR_FR", label: "France (Français)", hl: "fr-FR", gl: "FR", ceid: "FR:fr" }, + + { id: "GR_EL", label: "Greece (Ελληνικά)", hl: "el-GR", gl: "GR", ceid: "GR:el" }, + + { id: "HU_HU", label: "Hungary (Magyar)", hl: "hu-HU", gl: "HU", ceid: "HU:hu" }, + + { id: "IT_IT", label: "Italy (Italiano)", hl: "it-IT", gl: "IT", ceid: "IT:it" }, + + { id: "LT_LT", label: "Lithuania (Lietuvių)", hl: "lt-LT", gl: "LT", ceid: "LT:lt" }, + + { id: "LV_EN", label: "Latvia (English)", hl: "en-LV", gl: "LV", ceid: "LV:en" }, + { id: "LV_LV", label: "Latvia (Latviešu)", hl: "lv-LV", gl: "LV", ceid: "LV:lv" }, + + { id: "NL_NL", label: "Netherlands (Nederlands)", hl: "nl-NL", gl: "NL", ceid: "NL:nl" }, + + { id: "NO_NO", label: "Norway (Norsk)", hl: "no-NO", gl: "NO", ceid: "NO:no" }, + + { id: "PL_PL", label: "Poland (Polski)", hl: "pl-PL", gl: "PL", ceid: "PL:pl" }, + + { id: "PT_PT", label: "Portugal (Português)", hl: "pt-PT", gl: "PT", ceid: "PT:pt-150" }, + + { id: "RO_RO", label: "Romania (Română)", hl: "ro-RO", gl: "RO", ceid: "RO:ro" }, + + { id: "RS_SR", label: "Serbia (Srpski)", hl: "sr-RS", gl: "RS", ceid: "RS:sr" }, + + { id: "SE_SV", label: "Sweden (Svenska)", hl: "sv-SE", gl: "SE", ceid: "SE:sv" }, + + { id: "SI_SL", label: "Slovenia (Slovenščina)", hl: "sl-SI", gl: "SI", ceid: "SI:sl" }, + + { id: "SK_SK", label: "Slovakia (Slovenčina)", hl: "sk-SK", gl: "SK", ceid: "SK:sk" }, + + { id: "UA_UK", label: "Ukraine (Українська)", hl: "uk-UA", gl: "UA", ceid: "UA:uk" }, + { id: "UA_RU", label: "Ukraine (Русский)", hl: "ru-UA", gl: "UA", ceid: "UA:ru" }, + + { id: "TR_TR", label: "Turkey (Türkçe)", hl: "tr-TR", gl: "TR", ceid: "TR:tr" }, + + // ---- Russia ---- + { id: "RU_RU", label: "Russia (Русский)", hl: "ru-RU", gl: "RU", ceid: "RU:ru" }, + + // ---- Asia ---- + { id: "JP_JA", label: "Japan (日本語)", hl: "ja-JP", gl: "JP", ceid: "JP:ja" }, + { id: "KR_KO", label: "Korea (한국어)", hl: "ko-KR", gl: "KR", ceid: "KR:ko" }, + { id: "CN_ZH", label: "China (简体中文)", hl: "zh-CN", gl: "CN", ceid: "CN:zh-Hans" }, + + // ---- India ---- + { id: "IN_EN", label: "India (English)", hl: "en-IN", gl: "IN", ceid: "IN:en" }, + { id: "IN_HI", label: "India (हिन्दी)", hl: "hi-IN", gl: "IN", ceid: "IN:hi" }, + ]; + + + window.NEWS_DEFAULT_FEED_ID = "top"; + window.NEWS_DEFAULT_REGION_ID = "US"; + })(); + \ No newline at end of file diff --git a/website/news/index.html b/website/news/index.html new file mode 100644 index 0000000..9f184fc --- /dev/null +++ b/website/news/index.html @@ -0,0 +1,53 @@ + + + + + SumiNews (68k-style) + + + + + + + + + +
+

SumiNews

+ +
+
+ + + + + +
+ +
+ Uses Google News RSS. +
+ +
+ + + +
+
+ +

+ Basic HTML news for old/new browsers. Inspired by the 68k.news approach (topics + region editions).
+ Based on LunarProject.org. +

+
+ + + + + diff --git a/website/news/style.css b/website/news/style.css new file mode 100644 index 0000000..194a4c0 --- /dev/null +++ b/website/news/style.css @@ -0,0 +1,109 @@ +/* + Lunar-ish styling (similar vibe to your Launchpad/Weather) +*/ + +html, body { + font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: #222; + background-color: #ddd; + margin: 0; + padding: 0.5rem; + } + + a, a:visited { + text-decoration: none; + color: #a22121; + } + + .center { + margin-left: auto; + margin-right: auto; + width: 100%; + max-width: 520px; + text-align: center; + } + + .grid { + border: 1px solid #000; + padding: 10px; + background-color: #fff; + text-align: left; + } + + h2 { + text-align: center; + margin: 0.4rem 0 0.6rem 0; + } + + .controls { + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + } + + .controls label { + font-size: 14px; + display: inline-flex; + align-items: center; + gap: 6px; + } + + select, button { + font-size: 14px; + padding: 4px 6px; + } + + button { + cursor: pointer; + } + + hr { + border: none; + border-top: 1px solid #000; + margin: 10px 0; + } + + .fineprint { + margin-top: 8px; + font-size: 12px; + text-align: left; + color: #333; + } + + .error { + border: 1px solid #a22121; + background: #fff3f3; + color: #7a1111; + padding: 8px; + margin: 8px 0; + font-size: 13px; + } + + .item { + padding: 8px 0; + border-bottom: 1px dashed #bbb; + } + + .item:last-child { + border-bottom: none; + } + + .item .title { + font-weight: bold; + display: inline-block; + } + + .item .meta { + font-size: 12px; + color: #444; + margin-top: 2px; + } + + .item .desc { + font-size: 13px; + margin-top: 6px; + color: #222; + } + \ No newline at end of file