Upload files to "website/news"

This commit is contained in:
2026-01-19 15:06:37 +00:00
parent 76aac92fc5
commit 5db462d9cb
4 changed files with 558 additions and 0 deletions

280
website/news/app.js Normal file
View File

@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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 =
"<div class='item'><span class='title'>No items.</span>" +
"<div class='meta'>Press Refresh to load.</div></div>";
return;
}
var html = [];
for (var i = 0; i < items.length; i++) {
var it = items[i];
html.push("<div class='item'>");
html.push(
"<a class='title' href='" + escapeAttr(it.link) + "' target='_blank' rel='noopener noreferrer'>" +
escapeHtml(it.title) +
"</a>"
);
var meta = [];
if (it.source) meta.push(escapeHtml(it.source));
if (it.pubDate) meta.push(escapeHtml(it.pubDate));
if (meta.length) html.push("<div class='meta'>" + meta.join(" · ") + "</div>");
var snippet = stripHtml(safeText(it.description)).replace(/\s+/g, " ").trim();
if (snippet.length > 220) snippet = snippet.substring(0, 220) + "...";
if (snippet) html.push("<div class='desc'>" + escapeHtml(snippet) + "</div>");
html.push("</div>");
}
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();
});
})();

116
website/news/feeds.js Normal file
View File

@@ -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";
})();

53
website/news/index.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SumiNews (68k-style)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="style.css" />
<!-- Optional: match your other Lunar apps favicon/theme -->
<meta name="theme-color" content="#bef17c" />
</head>
<body>
<div class="center">
<h2>SumiNews</h2>
<div class="grid">
<div class="controls">
<label>
Feed:
<select id="feedSelect"></select>
</label>
<label>
Region:
<select id="regionSelect"></select>
</label>
<button id="refreshBtn" type="button">Refresh</button>
</div>
<div class="fineprint">
Uses Google News RSS. <!-- If nothing loads, set a working proxy in feeds.js -->
</div>
<hr />
<!-- No loading box on purpose -->
<div id="error" class="error" style="display:none;"></div>
<div id="items"></div>
</div>
<p style="font-size: 12px;">
Basic HTML news for old/new browsers. Inspired by the 68k.news approach (topics + region editions).</br>
Based on <a href="http://lunarproject.org" target="_blank">LunarProject.org</a>.
</p>
</div>
<script src="feeds.js"></script>
<script src="app.js"></script>
</body>
</html>

109
website/news/style.css Normal file
View File

@@ -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;
}