first commit

This commit is contained in:
Auric Vente
2024-08-01 22:12:44 -06:00
commit 0bcefa0b61
64 changed files with 13272 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
const App = {}
App.setup = () => {
NeedContext.init()
Block.setup()
Curls.setup()
Colors.setup()
Infobar.setup()
Container.setup()
Select.setup()
Drag.setup()
Move.setup()
Update.setup()
Sort.setup()
Change.setup()
Picker.setup()
Status.setup()
Filter.setup()
Menu.setup()
More.setup()
Font.setup()
Border.setup()
Dates.setup()
Controls.setup()
Windows.setup()
Footer.setup()
Intro.setup()
Storage.setup()
App.start_mouse()
App.update_autocomplete()
Update.do_update()
}
App.update_title = () => {
let color = Utils.capitalize(Colors.mode)
document.title = `Curls - ${color}`
}
App.start_mouse = () => {
DOM.evs(DOM.el(`#main`), [`mousedown`], (e) => {
App.check_selection(e)
})
DOM.ev(window, `mouseup`, (e) => {
Select.mouseup()
})
}
App.update_autocomplete = () => {
let data_list = DOM.el(`#curls_datalist`)
data_list.innerHTML = ``
for (let word of Curls.get_curls()) {
var option = document.createElement(`option`)
option.value = word
data_list.append(option)
}
}
App.check_selection = (e) => {
if (e.ctrlKey || e.shiftKey) {
return
}
if (e.button !== 0) {
return
}
if (e.target.closest(`.item_icon`)) {
return
}
if (e.target.closest(`#infobar_curls`)) {
return
}
Select.deselect_all()
}

View File

@@ -0,0 +1,48 @@
/*
This is used to rate limit certain operations
Every operation adds 1 charge to a registered instance
If the charge is above the limit, the operation is blocked
Charges are decreased over time
*/
class Block {
static instances = []
static interval_delay = 2000
static date_delay = 500
static relief = 0.1
constructor(limit = 180) {
this.limit = limit
this.charge = 0
this.date = 0
Block.instances.push(this)
}
static setup() {
setInterval(() => {
for (let block of Block.instances) {
if ((Utils.now() - block.date) < this.date_delay) {
continue
}
if (block.charge > 0) {
let dec = Math.max(1, Math.round(block.charge * this.relief))
block.charge -= parseInt(dec)
}
}
}, this.interval_delay)
}
add_charge(num = 1) {
this.date = Utils.now()
if (this.charge >= this.limit) {
return true
}
this.charge += num
return false
}
}

View File

@@ -0,0 +1,68 @@
/*
The border between the items of the container
*/
class Border {
static default_mode = `solid`
static ls_name = `border`
static modes = [
{value: `solid`, name: `Solid`, info: `Normal solid border`},
{value: `dotted`, name: `Dotted`, info: `Dotted border`},
{value: `dashed`, name: `Dashed`, info: `Dashed border`},
{value: Utils.separator},
{value: `none`, name: `None`, info: `No border`},
]
static setup() {
let border = DOM.el(`#border`)
this.mode = this.load_border()
this.combo = new Combo({
title: `Border Modes`,
items: this.modes,
value: this.mode,
element: border,
default: this.default_mode,
action: (value) => {
this.change(value)
this.apply()
},
get: () => {
return this.mode
},
})
this.apply()
}
static change(value) {
this.mode = value
Utils.save(this.ls_name, value)
}
static apply() {
let border
if (this.mode === `solid`) {
border = `1px solid var(--color_alpha_2)`
}
else if (this.mode === `dotted`) {
border = `2px dotted var(--color_alpha_2)`
}
else if (this.mode === `dashed`) {
border = `2px dashed var(--color_alpha_2)`
}
else {
border = `none`
}
document.documentElement.style.setProperty(`--border`, border)
}
static load_border() {
return Utils.load_modes(this.ls_name, this.modes, this.default_mode)
}
}

View File

@@ -0,0 +1,164 @@
/*
This changes the status of a curl
*/
class Change {
static debouncer_delay = 250
static changing = false
static clear_delay = 800
static status_max_length = 500
static key_length = 22
static setup() {
let curl = DOM.el(`#change_curl`)
let key = DOM.el(`#change_key`)
let submit = DOM.el(`#change_submit`)
DOM.ev(submit, `click`, () => {
this.change()
})
this.debouncer = Utils.create_debouncer(() => {
this.do_change()
}, this.debouncer_delay)
DOM.ev(curl, `keyup`, (e) => {
if (e.key === `Enter`) {
this.change()
}
})
DOM.ev(curl, `focus`, (e) => {
let value = curl.value
if (value) {
Select.curl(value)
}
})
DOM.ev(curl, `blur`, (e) => {
Select.deselect_all()
})
DOM.ev(curl, `wheel`, (e) => {
Utils.scroll_wheel(e)
})
DOM.ev(key, `keyup`, (e) => {
if (e.key === `Enter`) {
this.change()
}
})
DOM.ev(key, `wheel`, (e) => {
Utils.scroll_wheel(e)
})
}
static change() {
this.debouncer.call()
}
static do_change() {
this.debouncer.cancel()
Utils.info(`Change: Trigger`)
if (this.changing) {
Utils.error(`Slow down`)
return
}
let curl = DOM.el(`#change_curl`).value.toLowerCase()
let key = DOM.el(`#change_key`).value
let status = DOM.el(`#change_status`).value.trim()
if (!curl || !key || !status) {
return
}
if (curl.length > Curls.max_length) {
Utils.error(Utils.curl_too_long)
Windows.alert({title: `Error`, message: Utils.curl_too_long})
return
}
if (key.length > this.key_length) {
Utils.error(Utils.key_too_long)
Windows.alert({title: `Error`, message: Utils.key_too_long})
return
}
if (status.length > this.status_max_length) {
Utils.error(Utils.status_too_long)
Windows.alert({title: `Error`, message: Utils.status_too_long})
return
}
let url = `/change`
let params = new URLSearchParams()
params.append(`curl`, curl)
params.append(`key`, key)
params.append(`status`, status)
this.show_changing()
Status.save(status)
this.changing = true
Utils.info(`Change: Request ${Utils.network}`)
fetch(url, {
method: `POST`,
headers: {
"Content-Type": `application/x-www-form-urlencoded`
},
body: params,
})
.then(response => response.text())
.then(ans => {
Utils.info(`Response: ${ans}`)
this.clear_changing()
if (ans === `ok`) {
this.clear_status()
Update.update({ curls: [curl] })
Curls.add_owned(curl)
Picker.add()
}
else {
let lines = [
`You might have hit the rate limit`,
`Or the curl and key you used are incorrect`
]
let msg = lines.join(`\n`)
Windows.alert({message: msg})
}
})
.catch(e => {
Utils.error(`Failed to change`)
Utils.error(e)
this.clear_changing()
})
}
static clear_status() {
DOM.el(`#change_status`).value = ``
}
static show_changing() {
let button = DOM.el(`#change_submit`)
clearTimeout(this.clear_changing_timeout)
button.classList.add(`active`)
}
static clear_changing() {
this.changing = false
this.clear_changing_timeout = setTimeout(() => {
let button = DOM.el(`#change_submit`)
button.classList.remove(`active`)
}, this.clear_delay)
}
}

View File

@@ -0,0 +1,188 @@
/*
Color functions
*/
class Colors {
static default_mode = `green`
static ls_name = `color`
static alpha_0 = {}
static alpha_1 = {}
static alpha_2 = {}
static modes = [
{value: `green`, name: `Green`, info: `Go to Green`, icon: `🟢`},
{value: `blue`, name: `Blue`, info: `Go to Blue`, icon: `🔵`},
{value: `red`, name: `Red`, info: `Go to Red`, icon: `🔴`},
{value: `yellow`, name: `Yellow`, info: `Go to Yellow`, icon: `🟡`},
{value: `purple`, name: `Purple`, info: `Go to Purple`, icon: `🟣`},
{value: `white`, name: `White`, info: `Go to White`, icon: ``},
]
static colors = {
red: `rgb(255, 89, 89)`,
green: `rgb(87, 255, 87)`,
blue: `rgb(118, 120, 255)`,
yellow: `rgb(255, 219, 78)`,
purple: `rgb(193, 56, 255)`,
white: `rgb(255, 255, 255)`,
}
static setup() {
let color = DOM.el(`#color`)
this.mode = this.load_color()
this.combo = new Combo({
title: `Color Modes`,
items: this.modes,
value: this.mode,
element: color,
default: this.default_mode,
action: (value) => {
this.change(value)
},
get: () => {
return this.mode
},
extra_title: `Ctrl Left/Right to cycle`,
})
this.make_alpha(this.alpha_0, `0.055`)
this.make_alpha(this.alpha_1, `0.18`)
this.make_alpha(this.alpha_2, `0.5`)
this.apply()
}
static make_alpha(obj, a) {
for (let color in this.colors) {
let numbers = this.colors[color].match(/\d+/g)
let rgba = `rgba(${numbers[0]}, ${numbers[1]}, ${numbers[2]}, ${a})`
obj[color] = rgba
}
}
static set_value(value) {
if (this.mode === value) {
return
}
this.combo.set_value(value)
}
static change(value) {
if (this.mode === value) {
return
}
this.mode = value
Utils.save(this.ls_name, value)
this.apply()
Items.reset()
Container.show_loading()
Infobar.hide()
Update.update()
}
static load_color() {
return Utils.load_modes(this.ls_name, this.modes, this.default_mode)
}
static apply() {
let normal = this.colors[this.mode]
let alpha_0 = this.alpha_0[this.mode]
let alpha_1 = this.alpha_1[this.mode]
let alpha_2 = this.alpha_2[this.mode]
document.documentElement.style.setProperty(`--color`, normal)
document.documentElement.style.setProperty(`--color_alpha_0`, alpha_0)
document.documentElement.style.setProperty(`--color_alpha_1`, alpha_1)
document.documentElement.style.setProperty(`--color_alpha_2`, alpha_2)
App.update_title()
}
static move(curls, e) {
let items = []
let add = (mode) => {
if (this.mode === mode.value) {
return
}
items.push({
text: mode.name,
action: () => {
this.do_move(mode.value, curls)
},
icon: mode.icon,
})
}
for (let key in this.modes) {
add(this.modes[key])
}
Utils.context({items: items, e: e})
}
static do_move(color, curls) {
let current = Curls.get_curls()
let cleaned = []
for (let curl of current) {
if (!curls.includes(curl)) {
cleaned.push(curl)
}
}
let cleaned_items = []
for (let item of Items.list) {
if (!curls.includes(item.curl)) {
cleaned_items.push(item)
}
}
Items.list = cleaned_items
Curls.save_curls(cleaned)
let new_curls = Curls.get_curls(color)
for (let curl of curls) {
if (new_curls.includes(curl)) {
continue
}
new_curls.unshift(curl)
}
Curls.save_curls(new_curls, color)
Container.update()
}
static prev() {
let index = this.modes.findIndex(x => x.value === this.mode)
let prev = index - 1
if (prev < 0) {
prev = this.modes.length - 1
}
let value = this.modes[prev].value
this.set_value(value)
}
static next() {
let index = this.modes.findIndex(x => x.value === this.mode)
let next = index + 1
if (next >= this.modes.length) {
next = 0
}
let value = this.modes[next].value
this.set_value(value)
}
}

