Upload files to "website/news"
This commit is contained in:
280
website/news/app.js
Normal file
280
website/news/app.js
Normal 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, "&")
|
||||
.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 =
|
||||
"<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
116
website/news/feeds.js
Normal 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
53
website/news/index.html
Normal 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
109
website/news/style.css
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user