first commit
This commit is contained in:
		
							
								
								
									
										429
									
								
								server/static/dashboard/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										429
									
								
								server/static/dashboard/css/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,429 @@ | ||||
| :root { | ||||
|     --background: rgb(12, 12, 12); | ||||
|     --vertical_padding: 0.74rem; | ||||
|     --horizontal_padding: 0.66rem; | ||||
|     --alt_background_color: rgb(18, 18, 18); | ||||
| } | ||||
|  | ||||
| body, | ||||
| html | ||||
| { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     font-family: var(--font); | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     font-size: 18px; | ||||
| } | ||||
|  | ||||
| #main { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.8rem; | ||||
|     width: 100vw; | ||||
|     height: 100vh; | ||||
|     padding-left: var(--horizontal_padding); | ||||
|     padding-right: var(--horizontal_padding); | ||||
|     padding-top: var(--vertical_padding); | ||||
|     box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| #container_outer { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     overflow-x: hidden; | ||||
|     overflow-y: auto; | ||||
| } | ||||
|  | ||||
| #container { | ||||
|     display: table; | ||||
|     border-collapse: collapse; | ||||
|     table-layout: fixed; | ||||
|     width: 100%; | ||||
|     box-sizing: border-box; | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| #infobar { | ||||
|     cursor: default; | ||||
|     user-select: none; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #infobar_curls, | ||||
| #infobar_date | ||||
| { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .infobar_separator { | ||||
|     opacity: 0.5; | ||||
| } | ||||
|  | ||||
| .item { | ||||
|     display: table-row; | ||||
|     border: var(--border); | ||||
|     cursor: default; | ||||
| } | ||||
|  | ||||
| .item_icon, | ||||
| .item_curl, | ||||
| .item_status, | ||||
| .item_updated | ||||
| { | ||||
|     display: table-cell; | ||||
|     padding-top: 0.8rem; | ||||
|     padding-bottom: 0.8rem; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| .item_icon { | ||||
|     width: 2.5rem; | ||||
|     justify-content: center; | ||||
|     cursor: pointer; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| .item_icon_canvas { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     text-align: left; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .item_icon:hover .item_icon_canvas { | ||||
|     filter: brightness(1.5); | ||||
| } | ||||
|  | ||||
| .item_curl { | ||||
|     width: 150px; | ||||
|     max-width: 150px; | ||||
|     color: var(--color); | ||||
|     text-align: left; | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| .item_status { | ||||
|     text-align: left; | ||||
|     padding-right: 0.8rem; | ||||
|     padding-left: 0.8rem; | ||||
|     word-break: break-word; | ||||
|     white-space: pre-wrap; | ||||
| } | ||||
|  | ||||
| .item_status.nowrap { | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .item_updated { | ||||
|     width: 225px; | ||||
|     max-width: 225px; | ||||
|     color: var(--color); | ||||
|     white-space: nowrap; | ||||
|     text-align: right; | ||||
|     padding-right: 0.5rem; | ||||
| } | ||||
|  | ||||
| .item:hover { | ||||
|     background-color: var(--color_alpha_0); | ||||
| } | ||||
|  | ||||
| .item.selected { | ||||
|     background-color: var(--color_alpha_1); | ||||
| } | ||||
|  | ||||
| /*  */ | ||||
|  | ||||
| #controls { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     margin-bottom: 0.5rem; | ||||
|     row-gap: 0.8rem; | ||||
|     column-gap: 1.2rem; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| .control_bar | ||||
| { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     gap: 1.2rem; | ||||
| } | ||||
|  | ||||
| .control_section { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     gap: 0.66rem; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding: 0.5rem; | ||||
| } | ||||
|  | ||||
| a:visited, | ||||
| a:link, | ||||
| a:hover { | ||||
|     color: var(--color); | ||||
| } | ||||
|  | ||||
| .pointer { | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| input[type="text"], | ||||
| input[type="password"] | ||||
| { | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding: 0.09rem; | ||||
|     padding-left: 0.25rem; | ||||
|     outline: none; | ||||
|     width: 8rem; | ||||
|     font-size: 1rem; | ||||
|     font: var(--font); | ||||
| } | ||||
|  | ||||
| .button { | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     cursor: pointer; | ||||
|     outline: none; | ||||
|     font-size: 1rem; | ||||
|     padding: 0.05rem; | ||||
|     padding-left: 0.25rem; | ||||
|     padding-right: 0.25rem; | ||||
|     user-select: none; | ||||
|     white-space: nowrap; | ||||
|     min-width: 1rem; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .button:hover { | ||||
|     background-color: var(--color); | ||||
|     color: var(--background); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| @keyframes blink { | ||||
|     0% { | ||||
|         opacity: 1; | ||||
|     } | ||||
|  | ||||
|     50% { | ||||
|         opacity: 0; | ||||
|     } | ||||
|  | ||||
|     100% { | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .blink { | ||||
|     animation: blink 500ms infinite; | ||||
| } | ||||
|  | ||||
| @keyframes colorchange { | ||||
|     0% { | ||||
|         border-color: rgb(255, 102, 102); | ||||
|     } | ||||
|  | ||||
|     25% { | ||||
|         border-color: rgb(125, 255, 125); | ||||
|     } | ||||
|  | ||||
|     50% { | ||||
|         border-color: rgb(157, 157, 255); | ||||
|     } | ||||
|  | ||||
|     100% { | ||||
|         border-color: white; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .button.active { | ||||
|     animation: colorchange 1s infinite; | ||||
|     border-radius: 8px; | ||||
| } | ||||
|  | ||||
| .glow:hover { | ||||
|     text-shadow: 0 0 10px var(--color), 0 0 20px var(--color), 0 0 30px var(--color), 0 0 40px var(--color), 0 0 50px var(--color), 0 0 60px var(--color); | ||||
| } | ||||
|  | ||||
| .glow_white:hover { | ||||
|     text-shadow: 0 0 10px white, 0 0 20px white, 0 0 30px white, 0 0 40px white, 0 0 50px white, 0 0 60px white; | ||||
| } | ||||
|  | ||||
| .glow { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .glow_white { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .noselect { | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| #claim { | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| #footer { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: flex-start; | ||||
|     border-top: 1px solid var(--color_alpha_2); | ||||
|     margin-top: 0.35rem; | ||||
|     padding-top: 0.5rem; | ||||
|     padding-bottom: var(--vertical_padding); | ||||
|     gap: 1.25rem; | ||||
| } | ||||
|  | ||||
| .footer_item { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     gap: 0.5rem; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| #version { | ||||
|     margin-left: auto; | ||||
| } | ||||
|  | ||||
| .disabled { | ||||
|     pointer-events: none; | ||||
|     opacity: 0.6; | ||||
| } | ||||
|  | ||||
| .modal_message { | ||||
|     max-width: 30rem; | ||||
|     white-space: pre-wrap; | ||||
|     cursor: auto; | ||||
| } | ||||
|  | ||||
| .modal_button { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding-left: 0.4rem; | ||||
|     padding-right: 0.4rem; | ||||
|     padding-top: 0.05rem; | ||||
|     padding-bottom: 0.05rem; | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|     outline: none; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .modal_button:hover { | ||||
|     border: 1px solid var(--color); | ||||
| } | ||||
|  | ||||
| .modal_items { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #alert_buttons { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #alert_message_container { | ||||
|     max-height: 14rem; | ||||
|     overflow-x: hidden; | ||||
|     overflow-y: auto; | ||||
| } | ||||
|  | ||||
| #prompt_input { | ||||
|     width: 14rem; | ||||
|     padding: 0.2rem; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| #needcontext-main { | ||||
|     background-color: rgba(0, 0, 0, 0.44) !important; | ||||
| } | ||||
|  | ||||
| #needcontext-container { | ||||
|     background-color: var(--alt_background_color) !important; | ||||
|     border: 2px solid var(--color_alpha_2) !important; | ||||
|     color: var(--color) !important; | ||||
| } | ||||
|  | ||||
| .needcontext-text { | ||||
|     max-width: 300px !important; | ||||
|     white-space: wrap !important; | ||||
| } | ||||
|  | ||||
| .needcontext-item-selected { | ||||
|     background-color: var(--color_alpha_1) !important; | ||||
| } | ||||
|  | ||||
| .needcontext-text { | ||||
|     font-family: var(--font) !important; | ||||
| } | ||||
|  | ||||
| .Msg-window { | ||||
|     color: var(--color) !important; | ||||
|     background-color: var(--alt_background_color) !important; | ||||
|     border: 2px solid var(--color_alpha_2) !important; | ||||
| } | ||||
|  | ||||
| .Msg-titlebar { | ||||
|     color: var(--color) !important; | ||||
|     background-color: rgb(42, 42, 42) !important; | ||||
|     font-family: var(--font) !important; | ||||
| } | ||||
|  | ||||
| .Msg-progressbar { | ||||
|     background-color: var(--color_alpha_2) !important; | ||||
| } | ||||
|  | ||||
| .Msg-titlebar { | ||||
|     padding-top: 0.24rem !important; | ||||
|     padding-bottom: 0.24rem !important; | ||||
|     padding-left: 0.8rem !important; | ||||
|     padding-right: 0.8rem !important; | ||||
| } | ||||
|  | ||||
| .Msg-window-inner-x:hover { | ||||
|     background-color: var(--color_alpha_1) !important; | ||||
| } | ||||
|  | ||||
| .Msg-content-modal { | ||||
|     padding: 0.8rem !important; | ||||
| } | ||||
							
								
								
									
										125
									
								
								server/static/dashboard/js/libs/dateformat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								server/static/dashboard/js/libs/dateformat.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /* | ||||
| * Date Format 1.2.3 | ||||
| * (c) 2007-2009 Steven Levithan <stevenlevithan.com> | ||||
| * MIT license | ||||
| * | ||||
| * Includes enhancements by Scott Trenda <scott.trenda.net> | ||||
| * and Kris Kowal <cixar.com/~kris.kowal/> | ||||
| * | ||||
| * Accepts a date, a mask, or a date and a mask. | ||||
| * Returns a formatted version of the given date. | ||||
| * The date defaults to the current date/time. | ||||
| * The mask defaults to dateFormat.masks.default. | ||||
| */ | ||||
|  | ||||
| var dateFormat = function () { | ||||
|     var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, | ||||
| 		timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, | ||||
| 		timezoneClip = /[^-+\dA-Z]/g, | ||||
| 		pad = function (val, len) { | ||||
| 		    val = String(val); | ||||
| 		    len = len || 2; | ||||
| 		    while (val.length < len) val = "0" + val; | ||||
| 		    return val; | ||||
| 		}; | ||||
|  | ||||
|     // Regexes and supporting functions are cached through closure | ||||
|     return function (date, mask, utc) { | ||||
|         var dF = dateFormat; | ||||
|  | ||||
|         // You can't provide utc if you skip other args (use the "UTC:" mask prefix) | ||||
|         if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { | ||||
|             mask = date; | ||||
|             date = undefined; | ||||
|         } | ||||
|  | ||||
|         // Passing date through Date applies Date.parse, if necessary | ||||
|         date = date ? new Date(date) : new Date; | ||||
|         if (isNaN(date)) throw SyntaxError("invalid date"); | ||||
|  | ||||
|         mask = String(dF.masks[mask] || mask || dF.masks["default"]); | ||||
|  | ||||
|         // Allow setting the utc argument via the mask | ||||
|         if (mask.slice(0, 4) == "UTC:") { | ||||
|             mask = mask.slice(4); | ||||
|             utc = true; | ||||
|         } | ||||
|  | ||||
|         var _ = utc ? "getUTC" : "get", | ||||
| 			d = date[_ + "Date"](), | ||||
| 			D = date[_ + "Day"](), | ||||
| 			m = date[_ + "Month"](), | ||||
| 			y = date[_ + "FullYear"](), | ||||
| 			H = date[_ + "Hours"](), | ||||
| 			M = date[_ + "Minutes"](), | ||||
| 			s = date[_ + "Seconds"](), | ||||
| 			L = date[_ + "Milliseconds"](), | ||||
| 			o = utc ? 0 : date.getTimezoneOffset(), | ||||
| 			flags = { | ||||
| 			    d: d, | ||||
| 			    dd: pad(d), | ||||
| 			    ddd: dF.i18n.dayNames[D], | ||||
| 			    dddd: dF.i18n.dayNames[D + 7], | ||||
| 			    m: m + 1, | ||||
| 			    mm: pad(m + 1), | ||||
| 			    mmm: dF.i18n.monthNames[m], | ||||
| 			    mmmm: dF.i18n.monthNames[m + 12], | ||||
| 			    yy: String(y).slice(2), | ||||
| 			    yyyy: y, | ||||
| 			    h: H % 12 || 12, | ||||
| 			    hh: pad(H % 12 || 12), | ||||
| 			    H: H, | ||||
| 			    HH: pad(H), | ||||
| 			    M: M, | ||||
| 			    MM: pad(M), | ||||
| 			    s: s, | ||||
| 			    ss: pad(s), | ||||
| 			    l: pad(L, 3), | ||||
| 			    L: pad(L > 99 ? Math.round(L / 10) : L), | ||||
| 			    t: H < 12 ? "a" : "p", | ||||
| 			    tt: H < 12 ? "am" : "pm", | ||||
| 			    T: H < 12 ? "A" : "P", | ||||
| 			    TT: H < 12 ? "AM" : "PM", | ||||
| 			    Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), | ||||
| 			    o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), | ||||
| 			    S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] | ||||
| 			}; | ||||
|  | ||||
|         return mask.replace(token, function ($0) { | ||||
|             return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); | ||||
|         }); | ||||
|     }; | ||||
| } (); | ||||
|  | ||||
| // Some common format strings | ||||
| dateFormat.masks = { | ||||
|     "default": "ddd mmm dd yyyy HH:MM:ss", | ||||
|     shortDate: "m/d/yy", | ||||
|     mediumDate: "mmm d, yyyy", | ||||
|     longDate: "mmmm d, yyyy", | ||||
|     fullDate: "dddd, mmmm d, yyyy", | ||||
|     shortTime: "h:MM TT", | ||||
|     mediumTime: "h:MM:ss TT", | ||||
|     longTime: "h:MM:ss TT Z", | ||||
|     isoDate: "yyyy-mm-dd", | ||||
|     isoTime: "HH:MM:ss", | ||||
|     isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", | ||||
|     isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" | ||||
| }; | ||||
|  | ||||
| // Internationalization strings | ||||
| dateFormat.i18n = { | ||||
|     dayNames: [ | ||||
| 		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", | ||||
| 		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" | ||||
| 	], | ||||
|     monthNames: [ | ||||
| 		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", | ||||
| 		"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| // For convenience... | ||||
| Date.prototype.format = function (mask, utc) { | ||||
|     return dateFormat(this, mask, utc); | ||||
| }; | ||||
							
								
								
									
										143
									
								
								server/static/dashboard/js/libs/dom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								server/static/dashboard/js/libs/dom.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| // DOM v1.0.0 | ||||
| const DOM = {} | ||||
| DOM.dataset_obj = {} | ||||
| DOM.dataset_id = 0 | ||||
|  | ||||
| // Select a single element | ||||
| DOM.el = (query, root = document) => { | ||||
|   return root.querySelector(query) | ||||
| } | ||||
|  | ||||
| // Select an array of elements | ||||
| DOM.els = (query, root = document) => { | ||||
|   return Array.from(root.querySelectorAll(query)) | ||||
| } | ||||
|  | ||||
| // Select a single element or self | ||||
| DOM.el_or_self = (query, root = document) => { | ||||
|   let el = root.querySelector(query) | ||||
|  | ||||
|   if (!el) { | ||||
|     if (root.classList.contains(query.replace(`.`, ``))) { | ||||
|       el = root | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return el | ||||
| } | ||||
|  | ||||
| // Select an array of elements or self | ||||
| DOM.els_or_self = (query, root = document) => { | ||||
|   let els = Array.from(root.querySelectorAll(query)) | ||||
|  | ||||
|   if (els.length === 0) { | ||||
|     if (root.classList.contains(query.replace(`.`, ``))) { | ||||
|       els = [root] | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return els | ||||
| } | ||||
|  | ||||
| // Clone element | ||||
| DOM.clone = (el) => { | ||||
|   return el.cloneNode(true) | ||||
| } | ||||
|  | ||||
| // Clone element children | ||||
| DOM.clone_children = (query) => { | ||||
|   let items = [] | ||||
|   let children = Array.from(DOM.el(query).children) | ||||
|  | ||||
|   for (let c of children) { | ||||
|     items.push(DOM.clone(c)) | ||||
|   } | ||||
|  | ||||
|   return items | ||||
| } | ||||
|  | ||||
| // Data set manager | ||||
| DOM.dataset = (el, value, setvalue) => { | ||||
|   if (!el) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   let id = el.dataset.dataset_id | ||||
|  | ||||
|   if (!id) { | ||||
|     id = DOM.dataset_id | ||||
|     DOM.dataset_id += 1 | ||||
|     el.dataset.dataset_id = id | ||||
|     DOM.dataset_obj[id] = {} | ||||
|   } | ||||
|  | ||||
|   if (setvalue !== undefined) { | ||||
|     DOM.dataset_obj[id][value] = setvalue | ||||
|   } | ||||
|   else { | ||||
|     return DOM.dataset_obj[id][value] | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Create an html element | ||||
| DOM.create = (type, classes = ``, id = ``) => { | ||||
|   let el = document.createElement(type) | ||||
|  | ||||
|   if (classes) { | ||||
|     let classlist = classes.split(` `).filter(x => x != ``) | ||||
|  | ||||
|     for (let cls of classlist) { | ||||
|       el.classList.add(cls) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (id) { | ||||
|     el.id = id | ||||
|   } | ||||
|  | ||||
|   return el | ||||
| } | ||||
|  | ||||
| // Add an event listener | ||||
| DOM.ev = (element, event, callback, extra) => { | ||||
|   element.addEventListener(event, callback, extra) | ||||
| } | ||||
|  | ||||
| // Add multiple event listeners | ||||
| DOM.evs = (element, events, callback, extra) => { | ||||
|   for (let event of events) { | ||||
|     element.addEventListener(event, callback, extra) | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Like jQuery's nextAll | ||||
| DOM.next_all = function* (e, selector) { | ||||
|   while (e = e.nextElementSibling) { | ||||
|     if (e.matches(selector)) { | ||||
|       yield e; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Get item index | ||||
| DOM.index = (el) => { | ||||
|   return Array.from(el.parentNode.children).indexOf(el) | ||||
| } | ||||
|  | ||||
| // Show an element | ||||
| DOM.show = (item) => { | ||||
|   if (typeof item === `string`) { | ||||
|     item = DOM.el(item) | ||||
|   } | ||||
|  | ||||
|   item.classList.remove(`hidden`) | ||||
| } | ||||
|  | ||||
| // Hide an element | ||||
| DOM.hide = (item) => { | ||||
|   if (typeof item === `string`) { | ||||
|     item = DOM.el(item) | ||||
|   } | ||||
|  | ||||
|   item.classList.add(`hidden`) | ||||
| } | ||||
							
								
								
									
										1507
									
								
								server/static/dashboard/js/libs/jdenticon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1507
									
								
								server/static/dashboard/js/libs/jdenticon.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2339
									
								
								server/static/dashboard/js/libs/msg.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2339
									
								
								server/static/dashboard/js/libs/msg.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1019
									
								
								server/static/dashboard/js/libs/needcontext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1019
									
								
								server/static/dashboard/js/libs/needcontext.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										81
									
								
								server/static/dashboard/js/main/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/static/dashboard/js/main/app.js
									
									
									
									
									
										Normal 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() | ||||
| } | ||||
							
								
								
									
										48
									
								
								server/static/dashboard/js/main/block.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/static/dashboard/js/main/block.js
									
									
									
									
									
										Normal 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 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								server/static/dashboard/js/main/border.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								server/static/dashboard/js/main/border.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										164
									
								
								server/static/dashboard/js/main/change.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								server/static/dashboard/js/main/change.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										188
									
								
								server/static/dashboard/js/main/colors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								server/static/dashboard/js/main/colors.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										130
									
								
								server/static/dashboard/js/main/combo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								server/static/dashboard/js/main/combo.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										452
									
								
								server/static/dashboard/js/main/container.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										452
									
								
								server/static/dashboard/js/main/container.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								server/static/dashboard/js/main/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/static/dashboard/js/main/controls.js
									
									
									
									
										vendored
									
									
										Normal 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`) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										521
									
								
								server/static/dashboard/js/main/curls.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										521
									
								
								server/static/dashboard/js/main/curls.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										80
									
								
								server/static/dashboard/js/main/dates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/static/dashboard/js/main/dates.js
									
									
									
									
									
										Normal 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 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								server/static/dashboard/js/main/drag.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/static/dashboard/js/main/drag.js
									
									
									
									
									
										Normal 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`) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										278
									
								
								server/static/dashboard/js/main/filter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								server/static/dashboard/js/main/filter.js
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										52
									
								
								server/static/dashboard/js/main/font.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								server/static/dashboard/js/main/font.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								server/static/dashboard/js/main/footer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								server/static/dashboard/js/main/footer.js
									
									
									
									
									
										Normal 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() | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										131
									
								
								server/static/dashboard/js/main/infobar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								server/static/dashboard/js/main/infobar.js
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								server/static/dashboard/js/main/intro.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								server/static/dashboard/js/main/intro.js
									
									
									
									
									
										Normal 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}) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										240
									
								
								server/static/dashboard/js/main/items.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								server/static/dashboard/js/main/items.js
									
									
									
									
									
										Normal 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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										190
									
								
								server/static/dashboard/js/main/list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								server/static/dashboard/js/main/list.js
									
									
									
									
									
										Normal 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] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								server/static/dashboard/js/main/load.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								server/static/dashboard/js/main/load.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| window.onload = () => { | ||||
|     App.setup() | ||||
| } | ||||
							
								
								
									
										154
									
								
								server/static/dashboard/js/main/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/static/dashboard/js/main/menu.js
									
									
									
									
									
										Normal 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`) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										166
									
								
								server/static/dashboard/js/main/more.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								server/static/dashboard/js/main/more.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								server/static/dashboard/js/main/move.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								server/static/dashboard/js/main/move.js
									
									
									
									
									
										Normal 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`) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										165
									
								
								server/static/dashboard/js/main/picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								server/static/dashboard/js/main/picker.js
									
									
									
									
									
										Normal 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)) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										379
									
								
								server/static/dashboard/js/main/select.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								server/static/dashboard/js/main/select.js
									
									
									
									
									
										Normal 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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										127
									
								
								server/static/dashboard/js/main/sort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								server/static/dashboard/js/main/sort.js
									
									
									
									
									
										Normal 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								server/static/dashboard/js/main/status.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/static/dashboard/js/main/status.js
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								server/static/dashboard/js/main/storage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/static/dashboard/js/main/storage.js
									
									
									
									
									
										Normal 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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										209
									
								
								server/static/dashboard/js/main/update.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								server/static/dashboard/js/main/update.js
									
									
									
									
									
										Normal 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}` | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										287
									
								
								server/static/dashboard/js/main/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								server/static/dashboard/js/main/utils.js
									
									
									
									
									
										Normal 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, `<`).replace(/>/g, `>`) | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										213
									
								
								server/static/dashboard/js/main/windows.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								server/static/dashboard/js/main/windows.js
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								server/static/icon.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/icon.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 236 KiB | 
		Reference in New Issue
	
	Block a user
	 Auric Vente
					Auric Vente