View File

@@ -0,0 +1,130 @@
/*
This is a button widget
It can be used to cycle through a list of items
It uses NeedContext to show the menu
It's similar to a select widget
*/
class Combo {
constructor(args) {
this.args = args
this.prepare()
}
prepare() {
DOM.evs(this.args.element, [`click`, `contextmenu`], (e) => {
this.show_menu(e)
e.preventDefault()
})
DOM.ev(this.args.element, `auxclick`, (e) => {
if (e.button === 1) {
this.reset()
}
})
DOM.ev(this.args.element, `wheel`, (e) => {
let direction = Utils.wheel_direction(e)
this.cycle(direction)
e.preventDefault()
})
let lines = [
this.args.title,
`Click to pick option`,
`Wheel to cycle option`,
`Middle Click to reset`,
]
this.args.element.title = lines.join(`\n`)
if (this.args.extra_title) {
this.args.element.title += `\n${this.args.extra_title}`
}
this.block = new Block()
this.update_text()
}
get_item() {
return this.args.items.find(x => x.value === this.args.get())
}
update_text() {
let item = this.get_item(this.args)
this.args.element.textContent = item.name
}
show_menu(e) {
let items = []
let current = this.args.get()
for (let item of this.args.items) {
if (item.value === Utils.separator) {
items.push({ separator: true })
}
else {
items.push({
text: item.name,
action: () => {
this.action(item.value)
},
selected: item.value === current,
info: item.info,
icon: item.icon,
})
}
}
Utils.context({ items: items, e: e, input: this.args.input })
}
action(value) {
this.args.action(value)
this.update_text()
}
reset() {
this.action(this.args.default)
}
get_values() {
return this.args.items
.filter(x => x.value !== Utils.separator)
.filter(x => !x.skip)
.map(x => x.value)
}
cycle(direction) {
if (this.block.add_charge()) {
return
}
let value = this.args.get()
let values = this.get_values(this.args)
let index = values.indexOf(value)
if (direction === `up`) {
index -= 1
}
else if (direction === `down`) {
index += 1
}
if (index < 0) {
index = values.length - 1
}
else if (index >= values.length) {
index = 0
}
let new_value = values[index]
this.action(new_value)
}
set_value(value) {
this.action(value)
}
}

View File

@@ -0,0 +1,452 @@
/*
This is the main container widget with the vertical items
Most action happens here
*/
class Container {
static wrap_enabled = true
static ls_wrap = `wrap_enabled`
static scroll_step = 100
static setup() {
this.empty_info = [
`Add some curls to the list by using the menu.`,
`These will be monitored for status changes.`,
`Above you can change the status of your own curls.`,
`Each color has its own set of curls.`,
].join(`<br>`)
let outer = this.get_outer()
let container = this.get_container()
DOM.ev(container, `mousedown`, (e) => {
if (e.ctrlKey || e.shiftKey) {
e.preventDefault()
}
})
DOM.ev(container, `click`, (e) => {
let item = this.extract_item(e)
if (!item) {
return
}
if (this.extract_updated(e)) {
Dates.change_mode()
return
}
this.focus()
let is_icon = this.extract_icon(e)
if (e.shiftKey) {
Select.range(item)
e.preventDefault()
}
else if (e.ctrlKey) {
Select.toggle(item)
e.preventDefault()
}
else {
if (is_icon) {
Select.single(item)
e.preventDefault()
}
}
})
DOM.ev(container, `auxclick`, (e) => {
let item = this.extract_item(e)
if (!item) {
return
}
if (e.button == 1) {
let curl = this.extract_curl(item)
Select.check(item)
Curls.remove_selected(curl)
}
})
DOM.ev(container, `contextmenu`, (e) => {
let item = this.extract_item(e)
if (!item) {
return
}
let curl = this.extract_curl(item)
Select.check(item)
Items.show_menu({curl: curl, e: e})
e.preventDefault()
})
DOM.ev(outer, `contextmenu`, (e) => {
let item = this.extract_item(e)
e.preventDefault()
if (item) {
return
}
Menu.show(e)
})
DOM.ev(outer, `click`, (e) => {
this.focus()
})
DOM.ev(outer, `mousedown`, (e) => {
Select.mousedown(e)
})
DOM.ev(outer, `mouseup`, () => {
Select.mouseup()
})
DOM.ev(outer, `mouseover`, (e) => {
Select.mouseover(e)
})
this.wrap_enabled = this.load_wrap_enabled()
this.setup_keyboard()
this.focus()
}
static clear() {
let container = this.get_container()
container.innerHTML = ``
}
static show_empty() {
Infobar.hide()
this.set_info(this.empty_info)
}
static check_empty() {
let els = this.get_items()
if (!els || !els.length) {
this.show_empty()
}
}
static show_loading() {
this.set_info(`Loading...`)
}
static set_info(info) {
let container = this.get_container()
let item = DOM.create(`div`, `info_item`)
item.innerHTML = info
container.innerHTML = ``
container.append(item)
Utils.deselect()
}
static get_items() {
return DOM.els(`#container .item`)
}
static scroll_top() {
let item = this.get_items()[0]
Utils.scroll_element({item: item, behavior: `smooth`, block: `center`})
}
static scroll_bottom() {
let item = Utils.last(this.get_items())
Utils.scroll_element({item: item, behavior: `smooth`, block: `center`})
}
static save_wrap_enabled() {
Utils.save(this.ls_wrap, this.wrap_enabled)
}
static load_wrap_enabled() {
return Utils.load_boolean(this.ls_wrap)
}
static add(items, curls) {
let normal = Items.list.filter(item => !item.missing)
Items.list = [...items]
for (let item of normal) {
if (Items.list.find(x => x.curl === item.curl)) {
continue
}
Items.list.push(item)
}
let missing = Items.find_missing()
Items.list.push(...missing)
Items.fill()
this.update({select: curls})
}
static insert(items) {
Items.list = items
Items.list.map(x => x.missing = false)
let missing = Items.find_missing()
Items.list.push(...missing)
Items.fill()
this.update()
}
static update(args = {}) {
let def_args = {
items: Items.list,
check_filter: true,
select: [],
}
Utils.def_args(def_args, args)
Utils.info(`Updating Container`)
this.clear()
Sort.sort(args.items)
for (let item of args.items) {
this.create_element(item)
}
Utils.deselect()
this.check_empty()
if (args.check_filter) {
Filter.check()
}
if (args.select.length) {
Select.curls(args.select)
}
Infobar.update()
}
static create_element(item) {
let container = this.get_container()
let el = DOM.create(`div`, `item`)
let item_icon = DOM.create(`div`, `item_icon`)
item_icon.draggable = true
let lines = [
`Click to select`,
`Ctrl Click to toggle`,
`Shift Click to select range`,
`Middle Click to remove`,
`Drag to reorder`,
]
item_icon.title = lines.join(`\n`)
let canvas = DOM.create(`canvas`, `item_icon_canvas`)
jdenticon.update(canvas, item.curl)
item_icon.append(canvas)
let item_curl = DOM.create(`div`, `item_curl`)
let item_status = DOM.create(`div`, `item_status`)
if (!this.wrap_enabled) {
item_status.classList.add(`nowrap`)
}
item_curl.textContent = item.curl
item_curl.title = item.curl
let status = item.status || `Not updated yet`
item_status.innerHTML = Utils.sanitize(status)
Utils.urlize(item_status)
let item_updated = DOM.create(`div`, `item_updated glow`)
let dates = [
`Updated: ${item.updated_text}`,
`Added: ${item.added_text}`,
`Created: ${item.created_text}`,
]
let date_text = dates.join(`\n`)
if (Dates.enabled) {
item_status.title = status
if (item.missing) {
item_updated.textContent = `No Date`
item_updated.title = `No date information available`
}
else {
item_updated.textContent = item.updated_text
let lines_2 = [
date_text,
`Click to toggle between 12 and 24 hours`,
]
item_updated.title = lines_2.join(`\n`)
}
}
else {
item_status.title = `${status}\n${date_text}`
item_updated.classList.add(`hidden`)
}
if (!item.missing) {
item_status.title += `\nChanges: ${item.changes}`
}
el.append(item_icon)
el.append(item_curl)
el.append(item_status)
el.append(item_updated)
el.dataset.curl = item.curl
el.dataset.selected_id = 0
container.append(el)
container.append(el)
item.element = el
}
static extract_curl(item) {
return item.dataset.curl
}
static setup_keyboard() {
let container = this.get_container()
DOM.ev(container, `keydown`, (e) => {
if (e.key === `Delete`) {
Curls.remove_selected()
e.preventDefault()
}
else if (e.key === `ArrowUp`) {
if (e.ctrlKey) {
Move.up()
}
else {
Select.vertical(`up`, e.shiftKey)
}
e.preventDefault()
}
else if (e.key === `ArrowDown`) {
if (e.ctrlKey) {
Move.down()
}
else {
Select.vertical(`down`, e.shiftKey)
}
e.preventDefault()
}
else if (e.key === `ArrowLeft`) {
if (e.ctrlKey) {
Colors.prev()
e.preventDefault()
}
}
else if (e.key === `ArrowRight`) {
if (e.ctrlKey) {
Colors.next()
e.preventDefault()
}
}
else if (e.key === `Escape`) {
Select.deselect_all()
e.preventDefault()
}
else if (e.key === `a`) {
if (e.ctrlKey) {
Select.all()
e.preventDefault()
}
}
})
}
static is_visible(item) {
return !item.classList.contains(`hidden`)
}
static get_visible() {
let items = this.get_items()
return items.filter(x => this.is_visible(x))
}
static get_curls() {
let items = this.get_items()
return items.map(item => Container.extract_curl(item))
}
static get_item(curl) {
let items = this.get_items()
return items.find(x => x.dataset.curl === curl)
}
static focus() {
this.get_container().focus()
}
static get_outer() {
return DOM.el(`#container_outer`)
}
static get_container() {
return DOM.el(`#container`)
}
static extract_item(e) {
return e.target.closest(`.item`)
}
static extract_icon(e) {
return e.target.closest(`.item_icon`)
}
static extract_updated(e) {
return e.target.closest(`.item_updated`)
}
static scroll_up() {
let outer = this.get_outer()
outer.scrollBy(0, -this.scroll_step)
}
static scroll_down() {
let outer = this.get_outer()
outer.scrollBy(0, this.scroll_step)
}
static scroller() {
let outer = this.get_outer()
let height = outer.clientHeight
let scroll = outer.scrollHeight
let scrolltop = outer.scrollTop
if (scrolltop < (scroll - height)) {
this.scroll_bottom()
}
else {
this.scroll_top()
}
}
static scroll(e) {
let direction = Utils.wheel_direction(e)
if (direction === `up`) {
Container.scroll_up()
}
else {
Container.scroll_down()
}
}
static save_curls() {
let curls = Container.get_curls()
return Curls.save_curls(curls)
}
}

