280 lines
8.4 KiB
JavaScript
280 lines
8.4 KiB
JavaScript
/* 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();
|
|
});
|
|
})();
|
|
|