diff --git a/frontends/bringrss_flask/static/js/api.js b/frontends/bringrss_flask/static/js/api.js index a520dd2..07ea2c2 100644 --- a/frontends/bringrss_flask/static/js/api.js +++ b/frontends/bringrss_flask/static/js/api.js @@ -6,129 +6,161 @@ api.feeds = {}; api.feeds.add_feed = function add_feed(rss_url, title, isolate_guids, callback) { - const url = "/feeds/add"; - const data = {"rss_url": rss_url, "title": title, "isolate_guids": isolate_guids}; - return common.post(url, data, callback); + return http.post({ + url: "/feeds/add", + data: {"rss_url": rss_url, "title": title, "isolate_guids": isolate_guids}, + callback: callback, + }); } api.feeds.delete = function delete_feed(feed_id, callback) { - const url = `/feed/${feed_id}/delete`; - return common.post(url, null, callback); + return http.post({ + url: `/feed/${feed_id}/delete`, + callback: callback, + }); } api.feeds.get_feeds = function get_feeds(callback) { - const url = "/feeds.json"; - return common.get(url, callback); + return http.get({ + url: "/feeds.json", + callback: callback, + }); } api.feeds.refresh = function refresh(feed_id, callback) { - const url = `/feed/${feed_id}/refresh`; - return common.post(url, null, callback); + return http.post({ + url: `/feed/${feed_id}/refresh`, + callback: callback, + }); } api.feeds.refresh_all = function refresh_all(callback) { - const url = "/feeds/refresh_all"; - return common.post(url, null, callback); + return http.post({ + url: "/feeds/refresh_all", + callback: callback, + }); } api.feeds.set_autorefresh_interval = function set_autorefresh_interval(feed_id, interval, callback) { - const url = `/feed/${feed_id}/set_autorefresh_interval`; - const data = {"autorefresh_interval": interval}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_autorefresh_interval`, + data: {"autorefresh_interval": interval}, + callback: callback, + }); } api.feeds.set_filters = function set_filters(feed_id, filter_ids, callback) { - const url = `/feed/${feed_id}/set_filters`; - const data = {"filter_ids": filter_ids.join(",")}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_filters`, + data: {"filter_ids": filter_ids.join(",")}, + callback: callback, + }); } api.feeds.set_http_headers = function set_http_headers(feed_id, http_headers, callback) { - const url = `/feed/${feed_id}/set_http_headers`; - const data = {"http_headers": http_headers}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_http_headers`, + data: {"http_headers": http_headers}, + callback: callback, + }); } api.feeds.set_icon = function set_icon(feed_id, image_base64, callback) { - const url = `/feed/${feed_id}/set_icon`; - const data = {"image_base64": image_base64}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_icon`, + data: {"image_base64": image_base64}, + callback: callback, + }); } api.feeds.set_isolate_guids = function set_isolate_guids(feed_id, isolate_guids, callback) { - const url = `/feed/${feed_id}/set_isolate_guids`; - const data = {"isolate_guids": isolate_guids}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_isolate_guids`, + data: {"isolate_guids": isolate_guids}, + callback: callback, + }); } api.feeds.set_parent = function set_parent(feed_id, parent_id, ui_order_rank, callback) { - const url = `/feed/${feed_id}/set_parent`; - const data = {"parent_id": parent_id}; if (ui_order_rank !== null) { data["ui_order_rank"] = ui_order_rank; } - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_parent`, + data: {"parent_id": parent_id}, + callback: callback, + }); } api.feeds.set_refresh_with_others = function set_refresh_with_others(feed_id, refresh_with_others, callback) { - const url = `/feed/${feed_id}/set_refresh_with_others`; - const data = {"refresh_with_others": refresh_with_others}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_refresh_with_others`, + data: {"refresh_with_others": refresh_with_others}, + callback: callback, + }); } api.feeds.set_rss_url = function set_rss_url(feed_id, rss_url, callback) { - const url = `/feed/${feed_id}/set_rss_url`; - const data = {"rss_url": rss_url}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_rss_url`, + data: {"rss_url": rss_url}, + callback: callback, + }); } api.feeds.set_web_url = function set_web_url(feed_id, web_url, callback) { - const url = `/feed/${feed_id}/set_web_url`; - const data = {"web_url": web_url}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_web_url`, + data: {"web_url": web_url}, + callback: callback, + }); } api.feeds.set_title = function set_title(feed_id, title, callback) { - const url = `/feed/${feed_id}/set_title`; - const data = {"title": title}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_title`, + data: {"title": title}, + callback: callback, + }); } api.feeds.set_ui_order_rank = function set_ui_order_rank(feed_id, ui_order_rank, callback) { - const url = `/feed/${feed_id}/set_ui_order_rank`; - const data = {"ui_order_rank": ui_order_rank}; - return common.post(url, data, callback); + return http.post({ + url: `/feed/${feed_id}/set_ui_order_rank`, + data: {"ui_order_rank": ui_order_rank}, + callback: callback, + }); } /**************************************************************************************************/ @@ -137,67 +169,84 @@ api.filters = {}; api.filters.add_filter = function add_filter(name, conditions, actions, callback) { - const url = "/filters/add"; - const data = {"name": name, "conditions": conditions, "actions": actions}; - return common.post(url, data, callback); + return http.post({ + url: "/filters/add", + data: {"name": name, "conditions": conditions, "actions": actions}, + callback: callback, + }); } api.filters.delete_filter = function delete_filter(filter_id, callback) { - const url = `/filter/${filter_id}/delete`; - return common.post(url, null, callback); + return http.post({ + url: `/filter/${filter_id}/delete`, + callback: callback, + }); } api.filters.get_filters = function get_filters(callback) { - const url = "/filters.json"; - return common.get(url, callback); + return http.get({ + url: "/filters.json", + callback: callback, + }); } api.filters.run_filter_now = function run_filter_now(filter_id, feed_id, callback) { - const url = `/filter/${filter_id}/run_filter`; const data = {}; if (feed_id !== null) { data['feed_id'] = feed_id; } - return common.post(url, data, callback); + return http.post({ + url: `/filter/${filter_id}/run_filter`, + data: data, + callback: callback, + }); } api.filters.set_actions = function set_actions(filter_id, actions, callback) { - const url = `/filter/${filter_id}/set_actions`; - const data = {"actions": actions}; - return common.post(url, data, callback); + return http.post({ + url: `/filter/${filter_id}/set_actions`, + data: {"actions": actions}, + callback: callback, + }); } api.filters.set_conditions = function set_conditions(filter_id, conditions, callback) { - const url = `/filter/${filter_id}/set_conditions`; - const data = {"conditions": conditions}; - return common.post(url, data, callback); + return http.post({ + url: `/filter/${filter_id}/set_conditions`, + data: {"conditions": conditions}, + callback: callback, + }); } api.filters.set_name = function set_name(filter_id, name, callback) { - const url = `/filter/${filter_id}/set_name`; - const data = {"name": name}; - return common.post(url, data, callback); + return http.post({ + url: `/filter/${filter_id}/set_name`, + data: {"name": name}, + callback: callback, + }); } api.filters.update_filter = function update_filter(filter_id, name, conditions, actions, callback) { - const url = `/filter/${filter_id}/update`; - const data = {"name": name, "conditions": conditions, "actions": actions}; - return common.post(url, data, callback); + return http.post({ + url: `/filter/${filter_id}/update`, + data: {"name": name, "conditions": conditions, "actions": actions}, + callback: callback, + }); } /**************************************************************************************************/ @@ -206,9 +255,11 @@ api.news = {}; api.news.get_and_set_read = function get_and_set_read(news_id, callback) { - const url = `/news/${news_id}.json`; - const data = {"set_read": true}; - return common.post(url, data, callback); + return http.post({ + url: `/news/${news_id}.json`, + data: {"set_read": true}, + callback: callback, + }); } api.news.get_newss = @@ -230,38 +281,49 @@ function get_newss(feed_id, read, recycled, callback) } let url = (feed_id === null) ? "/news.json" : `/feed/${feed_id}/news.json`; url += parameters; - return common.get(url, callback); + return http.get({ + url: url, + callback: callback, callback, + }); } api.news.set_read = function set_read(news_id, read, callback) { - const url = `/news/${news_id}/set_read`; - const data = {"read": read}; - return common.post(url, data, callback); + return http.post({ + url: `/news/${news_id}/set_read`, + data: {"read": read}, + callback: callback, + }); } api.news.set_recycled = function set_recycled(news_id, recycled, callback) { - const url = `/news/${news_id}/set_recycled`; - const data = {"recycled": recycled}; - return common.post(url, data, callback); + return http.post({ + url: `/news/${news_id}/set_recycled`, + data: {"recycled": recycled}, + callback: callback, + }); } api.news.batch_set_read = function batch_set_read(news_ids, read, callback) { - const url = `/batch/news/set_read`; - const data = {"news_ids": news_ids.join(","), "read": read}; - return common.post(url, data, callback); + return http.post({ + url: `/batch/news/set_read`, + data: {"news_ids": news_ids.join(","), "read": read}, + callback: callback, + }); } api.news.batch_set_recycled = function batch_set_recycled(news_ids, recycled, callback) { - const url = `/batch/news/set_recycled`; - const data = {"news_ids": news_ids.join(","), "recycled": recycled}; - return common.post(url, data, callback); + return http.post({ + url: `/batch/news/set_recycled`, + data: {"news_ids": news_ids.join(","), "recycled": recycled}, + callback: callback, + }); } diff --git a/frontends/bringrss_flask/static/js/common.js b/frontends/bringrss_flask/static/js/common.js index 3dbaf83..20b1c90 100644 --- a/frontends/bringrss_flask/static/js/common.js +++ b/frontends/bringrss_flask/static/js/common.js @@ -34,6 +34,12 @@ function is_wide_mode() return getComputedStyle(document.documentElement).getPropertyValue("--wide").trim() === "1"; } +common.go_to_root = +function go_to_root() +{ + window.location.href = "/"; +} + common.refresh = function refresh() { @@ -51,146 +57,18 @@ function refresh_or_alert(response) window.location.reload(); } -//////////////////////////////////////////////////////////////////////////////////////////////////// -// HTTP //////////////////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////////////////////////// - -common.formdata = -function formdata(data) -{ - fd = new FormData(); - for (let [key, value] of Object.entries(data)) - { - if (value === undefined) - { - continue; - } - if (value === null) - { - value = ''; - } - fd.append(key, value); - } - return fd; -} - -common._request = -function _request(method, url, callback) -{ - /* - Perform an HTTP request and call the `callback` with the response. - - The response will have the following structure: - { - "meta": { - "completed": true / false, - "status": If the connection failed or request otherwise could not - complete, `status` will be 0. If the request completed, - `status` will be the HTTP response code. - "json_ok": If the server responded with parseable json, `json_ok` - will be true, and that data will be in `response.data`. If the - server response was not parseable json, `json_ok` will be false - and `response.data` will be undefined. - "request_url": The URL exactly as given to this call. - } - "data": {JSON parsed from server response if json_ok}. - } - - So, from most lenient to most strict, error catching might look like: - if response.meta.completed - if response.meta.json_ok - if response.meta.status === 200 - if response.meta.status === 200 and response.meta.json_ok - */ - const request = new XMLHttpRequest(); - const response = { - "meta": { - "completed": false, - "status": 0, - "json_ok": false, - "request_url": url, - }, - }; - - request.onreadystatechange = function() - { - /* - readystate values: - 0 UNSENT / ABORTED - 1 OPENED - 2 HEADERS_RECEIVED - 3 LOADING - 4 DONE - */ - if (request.readyState != 4) - {return;} - - if (callback == null) - {return;} - - response.meta.status = request.status; - - if (request.status != 0) - { - response.meta.completed = true; - try - { - response.data = JSON.parse(request.responseText); - response.meta.json_ok = true; - } - catch (exc) - { - response.meta.json_ok = false; - } - } - callback(response); - }; - const asynchronous = true; - request.open(method, url, asynchronous); - return request; -} - -common.get = -function get(url, callback) -{ - request = common._request("GET", url, callback); - request.send(); - return request; -} - -common.post = -function post(url, data, callback) -{ - /* - `data`: - a FormData object which you have already filled with values, or a - dictionary from which a FormData will be made, using common.formdata. - */ - if (data instanceof FormData || data === null) - { - ; - } - else - { - data = common.formdata(data); - } - request = common._request("POST", url, callback); - request.send(data); - return request; -} - //////////////////////////////////////////////////////////////////////////////////////////////////// // STRING TOOLS //////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// common.join_and_trail = -function join_and_trail(l, s) +function join_and_trail(list, separator) { - if (l.length === 0) + if (list.length === 0) { return ""; } - return l.join(s) + s + return list.join(separator) + separator } //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/frontends/bringrss_flask/static/js/http.js b/frontends/bringrss_flask/static/js/http.js new file mode 100644 index 0000000..42547cf --- /dev/null +++ b/frontends/bringrss_flask/static/js/http.js @@ -0,0 +1,210 @@ +const http = {}; + +http.HEADERS = {}; + +http.requests_in_flight = 0; + +http.request_queue = {}; +http.request_queue.array = []; + +http.request_queue.push = +function request_queue_push(func) +{ + http.request_queue.array.push(func) + if (http.requests_in_flight == 0) + { + http.request_queue_next(); + } +} + +http.request_queue.unshift = +function request_queue_unshift(func) +{ + http.request_queue.array.unshift(func) + if (http.requests_in_flight == 0) + { + http.request_queue_next(); + } +} +http.request_queue.pushleft = http.request_queue.unshift; + +http.request_queue_next = +function request_queue_next() +{ + if (http.requests_in_flight > 0) + { + return; + } + if (http.request_queue.array.length === 0) + { + return; + } + const func = http.request_queue.array.shift(); + func(); +} + +http.formdata = +function formdata(data) +{ + const fd = new FormData(); + for (let [key, value] of Object.entries(data)) + { + if (value === undefined) + { + continue; + } + if (value === null) + { + value = ''; + } + fd.append(key, value); + } + return fd; +} + +http._request = +function _request(kwargs) +{ + /* + Perform an HTTP request and call the `callback` with the response. + + The response will have the following structure: + { + "meta": { + "request": the XMLHttpRequest object, + "completed": true / false, + "status": If the connection failed or request otherwise could not + complete, `status` will be 0. If the request completed, + `status` will be the HTTP response code. + "json_ok": If the server responded with parseable json, `json_ok` + will be true, and that data will be in `response.data`. If the + server response was not parseable json, `json_ok` will be false + and `response.data` will be undefined. + "kwargs": The kwargs exactly as given to this call. + } + "data": {JSON parsed from server response if json_ok}, + "retry": function you can call to retry the request. + } + + So, from most lenient to most strict, error catching might look like: + if response.meta.completed + if response.meta.json_ok + if response.meta.status === 200 + if response.meta.status === 200 and response.meta.json_ok + */ + const request = new XMLHttpRequest(); + const response = { + "meta": { + "request": request, + "completed": false, + "status": 0, + "json_ok": false, + "kwargs": kwargs, + }, + "retry": function(){http._request(kwargs)}, + }; + + request.onreadystatechange = function() + { + /* + readystate values: + 0 UNSENT / ABORTED + 1 OPENED + 2 HEADERS_RECEIVED + 3 LOADING + 4 DONE + */ + if (request.readyState != 4) + { + return; + } + + http.requests_in_flight -= 1; + setTimeout(http.request_queue_next, 0); + + if (kwargs["callback"] == null) + { + return; + } + + response.meta.status = request.status; + + if (request.status != 0) + { + response.meta.completed = true; + try + { + response.data = JSON.parse(request.responseText); + response.meta.json_ok = true; + } + catch (exc) + { + response.meta.json_ok = false; + } + } + kwargs["callback"](response); + }; + + // Headers + + const asynchronous = "asynchronous" in kwargs ? kwargs["asynchronous"] : true; + request.open(kwargs["method"], kwargs["url"], asynchronous); + + for (const [header, value] of Object.entries(http.HEADERS)) + { + request.setRequestHeader(header, value); + } + + const more_headers = kwargs["headers"] || {}; + for (const [header, value] of Object.entries(more_headers)) + { + request.setRequestHeader(header, value); + } + + if (kwargs["with_credentials"]) + { + request.withCredentials = true; + } + + // Send + + let data = kwargs["data"]; + if (data === undefined || data === null) + { + request.send(); + } + else if (data instanceof FormData) + { + request.send(data); + } + else if (typeof(data) === "string" || data instanceof String) + { + request.send(data); + } + else + { + request.send(http.formdata(data)); + } + http.requests_in_flight += 1; + + return request; +} + +http.get = +function get(kwargs) +{ + kwargs["method"] = "GET"; + return http._request(kwargs); +} + +http.post = +function post(kwargs) +{ + /* + `data`: + a FormData object which you have already filled with values, or a + dictionary from which a FormData will be made, using http.formdata. + */ + kwargs["method"] = "POST"; + return http._request(kwargs); +} diff --git a/frontends/bringrss_flask/templates/feed_settings.html b/frontends/bringrss_flask/templates/feed_settings.html index cab38df..a0c10ad 100644 --- a/frontends/bringrss_flask/templates/feed_settings.html +++ b/frontends/bringrss_flask/templates/feed_settings.html @@ -11,6 +11,7 @@ {% if theme %}{% endif %} +