View File

@@ -0,0 +1,32 @@
/*
This shows or hides the controls
*/
class Controls {
static enabled = true
static ls_name = `controls_enabled`
static setup() {
this.enabled = this.load_enabled()
this.check_enabled()
}
static save_enabled() {
Utils.save(this.ls_name, this.enabled)
}
static load_enabled() {
return Utils.load_boolean(this.ls_name)
}
static check_enabled() {
if (this.enabled) {
DOM.show(`#controls`)
}
else {
DOM.hide(`#controls`)
}
}
}

View File

@@ -0,0 +1,521 @@
/*
These are curl operations
This takes care of storing curl data
*/
class Curls {
static max_curls = 100
static max_length = 20
static old_delay = Utils.YEAR * 1
static colors = {}
static setup() {
this.fill_colors()
}
static fill_colors() {
for (let color in Colors.colors) {
this.colors[color] = this.load_curls(color)
if (this.fill(this.colors[color])) {
this.save(this.colors[color], color, true)
}
}
}
static add() {
Windows.prompt({title: `Add Curls`, callback: (value) => {
this.add_submit(value)
}, message: `Enter one or more curls`})
}
static add_submit(curls) {
if (!curls) {
return
}
let added = Utils.smart_list(curls)
if (!added.length) {
return
}
this.prepend(added)
}
static prepend(added) {
added = added.filter(x => this.check(x))
if (!added.length) {
return
}
let curls = this.get_curls()
let new_curls = Array.from(new Set([...added, ...curls]))
if (this.save_curls(new_curls)) {
added.reverse()
Update.update({ curls: added })
}
}
static new_item(curl) {
return {
curl: curl,
added: this.default_added(),
}
}
static add_owned(curl) {
let curls = this.get_curls()
if (curls.includes(curl)) {
return
}
this.prepend([curl])
}
static to_top(curls) {
let cleaned = [...curls]
for (let curl of this.get_curls()) {
if (cleaned.includes(curl)) {
continue
}
cleaned.push(curl)
}
this.after_move(cleaned, curls)
}
static to_bottom(curls) {
let cleaned = []
for (let curl of this.get_curls()) {
if (cleaned.includes(curl)) {
continue
}
if (curls.includes(curl)) {
continue
}
cleaned.push(curl)
}
cleaned.push(...curls)
this.after_move(cleaned, curls)
}
static after_move(new_curls, curls) {
this.save_curls(new_curls)
Sort.set_value(`order`)
Sort.sort_if_order()
Select.deselect_all()
for (let curl of curls) {
Select.curl(curl)
}
}
static save(items, color = Colors.mode, force = false) {
items = this.clean(items)
let same = true
let current = this.get(color)
if (current.length !== items.length) {
same = false
}
if (same) {
for (let i = 0; i < current.length; i++) {
if (current[i].curl !== items[i].curl) {
same = false
break
}
}
}
if (same && !force) {
return false
}
let name = this.get_name(color)
this.colors[color] = [...items]
Utils.save(name, JSON.stringify(items))
return true
}
static default_added() {
return Utils.now()
}
static fill(items) {
let filled = false
for (let item of items) {
if (item.added === undefined) {
item.added = this.default_added()
filled = true
}
}
return filled
}
static get(color = Colors.mode) {
return this.colors[color]
}
static get_curls(color = Colors.mode) {
return this.get(color).map(x => x.curl)
}
static load_curls(color = Colors.mode) {
let name = this.get_name(color)
let saved = Utils.load_array(name)
return this.clean(saved)
}
static replace() {
Windows.prompt({title: `Replace Curls`, callback: (value) => {
this.replace_submit(value)
}, message: `Replace the entire list with this`})
}
static replace_submit(curls) {
if (!curls) {
return
}
let units = curls.split(` `).filter(x => x)
if (!units.length) {
return
}
this.clear()
let added = []
for (let curl of units) {
if (this.check(curl)) {
added.push(curl)
}
}
if (added) {
if (this.save_curls(added)) {
Update.update()
}
}
}
static clear(color = Colors.mode) {
this.save([], color)
}
static edit(curl) {
Windows.prompt({title: `Edit Curl`, callback: (value) => {
this.edit_submit(curl, value)
}, value: curl, message: `Change the name of this curl`})
}
static edit_submit(curl, new_curl) {
if (!new_curl) {
return
}
this.do_edit(curl, new_curl)
}
static do_edit(curl, new_curl) {
if (!this.check(new_curl)) {
return
}
if (curl === new_curl) {
return
}
let curls = this.get_curls().slice()
let index = curls.indexOf(curl)
if (index === -1) {
return
}
curls[index] = new_curl
if (this.save_curls(curls)) {
Items.remove_curl(curl)
Update.update({ curls: [new_curl] })
}
}
static check(curl) {
if (!curl) {
return false
}
if (curl.length > this.max_length) {
return false
}
if (!/^[a-zA-Z0-9]+$/.test(curl)) {
return false
}
return true
}
static clean(items) {
let cleaned = []
for (let item of items) {
if (cleaned.some(x => x.curl === item.curl)) {
continue
}
if (!this.check(item.curl)) {
continue
}
cleaned.push(item)
if (cleaned.length >= this.max_curls) {
break
}
}
return cleaned
}
static get_name(color) {
return `curls_${color}`
}
static remove(curls) {
let cleaned = []
let removed = []
for (let curl of this.get_curls()) {
if (!curls.includes(curl)) {
cleaned.push(curl)
}
else {
removed.push(curl)
}
}
if (!removed.length) {
return
}
this.save_cleaned(cleaned, removed)
}
static remove_selected(curl = ``) {
let curls = Select.get_curls()
if (curl) {
if (!curls.includes(curl)) {
curls = [curl]
}
}
this.remove(curls)
}
static remove_all() {
Windows.confirm({title: `Remove All Curls`, ok: () => {
this.clear()
Container.show_empty()
}, message: `Remove all curls in the current color`})
}
static show_remove_menu(e) {
let items = [
{
text: `Remove One`,
action: () => {
this.remove_one()
}
},
{
text: `Remove Not Found`,
action: () => {
this.remove_not_found()
}
},
{
text: `Remove Empty`,
action: () => {
this.remove_empty()
}
},
{
text: `Remove Old`,
action: () => {
this.remove_old()
}
},
{
text: `Remove All`,
action: () => {
this.remove_all()
}
},
]
Utils.context({items: items, e: e})
}
static remove_one() {
Windows.prompt({title: `Remove Curl`, callback: (value) => {
this.remove_one_submit(value)
}, message: `Enter the curl to remove`})
}
static remove_one_submit(curl) {
if (!curl) {
return
}
this.do_remove(curl)
}
static do_remove(curl, remove_item = true) {
let cleaned = []
for (let curl_ of this.get_curls()) {
if (curl_ !== curl) {
cleaned.push(curl_)
}
}
this.save_curls(cleaned)
if (remove_item) {
Items.remove([curl])
}
}
static remove_not_found() {
let missing = Items.get_missing().map(x => x.curl)
let cleaned = []
let removed = []
for (let curl of this.get_curls()) {
if (!missing.includes(curl)) {
cleaned.push(curl)
}
else {
removed.push(curl)
}
}
if (!removed.length) {
return
}
this.save_cleaned(cleaned, removed)
}
static remove_empty() {
let cleaned = []
let removed = []
for (let curl of this.get_curls()) {
let item = Items.get(curl)
if (!item) {
continue
}
if (!item.status) {
removed.push(curl)
continue
}
cleaned.push(curl)
}
if (!removed.length) {
return
}
this.save_cleaned(cleaned, removed)
}
static remove_old() {
let now = Utils.now()
let cleaned = []
let removed = []
for (let curl of this.get_curls()) {
let item = Items.get(curl)
if (!item) {
continue
}
let date = item.updated
if (date) {
let datetime = new Date(date + `Z`).getTime()
if ((now - datetime) > (this.old_delay)) {
removed.push(curl)
continue
}
}
cleaned.push(curl)
}
if (!removed.length) {
return
}
this.save_cleaned(cleaned, removed)
}
static save_cleaned(cleaned, removed) {
let s = Utils.plural(removed.length, `Curl`, `Curls`)
let curls = removed.join(`, `)
Windows.confirm({title: `Remove ${removed.length} ${s}`, ok: () => {
this.save_curls(cleaned)
Items.remove(removed)
}, message: curls})
}
static copy() {
let curls = this.get_curls()
let text = curls.join(` `)
Utils.copy_to_clipboard(text)
}
static save_curls(curls, color = Colors.mode) {
let current = this.get(color)
let items = []
for (let curl of curls) {
let item = current.find(x => x.curl === curl)
if (item) {
items.push(item)
}
else {
item = this.new_item(curl)
items.push(item)
}
}
return this.save(items, color)
}
}

View File

