├── .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 | |  |  |
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 |
22 |
23 |
24 |
25 |
CONFIGURATION
26 |
27 |
Shortcuts
28 |
29 |
30 |
Feeds
31 |
32 |
33 |
Max feeds
34 |
35 |
36 |
37 |
Other configs
38 | Font family:
39 |
40 | Dark mode:
41 |
42 |
43 |
44 |
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 |
--------------------------------------------------------------------------------