├── .gitattributes ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── config.js ├── constants.js ├── icons ├── icon_16.png └── icon_48.png ├── libs └── feednami-client-v1.1.js ├── manifest.json ├── newtab.html ├── script.js ├── styles.css └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.md text 5 | *.xsd text 6 | *.sh text 7 | *.txt text 8 | *.ui text 9 | *.js text 10 | *.css text 11 | *.html text 12 | 13 | *.png binary 14 | *.jpg binary 15 | *.gif binary 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .DS_Store 3 | 4 | buid 5 | dist 6 | .cache 7 | target 8 | node_modules 9 | 10 | *.db 11 | *.zip 12 | *.log 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tikaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | 3 | clean: 4 | rm build.zip 2>/dev/null || echo "No build exists" 5 | build: 6 | zip -r build.zip * >/dev/null && echo "Build available in build.zip" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Startpage 2 | 3 | | ![](https://i.imgur.com/TCvyBDr.png) | ![](https://i.imgur.com/0EV7lRS.png) | 4 | | :----------------------------------: | :----------------------------------: | 5 | | Light Mode | Dark Mode | 6 | 7 | It's a startpage... with favs and feeds! 8 | 9 | Pieced together from [GandaG/startpage](https://github.com/GandaG/startpage) and [Jaredk3nt/homepage](https://github.com/Jaredk3nt/homepage). 10 | 11 | > **Config on top left** 12 | 13 | ### Usage 14 | 15 | #### From addon store 16 | 17 | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/meain-quicktab/) 18 | 19 | 20 | ### On your own 21 | - run `make build` 22 | - import `build.zip` as a plugin in Firefox 23 | 24 | *You used to be able to just pass the filename in `userChrome.js`, but firefox changed that behaviour in 72* 25 | 26 | To switch themes change swap `--text-color` and `--bg-color` in `style.css`. 27 | 28 | 29 | ### TODO 30 | 31 | - [ ] add option to toggle dark mode (save preference) 32 | - [ ] add option to show read blogs 33 | - [ ] add another button called `not my type` along with `mark read` 34 | - [x] let user add blog without having to editing the `constants.js` file 35 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const sh_el = document.getElementById("modal-shortcuts"); 2 | const fl_el = document.getElementById("modal-feeds"); 3 | const mx_fl_el = document.getElementById("modal-max-feeds"); 4 | const mx_font = document.getElementById("modal-font"); 5 | const mx_dark = document.getElementById("modal-dark-mode"); 6 | 7 | function config_value(item, element, defaultValue) { 8 | let val = localStorage.getItem(item); 9 | if (val === null) val = defaultValue; 10 | // else val = JSON.parse(val); 11 | 12 | element.value = val; 13 | element.onchange = () => { 14 | let vc = element.value; 15 | if (item === "dark-mode") vc = element.checked; 16 | localStorage.setItem(item, vc); 17 | }; 18 | } 19 | 20 | config_value("max_feeds", mx_fl_el, MAX_FEED_DEFAULT); 21 | config_value("font-family", mx_font, "consolas"); 22 | config_value("dark-mode", mx_dark, false); 23 | 24 | var html = document.getElementsByTagName("html")[0]; 25 | if (localStorage.getItem("dark-mode") === "true") { 26 | html.style.setProperty("--bg-color", "33, 33, 33"); 27 | html.style.setProperty("--text-color", "255, 255, 255"); 28 | } 29 | if (localStorage.getItem("font-family")) { 30 | html.style.setProperty("--font-family", localStorage.getItem("font-family")); 31 | } 32 | 33 | function update_store(store, text_data) { 34 | const parsed = []; 35 | for (let data of text_data.trim().split("\n")) { 36 | if (data.trim().length === 0) continue; 37 | const splits = data.split(" "); 38 | const first = splits[0]; 39 | splits.shift(); 40 | const rest = splits.join(" "); 41 | parsed.push([first, rest]); 42 | } 43 | localStorage.setItem(store, JSON.stringify(parsed)); 44 | } 45 | 46 | function render_editor(data, store) { 47 | const textified_data = data.map(d => d.join(" ")).join("\n"); 48 | const ta = document.createElement("textarea"); 49 | ta.value = textified_data; 50 | ta.addEventListener( 51 | "input", 52 | function() { 53 | update_store(store, ta.value); 54 | }, 55 | false 56 | ); 57 | return ta; 58 | } 59 | 60 | function show_shorcuts() { 61 | let shorcuts = localStorage.getItem("shortcuts"); 62 | if (shorcuts === null) shorcuts = DEFAULT_SHORTCUTS; 63 | else shorcuts = JSON.parse(shorcuts); 64 | 65 | let feeds = localStorage.getItem("feeds"); 66 | if (feeds === null) feeds = DEFAULT_FEEDS; 67 | else feeds = JSON.parse(feeds); 68 | 69 | sh_el.appendChild(render_editor(shorcuts, "shortcuts")); 70 | fl_el.appendChild(render_editor(feeds, "feeds")); 71 | } 72 | 73 | show_shorcuts(); 74 | 75 | const done_button = document.getElementById("done-button"); 76 | const config_button = document.getElementById("config-button"); 77 | const modal_wrapper = document.getElementById("modal-wrapper"); 78 | 79 | config_button.onclick = () => { 80 | modal_wrapper.style.display = "flex"; 81 | }; 82 | done_button.onclick = () => { 83 | modal_wrapper.style.display = "none"; 84 | }; 85 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_SHORTCUTS = [ 2 | ["github.com", "github"], 3 | ["reddit.com", "reddit"], 4 | ["news.ycombinator.com", "hackernews"], 5 | ["dev.to", "dev"], 6 | ["lobste.rs", "lobsters"] 7 | ]; 8 | 9 | const DEFAULT_FEEDS = [ 10 | ["https://meain.io/feed.xml", "meain"], 11 | ["https://overreacted.io/rss.xml", "Overreacted"], 12 | ["https://hacks.mozilla.org/feed", "MozillaHacks"], 13 | ["https://blog.rust-lang.org/feed.xml", "Rustlang"] 14 | ]; 15 | 16 | const MAX_FEED_DEFAULT = 6; 17 | -------------------------------------------------------------------------------- /icons/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meain/startpage/2fe280c415ac1e80b961837c4bece97803e88cf4/icons/icon_16.png -------------------------------------------------------------------------------- /icons/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meain/startpage/2fe280c415ac1e80b961837c4bece97803e88cf4/icons/icon_48.png -------------------------------------------------------------------------------- /libs/feednami-client-v1.1.js: -------------------------------------------------------------------------------- 1 | 'use strict';var _typeof=typeof Symbol==='function'&&typeof Symbol.iterator==='symbol'?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==='function'&&obj.constructor===Symbol?'symbol':typeof obj};function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError('Cannot call a class as a function')}}(function(){var FeednamiClientRequest=function(){function FeednamiClientRequest(client,url,options,callback){_classCallCheck(this,FeednamiClientRequest);this.client=client;this.url=url;this.options=options||{};this.callback=callback}FeednamiClientRequest.prototype.run=function run(){var _this=this;if(!this.callback){if('fetch'in window){return this.send().then(function(data){if(data.error){var error=new Error(data.error.message);error.code=data.error.code;return Promise.reject(error)}return data.feed})}return new Promise(function(resolve,reject){_this.send(function(data){if(data.error){var error=new Error(data.error.message);error.code=data.error.code;reject(error)}else{resolve(data.feed)}})})}else{this.send(this.callback)}};FeednamiClientRequest.prototype.send=function send(callback){var endpoint=window.feednamiEndpoint||'https://api.feednami.com';var apiRoot=endpoint+'/api/v1.1';var feedUrl=this.url;var options=this.options;var qs='url='+encodeURIComponent(feedUrl);if(this.client.publicApiKey){qs+='&public_api_key='+this.client.publicApiKey}if(options.format){qs+='&include_xml_document&format='+options.format}if(options.includeXml){qs+='&include_xml_document'}var url=apiRoot+'/feeds/load?'+qs;if('fetch'in window){return fetch(url).then(function(res){return res.json()}).then(function(data){if(callback){callback(data)}return data})}else if(window.XDomainRequest){(function(){var script=document.createElement('script');var callbackName='jsonp_callback_'+new Date().getTime()+'_'+Math.round(1000000*Math.random());url+='&jsonp_callback='+callbackName;window[callbackName]=function(data){callback(data);document.body.removeChild(script);window[callbackName]=null;try{delete window[callbackName]}catch(e){}};script.src=url;document.body.appendChild(script)})()}else{(function(){var req=new XMLHttpRequest;req.onreadystatechange=function(){if(req.readyState==4){callback(JSON.parse(req.responseText))}};req.open('GET',url);req.send()})()}};return FeednamiClientRequest}();var FeednamiClient=function(){function FeednamiClient(){_classCallCheck(this,FeednamiClient);this.promisePolyfillCallbacks=[];this.promiseLoaded='Promise'in window}FeednamiClient.prototype.loadPolyfills=function loadPolyfills(callback){if(this.promiseLoaded){callback()}else{this.promisePolyfillCallbacks.push(callback);if(!this.polyfillScriptsAdded){this.polyfillScriptsAdded=true;var script=document.createElement('script');script.src='https://feednami-static.storage.googleapis.com/js/v1.1/promise.js';document.head.appendChild(script)}}};FeednamiClient.prototype.loadPromiseCallback=function loadPromiseCallback(){this.promiseLoaded=true;for(var _iterator=this.promisePolyfillCallbacks,_isArray=Array.isArray(_iterator),_i=0,_iterator=_isArray?_iterator:_iterator[Symbol.iterator]();;){var _ref;if(_isArray){if(_i>=_iterator.length)break;_ref=_iterator[_i++]}else{_i=_iterator.next();if(_i.done)break;_ref=_i.value}var callback=_ref;console.log(callback);callback()}};FeednamiClient.prototype.load=function load(url,options,callback){if((typeof url==='undefined'?'undefined':_typeof(url))=='object'){return this.load(options.url,options,callback)}if(typeof options=='function'){return this.load(url,{},options)}var request=new FeednamiClientRequest(this,url,options,callback);return request.run()};FeednamiClient.prototype.loadGoogleFormat=function loadGoogleFormat(url,callback){return feednami.load(url,{format:'google',includeXml:true},callback)};FeednamiClient.prototype.setPublicApiKey=function setPublicApiKey(key){this.publicApiKey=key};return FeednamiClient}();window.feednami=new FeednamiClient})(); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QuickTab", 3 | "short_name": "QT", 4 | "version": "0.6", 5 | "manifest_version": 2, 6 | "description": "Simple clean newtab page with feeds", 7 | "icons": { 8 | "48": "icons/icon_48.png", 9 | "16": "icons/icon_16.png" 10 | }, 11 | "permissions": ["tabs"], 12 | "chrome_url_overrides": 13 | { 14 | "newtab": "newtab.html" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /newtab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 |
18 |

19 |
^.^
20 |
21 |
22 |
23 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | let articles = []; 2 | let read_articles = []; 3 | 4 | let max_feed_num = localStorage.getItem("max_feeds"); 5 | if (max_feed_num === null) max_feed_num = MAX_FEED_DEFAULT; 6 | 7 | function setup_groups() { 8 | let $container = document.getElementById("content"); 9 | 10 | let shorcuts = localStorage.getItem("shortcuts"); 11 | if (shorcuts === null) shorcuts = DEFAULT_SHORTCUTS; 12 | else shorcuts = JSON.parse(shorcuts); 13 | 14 | for (let shorcut of shorcuts) { 15 | let value = shorcut[1]; 16 | 17 | let group = document.createElement("div"); 18 | group.className = "group"; 19 | $container.appendChild(group); 20 | 21 | let link = document.createElement("a"); 22 | link.setAttribute("href", "https://" + shorcut[0]); 23 | group.appendChild(link); 24 | 25 | let image = document.createElement("span"); 26 | image.setAttribute("src", value); 27 | 28 | const linkText = document.createTextNode(value); 29 | link.appendChild(linkText); 30 | } 31 | } 32 | 33 | class FeedItem { 34 | constructor(title, link, date, feed_title, read_time) { 35 | this.title = title; 36 | this.link = link; 37 | this.mseconds = date !== null ? date.getTime() : null; 38 | this.read_time = read_time; 39 | 40 | let hostname = feed_title; 41 | let elapsed = 42 | date !== null 43 | ? Math.trunc((Date.now() - date.getTime()) / 1000 / 60 / 60) 44 | : null; 45 | this.summary = hostname + " - "; 46 | if (elapsed === null) { 47 | this.summary += "unknown"; 48 | } else if (elapsed == 0) { 49 | this.summary += "less than an hour ago"; 50 | } else if (elapsed > 24 * 30) { 51 | this.summary += Math.round(elapsed / 24 / 30) + " months ago"; 52 | } else if (elapsed > 24) { 53 | this.summary += Math.round(elapsed / 24) + " days ago"; 54 | } else { 55 | this.summary += elapsed + " hours ago"; 56 | } 57 | } 58 | } 59 | 60 | function feed_add(title, description, url, read_time) { 61 | let feed = document.getElementById("feed_list"); 62 | 63 | let feed_wrapper = document.createElement("div"); 64 | feed_wrapper.setAttribute("class", "feed_item"); 65 | feed.appendChild(feed_wrapper); 66 | 67 | let link_elem = document.createElement("a"); 68 | link_elem.setAttribute("href", url); 69 | feed_wrapper.appendChild(link_elem); 70 | 71 | let title_elem = document.createElement("p"); 72 | title_elem.setAttribute("class", "title"); 73 | title_elem.innerHTML = title; 74 | link_elem.appendChild(title_elem); 75 | 76 | let desc_elem = document.createElement("p"); 77 | desc_elem.setAttribute("class", "summary"); 78 | link_elem.appendChild(desc_elem); 79 | 80 | let url_favicon = document.createElement("img"); 81 | let hostname = get_hostname(url); 82 | url_favicon.setAttribute( 83 | "src", 84 | "https://s2.googleusercontent.com/s2/favicons?domain=" + hostname 85 | ); 86 | url_favicon.setAttribute("class", "feed_favicon"); 87 | 88 | let bottom_bar = document.createElement("div"); 89 | bottom_bar.setAttribute("class", "bottom-bar"); 90 | 91 | let read_time_elem = document.createElement("p"); 92 | read_time_elem.setAttribute("class", "bottom"); 93 | read_time_elem.innerHTML = read_time; 94 | bottom_bar.appendChild(read_time_elem); 95 | 96 | desc_elem.appendChild(url_favicon); 97 | desc_elem.innerHTML += " "; 98 | desc_elem.innerHTML += description; 99 | 100 | let read_button = document.createElement("button"); 101 | read_button.setAttribute("class", "button"); 102 | read_button.innerText = "Mark as read"; 103 | read_button.onclick = () => { 104 | add_read_article(url); 105 | }; 106 | bottom_bar.appendChild(read_button); 107 | feed_wrapper.appendChild(bottom_bar); 108 | } 109 | 110 | async function feed_mix() { 111 | let mixed_feeds = []; 112 | let promise_list = []; 113 | 114 | let feeds = localStorage.getItem("feeds"); 115 | if (feeds === null) feeds = DEFAULT_FEEDS; 116 | else feeds = JSON.parse(feeds); 117 | 118 | for (let feed of feeds) promise_list.push(feednami.load(feed[0])); 119 | 120 | let feed_list = await Promise.all( 121 | promise_list.map(p => p.catch(error => null)) 122 | ); 123 | 124 | // create object 125 | for (let f in feeds) { 126 | const feed = feed_list[f]; 127 | const feed_title = feeds[f][1]; 128 | 129 | if (feed == null) continue; 130 | let flist = []; 131 | 132 | for (let entry of feed.entries) { 133 | if (!entry.title || !entry.link) continue; 134 | let read_time = "-"; 135 | if (entry.description && entry.description !== null) 136 | read_time = 137 | Math.ceil(entry.description.split(" ").length / 200) + " minutes"; 138 | let feed_item = new FeedItem( 139 | entry.title, 140 | entry.link, 141 | entry.date !== null ? new Date(entry.date) : null, 142 | feed_title, 143 | read_time 144 | ); 145 | flist.push(feed_item); 146 | } 147 | feed_list[f] = flist; 148 | } 149 | 150 | // mix 151 | let combined_feeds = []; 152 | for (let feed of feed_list) combined_feeds = [...combined_feeds, ...feed]; 153 | feed_list = filter_feed( 154 | combined_feeds.sort((a, b) => { 155 | if (a.mseconds !== null && b.mseconds !== null) { 156 | return b.mseconds - a.mseconds; 157 | } else if (a.mseconds === null) { 158 | return 1; 159 | } else if (b.mseconds === null) { 160 | return -1; 161 | } else { 162 | return 0; 163 | } 164 | }), 165 | true 166 | ); 167 | 168 | return feed_list; 169 | } 170 | 171 | function filter_feed(list, fromLocalStorage) { 172 | if (fromLocalStorage) { 173 | let rf = localStorage.getItem("read"); 174 | if (rf == null) return list; 175 | read_articles = JSON.parse(rf); 176 | } 177 | return list.filter(l => !read_articles.includes(l.link)); 178 | } 179 | 180 | function add_read_article(article) { 181 | if (!read_articles.includes(article)) { 182 | read_articles.push(article); 183 | read_articles = read_articles.slice( 184 | Math.max(read_articles.length - 3000, 0) 185 | ); 186 | populate_feed(articles); 187 | localStorage.setItem("read", JSON.stringify(read_articles)); 188 | } 189 | } 190 | 191 | function feed_clean() { 192 | let feed = document.getElementById("feed_list"); 193 | feed.innerHTML = null; 194 | } 195 | 196 | function populate_feed(list, fromLocalStorage = false) { 197 | feed_clean(); 198 | for (let item of filter_feed(list, fromLocalStorage).slice(0, max_feed_num)) { 199 | feed_add(item.title, item.summary, item.link, item.read_time); 200 | } 201 | } 202 | 203 | function flashReloadButon() { 204 | let reloadButton = document.getElementById("reload-button"); 205 | reloadButton.innerText = "Content reloaded"; 206 | setTimeout(() => { 207 | reloadButton.innerText = "RELOAD"; 208 | }, 2000); 209 | } 210 | 211 | function setup_feed(ignoreCache = false) { 212 | if (ignoreCache) { 213 | let reloadButton = document.getElementById("reload-button"); 214 | reloadButton.innerText = "RELOADING"; 215 | } 216 | const lut = localStorage.getItem("lastArticlesUpdateTime"); 217 | if (lut == "null" || ignoreCache) { 218 | localStorage.setItem("lastArticlesUpdateTime", +new Date()); 219 | feed_mix().then(mixed_feeds => { 220 | flashReloadButon(); 221 | articles = mixed_feeds; 222 | populate_feed(mixed_feeds, true); 223 | localStorage.setItem("articles", JSON.stringify(articles)); 224 | }); 225 | } else { 226 | const cachedArticles = JSON.parse(localStorage.getItem("articles")); 227 | articles = cachedArticles; 228 | if (articles) populate_feed(articles, true); 229 | if (getHours(lut) > 48) { 230 | feed_mix().then(mixed_feeds => { 231 | localStorage.setItem("lastArticlesUpdateTime", +new Date()); 232 | articles = mixed_feeds; 233 | populate_feed(mixed_feeds, true); 234 | localStorage.setItem("articles", JSON.stringify(articles)); 235 | }); 236 | } 237 | } 238 | } 239 | 240 | function fill_message() { 241 | setInterval(() => { 242 | document.getElementById("welcome-string").innerText = getTime(); 243 | }, 100); 244 | } 245 | 246 | function main() { 247 | fill_message(); 248 | setup_groups(); 249 | setup_feed(); 250 | } 251 | 252 | function inject() { 253 | let reloadButton = document.getElementById("reload-button"); 254 | reloadButton.onclick = () => setup_feed(true); 255 | } 256 | 257 | main(); 258 | window.onload = inject; 259 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Red borders on visible elements - handy for debugging */ 2 | /* * { border: 1px solid red; } */ 3 | 4 | html { 5 | /* reverse for dark mode */ 6 | --bg-color: 255, 255, 255; 7 | --text-color: 33, 48, 48; 8 | --font-family: "Consolas", monaco, monospace; 9 | } 10 | 11 | body { 12 | padding: 0; 13 | margin: 0; 14 | 15 | background-color: rgba(var(--bg-color), 1); 16 | background-size: cover; 17 | color: rgba(var(--text-color), 1); 18 | 19 | font-family: var(--font-family); 20 | font-size: 22px; 21 | 22 | animation: fadein 1s; 23 | } 24 | 25 | .strings { 26 | margin-bottom: 5vh; 27 | } 28 | 29 | #welcome-string { 30 | height: 10vh; 31 | padding-top: 10vh; 32 | text-align: center; 33 | font-weight: 200; 34 | font-size: 4rem; 35 | margin-bottom: 0; 36 | } 37 | #nowelcome-string { 38 | text-align: center; 39 | font-weight: 100; 40 | color: rgba(var(--text-color), 1); 41 | opacity: 0.5; 42 | } 43 | 44 | #content { 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | /* bottom: 20px; */ 49 | /* position: absolute; */ 50 | /* width: 100%; */ 51 | } 52 | .group a { 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | padding: 10px; 57 | filter: grayscale(100%); 58 | } 59 | .group img { 60 | height: 30px; 61 | background-color: rgba(var(--bg-color), 1); 62 | border-radius: 5px; 63 | } 64 | 65 | .title { 66 | font-size: 20px; 67 | font-weight: 350; 68 | margin-top: 5px; 69 | margin-bottom: 0; 70 | } 71 | .summary { 72 | font-size: 15px; 73 | margin: 5px; 74 | vertical-align: middle; 75 | } 76 | .feed_favicon { 77 | height: 18px; 78 | vertical-align: middle; 79 | } 80 | 81 | a, 82 | a:hover { 83 | transition: all 0.4s ease; 84 | } 85 | a { 86 | color: rgba(var(--text-color), 1); 87 | text-decoration: none; 88 | opacity: 0.5; 89 | } 90 | a:hover { 91 | opacity: 1; 92 | filter: grayscale(0%); 93 | } 94 | 95 | @keyframes fadein { 96 | from { 97 | opacity: 0; 98 | } 99 | to { 100 | opacity: 1; 101 | } 102 | } 103 | 104 | #feed_list { 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | flex-wrap: wrap; 109 | padding: 50px; 110 | } 111 | @media only screen and (max-height: 800px) { 112 | #feed_list { 113 | height: 40vh; 114 | overflow: hidden; 115 | padding: 30px; 116 | } 117 | } 118 | @media only screen and (max-width: 900px) { 119 | #feed_list { 120 | height: 40vh; 121 | overflow: hidden; 122 | padding: 30px; 123 | } 124 | } 125 | @media only screen and (max-width: 600px) { 126 | #feed_list { 127 | overflow: auto; 128 | padding: 30px; 129 | } 130 | } 131 | 132 | .feed_item { 133 | padding: 5px 20px; 134 | margin: 10px; 135 | border: 3px solid rgba(var(--text-color), 0.2); 136 | border-radius: 5px; 137 | background-color: rgba(var(--bg-color), 1); 138 | } 139 | .feed_item:hover { 140 | filter: invert(1); 141 | } 142 | .feed_item:hover a { 143 | opacity: 1; 144 | } 145 | .feed_item:hover img { 146 | filter: invert(1); 147 | } 148 | 149 | .buttons { 150 | display: flex; 151 | justify-content: space-between; 152 | } 153 | .button { 154 | margin: 0; 155 | outline: none; 156 | background: transparent; 157 | background-color: rgba(var(--bg-color), 1); 158 | color: rgba(var(--text-color), 0.7); 159 | opacity: 0.3; 160 | border: 0; 161 | text-transform: uppercase; 162 | font-weight: 200; 163 | font-family: var(--font-family); 164 | cursor: pointer; 165 | transition: all 0.4s ease; 166 | white-space: nowrap; 167 | } 168 | 169 | .button:hover { 170 | opacity: 1; 171 | } 172 | 173 | #reload-button { 174 | color: rgba(var(--text-color), 1); 175 | background-color: rgba(var(--bg-color), 1); 176 | padding: 10px; 177 | } 178 | 179 | .bottom-bar { 180 | display: flex; 181 | } 182 | 183 | p.bottom { 184 | margin: 0; 185 | outline: none; 186 | background: transparent; 187 | background-color: rgba(var(--bg-color), 1); 188 | opacity: 0.3; 189 | border: 0; 190 | text-align: left; 191 | width: 100%; 192 | font-weight: 200; 193 | font-family: var(--font-family); 194 | cursor: pointer; 195 | transition: all 0.4s ease; 196 | font-size: 0.9rem; 197 | } 198 | 199 | .modal-wrapper { 200 | position: absolute; 201 | display: flex; 202 | justify-content: center; 203 | align-items: center; 204 | background-color: rgba(var(--bg-color), 0.7); 205 | z-index: 10; 206 | width: 100vw; 207 | height: 100vh; 208 | left: 0; 209 | top: 0; 210 | } 211 | 212 | .modal { 213 | background-color: rgba(var(--bg-color), 1); 214 | border-radius: 5px; 215 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0px 2px rgba(0, 0, 0, 0.24); 216 | padding: 40px 20px; 217 | } 218 | 219 | .modal textarea { 220 | width: 500px; 221 | height: 100px; 222 | border-radius: 5px; 223 | border: 1px solid #ccc; 224 | font-family: var(--font-family); 225 | font-size: 15px; 226 | padding: 10px; 227 | background-color: #f5f5f5; 228 | } 229 | 230 | .modal h5 { 231 | margin-bottom: 0; 232 | } 233 | 234 | .done-button { 235 | margin-top: 25px; 236 | padding: 10px 20px; 237 | border: none; 238 | width: 100%; 239 | box-sizing: border-box; 240 | border-radius: 5px; 241 | cursor: pointer; 242 | } 243 | 244 | .done-button:hover { 245 | background-color: #bbdaff; 246 | } 247 | 248 | .input-label { 249 | font-size: 1rem; 250 | } 251 | 252 | .modal-wrapper { 253 | display: none; 254 | } 255 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function getTime() { 2 | let date = new Date(), 3 | min = date.getMinutes(), 4 | sec = date.getSeconds(), 5 | hour = date.getHours(); 6 | hour = hour > 12 ? hour - 12 : hour; 7 | return ( 8 | "" + 9 | (hour < 10 ? "0" + hour : hour) + 10 | ":" + 11 | (min < 10 ? "0" + min : min) + 12 | ":" + 13 | (sec < 10 ? "0" + sec : sec) 14 | ); 15 | } 16 | 17 | 18 | function get_hostname(url) { 19 | return new URL(url).hostname; 20 | } 21 | 22 | 23 | function getHours(date) { 24 | const diffTime = Math.abs(+new Date() - date); 25 | return Math.ceil(diffTime / (1000 * 60 * 60)); 26 | } 27 | --------------------------------------------------------------------------------