@@ -0,0 +1,80 @@
/*
This manages the dates shown in the container
*/
class Dates {
static ls_mode = `date_mode`
static ls_enabled = `date_enabled`
static default_mode = `12`
static setup() {
this.mode = this.load_mode()
this.enabled = this.load_enabled()
}
static change_mode() {
let selected = window.getSelection().toString()
if (selected) {
return
}
this.mode = this.mode === `12` ? `24` : `12`
Utils.save(this.ls_mode, this.mode)
Items.fill()
Container.update()
}
static load_mode() {
return Utils.load_string(this.ls_mode, this.default_mode)
}
static save_enabled() {
Utils.save(this.ls_enabled, this.enabled)
}
static load_enabled() {
return Utils.load_boolean(this.ls_enabled)
}
static fill(item) {
// Updated
let date = new Date(item.updated + `Z`)
let s_date
if (this.mode === `12`) {
s_date = dateFormat(date, `dd/mmm/yy - h:MM tt`)
}
else if (this.mode === `24`) {
s_date = dateFormat(date, `dd/mmm/yy - HH:MM`)
}
item.updated_text = s_date
// Created
date = new Date(item.created + `Z`)
if (this.mode === `12`) {
s_date = dateFormat(date, `dd/mmm/yy - h:MM tt`)
}
else if (this.mode === `24`) {
s_date = dateFormat(date, `dd/mmm/yy - HH:MM`)
}
item.created_text = s_date
// Added
date = new Date(item.added + `Z`)
if (this.mode === `12`) {
s_date = dateFormat(item.added, `dd/mmm/yy - h:MM tt`)
}
else if (this.mode === `24`) {
s_date = dateFormat(item.added, `dd/mmm/yy - HH:MM`)
}
item.added_text = s_date
}
}

View File

@@ -0,0 +1,74 @@
/*
Controls dragging of items in the container
*/
class Drag {
static drag_items = []
static drag_y = 0
static setup() {
let container = Container.get_container()
DOM.ev(container, `dragstart`, (e) => {
this.drag_start(e)
})
DOM.ev(container, `dragenter`, (e) => {
this.drag_enter(e)
})
DOM.ev(container, `dragend`, (e) => {
this.drag_end(e)
})
}
static drag_start(e) {
let item = Container.extract_item(e)
let curl = Container.extract_curl(item)
this.drag_y = e.clientY
e.dataTransfer.setData(`text`, curl)
e.dataTransfer.setDragImage(new Image(), 0, 0)
let selected = Select.get()
if (selected.length && selected.includes(item)) {
this.drag_items = selected
}
else {
if (!selected.includes(item)) {
Select.single(item)
}
this.drag_items = [item]
}
}
static drag_enter(e) {
let items = Container.get_items()
let item = Container.extract_item(e)
let index = items.indexOf(item)
if (index === -1) {
return
}
let direction = (e.clientY > this.drag_y) ? `down` : `up`
this.drag_y = e.clientY
if (direction === `up`) {
item.before(...this.drag_items)
}
else if (direction === `down`) {
item.after(...this.drag_items)
}
}
static drag_end(e) {
if (Container.save_curls()) {
Sort.set_value(`order`)
}
}
}

View File

@@ -0,0 +1,278 @@
/*
This is the filter for the container
*/
class Filter {
static debouncer_delay = 250
static default_mode = `all`
static timeout_delay = Utils.SECOND * 3
static ls_items = `filter_items`
static max_items = 100
static modes = [
{ value: `all`, name: `All`, info: `Show all curls` },
{ value: Utils.separator },
{ value: `today`, name: `Today`, info: `Show the curls that changed today` },
{ value: `week`, name: `Week`, info: `Show the curls that changed this week` },
{ value: `month`, name: `Month`, info: `Show the curls that changed this month` },
{ value: Utils.separator },
{ value: `curl`, name: `Curl`, info: `Filter by curl` },
{ value: `status`, name: `Status`, info: `Filter by status` },
{ value: `date`, name: `Date`, info: `Filter by date` },
{ value: Utils.separator },
{ value: `owned`, name: `Owned`, info: `Show the curls that you control` },
]
static setup() {
let filter = this.get_filter()
DOM.ev(filter, `keydown`, (e) => {
this.filter()
})
this.debouncer = Utils.create_debouncer(() => {
this.do_filter()
}, this.debouncer_delay)
filter.value = ``
let lines = [
`Filter the items`,
`Press Escape to clear`,
]
filter.title = lines.join(`\n`)
let modes_button = DOM.el(`#filter_modes`)
this.mode = this.default_mode
this.combo = new Combo({
title: `Filter Modes`,
items: this.modes,
value: this.filer_mode,
element: modes_button,
default: this.default_mode,
input: filter,
action: (value) => {
this.change(value)
},
get: () => {
return this.mode
},
})
let button = DOM.el(`#filter_button`)
DOM.ev(filter, `wheel`, (e) => {
Utils.scroll_wheel(e)
})
this.list = new List(
button,
filter,
this.ls_items,
this.max_items,
(value) => {
this.action(value)
},
() => {
this.clear()
},
)
}
static change(value) {
if (this.mode === value) {
return
}
this.mode = value
this.do_filter()
}
static unfilter() {
let els = DOM.els(`#container .item`)
if (!els.length) {
return
}
for (let el of els) {
DOM.show(el)
}
this.after()
}
static clear() {
this.get_filter().value = ``
this.unfilter()
}
static filter() {
this.debouncer.call()
}
static do_filter() {
this.debouncer.cancel()
let els = Container.get_items()
if (!els.length) {
return
}
let value = this.get_value()
let is_special = false
let special = []
let scope = `all`
if (this.mode === `owned`) {
special = Items.get_owned()
is_special = true
}
else if (this.mode === `today`) {
special = Items.get_today()
is_special = true
}
else if (this.mode === `week`) {
special = Items.get_week()
is_special = true
}
else if (this.mode === `month`) {
special = Items.get_month()
is_special = true
}
else if (this.mode === `curl`) {
scope = `curl`
is_special = true
}
else if (this.mode === `status`) {
scope = `status`
is_special = true
}
else if (this.mode === `date`) {
scope = `date`
is_special = true
}
if (!value && !is_special) {
this.unfilter()
return
}
if ((scope !== `all`) && !value) {
this.unfilter()
return
}
let check = (curl, status, updated) => {
return curl.includes(value) || status.includes(value) || updated.includes(value)
}
let hide = (el) => {
DOM.hide(el)
}
let show = (el) => {
DOM.show(el)
}
for (let el of els) {
let item = Items.get(el.dataset.curl)
let curl = item.curl.toLowerCase()
let status = item.status.toLowerCase()
let updated = item.updated_text.toLowerCase()
if (scope === `curl`) {
if (curl.includes(value)) {
show(el)
}
else {
hide(el)
}
}
else if (scope === `status`) {
if (status.includes(value)) {
show(el)
}
else {
hide(el)
}
}
else if (scope === `date`) {
if (updated.includes(value)) {
show(el)
}
else {
hide(el)
}
}
else if (is_special) {
if (special.find(s => s.curl === item.curl)) {
if (check(curl, status, updated)) {
show(el)
}
else {
hide(el)
}
}
else {
hide(el)
}
}
else {
if (check(curl, status, updated)) {
show(el)
}
else {
hide(el)
}
}
}
this.after()
}
static check() {
let filter = this.get_filter()
if (filter.value || (this.mode !== this.default_mode)) {
this.do_filter()
}
}
static after() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.save()
}, this.timeout_delay)
Infobar.update_curls()
}
static save() {
let value = this.get_value()
this.list.save(value)
}
static get_items() {
return Utils.load_array(this.ls_items)
}
static get_value() {
return this.get_filter().value.toLowerCase().trim()
}
static get_filter() {
return DOM.el(`#filter`)
}
static action(value) {
let filter = this.get_filter()
filter.value = value
filter.focus()
this.filter()
}
}

View File

@@ -0,0 +1,52 @@
/*
The font of the interface
*/
class Font {
static default_mode = `sans-serif`
static ls_name = `font`
static modes = [
{value: `sans-serif`, name: `Sans`, info: `Use Sans-Serif as the font`},
{value: `serif`, name: `Serif`, info: `Use Serif as the font`},
{value: `monospace`, name: `Mono`, info: `Use Monospace as the font`},
{value: `cursive`, name: `Cursive`, info: `Use Cursive as the font`},
]
static setup() {
let font = DOM.el(`#font`)
this.mode = this.load_font()
this.combo = new Combo({
title: `Font Modes`,
items: this.modes,
value: this.mode,
element: font,
default: this.default_mode,
action: (value) => {
this.change(value)
this.apply()
},
get: () => {
return this.mode
},
})
this.apply()
}
static change(value) {
this.mode = value
Utils.save(this.ls_name, value)
}
static apply() {
document.documentElement.style.setProperty(`--font`, this.mode)
}
static load_font() {
return Utils.load_modes(this.ls_name, this.modes, this.default_mode)
}
}

View File

@@ -0,0 +1,49 @@
class Footer {
static setup() {
let footer = DOM.el(`#footer`)
DOM.ev(footer, `contextmenu`, (e) => {
e.preventDefault()
Menu.show(e)
})
DOM.ev(footer, `dblclick`, (e) => {
if (e.target !== footer) {
return
}
Curls.add()
})
DOM.ev(footer, `wheel`, (e) => {
if (e.target !== footer) {
return
}
Container.scroll(e)
})
let lines = [
`Right Click to show the main menu`,
`Double Click to add curls`,
`Wheel to scroll the container`,
]
footer.title = lines.join(`\n`)
let scroller = DOM.el(`#scroller`)
DOM.ev(scroller, `click`, () => {
Container.scroller()
})
DOM.ev(scroller, `wheel`, (e) => {
Container.scroll(e)
})
let version = DOM.el(`#version`)
DOM.ev(version, `click`, () => {
Intro.show()
})
}
}

View File

