diff --git a/js/actions.js b/js/actions.js new file mode 100644 index 0000000..0213ea0 --- /dev/null +++ b/js/actions.js @@ -0,0 +1,55 @@ +import { actions, downloads } from '/js/api.js' + +export async function enable() { + const actions_container = document.getElementById('actions'); + const tab2 = document.querySelector("input[type=radio]#tab-2"); + console.log(tab2); + actions_container.innerHTML = ""; + + + async function get_current_tab_url() { + return (await browser.tabs.query({ currentWindow: true, active: true }))[0].url + } + + const groups = {}; + + function get_group(name) { + if (!name) name = "Ungrouped"; + if (name in groups) return groups[name]; + const fieldset = document.createElement('fieldset'); + // fieldset.style.background = get_color(); + const legend = document.createElement('legend'); + legend.innerText = name; + fieldset.className = "actionset"; + + fieldset.appendChild(legend); + actions_container.appendChild(fieldset); + + groups[name] = fieldset; + return groups[name]; + } + + for (const preset of await actions.get_all()) { + const button = document.createElement('button'); + const fieldset = get_group(preset.folder); + fieldset.appendChild(button); + + button.innerText = preset.name; + button.addEventListener('click', async function() { + const tab_url = await get_current_tab_url() + console.log(tab_url, preset.id); + downloads.create(tab_url, preset.id); + tab2.checked = true; + }); + } + + const hr = document.createElement('hr'); + actions_container.appendChild(hr); + + const reload_button = document.createElement('button'); + reload_button.innerText = "Reload"; + reload_button.addEventListener('click', enable); + actions_container.appendChild(reload_button); + + +} diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..22b24cd --- /dev/null +++ b/js/api.js @@ -0,0 +1,51 @@ +import { api_url } from '/js/settings.js'; + +const _get = async (route) => { + const res = await fetch(await api_url(route)); + return await res.json(); + // const mime_type = res.headers.get('content-type').split(";")[0].trim(); + // if(!mime_type) return; + // return +}; +const _post = async (route, data) => await fetch(await api_url(route), { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: typeof data === 'string' ? data : JSON.stringify(data) +}); +const _delete = async (route) => await fetch(await api_url(route), { method: 'DELETE' }) + +export const actions = { + get_all() { return _get('actions') } +} + +export const downloads = { + create(download_url, preset_id) { + return _post('download', { + url: download_url, + preset_id, + }) + } +} + +export const notifications = { + get_all() { + return _get('notifications'); + }, + remove() { + return _delete(`notifications/${notification_id}`); + }, + remove_all() { + return _delete('notifications/inactive'); + }, + get_active() { + return _get('notifications/active'); + }, + get_failed() { + return _get('notifications/failed'); + }, + get_succeeded() { + return _get('notifications/succeeded'); + }, +} diff --git a/js/notifications.js b/js/notifications.js new file mode 100644 index 0000000..171e29b --- /dev/null +++ b/js/notifications.js @@ -0,0 +1,221 @@ +import { api_url } from '/js/settings.js'; +import * as api from '/js/api.js'; + +function create_notification_element_from_notification_object(notification) { + + const e_container = document.createElement('div'); + e_container.classList = `status status-${notification.type}`; + e_container.setAttribute('notification_id', "" + notification.id); + e_container.setAttribute('notification_hash', "" + notification.hash); + + const e_title_span = document.createElement('span'); + e_title_span.innerText = notification.title; + + const e_title = document.createElement('div'); + e_title.classList = 'status-title'; + e_title.appendChild(e_title_span) + + const e_progress = document.createElement('progress'); + if ('progress' in notification) { + if(notification.progress !== null) { + e_progress.setAttribute('value', notification.progress); + } + } else e_progress.style.display = 'none'; + + const e_text = document.createElement('div'); + e_text.classList = 'status-text'; + e_text.innerHTML = notification.text; + + const e_delete_button = document.createElement('button'); + e_delete_button.classList = 'dismiss'; + e_delete_button.innerText = "Dismiss"; + e_delete_button.addEventListener('click', () => { + api.notifications.remove(notification.id); + }) + + e_container.appendChild(e_title); + e_container.appendChild(e_progress); + e_container.appendChild(e_text); + // e_container.appendChild(e_delete_button); + + return e_container; +} + +function begin_status_update_loop() { + const statuses_container = document.getElementById('statuses'); + const clear_button = document.getElementById('clear-notifications-button'); + const notif_header = document.getElementById('notif-header'); + + clear_button.addEventListener('click', () => { + api.notifications.remove_all(); + }) + + function get_current_shown_status_ids() { + let arr = []; + for(const element of statuses_container.children) { + arr.push(element.getAttribute("notification_id")) + } + return arr; + } + + async function check_statuses() { + const active = await api.notifications.get_active(); + const failed = await api.notifications.get_failed(); + const succeeded = await api.notifications.get_succeeded(); + + notif_header.textContent = `${active.length} Active | ${failed.length} Failed | ${succeeded.length} Succeeded`; + + let req = await fetch(await api_url('notifications')); + let json = await req.json(); + + const current_status_ids = get_current_shown_status_ids(); + const new_status_ids = json.map(v => v.id); + + const combined_ids = Array.from(new Set([ + ...current_status_ids, + ...new_status_ids + ])); + + const first = new Map(combined_ids.map(notif_id => [ + notif_id, + document.querySelector(`[notification_id="${notif_id}"]`)?.getBoundingClientRect() ?? null + ])); + + console.log(first); + + for(const status_id of combined_ids) { + const in_current = current_status_ids.includes(status_id); + const in_new = new_status_ids.includes(status_id); + + if (in_current && !in_new) { + const status_element = statuses_container.querySelector(`[notification_id="${status_id}"]`); + status_element.outerHTML = ""; + } else if (!in_current && in_new) { + const status_object = json.find(status => status.id === status_id); + const new_element = create_notification_element_from_notification_object(status_object) + statuses_container.appendChild(new_element); + } else if (in_current && in_new) { + const status_object = json.find(status => status.id === status_id); + const current_element = statuses_container.querySelector(`[notification_id="${status_id}"]`); + const new_element = create_notification_element_from_notification_object(status_object); + current_element.replaceWith(new_element); + } + } + + new_status_ids.forEach((id, index) => { + const el = document.querySelector(`[notification_id="${id}"]`); + if (el) el.style.order = index; + }); + + + for(const [id, old_rect] of first.entries()) { + if(!old_rect) continue; + + const el = document.querySelector(`[notification_id="${id}"]`); + if(!el) continue; + + const current_rect = el.getBoundingClientRect(); + const dy = old_rect.top - current_rect.top; + + el.style.transition = 'none'; + el.style.transform = `translate(0px, ${dy}px)`; + + requestAnimationFrame(() => { + el.style.transition = 'transform 300ms ease'; + el.style.transform = ''; + }); + } + // notification_els.forEach(el => { + // const prev = first.get(el); + // const next = el.getBoundingClientRect(); + // + // const dx = prev.left - next.left; + // const dy = prev.top - next.top; + // + // if (dx === 0 && dy === 0) return; + // + // el.style.transition = 'none'; + // el.style.transform = `translate(${dx}px, ${dy}px)`; + // + // }); + + setTimeout(check_statuses, 500); + } + check_statuses(); +} + + +export function enable() { + begin_status_update_loop(); +} + + + +function alignElements(liveEl, templateEl) { + // If node types differ, replace entirely + if (liveEl.nodeType !== templateEl.nodeType) { + liveEl.replaceWith(templateEl.cloneNode(true)); + return; + } + + // If element tag names differ, replace + if (liveEl.nodeType === Node.ELEMENT_NODE && + liveEl.tagName !== templateEl.tagName) { + liveEl.replaceWith(templateEl.cloneNode(true)); + return; + } + + // Text node + if (liveEl.nodeType === Node.TEXT_NODE) { + if (liveEl.textContent !== templateEl.textContent) { + liveEl.textContent = templateEl.textContent; + } + return; + } + + // Element node — sync attributes + syncAttributes(liveEl, templateEl); + + // Sync children + syncChildren(liveEl, templateEl); +} + +function syncAttributes(liveEl, templateEl) { + // Remove old attributes + for (const attr of [...liveEl.attributes]) { + if (!templateEl.hasAttribute(attr.name)) { + liveEl.removeAttribute(attr.name); + } + } + + // Add / update attributes + for (const attr of [...templateEl.attributes]) { + if (liveEl.getAttribute(attr.name) !== attr.value) { + liveEl.setAttribute(attr.name, attr.value); + } + } +} + +function syncChildren(liveEl, templateEl) { + const liveChildren = [...liveEl.childNodes]; + const templateChildren = [...templateEl.childNodes]; + + const max = Math.max(liveChildren.length, templateChildren.length); + + for (let i = 0; i < max; i++) { + const liveChild = liveChildren[i]; + const templateChild = templateChildren[i]; + + if (!liveChild && templateChild) { + liveEl.appendChild(templateChild.cloneNode(true)); + continue; + } + + if (liveChild && !templateChild) { + liveChild.remove(); + continue; + } + + alignElements(liveChild, templateChild); + } +} diff --git a/js/popup.js b/js/popup.js new file mode 100644 index 0000000..dcb4731 --- /dev/null +++ b/js/popup.js @@ -0,0 +1,17 @@ +// const colors = ['red', 'green', 'blue', 'cyan', 'magenta']; +// let current_color = -1; +// function get_color() { return colors[current_color]; } +// function next_color() { current_color ++; current_color = current_color % colors.length }; + +import * as settings from '/js/settings.js'; +import * as notifications from '/js/notifications.js'; +import * as actions from '/js/actions.js'; + +document.addEventListener("DOMContentLoaded", () => { + settings.enable(); + notifications.enable(); + actions.enable() + // load_domain_from_storage_and_enable_interactions(); + // arm_action_buttons(); + // begin_status_update_loop(); +}); diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..4918f1d --- /dev/null +++ b/js/settings.js @@ -0,0 +1,64 @@ + +export async function api_url(api_path = "") { + const domain = await get_saved_domain(); + if (!domain) throw Error("No Domain Set"); + if (api_path.startsWith('/')) api_path = api_path.substring(1); + return domain + api_path; +} + +const get_saved_domain = async () => (await browser.storage.local.get("domain")).domain; +const set_saved_domain = async (domain) => await browser.storage.local.set({ domain }); +const get_saved_debug = async () => (await browser.storage.local.get("debug")).debug; +const set_saved_debug = async (enabled) => await browser.storage.local.set({ debug: enabled }); + +function enable_domain_input() { + const save_button = document.getElementById("save_domain"); + const host_input = document.getElementById("host"); + + const disable_save_button = () => save_button.setAttribute('disabled', true); + const enable_save_button = () => save_button.removeAttribute('disabled'); + + async function populate_domain_textbox_from_storage() { + const domain = await get_saved_domain() + if (!domain) return; + host_input.value = domain; + disable_save_button(); + } + + async function domain_input_changed() { + const saved_domain = await get_saved_domain(); + const current_domain = host_input.value; + if(saved_domain !== current_domain) enable_save_button(); + else disable_save_button(); + } + + async function save_domain_input_to_storage() { + if (!host_input.value.endsWith("/")) + host_input.value = host_input.value + "/"; + const saved = set_saved_domain(host_input.value); + + const granted = await browser.permissions.request({ + origins: [host_input.value] + }); + if (!granted) throw new Error("Permission denied"); + + await saved; + disable_save_button(); + } + + populate_domain_textbox_from_storage() + host_input.addEventListener('change', domain_input_changed); + host_input.addEventListener('keypress', domain_input_changed); + host_input.addEventListener('keyup', domain_input_changed); + host_input.addEventListener('keydown', domain_input_changed); + save_button.addEventListener('click', save_domain_input_to_storage); +} + +function enable_debug_toggle() { + +} + +export function enable() { + enable_domain_input(); + enable_debug_toggle(); +} diff --git a/package.sh b/package.sh index 2f29ea9..7bc4163 100755 --- a/package.sh +++ b/package.sh @@ -1,5 +1,4 @@ zip -r -FS downloader.zip \ - background.js \ manifest.json \ popup.html \ - popup.js + js/* diff --git a/popup.html b/popup.html index 3ace65c..3f1dbba 100644 --- a/popup.html +++ b/popup.html @@ -15,19 +15,23 @@ hr { border: none; border-bottom: 1px solid #CCC; } body:has(#tab-1:checked) .tab-panel:not(.tab-panel-1) { display: none; } body:has(#tab-2:checked) .tab-panel:not(.tab-panel-2) { display: none; } body:has(#tab-3:checked) .tab-panel:not(.tab-panel-3) { display: none; } - +#statuses { display: flex; flex-direction: column; } .status { font-size: 0.8em; border: 1px solid black; padding: 8px; border-radius: 8px; overflow: hidden; margin-bottom: 8px; } -.status .status-title { white-space: nowrap; font-weight: 900; } +/* .status .status-title { white-space: nowrap; font-weight: 900; overflow: hidden; } */ +/* .status .status-title span { display: inline-block; } */ .status-debug { background: #ff8cff } .status-success { background: #8cff8c } .status-info { background: #b3d9ff } -.status-warning.status-warn { background: #ffff8c } +.status-warning, .status-warn { background: #ffff8c } .status-error { background: #ff8c8c } +.tab-panel-3 { display: flex; gap: 8px; flex-direction: column; } +progress { width: 100%; margin: 0px; } + fieldset.actionset { display: flex; flex-direction: column; gap: 6px; } pre { @@ -52,7 +56,10 @@ pre {
-

+	
+ 1 Active | 2 Failed | 3 Completed + +
@@ -61,8 +68,12 @@ pre { + - + diff --git a/popup.js b/popup.js deleted file mode 100644 index e7456d3..0000000 --- a/popup.js +++ /dev/null @@ -1,206 +0,0 @@ -const get_saved_domain = async () => (await browser.storage.local.get("domain")).domain; -const set_saved_domain = async (domain) => await browser.storage.local.set({ domain }); - -function load_domain_from_storage_and_enable_interactions() { - const save_button = document.getElementById("save_domain"); - const host_input = document.getElementById("host"); - - const disable_save_button = () => save_button.setAttribute('disabled', true); - const enable_save_button = () => save_button.removeAttribute('disabled'); - - async function populate_domain_textbox_from_storage() { - const domain = await get_saved_domain() - if (!domain) return; - host_input.value = domain; - disable_save_button(); - } - - async function domain_input_changed() { - const saved_domain = await get_saved_domain(); - const current_domain = host_input.value; - if(saved_domain !== current_domain) enable_save_button(); - else disable_save_button(); - } - - async function save_domain_input_to_storage() { - if (!host_input.value.endsWith("/")) - host_input.value = host_input.value + "/"; - const saved = set_saved_domain(host_input.value); - - const granted = await browser.permissions.request({ - origins: [host_input.value] - }); - if (!granted) throw new Error("Permission denied"); - - await saved; - disable_save_button(); - } - - populate_domain_textbox_from_storage() - host_input.addEventListener('change', domain_input_changed); - host_input.addEventListener('keypress', domain_input_changed); - host_input.addEventListener('keyup', domain_input_changed); - host_input.addEventListener('keydown', domain_input_changed); - save_button.addEventListener('click', save_domain_input_to_storage); -} - -const colors = ['red', 'green', 'blue', 'cyan', 'magenta']; -let current_color = -1; -function get_color() { return colors[current_color]; } -function next_color() { current_color ++; current_color = current_color % colors.length }; - -async function arm_action_buttons() { - next_color(); - const actions_container = document.getElementById('actions'); - actions_container.innerHTML = ""; - - async function send_data(route, payload) { - const domain = (await get_saved_domain()) + route; - await browser.runtime.sendMessage({ - type: 'send', - host: domain, - data: JSON.stringify(payload) - }); - } - - async function get_current_tab_url() { - return (await browser.tabs.query({ currentWindow: true, active: true }))[0].url - } - - const presets_req = await fetch(await get_saved_domain() + 'download/presets'); - const presets = await presets_req.json(); - - const groups = {}; - - function get_group(name) { - if (!name) name = "Ungrouped"; - if (name in groups) return groups[name]; - const fieldset = document.createElement('fieldset'); - // fieldset.style.background = get_color(); - const legend = document.createElement('legend'); - legend.innerText = name; - fieldset.className = "actionset"; - - fieldset.appendChild(legend); - actions_container.appendChild(fieldset); - - groups[name] = fieldset; - return groups[name]; - } - - for (const preset of presets) { - const button = document.createElement('button'); - const fieldset = get_group(preset.folder); - fieldset.appendChild(button); - - - - button.innerText = preset.name; - button.addEventListener('click', async function() { - send_data('download', { - url: await get_current_tab_url(), - preset_id: preset.id, - }); - }); - } - - const hr = document.createElement('hr'); - actions_container.appendChild(hr); - - const reload_button = document.createElement('button'); - reload_button.innerText = "Reload"; - reload_button.addEventListener('click', arm_action_buttons); - actions_container.appendChild(reload_button); - - -} - -async function dismiss_notif(status_id) { - url = await get_saved_domain() + 'notification/' + status_id - fetch(url, { - method: 'DELETE' - }) -} - -function create_notification_element_from_notification_object(notification) { - - const e_container = document.createElement('div'); - e_container.classList = `status status-${notification.type}`; - e_container.setAttribute('notification_id', "" + notification.id); - e_container.setAttribute('notification_hash', "" + notification.hash); - - const e_title = document.createElement('div'); - e_title.classList = 'status-title'; - e_title.innerText = notification.title; - - const e_text = document.createElement('div'); - e_text.classList = 'status-text'; - e_text.innerHTML = notification.text; - - const e_delete_button = document.createElement('button'); - e_delete_button.classList = 'dismiss'; - e_delete_button.innerText = "Dismis"; - e_delete_button.addEventListener('click', () => { - dismiss_notif(notification.id); - }) - - e_container.appendChild(e_title); - e_container.appendChild(e_text); - e_container.appendChild(e_delete_button); - - return e_container; -} - -function begin_status_update_loop() { - const statuses_container = document.getElementById('statuses'); - - function get_current_shown_status_ids() { - let arr = []; - for(const element of statuses_container.children) { - arr.push(element.getAttribute("notification_id")) - } - return arr; - } - - async function check_statuses() { - let domain = await get_saved_domain(); - let req = await fetch(domain + 'notifications'); - let json = await req.json(); - - const current_status_ids = get_current_shown_status_ids(); - const new_status_ids = json.map(v => v.id); - - const combined_ids = Array.from(new Set([ - ...current_status_ids, - ...new_status_ids - ])); - - for(const status_id of combined_ids) { - const in_current = current_status_ids.includes(status_id); - const in_new = new_status_ids.includes(status_id); - - if (in_current && !in_new) { - const status_element = statuses_container.querySelector(`[notification_id="${status_id}"]`); - status_element.outerHTML = ""; - } else if (!in_current && in_new) { - const status_object = json.find(status => status.id === status_id); - const new_element = create_notification_element_from_notification_object(status_object) - statuses_container.appendChild(new_element); - } else if (in_current && in_new) { - const status_object = json.find(status => status.id === status_id); - const current_element = statuses_container.querySelector(`[notification_id="${status_id}"]`); - const new_element = create_notification_element_from_notification_object(status_object); - current_element.replaceWith(new_element); - } - } - - setTimeout(check_statuses, 500); - } - check_statuses(); -} - -document.addEventListener("DOMContentLoaded", () => { - load_domain_from_storage_and_enable_interactions(); - arm_action_buttons(); - begin_status_update_loop(); -});