@@ -0,0 +1,131 @@
class Infobar {
static interval_delay = Utils.SECOND * 30
static curls_debouncer_delay = 100
static date_debouncer_delay = 100
static setup() {
let infobar = DOM.el(`#infobar`)
this.hide()
DOM.ev(infobar, `click`, () => {
Container.scroll_top()
})
DOM.ev(infobar, `contextmenu`, (e) => {
e.preventDefault()
Menu.show(e)
})
DOM.ev(infobar, `auxclick`, (e) => {
if (e.button === 1) {
Container.scroll_bottom()
}
})
DOM.ev(infobar, `wheel`, (e) => {
Container.scroll(e)
})
this.start_interval()
this.curls_debouncer = Utils.create_debouncer(() => {
this.do_update_curls()
}, this.curls_debouncer_delay)
this.date_debouncer = Utils.create_debouncer(() => {
this.do_update_date()
}, this.date_debouncer_delay)
let curls = DOM.el(`#infobar_curls`)
curls.title = `Number of curls being monitored\nClick to select all`
DOM.ev(curls, `click`, () => {
this.curls_action()
})
let date = DOM.el(`#infobar_date`)
date.title = `How long ago items were updated\nClick to update now`
DOM.ev(date, `click`, () => {
this.date_action()
})
}
static start_interval() {
clearInterval(this.interval)
this.interval = setInterval(() => {
this.update_date()
}, this.interval_delay)
}
static update() {
if (!Items.list.length) {
this.hide()
return
}
this.show()
this.do_update_curls()
this.do_update_date()
this.start_interval()
}
static update_curls() {
this.curls_debouncer.call()
}
static do_update_curls() {
this.curls_debouncer.cancel()
let el = DOM.el(`#infobar_curls`)
let visible = Container.get_visible()
let selected = Select.get()
let text
if (visible.length === Items.list.length) {
text = `${Items.list.length} Curls`
}
else {
text = `${visible.length} / ${Items.list.length} Curls`
}
if (selected.length) {
if (selected.length === visible.length) {
text += ` (All)`
}
else {
text += ` (${selected.length})`
}
}
el.textContent = text
}
static update_date() {
this.date_debouncer.call()
}
static do_update_date() {
this.date_debouncer.cancel()
let el = DOM.el(`#infobar_date`)
let ago = Utils.timeago(Update.last_update)
el.textContent = ago
}
static hide() {
DOM.hide(`#infobar`)
}
static show() {
DOM.show(`#infobar`)
}
static curls_action() {
Select.toggle_all()
}
static date_action() {
DOM.el(`#infobar_date`).textContent = `Updating`
Update.update()
}
}

View File

@@ -0,0 +1,40 @@
/*
This shows an intro on the first visit
*/
class Intro {
static ls_name = `intro_shown`
static setup() {
this.intro = [
`Curls are pointers to text that you control.`,
`You can claim your own curl and receive a key.`,
`With this key you can change the status of the curl.`,
`The key can't be recovered and should be saved securely.`,
`In this Dashboard you can monitor the curls you want.`,
`Each color has its own set of curls.`,
`You are limited to 100 curls per color.`,
].join(`\n`)
let shown = this.load_intro()
if (!shown) {
this.show()
this.save()
}
}
static save() {
Utils.save(this.ls_name, true)
}
static load_intro() {
return Utils.load_boolean(this.ls_name, false)
}
static show() {
Windows.alert({title: `Curls ${App.version}`, message: this.intro})
}
}

View File

@@ -0,0 +1,240 @@
/*
This manages the item list
*/
class Items {
static list = []
static get(curl) {
return this.list.find(item => item.curl === curl)
}
static find_missing() {
let used = Curls.get_curls()
let curls = used.filter(curl => !this.list.find(item => item.curl === curl))
let missing = []
for (let curl of curls) {
missing.push({
curl: curl,
status: `Not found`,
created: `0`,
updated: `0`,
changes: 0,
missing: true,
})
}
return missing
}
static get_missing() {
return this.list.filter(item => item.missing)
}
static get_owned() {
let picker_items = Picker.get_items()
return this.list.filter(item => picker_items.find(
picker => picker.curl === item.curl))
}
static get_by_date(what) {
let cleaned = []
let now = Utils.now()
for (let item of this.list) {
let date = new Date(item.updated + `Z`)
let diff = now - date
if (diff < what) {
cleaned.push(item)
}
}
return cleaned
}
static get_today() {
return this.get_by_date(Utils.DAY)
}
static get_week() {
return this.get_by_date(Utils.WEEK)
}
static get_month() {
return this.get_by_date(Utils.MONTH)
}
static reset() {
this.list = []
}
static copy(curl) {
let blink = (icon) => {
if (!icon) {
return
}
icon.classList.add(`blink`)
setTimeout(() => {
icon.classList.remove(`blink`)
}, 1000)
}
let curls = Select.get_curls()
if (!curls.includes(curl)) {
curls = [curl]
}
let msgs = []
for (let curl of curls) {
let item = this.get(curl)
msgs.push(`${item.curl}\n${item.status}\n${item.updated_text}`)
blink(DOM.el(`.item_icon`, item.element))
}
let msg = msgs.join(`\n\n`)
Utils.copy_to_clipboard(msg)
}
static show_menu(args = {}) {
let items = []
let curls = Select.get_curls()
if (curls.length > 1) {
items = [
{
text: `Copy`,
action: () => {
this.copy(args.curl)
}
},
{
text: `Move`,
action: () => {
Colors.move(curls, args.e)
}
},
{
text: `Remove`,
action: () => {
Curls.remove_selected()
}
},
{
separator: true,
},
{
text: `To Top`,
action: () => {
Curls.to_top(curls)
}
},
{
text: `To Bottom`,
action: () => {
Curls.to_bottom(curls)
}
},
]
}
else {
items = [
{
text: `Copy`,
action: () => {
this.copy(args.curl)
}
},
{
text: `Edit`,
action: () => {
Curls.edit(args.curl)
}
},
{
text: `Move`,
action: () => {
Colors.move([args.curl], args.e)
}
},
{
text: `Remove`,
action: () => {
Curls.remove([args.curl])
}
},
{
separator: true,
},
{
text: `To Top`,
action: () => {
Curls.to_top([args.curl])
}
},
{
text: `To Bottom`,
action: () => {
Curls.to_bottom([args.curl])
}
},
]
}
Utils.context({items: items, e: args.e})
}
static remove_curl(curl) {
let cleaned = []
for (let item of this.list) {
if (item.curl !== curl) {
cleaned.push(item)
}
}
this.list = cleaned
}
static remove(removed) {
for (let curl of removed) {
let item = this.get(curl)
let el = item.element
if (el) {
el.remove()
}
let index = this.list.indexOf(item)
this.list.splice(index, 1)
}
Container.check_empty()
Infobar.update_curls()
}
static fill() {
let items = Curls.get()
for (let item of Items.list) {
let item_ = items.find(x => x.curl === item.curl)
if (item_ && item_.added) {
item.added = item_.added
}
else {
item.added = `0`
}
Dates.fill(item)
}
}
}

View File

@@ -0,0 +1,190 @@
class List {
constructor(button, input, ls_items, max_items, action, clear_action) {
this.button = button
this.input = input
this.ls_items = ls_items
this.max_items = max_items
this.action = action
this.clear_action = clear_action
this.menu_max_length = 110
this.prepare()
}
prepare() {
DOM.ev(this.button, `click`, (e) => {
this.show_menu(e)
})
DOM.ev(this.button, `auxclick`, (e) => {
if (e.button === 1) {
this.clear_action()
}
})
DOM.ev(this.button, `wheel`, (e) => {
this.cycle(e)
})
DOM.ev(this.input, `keydown`, (e) => {
if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) {
e.preventDefault()
}
else if (e.key === `Escape`) {
this.input.value = ``
e.preventDefault()
}
})
DOM.ev(this.input, `keyup`, (e) => {
if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) {
this.show_menu()
e.preventDefault()
}
})
let lines = [
`Use previous items`,
`Middle Click to clear input`,
`Middle Click items to remove`,
`Wheel to cycle`,
]
this.button.title = lines.join(`\n`)
}
get_items() {
return Utils.load_array(this.ls_items)
}
show_menu(e, show_empty = true) {
let list = this.get_items()
if (!list.length) {
if (show_empty) {
Windows.alert({
title: `Empty List`,
message: `Items appear here after you use them`,
})
}
return
}
let items = []
for (let item of list) {
items.push({
text: item.substring(0, this.menu_max_length),
action: () => {
this.action(item)
},
alt_action: () => {
this.remove(item)
},
})
}
items.push({
separator: true,
})
items.push({
text: `Clear`,
action: () => {
this.clear()
},
})
this.last_e = e
Utils.context({
e: e,
items: items,
element: this.button,
})
}
save(value) {
value = value.trim()
if (!value) {
return
}
let cleaned = []
for (let item of this.get_items()) {
if (item !== value) {
cleaned.push(item)
}
}
let list = [value, ...cleaned].slice(0, this.max_items)
Utils.save(this.ls_items, JSON.stringify(list))
}
remove(status) {
let cleaned = []
for (let status_ of this.get_items()) {
if (status_ === status) {
continue
}
cleaned.push(status_)
}
Utils.save(this.ls_items, JSON.stringify(cleaned))
this.show_menu(this.last_e, false)
}
clear() {
Windows.confirm({title: `Clear List`, ok: () => {
Utils.save(this.ls_items, `[]`)
}, message: `Remove all items from the list`})
}
cycle(e) {
let direction = Utils.wheel_direction(e)
if (direction === `up`) {
this.action(this.get_prev())
}
else {
this.action(this.get_next())
}
}
get_next() {
let list = this.get_items()
let current = this.input.value.trim()
let index = list.indexOf(current)
if (index === -1) {
return list[0]
}
if (index === list.length - 1) {
return list[0]
}
return list[index + 1]
}
get_prev() {
let list = this.get_items()
let current = this.input.value.trim()
let index = list.indexOf(current)
if (index === -1) {
return Utils.last(list)
}
if (index === 0) {
return list[list.length - 1]
}
return list[index - 1]
}
}

View File

@@ -0,0 +1,3 @@
window.onload = () => {
App.setup()
}

View File

@@ -0,0 +1,154 @@
class Menu {
static setup() {
let menu = DOM.el(`#menu`)
DOM.ev(menu, `click`, (e) => {
this.show(e)
})
}
static show(e) {
let curls = Curls.get_curls()
let items
let data = [
{
separator: true,
},
{
text: `Export`,
action: (e) => {
this.export(e)
}
},
{
text: `Import`,
action: () => {
this.import()
}
},
]
if (curls.length) {
items = [
{
text: `Add`,
action: () => {
Curls.add()
}
},
{
separator: true,
},
{
text: `Copy`,
action: () => {
Curls.copy()
}
},
{
text: `Replace`,
action: () => {
Curls.replace()
}
},
{
text: `Remove`,
action: (e) => {
Curls.show_remove_menu(e)
}
},
...data,
]
}
else {
items = [
{
text: `Add`,
action: () => {
Curls.add()
}
},
...data,
]
}
items.push({
separator: true,
})
items.push({
text: `Claim`,
action: () => {
this.claim()
}
})
Utils.context({items: items, e: e})
}
static export() {
let colors = {}
for (let color in Colors.colors) {
let curls = Curls.get_curls(color)
if (!curls.length) {
continue
}
colors[color] = curls
}
if (!Object.keys(colors).length) {
Windows.alert({message: `No curls to export`})
return
}
Windows.alert_export(colors)
}
static import() {
Windows.prompt({title: `Paste Data`, callback: (value) => {
this.import_submit(value)
}, message: `You get this data in Export`})
}
static import_submit(data) {
if (!data) {
return
}
try {
let colors = JSON.parse(data)
let modified = false
for (let color in colors) {
let curls = colors[color]
if (!curls.length) {
continue
}
Curls.clear(color)
Curls.save_curls(curls, color)
modified = true
}
if (!modified) {
Windows.alert({message: `No curls to import`})
return
}
Update.update()
}
catch (err) {
Utils.error(err)
Windows.alert({title: `Error`, message: err})
}
}
static claim() {
window.open(`/claim`, `_blank`)
}
}

View File

@@ -0,0 +1,166 @@
/*
This is a button that sits on the footer
It is used to toggle some options
*/
class More {
static setup() {
let button = DOM.el(`#footer_more`)
DOM.ev(button, `click`, (e) => {
this.show_menu(e)
})
DOM.ev(button, `auxclick`, (e) => {
if (e.button == 1) {
this.reset(e)
}
})
let lines = [
`More options`,
`Middle Click to reset`,
]
button.title = lines.join(`\n`)
}
static change_wrap(what, actions = true) {
if (Container.wrap_enabled == what) {
return
}
Container.wrap_enabled = what
Container.save_wrap_enabled()
if (actions) {
Container.update()
}
this.popup(`Wrap`, what)
}
static change_controls(what, actions = true) {
if (Controls.enabled == what) {
return
}
Controls.enabled = what
Controls.save_enabled()
if (actions) {
Controls.check_enabled()
}
this.popup(`Controls`, what)
}
static change_dates(what, actions = true) {
if (Dates.enabled == what) {
return
}
Dates.enabled = what
Dates.save_enabled()
if (actions) {
Container.update()
}
this.popup(`Dates`, what)
}
static show_menu(e) {
let items = []
if (Container.wrap_enabled) {
items.push({
text: `Disable Wrap`,
action: () => {
this.change_wrap(false)
},
info: `Disable text wrapping in the container`,
})
}
else {
items.push({
text: `Enable Wrap`,
action: () => {
this.change_wrap(true)
},
info: `Enable text wrapping in the container`,
})
}
if (Dates.enabled) {
items.push({
text: `Disable Dates`,
action: () => {
this.change_dates(false)
},
info: `Disable dates in the container`,
})
}
else {
items.push({
text: `Enable Dates`,
action: () => {
this.change_dates(true)
},
info: `Enable dates in the container`,
})
}
if (Controls.enabled) {
items.push({
text: `Disable Controls`,
action: () => {
this.change_controls(false)
},
info: `Disable the controls`,
})
}
else {
items.push({
text: `Enable Controls`,
action: () => {
this.change_controls(true)
},
info: `Enable the controls`,
})
}
Utils.context({items: items, e: e})
}
static reset() {
let vars = [
Container.wrap_enabled,
Dates.enabled,
Controls.enabled,
]
if (vars.every((x) => x)) {
return
}
Windows.confirm({title: `Reset Options`, ok: () => {
this.do_reset()
}, message: `Reset all options to default`})
}
static do_reset() {
this.change_wrap(true, false)
this.change_controls(true, false)
this.change_dates(true, false)
Controls.check_enabled()
Container.update()
}
static popup(what, value) {
let text = `${what} ${value ? `Enabled` : `Disabled`}`
Windows.popup(text)
}
}

View File

@@ -0,0 +1,54 @@
/*
Moves items up and down in the container
*/
class Move {
static setup() {
this.block = new Block()
}
static up() {
if (this.block.add_charge()) {
return
}
let items = Container.get_visible()
let selected = Select.get()
let first_index = items.indexOf(selected[0])
if (first_index > 0) {
first_index -= 1
}
let prev = items[first_index]
prev.before(...selected)
Utils.scroll_element({item: selected[0]})
this.save()
}
static down() {
if (this.block.add_charge()) {
return
}
let items = Container.get_visible()
let selected = Select.get()
let last_index = items.indexOf(Utils.last(selected))
if (last_index < (items.length - 1)) {
last_index += 1
}
let next = items[last_index]
next.after(...selected)
Utils.scroll_element({item: Utils.last(selected)})
this.save()
}
static save() {
Container.save_curls()
Sort.set_value(`order`)
}
}

View File

@@ -0,0 +1,165 @@
/*
The picker stores owned curls
*/
class Picker {
static max_items = 1000
static ls_name = `picker`
static setup() {
let picker = DOM.el(`#picker`)
DOM.ev(picker, `click`, (e) => {
this.show(e)
})
let items = this.get_items()
if (items.length) {
let first = items[0]
DOM.el(`#change_curl`).value = first.curl
DOM.el(`#change_key`).value = first.key
}
let curl = DOM.el(`#change_curl`)
DOM.ev(curl, `keydown`, (e) => {
if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) {
e.preventDefault()
}
})
DOM.ev(curl, `keyup`, (e) => {
if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) {
this.show(e)
e.preventDefault()
}
})
}
static get_items() {
return Utils.load_array(this.ls_name)
}
static add() {
let curl = DOM.el(`#change_curl`).value.toLowerCase()
let key = DOM.el(`#change_key`).value
let cleaned = [{curl, key}]
for (let item of this.get_items()) {
if (item.curl === curl) {
continue
}
cleaned.push(item)
if (cleaned.length >= this.max_items) {
break
}
}
Utils.save(this.ls_name, JSON.stringify(cleaned))
}
static show(e) {
let items = []
let picker_items = this.get_items()
if (!picker_items.length) {
items.push({
text: `Import`,
action: () => {
this.import()
},
})
}
else {
for (let item of picker_items) {
items.push({
text: item.curl,
action: () => {
let curl = DOM.el(`#change_curl`)
let key = DOM.el(`#change_key`)
curl.value = item.curl
key.value = item.key
curl.focus()
this.add()
},
alt_action: () => {
this.remove_item(item.curl)
},
})
}
if (items.length) {
items.push({
separator: true,
})
}
items.push({
text: `Export`,
action: () => {
this.export()
},
})
items.push({
text: `Import`,
action: () => {
this.import()
},
})
}
let el = DOM.el(`#picker`)
Utils.context({items: items, element: el, e: e})
}
static export() {
Windows.alert_export(this.get_items())
}
static import() {
Windows.prompt({title: `Paste Data`, callback: (value) => {
this.import_submit(value)
}, message: `You get this data in Export`})
}
static import_submit(data) {
if (!data) {
return
}
try {
let items = JSON.parse(data)
Utils.save(this.ls_name, JSON.stringify(items))
}
catch (err) {
Utils.error(err)
Windows.alert({title: `Error`, message: err})
}
}
static remove_item(curl) {
Windows.confirm({title: `Remove Picker Item`, ok: () => {
this.do_remove_item(curl)
}, message: curl})
}
static do_remove_item(curl) {
let cleaned = []
for (let item of this.get_items()) {
if (item.curl === curl) {
continue
}
cleaned.push(item)
}
Utils.save(this.ls_name, JSON.stringify(cleaned))
}
}

View File

@@ -0,0 +1,379 @@
/*
Used for selecting items in the container
*/
class Select {
static selected_class = `selected`
static selected_id = 0
static mouse_down = false
static mouse_selected = false
static setup() {
this.block = new Block()
}
static curl(curl) {
let item = Container.get_item(curl)
if (item) {
this.select(item)
}
}
static curls(curls) {
for (let curl of curls) {
this.curl(curl)
}
}
static check(item) {
let selected = this.get()
if (!selected.includes(item)) {
this.single(item)
}
}
static range(item) {
let selected = this.get()
if (!selected.length) {
this.single(item)
return
}
let items = Container.get_visible()
if (items.length <= 1) {
return
}
let history = this.history()
let prev = history[0]
let prev_prev = history[1] || prev
if (item === prev) {
return
}
let index = items.indexOf(item)
let prev_index = items.indexOf(prev)
let prev_prev_index = items.indexOf(prev_prev)
let direction
if (prev_index === prev_prev_index) {
if (index < prev_index) {
direction = `up`
}
else {
direction = `down`
}
}
else if (prev_index < prev_prev_index) {
direction = `up`
}
else {
direction = `down`
}
let action = (a, b) => {
this.do_range(a, b, direction)
}
if (direction === `up`) {
if (index < prev_index) {
action(index, prev_index)
}
else {
action(index, prev_prev_index)
}
}
else if (direction === `down`) {
if (index > prev_index) {
action(prev_index, index)
}
else {
action(prev_prev_index, index)
}
}
this.id_item(item)
}
static do_range(start, end, direction) {
let items = Container.get_visible()
let select = []
for (let i = 0; i < items.length; i++) {
if (i < start) {
if (direction === `up`) {
this.deselect(items[i])
}
continue
}
if (i > end) {
if (direction === `down`) {
this.deselect(items[i])
}
continue
}
select.push(items[i])
}
if (direction === `up`) {
select.reverse()
}
for (let item of select) {
this.select(item)
}
}
static vertical(direction, shift) {
if (this.block.add_charge()) {
return
}
let items = Container.get_visible()
if (!items.length) {
return
}
if (items.length === 1) {
this.single(items[0])
return
}
let selected = this.get()
let history = this.history()
let prev = history[0]
let prev_index = items.indexOf(prev)
let first_index = items.indexOf(selected[0])
if (!selected.length) {
let item
if (direction === `up`) {
item = Utils.last(items)
}
else if (direction === `down`) {
item = items[0]
}
this.single(item)
return
}
if (direction === `up`) {
if (shift) {
let item = items[prev_index - 1]
if (!item) {
return
}
this.range(item)
}
else {
let item
if (selected.length > 1) {
item = selected[0]
}
else {
let index = first_index - 1
if (index < 0) {
index = items.length - 1
}
item = items[index]
}
if (!item) {
return
}
this.single(item)
}
}
else if (direction === `down`) {
if (shift) {
let item = items[prev_index + 1]
if (!item) {
return
}
this.range(item)
}
else {
let item
if (selected.length > 1) {
item = Utils.last(selected)
}
else {
let index = first_index + 1
if (index >= items.length) {
index = 0
}
item = items[index]
}
if (!item) {
return
}
this.single(item)
}
}
}
static history() {
let items = this.get()
items = items.filter(item => {
return parseInt(item.dataset.selected_id) > 0
})
items.sort((a, b) => {
return parseInt(b.dataset.selected_id) - parseInt(a.dataset.selected_id)
})
return items
}
static get() {
return DOM.els(`#container .item.${this.selected_class}`)
}
static get_curls() {
let selected = this.get()
return selected.map(item => Container.extract_curl(item))
}
static id_item(item) {
this.selected_id += 1
item.dataset.selected_id = this.selected_id
}
static select(item, set_id = false) {
item.classList.add(this.selected_class)
if (set_id) {
this.id_item(item)
}
Utils.scroll_element({item: item})
Infobar.update_curls()
}
static deselect(item) {
item.classList.remove(this.selected_class)
item.dataset.selected_id = 0
Infobar.update_curls()
}
static toggle(item) {
if (item.classList.contains(this.selected_class)) {
this.deselect(item)
}
else {
this.select(item, true)
}
}
static deselect_all() {
let items = this.get()
for (let item of items) {
this.deselect(item)
}
this.selected_id = 0
}
static single(item) {
this.deselect_all()
this.selected_id = 0
this.select(item, true)
}
static mousedown(e) {
let item = Container.extract_item(e)
if (item) {
return
}
this.mouse_down = true
this.mouse_selected = false
e.preventDefault()
}
static mouseup() {
this.mouse_down = false
this.mouse_selected = false
}
static mouseover(e) {
if (!this.mouse_down) {
return
}
let item = Container.extract_item(e)
if (!item) {
return
}
let items = Container.get_visible()
let index = items.indexOf(item)
for (let i = 0; i < items.length; i++) {
if (i < index) {
this.deselect(items[i])
}
else {
this.select(items[i])
}
}
this.id_item(item)
}
static all() {
let items = Container.get_items()
for (let item of items) {
if (Container.is_visible(item)) {
this.select(item)
}
else {
this.deselect(item)
}
}
}
static toggle_all() {
let visible = Container.get_visible()
let selected = this.get()
if (selected.length === visible.length) {
this.deselect_all()
}
else {
this.all()
}
}
}

View File

@@ -0,0 +1,127 @@
/*
This sorts the container
*/
class Sort {
static default_mode = `recent`
static ls_name = `sort`
static modes = [
{value: `order`, name: `Order`, info: `Keep the custom order of items`},
{value: Utils.separator},
{value: `recent`, name: `Recent`, info: `Sort by the date when curls were last updated`},
{value: `added`, name: `Added`, info: `Sort by the date when curls were added by you`},
{value: Utils.separator},
{value: `curls`, name: `Curls`, info: `Sort curls alphabetically`},
{value: `status`, name: `Status`, info: `Sort status alphabetically`},
{value: Utils.separator},
{value: `active`, name: `Active`, info: `Sort by the number of changes`},
]
static setup() {
let sort = DOM.el(`#sort`)
this.mode = this.load_sort()
this.combo = new Combo({
title: `Sort Modes`,
items: this.modes,
value: this.mode,
element: sort,
default: this.default_mode,
action: (value) => {
this.change(value)
},
get: () => {
return this.mode
},
})
}
static set_value(value) {
if (this.mode === value) {
return
}
this.combo.set_value(value)
}
static change(value) {
if (this.mode === value) {
return
}
this.mode = value
Utils.save(this.ls_name, value)
if (this.mode === `order`) {
Container.save_curls()
}
else {
Container.update()
}
}
static sort_if_order() {
if (this.mode == `order`) {
Container.update()
}
}
static sort(items) {
let mode = this.mode
if (mode === `order`) {
let curls = Curls.get_curls()
items.sort((a, b) => {
let a_index = curls.indexOf(a.curl)
let b_index = curls.indexOf(b.curl)
return a_index - b_index
})
}
else if (mode === `recent`) {
items.sort((a, b) => {
let compare = b.updated.localeCompare(a.updated)
return compare !== 0 ? compare : a.curl.localeCompare(b.curl)
})
}
else if (mode === `added`) {
let items_ = Curls.get()
items.sort((a, b) => {
let item_a = Utils.find_item(items_, `curl`, a.curl)
let item_b = Utils.find_item(items_, `curl`, b.curl)
let diff = item_b.added - item_a.added
if (diff !== 0) {
return diff
}
return a.curl.localeCompare(b.curl)
})
}
else if (mode === `curls`) {
items.sort((a, b) => {
return a.curl.localeCompare(b.curl)
})
}
else if (mode === `status`) {
items.sort((a, b) => {
let compare = a.status.localeCompare(b.status)
return compare !== 0 ? compare : a.curl.localeCompare(b.curl)
})
}
else if (mode === `active`) {
items.sort((a, b) => {
let compare = b.changes - a.changes
return compare !== 0 ? compare : a.curl.localeCompare(b.curl)
})
}
}
static load_sort() {
return Utils.load_modes(this.ls_name, this.modes, this.default_mode)
}
}

View File

@@ -0,0 +1,74 @@
/*
This stores status items
*/
class Status {
static max_items = 100
static ls_items = `status_items`
static setup() {
let status = this.get_status()
let button = DOM.el(`#status_button`)
DOM.ev(status, `keyup`, (e) => {
if (e.key === `Enter`) {
Change.change()
}
})
DOM.ev(status, `wheel`, (e) => {
Utils.scroll_wheel(e)
})
status.value = ``
let lines = [
`Enter the new status of the curl`,
`Press Enter to submit the change`,
`Press Escape to clear`,
]
status.title = lines.join(`\n`)
this.list = new List(
button,
status,
this.ls_items,
this.max_items,
(value) => {
this.action(value)
},
() => {
this.clear()
},
)
}
static save(status) {
this.list.save(status)
}
static get_items() {
return Utils.load_array(this.ls_items)
}
static focus() {
this.get_status().focus()
}
static clear() {
this.get_status().value = ``
}
static get_status() {
return DOM.el(`#change_status`)
}
static action(value) {
let status = this.get_status()
status.value = value
status.focus()
}
}

View File

@@ -0,0 +1,24 @@
class Storage {
static debouncer_delay = 250
static setup = () => {
this.debouncer = Utils.create_debouncer((key) => {
this.do_check(key)
}, this.debouncer_delay)
window.addEventListener(`storage`, (e) => {
this.check(e.key)
})
}
static check = (key) => {
this.debouncer.call(key)
}
static do_check = (key) => {
if (key.startsWith(`curls_data`)) {
Curls.fill_colors()
Update.update()
}
}
}

View File

@@ -0,0 +1,209 @@
/*
Update manager
*/
class Update {
static default_mode = `minutes_5`
static enabled = false
static delay = Utils.MINUTE * 5
static debouncer_delay = 250
static updating = false
static clear_delay = 800
static ls_name = `update`
static last_update = 0
static modes = [
{value: `now`, name: `Update`, skip: true, info: `Update now`},
{value: Utils.separator},
{value: `minutes_1`},
{value: `minutes_5`},
{value: `minutes_10`},
{value: `minutes_20`},
{value: `minutes_30`},
{value: `minutes_60`},
{value: Utils.separator},
{value: `disabled`, name: `Disabled`, info: `Do not update automatically`},
]
static setup() {
let updater = DOM.el(`#updater`)
this.mode = this.load_update()
this.fill_modes()
this.combo = new Combo({
title: `Update Modes`,
items: this.modes,
value: this.mode,
element: updater,
default: this.default_mode,
action: (value) => {
this.change(value)
},
get: () => {
return this.mode
},
})
this.debouncer = Utils.create_debouncer(async (args) => {
await this.do_update(args)
this.restart()
}, this.debouncer_delay)
this.check()
}
static load_update() {
return Utils.load_modes(this.ls_name, this.modes, this.default_mode)
}
static check() {
let mode = this.mode
if (mode.startsWith(`minutes_`)) {
let minutes = parseInt(mode.split(`_`)[1])
this.delay = Utils.MINUTE * minutes
this.enabled = true
}
else {
this.enabled = false
}
this.restart()
}
static restart() {
clearTimeout(this.timeout)
if (this.enabled) {
this.start_timeout()
}
}
static start_timeout() {
this.timeout = setTimeout(() => {
this.update()
}, this.delay)
}
static update(args) {
this.debouncer.call(args)
}
static async do_update(args = {}) {
this.debouncer.cancel()
let def_args = {
curls: [],
}
Utils.def_args(def_args, args)
Utils.info(`Update: Trigger`)
if (this.updating) {
Utils.error(`Slow down`)
return
}
let add = false
if (args.curls.length) {
add = true
}
else {
args.curls = Curls.get_curls()
}
if (!args.curls.length) {
Container.show_empty()
return
}
let url = `/curls`
let params = new URLSearchParams()
for (let curl of args.curls) {
params.append(`curl`, curl);
}
this.show_updating()
let response = ``
this.updating = true
Utils.info(`Update: Request ${Utils.network} (${args.curls.length})`)
if (!Items.list.length) {
Container.show_loading()
}
try {
response = await fetch(url, {
method: `POST`,
headers: {
"Content-Type": `application/x-www-form-urlencoded`
},
body: params,
})
}
catch (e) {
Utils.error(`Failed to update`)
this.hide_updating()
return
}
try {
let items = await response.json()
this.last_update = Utils.now()
if (add) {
Container.add(items, args.curls)
}
else {
Container.insert(items)
}
}
catch (e) {
Utils.error(`Failed to parse response`)
Utils.error(e)
}
this.hide_updating()
}
static show_updating() {
let button = DOM.el(`#updater`)
clearTimeout(this.clear_timeout)
button.classList.add(`active`)
}
static hide_updating() {
this.updating = false
this.clear_timeout = setTimeout(() => {
let button = DOM.el(`#updater`)
button.classList.remove(`active`)
}, this.clear_delay)
}
static change(mode) {
if (mode === `now`) {
this.update()
return
}
this.mode = mode
Utils.save(this.ls_name, mode)
this.check()
}
static fill_modes() {
for (let mode of this.modes) {
if (mode.value.startsWith(`minutes_`)) {
let minutes = parseInt(mode.value.split(`_`)[1])
let word = Utils.plural(minutes, `minute`, `minutes`)
mode.name = `${minutes} ${word}`
mode.info = `Update automatically every ${minutes} ${word}`
}
}
}
}

View File

@@ -0,0 +1,287 @@
/*
These are some utility functions
*/
class Utils {
static console_logs = true
static SECOND = 1000
static MINUTE = this.SECOND * 60
static HOUR = this.MINUTE * 60
static DAY = this.HOUR * 24
static WEEK = this.DAY * 7
static MONTH = this.DAY * 30
static YEAR = this.DAY * 365
static scroll_wheel_step = 25
static network = `🛜`
static separator = `__separator__`
static curl_too_long = `Curl is too long`
static key_too_long = `Key is too long`
static status_too_long = `Status is too long`
static deselect() {
window.getSelection().removeAllRanges()
}
static plural(n, singular, plural) {
if (n === 1) {
return singular
}
else {
return plural
}
}
static info(msg) {
if (this.console_logs) {
// eslint-disable-next-line no-console
console.info(`💡 ${msg}`)
}
}
static error(msg) {
if (this.console_logs) {
// eslint-disable-next-line no-console
console.info(`${msg}`)
}
}
static sanitize(s) {
return s.replace(/</g, `&lt;`).replace(/>/g, `&gt;`)
}
static urlize(el) {
let html = el.innerHTML
let urlRegex = /(https?:\/\/[^\s]+)/g
let replacedText = html.replace(urlRegex, `<a href="$1" target="_blank">$1</a>`)
el.innerHTML = replacedText
}
static create_debouncer(func, delay) {
if (typeof func !== `function`) {
this.error(`Invalid debouncer function`)
return
}
if (!delay) {
this.error(`Invalid debouncer delay`)
return
}
let timer
let obj = {}
let clear = () => {
clearTimeout(timer)
}
let run = (...args) => {
func(...args)
}
obj.call = (...args) => {
clear()
timer = setTimeout(() => {
run(...args)
}, delay)
}
obj.now = (...args) => {
clear()
run(...args)
}
obj.cancel = () => {
clear()
}
return obj
}
static wheel_direction(e) {
if (e.deltaY > 0) {
return `down`
}
else {
return `up`
}
}
static capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1)
}
static now() {
return Date.now()
}
static copy_to_clipboard(text) {
navigator.clipboard.writeText(text)
}
static smart_list(string) {
return string.split(/[\s,;]+/).filter(Boolean)
}
static clean_modes(modes) {
return modes.filter(x => x.value !== Utils.separator)
}
static def_args(def, args) {
for (let key in def) {
if ((args[key] === undefined) && (def[key] !== undefined)) {
args[key] = def[key]
}
}
}
static scroll_element(args = {}) {
let def_args = {
behavior: `instant`,
block: `nearest`,
}
if (!args.item) {
return
}
this.def_args(def_args, args)
args.item.scrollIntoView({ behavior: args.behavior, block: args.block })
window.scrollTo(0, window.scrollY)
}
static last(list) {
return list.slice(-1)[0]
}
static load_modes(name, modes, def_value) {
let saved = localStorage.getItem(name) || def_value
let values = this.clean_modes(modes).map(x => x.value)
if (!values.includes(saved)) {
saved = def_value
}
return saved
}
static load_boolean(name, positive = true) {
let value = positive ? `true` : `false`
let saved = localStorage.getItem(name) || value
return saved === `true`
}
static load_array(name) {
try {
return JSON.parse(localStorage.getItem(name) || `[]`)
}
catch (err) {
this.error(err)
return []
}
}
static load_string(name, def_value = ``) {
return localStorage.getItem(name) || def_value
}
static save(name, value) {
localStorage.setItem(name, value)
}
static context(args) {
let def_args = {
focus: true,
}
this.def_args(def_args, args)
if (args.focus) {
args.after_hide = () => {
if (args.input) {
args.input.focus()
}
else {
Container.focus()
}
}
}
NeedContext.show({
e: args.e,
items: args.items,
element: args.element,
after_hide: args.after_hide,
})
}
static timeago = (date) => {
let diff = this.now() - date
let decimals = true
let n = 0
let m = ``
if (diff < this.SECOND) {
return `Just Now`
}
else if (diff < this.MINUTE) {
n = diff / this.SECOND
m = [`second`, `seconds`]
decimals = false
}
else if (diff < this.HOUR) {
n = diff / this.MINUTE
m = [`minute`, `minutes`]
decimals = false
}
else if (diff >= this.HOUR && diff < this.DAY) {
n = diff / this.HOUR
m = [`hour`, `hours`]
}
else if (diff >= this.DAY && diff < this.MONTH) {
n = diff / this.DAY
m = [`day`, `days`]
}
else if (diff >= this.MONTH && diff < this.YEAR) {
n = diff / this.MONTH
m = [`month`, `months`]
}
else if (diff >= this.YEAR) {
n = diff / this.YEAR
m = [`year`, `years`]
}
if (decimals) {
n = this.round(n, 1)
}
else {
n = Math.round(n)
}
let w = this.plural(n, m[0], m[1])
return `${n} ${w} ago`
}
static round = (n, decimals) => {
return Math.round(n * Math.pow(10, decimals)) / Math.pow(10, decimals)
}
static find_item(items, key, value) {
return items.find(x => x[key] === value)
}
static scroll_wheel(e) {
let direction = this.wheel_direction(e)
if (direction === `up`) {
e.target.scrollLeft -= this.scroll_wheel_step
}
else {
e.target.scrollLeft += this.scroll_wheel_step
}
}
}

View File

@@ -0,0 +1,213 @@
/*
This creates and shows modal windows
*/
class Windows {
static max_items = 1000
static popup_delay = 2750
static setup() {
this.make_alert()
this.make_prompt()
this.make_confirm()
}
static create() {
let common = {
enable_titlebar: true,
center_titlebar: true,
window_x: `none`,
after_close: () => {
Container.focus()
},
}
return Msg.factory(Object.assign({}, common, {
class: `modal`,
}))
}
static make_alert() {
this.alert_window = this.create()
let template = DOM.el(`#alert_template`)
let html = template.innerHTML
this.alert_window.set(html)
let copy = DOM.el(`#alert_copy`)
DOM.ev(copy, `click`, () => {
this.alert_copy()
})
let ok = DOM.el(`#alert_ok`)
DOM.ev(ok, `click`, (e) => {
this.alert_window.close()
})
}
static make_prompt() {
this.prompt_window = this.create()
let template = DOM.el(`#prompt_template`)
let html = template.innerHTML
this.prompt_window.set(html)
this.prompt_window.set_title(`Prompt`)
let submit = DOM.el(`#prompt_submit`)
let input = DOM.el(`#prompt_input`)
DOM.ev(submit, `click`, () => {
this.prompt_submit()
})
DOM.ev(input, `keydown`, (e) => {
if (e.key === `Enter`) {
this.prompt_submit()
}
})
DOM.ev(input, `wheel`, (e) => {
Utils.scroll_wheel(e)
})
}
static make_confirm() {
this.confirm_window = this.create()
let template = DOM.el(`#confirm_template`)
let html = template.innerHTML
this.confirm_window.set(html)
let ok = DOM.el(`#confirm_ok`)
DOM.ev(ok, `click`, () => {
this.confirm_ok()
this.confirm_window.close()
})
DOM.ev(ok, `keydown`, (e) => {
if (e.key === `Enter`) {
this.confirm_ok()
this.confirm_window.close()
}
})
}
static alert(args = {}) {
let def_args = {
title: `Information`,
message: ``,
copy: false,
ok: true,
}
Utils.def_args(def_args, args)
this.alert_window.set_title(args.title)
let msg = DOM.el(`#alert_message`)
if (args.message) {
DOM.show(msg)
msg.textContent = args.message
}
else {
DOM.hide(msg)
}
let copy = DOM.el(`#alert_copy`)
if (args.copy) {
DOM.show(copy)
}
else {
DOM.hide(copy)
}
let ok = DOM.el(`#alert_ok`)
if (args.ok) {
DOM.show(ok)
}
else {
DOM.hide(ok)
}
this.alert_window.show()
}
static alert_copy() {
let text = DOM.el(`#alert_message`)
Utils.copy_to_clipboard(text.textContent)
this.alert_window.close()
}
static confirm(args = {}) {
let def_args = {
message: ``,
}
Utils.def_args(def_args, args)
this.confirm_ok = args.ok
this.confirm_window.set_title(args.title)
this.confirm_window.show()
DOM.el(`#confirm_ok`).focus()
let msg = DOM.el(`#confirm_message`)
if (args.message) {
msg.textContent = args.message
DOM.show(msg)
}
else {
DOM.hide(msg)
}
}
static prompt_submit() {
let value = DOM.el(`#prompt_input`).value.trim()
this.prompt_callback(value)
this.prompt_window.close()
}
static prompt(args = {}) {
let def_args = {
value: ``,
message: ``,
}
Utils.def_args(def_args, args)
this.prompt_callback = args.callback
let input = DOM.el(`#prompt_input`)
input.value = args.value
this.prompt_window.set_title(args.title)
let msg = DOM.el(`#prompt_message`)
if (args.message) {
msg.textContent = args.message
DOM.show(msg)
}
else {
DOM.hide(msg)
}
this.prompt_window.show()
input.focus()
}
static alert_export(data) {
let data_str = Utils.sanitize(JSON.stringify(data))
this.alert({title: `Copy the data below`, message: data_str, copy: true, ok: false})
}
static popup(message) {
let popup = Msg.factory({
preset: `popup_autoclose`,
position: `bottomright`,
window_x: `none`,
enable_titlebar: true,
center_titlebar: true,
autoclose_delay: this.popup_delay,
class: `popup`,
})
popup.set(message)
popup.set_title(`Information`)
popup.show()
}
}