├── .gitignore ├── LICENSE ├── README.md ├── audio ├── effect_1.mp3 └── effect_2.mp3 ├── background ├── background.js └── bookmarks_server.js ├── css ├── addlist.css ├── menubutton.css ├── needcontext.css ├── overrides.css └── style.css ├── eslint.config.mjs ├── fonts └── NovaSquare-Regular.ttf ├── header ├── base.js ├── index.html ├── init.js └── style.css ├── img ├── backgrounds │ ├── background_1.jpg │ ├── background_2.jpg │ ├── background_3.jpg │ ├── background_4.jpg │ ├── background_5.jpg │ └── background_6.jpg ├── favicon.jpg ├── folder.jpg ├── grasshopper.png ├── icon128.png ├── info.jpg ├── leaf.jpg └── lock.jpg ├── js ├── app.js ├── init.js ├── libs │ ├── acolorpicker.min.js │ ├── colorlib.js │ ├── dateformat.js │ ├── dom.js │ ├── jdenticon.js │ ├── needcontext.js │ └── nicegesture.js ├── main │ ├── about.js │ ├── actions_menu.js │ ├── active_trace.js │ ├── addlist.js │ ├── alert.js │ ├── autoclick.js │ ├── bookmarks.js │ ├── browser.js │ ├── clock.js │ ├── close_button.js │ ├── close_tabs.js │ ├── closed.js │ ├── colors.js │ ├── combos.js │ ├── command_props.js │ ├── commands.js │ ├── containers.js │ ├── context.js │ ├── data.js │ ├── dialog.js │ ├── drag.js │ ├── edits.js │ ├── favorites.js │ ├── filter.js │ ├── footer.js │ ├── gestures.js │ ├── gets.js │ ├── guides.js │ ├── history.js │ ├── hover_button.js │ ├── icons.js │ ├── input.js │ ├── item_menu.js │ ├── items.js │ ├── jump.js │ ├── keyboard.js │ ├── lock_screen.js │ ├── main_menu.js │ ├── main_title.js │ ├── media.js │ ├── menubutton.js │ ├── messages.js │ ├── modes.js │ ├── mouse.js │ ├── mouse_actions.js │ ├── mute.js │ ├── notes.js │ ├── obfuscate.js │ ├── other.js │ ├── palette.js │ ├── pinline.js │ ├── pins.js │ ├── playing.js │ ├── popups.js │ ├── process.js │ ├── prompt.js │ ├── recent_tabs.js │ ├── restore.js │ ├── root_urls.js │ ├── rules.js │ ├── scroll.js │ ├── setting_props.js │ ├── settings.js │ ├── signals.js │ ├── sort.js │ ├── step_back.js │ ├── storage.js │ ├── tab_box.js │ ├── tab_list.js │ ├── tabs.js │ ├── taglist.js │ ├── tags.js │ ├── templates.js │ ├── textarea.js │ ├── theme.js │ ├── titles.js │ ├── toys.js │ ├── tree.js │ ├── unloaded.js │ ├── users.js │ ├── utils.js │ ├── warns.js │ ├── windows.js │ └── zones.js └── overrides.js ├── main.html ├── manifest.json ├── more └── signals │ ├── main.py │ └── requirements.txt ├── package.json └── utils ├── bundle.rb ├── check.sh ├── dups.rb ├── fix.sh ├── header.rb ├── remove_header.rb ├── replace.rb ├── search.sh ├── stats.rb ├── stylecheck.sh ├── tag.rb └── zip.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | .directory 3 | js/bundle* 4 | node_modules/ 5 | package-lock.json 6 | .eslintcache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grasshopper 2 | 3 | [Advanced Tab Manager For Firefox](https://addons.mozilla.org/firefox/addon/grasshopper-urls/) 4 | 5 | Programmed by [madprops](https://github.com/madprops) 6 | 7 | Lots of ideas by [N3C2L](https://github.com/N3C2L) 8 | 9 | Good feedback by [user0022](https://github.com/user0022) 10 | 11 | --- 12 | 13 | 14 | 15 | Grasshoppers are a group of insects belonging to the suborder Caelifera. They are among what is possibly the most ancient living group of chewing herbivorous insects, dating back to the early Triassic around 250 million years ago. 16 | 17 | Grasshoppers are typically ground-dwelling insects with powerful hind legs which allow them to escape from threats by leaping vigorously. Their front leg is shorter and used for grasping food. As hemimetabolous insects, they do not undergo complete metamorphosis; they hatch from an egg into a nymph or "hopper" which undergoes five moults, becoming more similar to the adult insect at each developmental stage. The grasshopper hears through the tympanal organ which can be found in the first segment of the abdomen attached to the thorax; while its sense of vision is in the compound eyes, the change in light intensity is perceived in the simple eyes (ocelli). At high population densities and under certain environmental conditions, some grasshopper species can change color and behavior and form swarms. Under these circumstances, they are known as locusts. 18 | 19 | Grasshoppers are plant-eaters, with a few species at times becoming serious pests of cereals, vegetables and pasture, especially when they swarm in the millions as locusts and destroy crops over wide areas. They protect themselves from predators by camouflage; when detected, many species attempt to startle the predator with a brilliantly coloured wing flash while jumping and (if adult) launching themselves into the air, usually flying for only a short distance. Other species such as the rainbow grasshopper have warning coloration which deters predators. Grasshoppers are affected by parasites and various diseases, and many predatory creatures feed on both nymphs and adults. The eggs are subject to attack by parasitoids and predators. Grasshoppers are diurnal insects—meaning, they are most active during the day time. 20 | 21 | Grasshoppers have had a long relationship with humans. Swarms of locusts can have devastating effects and cause famine, having done so since Biblical times. Even in smaller numbers, the insects can be serious pests. They are used as food in countries such as Mexico and Indonesia. They feature in art, symbolism and literature. The study of grasshopper species is called acridology. 22 | 23 | ![](img/info.jpg) 24 | 25 | ## Reviews 26 | 27 | >Now I don’t know if other people think this, but I’m the only one that I know that is terrified of grasshoppers. Their body looks like a demon out of hell and the way they can swarm and decimate crops is terrifying. I can just imagine them going piranha mode on a live human. They make me shake and I can’t talk when I see them too close. 28 | 29 | >They're pretty heavy too, so when they go all Kamikaze into my legs or face it makes a pretty noticeable thud, which always totally catches me off guard. 30 | 31 | >I had to dissect one in biology. Hated every minute of it. 32 | 33 | >I concur! Years ago we were riding quads and unknowingly drove right into an area with thousands of them. They were all over us! I was freaking out and the friend I was riding with knew how afraid of them I was and couldn’t stop laughing to get us out of there. I almost killed him. -------------------------------------------------------------------------------- /audio/effect_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/audio/effect_1.mp3 -------------------------------------------------------------------------------- /audio/effect_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/audio/effect_2.mp3 -------------------------------------------------------------------------------- /background/background.js: -------------------------------------------------------------------------------- 1 | function print(msg) { 2 | // eslint-disable-next-line no-console 3 | console.info(msg) 4 | } 5 | 6 | function debouncer(func, delay) { 7 | if (typeof func !== `function`) { 8 | App.error(`Invalid debouncer function`) 9 | return 10 | } 11 | 12 | if (!delay) { 13 | App.error(`Invalid debouncer delay`) 14 | return 15 | } 16 | 17 | let timer 18 | let obj = {} 19 | 20 | function clear() { 21 | clearTimeout(timer) 22 | } 23 | 24 | function run(...args) { 25 | func(...args) 26 | } 27 | 28 | obj.call = (...args) => { 29 | clear() 30 | 31 | timer = setTimeout(() => { 32 | run(...args) 33 | }, delay) 34 | } 35 | 36 | obj.now = (...args) => { 37 | clear() 38 | run(...args) 39 | } 40 | 41 | obj.cancel = () => { 42 | clear() 43 | } 44 | 45 | return obj 46 | } 47 | 48 | function open_popup() { 49 | browser.browserAction.openPopup() 50 | } 51 | 52 | function set_item(what, value) { 53 | localStorage.setItem(`init_${what}`, value) 54 | } 55 | 56 | function open_popup_mode(mode) { 57 | set_item(`mode`, mode) 58 | open_popup() 59 | } 60 | 61 | function browser_command(num) { 62 | try { 63 | browser.runtime.sendMessage({action: `browser_command`, number: num}) 64 | } 65 | catch (err) { 66 | // Ignore 67 | } 68 | } 69 | 70 | function popup_command(num) { 71 | set_item(`popup_command`, num) 72 | open_popup() 73 | } 74 | 75 | browser.commands.onCommand.addListener((command) => { 76 | if (command === `popup_tabs`) { 77 | open_popup_mode(`tabs`) 78 | } 79 | else if (command === `popup_history`) { 80 | open_popup_mode(`history`) 81 | } 82 | else if (command === `popup_bookmarks`) { 83 | open_popup_mode(`bookmarks`) 84 | } 85 | else if (command === `popup_closed`) { 86 | open_popup_mode(`closed`) 87 | } 88 | else if (command.startsWith(`browser_command_`)) { 89 | let num = command.split(`_`).at(-1) 90 | 91 | if (num) { 92 | browser_command(num) 93 | } 94 | } 95 | else if (command.startsWith(`popup_command_`)) { 96 | let num = command.split(`_`).at(-1) 97 | 98 | if (num) { 99 | popup_command(num) 100 | } 101 | } 102 | }) 103 | 104 | browser.contextMenus.create({ 105 | id: `toggle_sidebar`, 106 | title: `Toggle Sidebar`, 107 | contexts: [`browser_action`], 108 | }) 109 | 110 | browser.contextMenus.create({ 111 | type: `separator`, 112 | id: `separator1`, 113 | contexts: [`browser_action`], 114 | }) 115 | 116 | browser.contextMenus.create({ 117 | id: `open_tabs`, 118 | title: `Open Tabs`, 119 | contexts: [`browser_action`], 120 | }) 121 | 122 | browser.contextMenus.create({ 123 | id: `open_history`, 124 | title: `Open History`, 125 | contexts: [`browser_action`], 126 | }) 127 | 128 | browser.contextMenus.create({ 129 | id: `open_bookmarks`, 130 | title: `Open Bookmarks`, 131 | contexts: [`browser_action`], 132 | }) 133 | 134 | browser.contextMenus.create({ 135 | id: `open_closed`, 136 | title: `Open Closed`, 137 | contexts: [`browser_action`], 138 | }) 139 | 140 | browser.contextMenus.onClicked.addListener((info, tab) => { 141 | let id = info.menuItemId 142 | 143 | if (id === `toggle_sidebar`) { 144 | browser.sidebarAction.toggle() 145 | } 146 | else if (id === `open_tabs`) { 147 | open_popup_mode(`tabs`) 148 | } 149 | else if (id === `open_history`) { 150 | open_popup_mode(`history`) 151 | } 152 | else if (id === `open_bookmarks`) { 153 | open_popup_mode(`bookmarks`) 154 | } 155 | else if (id === `open_closed`) { 156 | open_popup_mode(`closed`) 157 | } 158 | }) -------------------------------------------------------------------------------- /background/bookmarks_server.js: -------------------------------------------------------------------------------- 1 | // Here bookmarks are cached for fast retrieval on the sidebar or popup 2 | // On bookmark modifications the cache is updated to refresh the data 3 | // On refresh the bookmarks are sent to the applications 4 | // Updates work with a 1 second bouncer to avoid many updates in a short time 5 | // The applications store this data in their own cache arrays that they get from here 6 | // This is a persistent background script and should be always accesible 7 | // The idea of this is to make bookmark retrieval much faster when many bookmarks exist 8 | // Since the native getTree function is currently slow 9 | 10 | let bookmarks_active = false 11 | let bookmark_items = [] 12 | let bookmark_folders = [] 13 | let bookmark_debouncer 14 | 15 | browser.runtime.onMessage.addListener((request, sender, respond) => { 16 | if (request.action === `send_bookmarks`) { 17 | if (bookmarks_active) { 18 | send_bookmarks() 19 | } 20 | } 21 | }) 22 | 23 | browser.permissions.onAdded.addListener(async (obj) => { 24 | if (obj.permissions.includes(`bookmarks`)) { 25 | print(`BG: Bookmarks permission granted`) 26 | 27 | if (!bookmarks_active) { 28 | await start_bookmarks(false) 29 | await refresh_bookmarks(false) 30 | send_bookmarks(true) 31 | } 32 | } 33 | }) 34 | 35 | async function refresh_bookmarks(send = true) { 36 | let items = [] 37 | let folders = [] 38 | let nodes = await browser.bookmarks.getTree() 39 | 40 | function traverse(bookmarks) { 41 | for (let bookmark of bookmarks) { 42 | let title = bookmark.title 43 | 44 | if (title) { 45 | items.push(bookmark) 46 | } 47 | 48 | if (bookmark.type === `folder`) { 49 | if (title) { 50 | folders.push(bookmark) 51 | } 52 | 53 | if (bookmark.children) { 54 | traverse(bookmark.children) 55 | } 56 | } 57 | } 58 | } 59 | 60 | traverse(nodes) 61 | 62 | items.sort((a, b) => b.dateAdded - a.dateAdded) 63 | folders.sort((a, b) => b.dateGroupModified - a.dateGroupModified) 64 | 65 | bookmark_items = items 66 | bookmark_folders = folders 67 | 68 | if (send) { 69 | send_bookmarks() 70 | } 71 | 72 | print(`BG: Bookmarks refreshed: ${folders.length} folders and ${items.length} items`) 73 | } 74 | 75 | function send_bookmarks(show_mode = false) { 76 | try { 77 | browser.runtime.sendMessage({ 78 | action: `refresh_bookmarks`, 79 | items: bookmark_items, 80 | folders: bookmark_folders, 81 | show_mode, 82 | }) 83 | } 84 | catch (err) { 85 | // Ignore 86 | } 87 | } 88 | 89 | async function start_bookmarks(refresh = true) { 90 | let perm = await browser.permissions.contains({permissions: [`bookmarks`]}) 91 | 92 | if (!perm) { 93 | print(`BG: No bookmarks permission`) 94 | return 95 | } 96 | 97 | // eslint-disable-next-line no-undef 98 | bookmark_debouncer = debouncer(() => { 99 | refresh_bookmarks() 100 | }, 1000) 101 | 102 | browser.bookmarks.onCreated.addListener((id, info) => { 103 | bookmark_debouncer.call() 104 | }) 105 | 106 | browser.bookmarks.onRemoved.addListener((id, info) => { 107 | bookmark_debouncer.call() 108 | }) 109 | 110 | browser.bookmarks.onChanged.addListener((id, info) => { 111 | bookmark_debouncer.call() 112 | }) 113 | 114 | browser.bookmarks.onMoved.addListener((id, info) => { 115 | bookmark_debouncer.call() 116 | }) 117 | 118 | if (refresh) { 119 | bookmark_debouncer.call() 120 | } 121 | 122 | bookmarks_active = true 123 | } 124 | 125 | start_bookmarks() -------------------------------------------------------------------------------- /css/addlist.css: -------------------------------------------------------------------------------- 1 | .addlist_container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | gap: 1.35rem; 7 | background-color: var(--background_color); 8 | height: 100%; 9 | width: 100%; 10 | outline: none; 11 | border: none; 12 | } 13 | 14 | .addlist_control { 15 | display: flex; 16 | flex-direction: row; 17 | align-items: center; 18 | justify-content: center; 19 | padding: 0.5rem; 20 | background-color: var(--alt_color_0); 21 | min-width: var(--widget_width); 22 | box-sizing: border-box; 23 | gap: 0.8rem; 24 | } 25 | 26 | .addlist_top { 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | justify-content: center; 31 | gap: 1rem; 32 | width: 100%; 33 | } 34 | 35 | .addlist_title { 36 | display: flex; 37 | font-size: 1.1rem; 38 | flex-grow: 1; 39 | justify-content: center; 40 | align-items: center; 41 | flex-shrink: 0; 42 | } 43 | 44 | .addlist_text { 45 | font-size: 1rem; 46 | min-width: var(--widget_width); 47 | max-width: var(--widget_width); 48 | text-align: center; 49 | box-sizing: border-box; 50 | } 51 | 52 | .addlist_textarea { 53 | min-width: var(--widget_width); 54 | max-width: var(--widget_width); 55 | text-align: center; 56 | box-sizing: border-box; 57 | } 58 | 59 | .addlist_menu { 60 | display: flex; 61 | flex-direction: column; 62 | align-items: center; 63 | justify-content: center; 64 | gap: 0.5rem; 65 | } 66 | 67 | .addlist_checkbox_container { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | justify-content: center; 72 | gap: 0.5rem; 73 | } 74 | 75 | .addlist_checkbox { 76 | width: 1.07rem; 77 | height: 1.07rem; 78 | } 79 | 80 | .addlist_buttons { 81 | display: flex; 82 | flex-direction: row; 83 | align-items: center; 84 | justify-content: center; 85 | margin-top: 0.5rem; 86 | gap: 0.5rem; 87 | } 88 | 89 | .addlist_color_container { 90 | display: flex; 91 | flex-direction: column; 92 | align-items: center; 93 | justify-content: center; 94 | gap: 0.5rem; 95 | } -------------------------------------------------------------------------------- /css/menubutton.css: -------------------------------------------------------------------------------- 1 | .menubutton_container { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 0.5rem; 5 | min-width: var(--widget_width); 6 | } 7 | 8 | .menubutton { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: center; 13 | gap: 0.5rem; 14 | flex-grow: 1; 15 | border-radius: unset; 16 | } -------------------------------------------------------------------------------- /css/needcontext.css: -------------------------------------------------------------------------------- 1 | #needcontext-main { 2 | background-color: var(--overlay_color) !important; 3 | scrollbar-color: var(--alt_color_2) var(--alt_color_1) !important; 4 | } 5 | 6 | #needcontext-container { 7 | background-color: var(--background_color) !important; 8 | color: var(--text_color) !important; 9 | border-color: var(--alt_color_2) !important; 10 | font-size: var(--font_size) !important; 11 | font-family: var(--font) !important; 12 | } 13 | 14 | .no_rounded #needcontext-container { 15 | border-radius: 0 !important; 16 | } 17 | 18 | .needcontext-item-selected { 19 | background-color: var(--alt_color_1) !important; 20 | } 21 | 22 | .needcontext-item-selected:active { 23 | background-color: var(--alt_color_1) !important; 24 | } 25 | 26 | #needcontext-title { 27 | background-color: var(--alt_color_0) !important; 28 | color: var(--text_color) !important; 29 | } -------------------------------------------------------------------------------- /css/overrides.css: -------------------------------------------------------------------------------- 1 | /* You can override CSS here */ -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | export default [ 5 | pluginJs.configs.recommended, 6 | 7 | {languageOptions: {globals: globals.browser}}, 8 | 9 | { 10 | rules: { 11 | "no-unused-vars": "off", 12 | "indent": ["error", 2], 13 | "linebreak-style": ["error", "unix"], 14 | "quotes": ["error", "backtick"], 15 | "no-console": "error", 16 | "no-multi-spaces": "error", 17 | "no-multiple-empty-lines": ["error", {"max": 1}], 18 | "object-shorthand": ["error", "always"], 19 | "semi": ["error", "never"], 20 | "no-else-return": "error", 21 | "padded-blocks": ["error", "never"], 22 | "no-lonely-if": "error", 23 | "eqeqeq": ["error", "always"], 24 | "curly": ["error", "all"], 25 | "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], 26 | "no-var": "error", 27 | "arrow-spacing": ["error", {"before": true, "after": true}], 28 | "space-in-parens": ["error", "never"], 29 | "object-curly-spacing": ["error", "never"], 30 | "prefer-object-spread": "error", 31 | "no-eval": "error", 32 | "no-useless-escape": "error", 33 | "default-param-last": "error", 34 | "dot-notation": "error", 35 | "keyword-spacing": "error", 36 | "space-infix-ops": "error", 37 | "comma-spacing": "error", 38 | "comma-dangle": ["error", "always-multiline"], 39 | "no-extra-parens": ["error", "all", { 40 | "nestedBinaryExpressions": false, 41 | "enforceForArrowConditionals": false, 42 | "returnAssign": false 43 | }], 44 | "padding-line-between-statements": [ 45 | "error", 46 | { "blankLine": "always", "prev": "block-like", "next": "*" } 47 | ], 48 | "func-call-spacing": ["error", "never"], 49 | "space-before-function-paren": ["error", { 50 | "anonymous": "never", 51 | "named": "never", 52 | "asyncArrow": "always" 53 | }], 54 | "no-mixed-operators": [ 55 | "error", 56 | { 57 | "groups": [ 58 | ["&&", "||"], 59 | ["&&", "==="], 60 | ["&&", "!=="], 61 | ["&&", "=="], 62 | ["&&", "!="], 63 | ["&&", ">"], 64 | ["&&", ">="], 65 | ["&&", "<"], 66 | ["&&", "<="], 67 | ["||", "==="], 68 | ["||", "!=="], 69 | ["||", "=="], 70 | ["||", "!="], 71 | ["||", ">"], 72 | ["||", ">="], 73 | ["||", "<"], 74 | ["||", "<="], 75 | ["==", "!=", "===", "!=="], 76 | ["in", "instanceof"], 77 | ], 78 | "allowSamePrecedence": true 79 | } 80 | ] 81 | }, 82 | languageOptions: { 83 | globals: { 84 | App: "writable", 85 | DOM: "writable", 86 | Addlist: "writable", 87 | NiceGesture: "writable", 88 | NeedContext: "writable", 89 | Menubutton: "writable", 90 | ColorLib: "writable", 91 | AColorPicker: "writable", 92 | dateFormat: "writable", 93 | jdenticon: "writable", 94 | browser: "writable", 95 | } 96 | } 97 | } 98 | ] -------------------------------------------------------------------------------- /fonts/NovaSquare-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/fonts/NovaSquare-Regular.ttf -------------------------------------------------------------------------------- /header/base.js: -------------------------------------------------------------------------------- 1 | const App = {} 2 | App.ls_state = `colorscreen_state_v1` 3 | App.colorlib = ColorLib() 4 | App.default_color = `#252933` 5 | App.visible = false 6 | App.timeout_delay = 250 7 | App.image_state = 0 8 | 9 | App.init = () => { 10 | App.setup_events() 11 | App.setup_state() 12 | } 13 | 14 | App.setup_events = () => { 15 | DOM.ev(DOM.el(`#fullscreen_button`), `click`, () => { 16 | if (App.disabled()) { 17 | return 18 | } 19 | 20 | App.toggle_fullscreen() 21 | }) 22 | 23 | DOM.ev(DOM.el(`#random_button`), `click`, () => { 24 | if (App.disabled()) { 25 | return 26 | } 27 | 28 | App.set_color(App.colorlib.get_random_hex()) 29 | }) 30 | 31 | DOM.ev(DOM.el(`#darker_button`), `click`, () => { 32 | if (App.disabled()) { 33 | return 34 | } 35 | 36 | App.set_color(App.colorlib.get_darker(App.get_reference(), 0.15)) 37 | }) 38 | 39 | DOM.ev(DOM.el(`#lighter_button`), `click`, () => { 40 | if (App.disabled()) { 41 | return 42 | } 43 | 44 | App.set_color(App.colorlib.get_lighter(App.get_reference(), 0.15)) 45 | }) 46 | 47 | DOM.ev(DOM.el(`#exact_button`), `click`, () => { 48 | if (App.disabled()) { 49 | return 50 | } 51 | 52 | App.get_exact_color() 53 | }) 54 | 55 | DOM.ev(DOM.el(`#red_button`), `click`, () => { 56 | if (App.disabled()) { 57 | return 58 | } 59 | 60 | App.set_color(`rgb(255, 0, 0)`) 61 | }) 62 | 63 | DOM.ev(DOM.el(`#green_button`), `click`, () => { 64 | if (App.disabled()) { 65 | return 66 | } 67 | 68 | App.set_color(`rgb(0, 255, 0)`) 69 | }) 70 | 71 | DOM.ev(DOM.el(`#blue_button`), `click`, () => { 72 | if (App.disabled()) { 73 | return 74 | } 75 | 76 | App.set_color(`rgb(0, 0, 255)`) 77 | }) 78 | 79 | DOM.ev(DOM.el(`#black_button`), `click`, () => { 80 | if (App.disabled()) { 81 | return 82 | } 83 | 84 | App.set_color(`rgb(0, 0, 0)`) 85 | }) 86 | 87 | DOM.ev(DOM.el(`#white_button`), `click`, () => { 88 | if (App.disabled()) { 89 | return 90 | } 91 | 92 | App.set_color(`rgb(255, 255, 255)`) 93 | }) 94 | 95 | DOM.ev(DOM.el(`#buttons`), `mouseenter`, () => { 96 | App.show() 97 | }) 98 | 99 | DOM.ev(DOM.el(`#buttons`), `mouseleave`, () => { 100 | App.hide() 101 | }) 102 | 103 | DOM.ev(DOM.el(`#image`), `click`, (e) => { 104 | App.image_click(e.target) 105 | }) 106 | 107 | DOM.ev(DOM.el(`#lock`), `click`, (e) => { 108 | App.toggle_lock(e.target) 109 | }) 110 | 111 | DOM.ev(DOM.el(`#sticky`), `click`, (e) => { 112 | App.toggle_sticky(e.target) 113 | }) 114 | } 115 | 116 | App.setup_state = () => { 117 | App.state = App.get_local_storage(App.ls_state) || {} 118 | 119 | if (App.state.locked !== undefined) { 120 | App.locked = App.state.locked 121 | } 122 | else { 123 | App.locked = false 124 | } 125 | 126 | if (App.state.sticky !== undefined) { 127 | App.sticky = App.state.sticky 128 | } 129 | else { 130 | App.sticky = true 131 | } 132 | 133 | if (App.locked) { 134 | DOM.el(`#lock`).checked = true 135 | } 136 | 137 | if (App.sticky) { 138 | DOM.el(`#sticky`).checked = true 139 | App.show() 140 | } 141 | 142 | App.set_color(App.state.color || App.default_color) 143 | } 144 | 145 | App.set_color = (color) => { 146 | App.set_reference(color) 147 | let color_1 = App.get_reference() 148 | let color_2 = App.colorlib.get_lighter_or_darker(color_1, 0.66) 149 | document.documentElement.style.setProperty(`--color_1`, color_1) 150 | document.documentElement.style.setProperty(`--color_2`, color_2) 151 | DOM.el(`#color_info`).textContent = color_1 152 | App.state.color = color_1 153 | App.save_state() 154 | } 155 | 156 | App.toggle_fullscreen = () => { 157 | if (document.fullscreenElement) { 158 | document.exitFullscreen() 159 | } 160 | else { 161 | document.documentElement.requestFullscreen() 162 | } 163 | } 164 | 165 | App.set_reference = (color) => { 166 | DOM.el(`#reference`).style.color = color 167 | } 168 | 169 | App.get_reference = () => { 170 | return window.getComputedStyle(DOM.el(`#reference`)).color 171 | } 172 | 173 | App.get_exact_color = () => { 174 | let input = prompt(`Enter color name, rgb, or hex`) 175 | 176 | if (!input) { 177 | return 178 | } 179 | 180 | App.set_color(input) 181 | } 182 | 183 | App.get_local_storage = (ls_name) => { 184 | let obj 185 | 186 | if (localStorage[ls_name]) { 187 | try { 188 | obj = JSON.parse(localStorage.getItem(ls_name)) 189 | } 190 | catch (err) { 191 | localStorage.removeItem(ls_name) 192 | obj = null 193 | } 194 | } 195 | else { 196 | obj = null 197 | } 198 | 199 | return obj 200 | } 201 | 202 | App.save_local_storage = (ls_name, obj) => { 203 | localStorage.setItem(ls_name, JSON.stringify(obj)) 204 | } 205 | 206 | App.save_state = () => { 207 | App.save_local_storage(App.ls_state, App.state) 208 | } 209 | 210 | App.toggle_lock = (checkbox) => { 211 | App.locked = checkbox.checked 212 | App.state.locked = App.locked 213 | App.save_state() 214 | } 215 | 216 | App.toggle_sticky = (checkbox) => { 217 | App.sticky = checkbox.checked 218 | 219 | if (App.sticky) { 220 | App.show(true) 221 | } 222 | else { 223 | App.hide() 224 | } 225 | 226 | App.state.sticky = App.sticky 227 | App.save_state() 228 | } 229 | 230 | App.disabled = () => { 231 | return !App.visible || App.locked 232 | } 233 | 234 | App.show = (instant = false) => { 235 | clearTimeout(App.show_timeout) 236 | let delay 237 | 238 | if (instant) { 239 | delay = 0 240 | } 241 | else { 242 | delay = App.timeout_delay 243 | } 244 | 245 | App.show_timeout = setTimeout(() => { 246 | DOM.el(`#buttons`).classList.add(`visible`) 247 | App.visible = true 248 | }, delay) 249 | } 250 | 251 | App.hide = () => { 252 | if (App.sticky) { 253 | return 254 | } 255 | 256 | clearTimeout(App.show_timeout) 257 | DOM.el(`#buttons`).classList.remove(`visible`) 258 | App.visible = false 259 | } 260 | 261 | App.image_click = (image) => { 262 | image.classList.remove(`invert`) 263 | image.classList.remove(`rotate_1`) 264 | image.classList.remove(`rotate_2`) 265 | image.classList.remove(`rotate_3`) 266 | 267 | App.image_state += 1 268 | 269 | if (App.image_state === 1) { 270 | image.classList.add(`invert`) 271 | } 272 | else if (App.image_state === 2) { 273 | image.classList.add(`rotate_1`) 274 | } 275 | else if (App.image_state === 3) { 276 | image.classList.add(`rotate_2`) 277 | } 278 | else if (App.image_state === 4) { 279 | image.classList.add(`rotate_3`) 280 | } 281 | else if (App.image_state === 5) { 282 | App.image_state = 0 283 | } 284 | } -------------------------------------------------------------------------------- /header/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Header 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 |
19 |
R
20 |
G
21 |
B
22 |
23 | 24 |
25 |
Black
26 |
White
27 |
28 | 29 |
Random
30 |
Exact
31 |
Darker
32 |
Lighter
33 |
Fullscreen
34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
Lock
45 | 46 |
47 | 48 |
49 |
Sticky
50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /header/init.js: -------------------------------------------------------------------------------- 1 | App.init() -------------------------------------------------------------------------------- /header/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color_1: black; 3 | --color_2: white; 4 | } 5 | 6 | body, html { 7 | width: 100vw; 8 | height: 100vh; 9 | margin: 0; 10 | padding: 0; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | font-family: sans-serif; 15 | font-size: 16px; 16 | } 17 | 18 | #main { 19 | background-color: var(--color_1); 20 | width: 100%; 21 | height: 100%; 22 | overflow: hidden; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | #buttons { 29 | display: flex; 30 | flex-direction: column; 31 | gap: 0.8rem; 32 | opacity: 0; 33 | transition: opacity 250ms; 34 | color: var(--color_2); 35 | } 36 | 37 | .button { 38 | font-size: 1.4rem; 39 | border: 1px solid currentColor; 40 | cursor: pointer; 41 | text-align: center; 42 | padding-left: 1.2rem; 43 | padding-right: 1.2rem; 44 | padding-top: 0.4rem; 45 | padding-bottom: 0.4rem; 46 | user-select: none; 47 | flex-basis: 100%; 48 | } 49 | 50 | .button:hover { 51 | outline: 1px solid currentColor; 52 | } 53 | 54 | .visible { 55 | opacity: 1 !important; 56 | pointer-events: all !important; 57 | } 58 | 59 | #color_info { 60 | font-size: 1rem; 61 | text-align: center; 62 | } 63 | 64 | .flex_row { 65 | display: flex; 66 | flex-direction: row; 67 | gap: 0.5rem; 68 | } 69 | 70 | #image { 71 | width: 180px; 72 | padding-bottom: 0.5rem; 73 | cursor: pointer; 74 | align-self: center; 75 | } 76 | 77 | .corner { 78 | display: flex; 79 | flex-direction: row; 80 | align-items: center; 81 | justify-content: center; 82 | position: fixed; 83 | color: var(--color_2); 84 | user-select: none; 85 | gap: 1rem; 86 | font-size: 1.2rem; 87 | } 88 | 89 | #top_left { 90 | top: 1rem; 91 | left: 1rem; 92 | } 93 | 94 | #top_right { 95 | top: 1rem; 96 | right: 1rem; 97 | } 98 | 99 | .toggle { 100 | display: flex; 101 | flex-direction: row; 102 | align-items: center; 103 | justify-content: center; 104 | gap: 0.38rem; 105 | } 106 | 107 | .invert { 108 | filter: invert(1); 109 | } 110 | 111 | .rotate_1 { 112 | filter: hue-rotate(90deg); 113 | } 114 | 115 | .rotate_2 { 116 | filter: hue-rotate(180deg); 117 | } 118 | 119 | .rotate_3 { 120 | filter: hue-rotate(270deg); 121 | } 122 | 123 | #frame { 124 | display: none; 125 | position: fixed; 126 | top: 0; 127 | left: 0; 128 | width: 100%; 129 | height: 100%; 130 | border: none; 131 | } 132 | 133 | a.linkbutton:hover, 134 | a.linkbutton:link, 135 | a.linkbutton:visited, 136 | a.linkbutton:active 137 | { 138 | color: var(--color_2); 139 | cursor: pointer; 140 | text-decoration: underline; 141 | transition: text-shadow 170ms; 142 | } 143 | 144 | a.linkbutton:hover { 145 | text-shadow: 0 0 0.18rem currentColor; 146 | } 147 | 148 | #notes { 149 | color: var(--color_2); 150 | background-color: var(--color_1); 151 | border: 1px solid var(--color_2); 152 | text-align: center; 153 | font-size: 1rem; 154 | outline: 0; 155 | } -------------------------------------------------------------------------------- /img/backgrounds/background_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_1.jpg -------------------------------------------------------------------------------- /img/backgrounds/background_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_2.jpg -------------------------------------------------------------------------------- /img/backgrounds/background_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_3.jpg -------------------------------------------------------------------------------- /img/backgrounds/background_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_4.jpg -------------------------------------------------------------------------------- /img/backgrounds/background_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_5.jpg -------------------------------------------------------------------------------- /img/backgrounds/background_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/backgrounds/background_6.jpg -------------------------------------------------------------------------------- /img/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/favicon.jpg -------------------------------------------------------------------------------- /img/folder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/folder.jpg -------------------------------------------------------------------------------- /img/grasshopper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/grasshopper.png -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/icon128.png -------------------------------------------------------------------------------- /img/info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/info.jpg -------------------------------------------------------------------------------- /img/leaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/leaf.jpg -------------------------------------------------------------------------------- /img/lock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprops/grasshopper/0338840eb134cf9f4bbffadf7fa0a27c6675e53f/img/lock.jpg -------------------------------------------------------------------------------- /js/init.js: -------------------------------------------------------------------------------- 1 | App.init = async () => { 2 | let win = await browser.windows.getCurrent({populate: false}) 3 | 4 | App.window_id = win.id 5 | App.manifest = browser.runtime.getManifest() 6 | App.header_url = browser.runtime.getURL(`header/index.html`) 7 | App.extension_id = browser.runtime.id 8 | 9 | App.print_intro() 10 | App.build_settings() 11 | 12 | await App.stor_compat_check() 13 | await App.stor_get_settings() 14 | await App.stor_get_command_history() 15 | await App.stor_get_tag_history() 16 | await App.stor_get_title_history() 17 | await App.stor_get_icon_history() 18 | await App.stor_get_first_time() 19 | await App.stor_get_notes() 20 | await App.stor_get_bookmark_folder_picks() 21 | await App.stor_get_history_picks() 22 | await App.check_init_mode() 23 | 24 | App.make_tab_box_modes() 25 | App.setup_commands() 26 | App.setup_tabs() 27 | App.setup_closed() 28 | App.setup_settings() 29 | App.setup_tab_box() 30 | App.setup_active_trace() 31 | App.setup_keyboard() 32 | App.setup_window() 33 | App.setup_gestures() 34 | App.setup_filter() 35 | App.setup_modes() 36 | App.setup_scroll() 37 | App.setup_items() 38 | App.setup_theme() 39 | App.setup_drag() 40 | App.setup_favorites() 41 | App.setup_playing() 42 | App.setup_messages() 43 | App.setup_mouse() 44 | App.do_apply_theme() 45 | App.setup_pinline() 46 | App.setup_footer() 47 | App.setup_recent_tabs() 48 | App.setup_context() 49 | App.build_shell() 50 | App.mouse_inside_check() 51 | App.resolve_icons() 52 | 53 | await App.clear_show() 54 | 55 | App.init_tab_box() 56 | App.make_window_visible() 57 | App.check_first_time() 58 | App.start_clock() 59 | App.start_main_title() 60 | App.check_init_commands() 61 | App.start_signal_intervals() 62 | 63 | App.start_date = App.now() 64 | } 65 | 66 | App.init() -------------------------------------------------------------------------------- /js/libs/dateformat.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /* 4 | * Date Format 1.2.3 5 | * (c) 2007-2009 Steven Levithan 6 | * MIT license 7 | * 8 | * Includes enhancements by Scott Trenda 9 | * and Kris Kowal 10 | * 11 | * Accepts a date, a mask, or a date and a mask. 12 | * Returns a formatted version of the given date. 13 | * The date defaults to the current date/time. 14 | * The mask defaults to dateFormat.masks.default. 15 | * 16 | * Modification: Allow to add escaped text [like this] 17 | */ 18 | 19 | var dateFormat = function () { 20 | var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|\[.*?\]|"[^"]*"|'[^']*'/g, 21 | timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, 22 | timezoneClip = /[^-+\dA-Z]/g, 23 | pad = function (val, len) { 24 | val = String(val); 25 | len = len || 2; 26 | while (val.length < len) val = "0" + val; 27 | return val; 28 | }; 29 | 30 | // Regexes and supporting functions are cached through closure 31 | return function (date, mask, utc) { 32 | var dF = dateFormat; 33 | 34 | // You can't provide utc if you skip other args (use the "UTC:" mask prefix) 35 | if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { 36 | mask = date; 37 | date = undefined; 38 | } 39 | 40 | // Passing date through Date applies Date.parse, if necessary 41 | date = date ? new Date(date) : new Date; 42 | 43 | var flags = { 44 | d: date.getDate(), 45 | dd: pad(date.getDate()), 46 | ddd: dF.i18n.dayNames[date.getDay()], 47 | dddd: dF.i18n.dayNames[date.getDay() + 7], 48 | m: date.getMonth() + 1, 49 | mm: pad(date.getMonth() + 1), 50 | mmm: dF.i18n.monthNames[date.getMonth()], 51 | mmmm: dF.i18n.monthNames[date.getMonth() + 12], 52 | yy: String(date.getFullYear()).slice(2), 53 | yyyy: date.getFullYear(), 54 | h: date.getHours() % 12 || 12, 55 | hh: pad(date.getHours() % 12 || 12), 56 | H: date.getHours(), 57 | HH: pad(date.getHours()), 58 | M: date.getMinutes(), 59 | MM: pad(date.getMinutes()), 60 | s: date.getSeconds(), 61 | ss: pad(date.getSeconds()), 62 | l: pad(date.getMilliseconds(), 3), 63 | L: pad(Math.round(date.getMilliseconds() / 10)), 64 | t: date.getHours() < 12 ? "a" : "p", 65 | tt: date.getHours() < 12 ? "am" : "pm", 66 | T: date.getHours() < 12 ? "A" : "P", 67 | TT: date.getHours() < 12 ? "AM" : "PM", 68 | Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), 69 | o: (utc ? 0 : date.getTimezoneOffset()), 70 | S: ["th", "st", "nd", "rd"][date.getDate() % 10 > 3 ? 0 : (date.getDate() % 100 - date.getDate() % 10 != 10) * date.getDate() % 10] 71 | }; 72 | 73 | return mask.replace(token, function ($0) { 74 | return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); 75 | }); 76 | }; 77 | }(); 78 | 79 | // Some necessary i18n settings 80 | dateFormat.masks = { 81 | "default": "ddd mmm dd yyyy HH:MM:ss", 82 | shortDate: "m/d/yy", 83 | mediumDate: "mmm d, yyyy", 84 | longDate: "mmmm d, yyyy", 85 | fullDate: "dddd, mmmm d, yyyy", 86 | shortTime: "h:MM TT", 87 | mediumTime: "h:MM:ss TT", 88 | longTime: "h:MM:ss TT Z", 89 | isoDate: "yyyy-mm-dd", 90 | isoTime: "HH:MM:ss", 91 | isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", 92 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" 93 | }; 94 | 95 | dateFormat.i18n = { 96 | dayNames: [ 97 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", 98 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 99 | ], 100 | monthNames: [ 101 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 102 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" 103 | ] 104 | }; -------------------------------------------------------------------------------- /js/libs/dom.js: -------------------------------------------------------------------------------- 1 | // DOM v2.0.0 2 | const DOM = {} 3 | DOM.dataset_obj = {} 4 | DOM.dataset_id = 0 5 | 6 | // Select a single element 7 | DOM.el = (query, root = document) => { 8 | if (typeof root === `string`) { 9 | root = DOM.el(root) 10 | } 11 | 12 | return root.querySelector(query) 13 | } 14 | 15 | // Select an array of elements 16 | DOM.els = (query, root = document) => { 17 | if (typeof root === `string`) { 18 | root = DOM.el(root) 19 | } 20 | 21 | return Array.from(root.querySelectorAll(query)) 22 | } 23 | 24 | // Select a single element or self 25 | DOM.el_or_self = (query, root = document) => { 26 | root = DOM.element(root) 27 | let el = root.querySelector(query) 28 | 29 | if (!el) { 30 | if (root.classList.contains(DOM.clean_dot(query))) { 31 | el = root 32 | } 33 | } 34 | 35 | return el 36 | } 37 | 38 | // Select an array of elements or self 39 | DOM.els_or_self = (query, root = document) => { 40 | root = DOM.element(root) 41 | let els = Array.from(root.querySelectorAll(query)) 42 | 43 | if (els.length === 0) { 44 | if (root.classList.contains(DOM.clean_dot(query))) { 45 | els = [root] 46 | } 47 | } 48 | 49 | return els 50 | } 51 | 52 | // Clone element 53 | DOM.clone = (el) => { 54 | el = DOM.element(el) 55 | return el.cloneNode(true) 56 | } 57 | 58 | // Clone element children 59 | DOM.clone_children = (el) => { 60 | el = DOM.element(el) 61 | let children = Array.from(el.children) 62 | let items = [] 63 | 64 | for (let c of children) { 65 | items.push(DOM.clone(c)) 66 | } 67 | 68 | return items 69 | } 70 | 71 | // Data set manager 72 | DOM.dataset = (el, value, setvalue) => { 73 | el = DOM.element(el) 74 | 75 | if (!el) { 76 | return 77 | } 78 | 79 | let id = el.dataset.dataset_id 80 | 81 | if (!id) { 82 | id = DOM.dataset_id 83 | DOM.dataset_id += 1 84 | el.dataset.dataset_id = id 85 | DOM.dataset_obj[id] = {} 86 | } 87 | 88 | if (setvalue !== undefined) { 89 | DOM.dataset_obj[id][value] = setvalue 90 | } 91 | else { 92 | return DOM.dataset_obj[id][value] 93 | } 94 | } 95 | 96 | // Create an html element 97 | DOM.create = (type, classes = ``, id = ``) => { 98 | let el = document.createElement(type) 99 | 100 | if (classes) { 101 | let classlist = classes.split(` `).filter(x => x !== ``) 102 | 103 | for (let cls of classlist) { 104 | el.classList.add(cls) 105 | } 106 | } 107 | 108 | if (id) { 109 | el.id = id 110 | } 111 | 112 | return el 113 | } 114 | 115 | // Add an event listener 116 | DOM.ev = (el, event, callback, extra) => { 117 | el = DOM.element(el) 118 | el.addEventListener(event, callback, extra) 119 | } 120 | 121 | // Add multiple event listeners 122 | DOM.evs = (el, events, callback, extra) => { 123 | el = DOM.element(el) 124 | 125 | for (let event of events) { 126 | el.addEventListener(event, callback, extra) 127 | } 128 | } 129 | 130 | // Get item index 131 | DOM.index = (el) => { 132 | el = DOM.element(el) 133 | return Array.from(el.parentNode.children).indexOf(el) 134 | } 135 | 136 | // Check if it contains any of these classes 137 | DOM.class = (el, classes) => { 138 | el = DOM.element(el) 139 | 140 | for (let cls of classes) { 141 | if (el.classList.contains(DOM.clean_dot(cls))) { 142 | return true 143 | } 144 | } 145 | 146 | return false 147 | } 148 | 149 | // Check if it contains any of these queries up the hierarchy 150 | DOM.parent = (el, queries) => { 151 | el = DOM.element(el) 152 | 153 | for (let query of queries) { 154 | let parent = el.closest(query) 155 | 156 | if (parent) { 157 | return parent 158 | } 159 | } 160 | 161 | return undefined 162 | } 163 | 164 | // Remove dot from classes 165 | DOM.clean_dot = (query) => { 166 | return query.replace(`.`, ``) 167 | } 168 | 169 | // Show an element 170 | DOM.show = (el, num = 1) => { 171 | el = DOM.element(el) 172 | el.classList.remove(DOM.hidden(num)) 173 | } 174 | 175 | // Hide an element 176 | DOM.hide = (el, num = 1) => { 177 | el = DOM.element(el) 178 | el.classList.add(DOM.hidden(num)) 179 | } 180 | 181 | // Get hidden class 182 | DOM.hidden = (num = 1) => { 183 | let cls = `hidden` 184 | 185 | if (num > 1) { 186 | cls += `_${num}` 187 | } 188 | 189 | return cls 190 | } 191 | 192 | // Check if an element is hidden 193 | DOM.is_hidden = (el, num = 1) => { 194 | return el.classList.contains(DOM.hidden(num)) 195 | } 196 | 197 | // Resolve the element 198 | DOM.element = (el) => { 199 | if (typeof el === `string`) { 200 | return DOM.el(el) 201 | } 202 | 203 | return el 204 | } -------------------------------------------------------------------------------- /js/libs/nicegesture.js: -------------------------------------------------------------------------------- 1 | const NiceGesture = {} 2 | NiceGesture.enabled = true 3 | NiceGesture.button = 1 4 | NiceGesture.threshold = 10 5 | 6 | NiceGesture.start = (container, actions) => { 7 | container.addEventListener(`mousedown`, (e) => { 8 | if (!NiceGesture.enabled) { 9 | return 10 | } 11 | 12 | NiceGesture.reset() 13 | 14 | if (e.button === NiceGesture.button) { 15 | NiceGesture.active = true 16 | NiceGesture.first_y = e.clientY 17 | NiceGesture.first_x = e.clientX 18 | } 19 | }) 20 | 21 | container.addEventListener(`mousemove`, (e) => { 22 | if (!NiceGesture.enabled || !NiceGesture.active || NiceGesture.coords.length > 1000) { 23 | return 24 | } 25 | 26 | let coord = { 27 | x: e.clientX, 28 | y: e.clientY, 29 | } 30 | 31 | NiceGesture.coords.push(coord) 32 | }) 33 | 34 | container.addEventListener(`mouseup`, (e) => { 35 | if (e.button !== NiceGesture.button) { 36 | return 37 | } 38 | 39 | if (!NiceGesture.enabled || !NiceGesture.active) { 40 | actions.default(e) 41 | return 42 | } 43 | 44 | NiceGesture.check(e, actions) 45 | }) 46 | 47 | NiceGesture.reset() 48 | } 49 | 50 | NiceGesture.reset = () => { 51 | NiceGesture.active = false 52 | NiceGesture.first_y = 0 53 | NiceGesture.first_x = 0 54 | NiceGesture.last_y = 0 55 | NiceGesture.last_x = 0 56 | NiceGesture.coords = [] 57 | } 58 | 59 | NiceGesture.check = (e, actions) => { 60 | NiceGesture.last_x = e.clientX 61 | NiceGesture.last_y = e.clientY 62 | 63 | if (NiceGesture.action(e, actions)) { 64 | e.preventDefault() 65 | } 66 | else if (actions.default) { 67 | actions.default(e) 68 | } 69 | 70 | NiceGesture.reset(actions) 71 | } 72 | 73 | NiceGesture.action = (e, actions) => { 74 | if (NiceGesture.coords.length === 0) { 75 | return false 76 | } 77 | 78 | let ys = NiceGesture.coords.map(c => c.y) 79 | let max_y = Math.max(...ys) 80 | let min_y = Math.min(...ys) 81 | 82 | let xs = NiceGesture.coords.map(c => c.x) 83 | let max_x = Math.max(...xs) 84 | let min_x = Math.min(...xs) 85 | 86 | if (Math.abs(max_y - min_y) < NiceGesture.threshold && 87 | Math.abs(max_x - min_x) < NiceGesture.threshold) { 88 | return false 89 | } 90 | 91 | let gt = NiceGesture.threshold 92 | let path_y, path_x 93 | 94 | if (min_y < NiceGesture.first_y - gt) { 95 | path_y = `up` 96 | } 97 | else if (max_y > NiceGesture.first_y + gt) { 98 | path_y = `down` 99 | } 100 | 101 | if (path_y === `up`) { 102 | if ((Math.abs(NiceGesture.last_y - min_y) > gt) || (max_y > NiceGesture.first_y + gt)) { 103 | path_y = `up_and_down` 104 | } 105 | } 106 | 107 | if (path_y === `down`) { 108 | if ((Math.abs(NiceGesture.last_y - max_y) > gt) || (min_y < NiceGesture.first_y - gt)) { 109 | path_y = `up_and_down` 110 | } 111 | } 112 | 113 | if (max_x > NiceGesture.first_x + gt) { 114 | path_x = `right` 115 | } 116 | else if (min_x < NiceGesture.first_x - gt) { 117 | path_x = `left` 118 | } 119 | 120 | if (path_x === `left`) { 121 | if ((Math.abs(NiceGesture.last_x - min_x) > gt) || (max_x > NiceGesture.first_x + gt)) { 122 | path_x = `left_and_right` 123 | } 124 | } 125 | 126 | if (path_x === `right`) { 127 | if ((Math.abs(NiceGesture.last_x - max_x) > gt) || (min_x < NiceGesture.first_x - gt)) { 128 | path_x = `left_and_right` 129 | } 130 | } 131 | 132 | let path 133 | 134 | if (max_y - min_y > max_x - min_x) { 135 | path = path_y 136 | } 137 | else { 138 | path = path_x 139 | } 140 | 141 | if (actions[path]) { 142 | actions[path](e) 143 | } 144 | 145 | return true 146 | } -------------------------------------------------------------------------------- /js/main/about.js: -------------------------------------------------------------------------------- 1 | App.start_about = () => { 2 | if (App.check_ready(`about`)) { 3 | return 4 | } 5 | 6 | App.create_window({ 7 | id: `about`, 8 | setup: () => { 9 | App.about_info_items = [ 10 | `Up, Down, and Enter keys navigate and pick items`, 11 | `Type to filter or search depending on mode`, 12 | `Cycle with Left and Right`, 13 | `Middle Click closes tabs (Configurable)`, 14 | `Esc does Step Back and closes windows`, 15 | `Ctrl + Click selects multiple items`, 16 | `Shift + Click selects an item range`, 17 | `Right Click on items shows the Item Menu`, 18 | `Delete closes selected tabs`, 19 | `Double click on empty space opens a new tab (Configurable)`, 20 | `Command palette commands take into account selected items and mode (Context Aware)`, 21 | `To filter by title start with title:`, 22 | `To filter by URL start with url:`, 23 | `To filter with regex start with re:`, 24 | `To filter with regex by title start with re_title:`, 25 | `To filter with regex by URL start with re_url:`, 26 | `To filter by color start with color:`, 27 | `To filter by tag start with tag:`, 28 | `Alt + Click selects items without triggering actions`, 29 | `Right Click on the Main Menu to show the Palette`, 30 | `Right Click on the Filter Menu to show Favorite Filters or filter commands on the Palette (Configurable)`, 31 | `Right Click on the Go To Playing button to show Playing Tabs`, 32 | `Right Click on the Step Back button to show Recent Tabs`, 33 | `Right Click on the Actions button to show the Browser Menu`, 34 | `Right Click on the Filter to show the Filter Context`, 35 | `In the filter, $day resolves to the current week day`, 36 | `In the filter, $month resolves to the current month name`, 37 | `In the filter, $year resolves to the year number`, 38 | `Context menus support filtering, just start typing something`, 39 | `Middle Click filter items to further refine the filter`, 40 | `Middle Click the filter input to show Refine Filters`, 41 | `There are 3 special tags: jump, jump2, and jump3`, 42 | `Use Alt + Up/Down to select items ignoring unloaded tabs`, 43 | `Use "quotes" in the filters for more "precise matching"`, 44 | ] 45 | 46 | let close = DOM.el(`#about_close`) 47 | 48 | DOM.ev(close, `click`, () => { 49 | App.hide_window() 50 | }) 51 | 52 | close.textContent = App.close_text 53 | 54 | let image = DOM.el(`#about_image`) 55 | 56 | DOM.ev(image, `click`, () => { 57 | if (DOM.class(image, [`rotate_1`])) { 58 | image.classList.remove(`rotate_1`) 59 | image.classList.add(`invert`) 60 | } 61 | else if (DOM.class(image, [`invert`])) { 62 | image.classList.remove(`invert`) 63 | 64 | if (DOM.class(image, [`flipped`])) { 65 | image.classList.remove(`flipped`) 66 | } 67 | else { 68 | image.classList.add(`flipped`) 69 | } 70 | } 71 | else { 72 | image.classList.add(`rotate_1`) 73 | } 74 | }) 75 | 76 | let dev = DOM.el(`#about_dev`) 77 | 78 | let what = DOM.create(`div`, `bigger`) 79 | what.textContent = `Advanced Tab Manager` 80 | dev.append(what) 81 | 82 | let devby = DOM.create(`div`, `bigger`) 83 | devby.textContent = `Developed by ${App.manifest.author}` 84 | dev.append(devby) 85 | 86 | let since = DOM.create(`div`, `bigger`) 87 | since.textContent = `Since 2022` 88 | dev.append(since) 89 | 90 | let info = DOM.el(`#about_info`) 91 | 92 | for (let item of App.about_info_items) { 93 | let el = DOM.create(`div`, `about_info_item filter_item filter_text`) 94 | el.textContent = item 95 | info.append(el) 96 | } 97 | 98 | let s = `${App.manifest.name} v${App.manifest.version}` 99 | DOM.el(`#about_name`).textContent = s 100 | let filter = DOM.el(`#about_filter`) 101 | 102 | DOM.ev(filter, `input`, () => { 103 | App.filter_about() 104 | }) 105 | 106 | let bottom = DOM.el(`#about_filter_bottom`) 107 | bottom.textContent = App.filter_bottom_icon 108 | bottom.title = App.filter_bottom_title 109 | 110 | DOM.ev(bottom, `click`, () => { 111 | App.about_bottom() 112 | }) 113 | 114 | let clear = DOM.el(`#about_filter_clear`) 115 | clear.textContent = App.filter_clear_icon 116 | clear.title = App.filter_clear_title 117 | 118 | DOM.ev(clear, `click`, () => { 119 | App.reset_generic_filter(`about`) 120 | }) 121 | 122 | let container = DOM.el(`#window_content_about`) 123 | App.generic_gestures(container) 124 | }, 125 | after_show: () => { 126 | let filter = DOM.el(`#about_filter`) 127 | 128 | if (filter.value) { 129 | App.clear_about_filter() 130 | } 131 | 132 | let info = `` 133 | info += `Started: ${App.timeago(App.start_date)}\n` 134 | info += `Installed: ${App.timeago(App.first_time.date)}\n` 135 | info += `Commands: ${Object.keys(App.commands).length}\n` 136 | info += `Settings: ${Object.keys(App.settings).length}` 137 | 138 | let image = DOM.el(`#about_image`) 139 | image.classList.remove(`rotate_1`) 140 | image.classList.remove(`invert`) 141 | image.classList.remove(`flipped`) 142 | image.title = info 143 | 144 | App.focus_about_filter() 145 | }, 146 | colored_top: true, 147 | }) 148 | 149 | App.filter_about_debouncer = App.create_debouncer(() => { 150 | App.do_filter_about() 151 | }, App.filter_delay_2) 152 | } 153 | 154 | App.about_filter_focused = () => { 155 | return document.activeElement.id === `about_filter` 156 | } 157 | 158 | App.clear_about_filter = () => { 159 | if (App.filter_has_value(`about`)) { 160 | App.reset_generic_filter(`about`) 161 | } 162 | else { 163 | App.hide_window() 164 | } 165 | } 166 | 167 | App.filter_about = () => { 168 | App.filter_about_debouncer.call() 169 | } 170 | 171 | App.do_filter_about = () => { 172 | App.filter_about_debouncer.cancel() 173 | App.do_filter_2(`about`) 174 | } 175 | 176 | App.show_about = () => { 177 | App.start_about() 178 | App.show_window(`about`) 179 | } 180 | 181 | App.focus_about_filter = () => { 182 | let filter = DOM.el(`#about_filter`) 183 | filter.focus() 184 | } 185 | 186 | App.about_bottom = () => { 187 | let container = DOM.el(`#window_content_about`) 188 | container.scrollTop = container.scrollHeight 189 | } -------------------------------------------------------------------------------- /js/main/actions_menu.js: -------------------------------------------------------------------------------- 1 | App.get_actions = (mode) => { 2 | let menu = App.get_setting(`actions_menu_${mode}`) 3 | return menu.map(item => item.cmd) || [] 4 | } 5 | 6 | App.create_actions_menu = (mode) => { 7 | App[`${mode}_actions`] = App.get_actions(mode) 8 | let btn = DOM.create(`div`, `actions_button button icon_button`, `${mode}_actions`) 9 | btn.append(App.get_svg_icon(`sun`)) 10 | let click = App.get_cmd_name(`show_actions_menu`) 11 | let rclick = App.get_cmd_name(`show_browser_menu`) 12 | 13 | if (App.tooltips()) { 14 | btn.title = `Click: ${click}\nRight Click: ${rclick}` 15 | App.trigger_title(btn, `middle_click_actions_button`) 16 | App.trigger_title(btn, `click_press_actions_button`) 17 | App.trigger_title(btn, `middle_click_press_actions_button`) 18 | App.trigger_title(btn, `wheel_up_actions_button`) 19 | App.trigger_title(btn, `wheel_down_actions_button`) 20 | } 21 | 22 | App.check_show_button(`actions`, btn) 23 | return btn 24 | } 25 | 26 | App.show_actions_menu = (mode, item, e) => { 27 | let mode_menu = App.get_setting(`actions_menu_${mode}`) 28 | 29 | if (mode_menu.length) { 30 | App.show_mode_menu(mode, item, e) 31 | return 32 | } 33 | 34 | let global = App.get_setting(`actions_menu`) 35 | 36 | if (global.length) { 37 | App.show_global_actions_menu(e) 38 | } 39 | } 40 | 41 | App.show_global_actions_menu = (e) => { 42 | let items = App.custom_menu_items({ 43 | name: `actions_menu`, 44 | }) 45 | 46 | let mode = App.active_mode 47 | let element = DOM.el(`#${mode}_actions`) 48 | let compact = App.get_setting(`compact_actions_menu`) 49 | App.show_context({items, e, element, compact}) 50 | } 51 | 52 | App.show_mode_menu = (mode, item, e) => { 53 | let items = App.custom_menu_items({ 54 | name: `actions_menu_${mode}`, 55 | item, 56 | }) 57 | 58 | let element = DOM.el(`#${mode}_actions`) 59 | let compact = App.get_setting(`compact_actions_menu`) 60 | App.show_context({items, e, element, compact}) 61 | } 62 | 63 | App.actions_middle_click = (e) => { 64 | let cmd = App.get_setting(`middle_click_actions_button`) 65 | App.run_command({cmd, from: `actions_menu`, e}) 66 | } -------------------------------------------------------------------------------- /js/main/active_trace.js: -------------------------------------------------------------------------------- 1 | App.setup_active_trace = () => { 2 | App.update_active_trace_debouncer = App.create_debouncer((what) => { 3 | App.do_update_active_trace(what) 4 | }, App.update_active_trace_delay) 5 | } 6 | 7 | App.create_active_trace = () => { 8 | return DOM.create(`div`, `item_trace item_node hidden`) 9 | } 10 | 11 | App.update_active_trace = () => { 12 | App.update_active_trace_debouncer.call() 13 | } 14 | 15 | App.do_update_active_trace = () => { 16 | App.update_active_trace_debouncer.cancel() 17 | 18 | if (!App.get_setting(`active_trace`)) { 19 | return 20 | } 21 | 22 | for (let item of App.get_items(`tabs`)) { 23 | let trace = DOM.el(`.item_trace`, item.element) 24 | DOM.hide(trace) 25 | } 26 | 27 | let n = 1 28 | let items = App.get_recent_tabs({max: 9}) 29 | 30 | for (let item of items) { 31 | if (item.active) { 32 | continue 33 | } 34 | 35 | let trace = DOM.el(`.item_trace`, item.element) 36 | DOM.show(trace) 37 | trace.textContent = n 38 | 39 | if (n === 9) { 40 | break 41 | } 42 | 43 | n += 1 44 | } 45 | } 46 | 47 | App.pick_active_trace = (index) => { 48 | let items = App.get_recent_tabs({max: 9}) 49 | let tab = items[index] 50 | 51 | if (tab) { 52 | App.tabs_action({item: tab, from: `active_trace`}) 53 | } 54 | } -------------------------------------------------------------------------------- /js/main/alert.js: -------------------------------------------------------------------------------- 1 | App.alert = (message, autohide_delay = 0) => { 2 | App.start_popups() 3 | let msg = DOM.el(`#alert_message`) 4 | let text = App.make_html_safe(message) 5 | text = text.replace(/\n/g, `
`) 6 | msg.innerHTML = text 7 | App.show_popup(`alert`) 8 | 9 | if (autohide_delay > 0) { 10 | App.alert_timeout = setTimeout(() => { 11 | App.hide_popup(`alert`) 12 | }, autohide_delay) 13 | } 14 | } 15 | 16 | App.alert_autohide = (message, force = false) => { 17 | if (!force) { 18 | if (!App.get_setting(`show_feedback`)) { 19 | App.footer_message(message) 20 | return 21 | } 22 | } 23 | 24 | App.alert(message, App.alert_autohide_delay) 25 | } -------------------------------------------------------------------------------- /js/main/autoclick.js: -------------------------------------------------------------------------------- 1 | App.autoclick_action = (e) => { 2 | if (!App.get_setting(`autoclick_enabled`)) { 3 | return 4 | } 5 | 6 | clearInterval(App.autoclick_timeout) 7 | 8 | let el = e.target 9 | let mode = App.active_mode 10 | let [item, item_alt] = App.get_mouse_item(mode, el) 11 | 12 | function check(what, cls, elem) { 13 | let aclick = App.get_setting(`${what}_autoclick`) 14 | let element 15 | 16 | if (cls) { 17 | element = DOM.parent(el, cls) 18 | } 19 | else { 20 | element = elem 21 | } 22 | 23 | if (aclick && Boolean(element)) { 24 | let delay = App.get_setting(`${what}_autoclick_delay`) 25 | 26 | App.autoclick_timeout = setTimeout(() => { 27 | App.click_element(element) 28 | }, delay) 29 | 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | if (item || item_alt) { 37 | let elem 38 | let name 39 | 40 | if (item_alt) { 41 | if (!App.get_setting(`tab_box_autoclick`)) { 42 | return 43 | } 44 | 45 | elem = item_alt 46 | name = `tab_box` 47 | } 48 | else { 49 | elem = item.element 50 | name = `item` 51 | } 52 | 53 | if (check(`hover_button`, [`.hover_button`])) { 54 | return 55 | } 56 | 57 | if (check(`close_button`, [`.close_button`])) { 58 | return 59 | } 60 | 61 | if (item.unloaded) { 62 | if (name === `tab_box`) { 63 | if (check(name, undefined, elem)) { 64 | return 65 | } 66 | } 67 | else if (check(`unloaded_tab`, undefined, elem)) { 68 | return 69 | } 70 | } 71 | else if (check(name, undefined, elem)) { 72 | return 73 | } 74 | 75 | return 76 | } 77 | 78 | if (check(`main_button`, [`.main_button`])) { 79 | return 80 | } 81 | 82 | if (check(`filter_button`, [`.filter_button`])) { 83 | return 84 | } 85 | 86 | if (check(`actions_button`, [`.actions_button`])) { 87 | return 88 | } 89 | 90 | if (check(`main_title`, [ 91 | `#main_title_left_button`, 92 | `#main_title_right_button`, 93 | `#main_title`, 94 | ])) { 95 | return 96 | } 97 | 98 | if (check(`palette`, [`.palette_item`])) { 99 | return 100 | } 101 | 102 | if (check(`pinline`, [`#pinline`])) { 103 | return 104 | } 105 | 106 | if (check(`footer`, [ 107 | `#footer_info`, 108 | `#footer_count`, 109 | `#footer_tab_box`, 110 | `#footer_up_tabs`, 111 | `#footer_down_tabs`, 112 | ])) { 113 | return 114 | } 115 | 116 | if (check(`settings`, [ 117 | `.settings_title`, 118 | `.settings_arrow`, 119 | `.settings_actions`, 120 | `.settings_close`, 121 | ])) { 122 | return 123 | } 124 | 125 | if (check(`favorites`, [`.favorites_bar_item`])) { 126 | return 127 | } 128 | } 129 | 130 | App.toggle_autoclick = () => { 131 | let aclick = App.get_setting(`autoclick_enabled`) 132 | App.set_setting({setting: `autoclick_enabled`, value: !aclick}) 133 | App.toggle_message(`Autoclick`, `autoclick_enabled`) 134 | } -------------------------------------------------------------------------------- /js/main/browser.js: -------------------------------------------------------------------------------- 1 | App.show_browser_menu = (e) => { 2 | let cmds = [ 3 | `browser_back`, 4 | `browser_forward`, 5 | `browser_reload`, 6 | `browser_hard_reload`, 7 | `browser_close`, 8 | ] 9 | 10 | let urls = App.get_setting(`custom_urls`) 11 | 12 | if (urls.length) { 13 | cmds.push(App.separator_string) 14 | 15 | for (let url of urls.slice(0, 5)) { 16 | cmds.push(`open_url_${url._id_}`) 17 | } 18 | } 19 | 20 | let items = App.cmd_list(cmds) 21 | App.show_context({items, e}) 22 | } 23 | 24 | App.open_custom_url = (item, num, from = `normal`, mode = `auto`) => { 25 | let urls = App.get_setting(`custom_urls`) 26 | 27 | if (!urls.length) { 28 | return 29 | } 30 | 31 | let url = urls[num - 1].url 32 | 33 | if (!url) { 34 | return 35 | } 36 | 37 | if (mode === `replace`) { 38 | let item = App.get_selected(`tabs`) 39 | App.change_url(item, url) 40 | return 41 | } 42 | 43 | let args = { 44 | url, 45 | } 46 | 47 | App.get_new_tab_args(item, from, args) 48 | 49 | if (mode === `pin`) { 50 | args.pinned = true 51 | } 52 | else if (mode === `normal`) { 53 | args.pinned = false 54 | } 55 | 56 | App.open_new_tab(args) 57 | App.after_focus({show_tabs: true}) 58 | } 59 | 60 | App.run_browser_command = (num) => { 61 | let cmd = App.get_setting(`browser_command_${num}`) 62 | App.run_command({cmd, from: `browser_command`}) 63 | } 64 | 65 | App.run_popup_command = (num) => { 66 | let cmd = App.get_setting(`popup_command_${num}`) 67 | App.run_command({cmd, from: `popup_command`}) 68 | } 69 | 70 | App.check_init_commands = () => { 71 | let init_cmd = localStorage.getItem(`init_popup_command`) || `nothing` 72 | localStorage.setItem(`init_popup_command`, `nothing`) 73 | 74 | if (init_cmd !== `nothing`) { 75 | App.prompt_mode = true 76 | 77 | setTimeout(() => { 78 | App.run_popup_command(parseInt(init_cmd)) 79 | }, App.popup_commands_delay) 80 | } 81 | } 82 | 83 | App.check_popup_command_close = () => { 84 | if (App.prompt_mode) { 85 | if (App.get_setting(`popup_command_close`)) { 86 | setTimeout(() => { 87 | App.close_window() 88 | }, App.prompt_close_delay) 89 | } 90 | } 91 | } 92 | 93 | App.browser_action = (item, action) => { 94 | if (item && (item.mode === `tabs`)) { 95 | let active = App.get_active_items({mode: `tabs`, item}) 96 | 97 | for (let it of active) { 98 | action(it.id) 99 | } 100 | } 101 | else { 102 | action() 103 | } 104 | } 105 | 106 | App.browser_reload = (item, bypass = false) => { 107 | App.browser_action(item, (id) => { 108 | browser.tabs.reload(id, {bypassCache: bypass}) 109 | }) 110 | } 111 | 112 | App.browser_back = (item) => { 113 | App.browser_action(item, (id) => { 114 | browser.tabs.goBack(id) 115 | }) 116 | } 117 | 118 | App.browser_forward = (item) => { 119 | App.browser_action(item, (id) => { 120 | browser.tabs.goForward(id) 121 | }) 122 | } -------------------------------------------------------------------------------- /js/main/clock.js: -------------------------------------------------------------------------------- 1 | App.start_clock = () => { 2 | let delay = App.check_clock_delay 3 | 4 | if (!delay || (delay < App.SECOND)) { 5 | App.error(`Clock delay is invalid`) 6 | return 7 | } 8 | 9 | setInterval(() => { 10 | App.check_clock() 11 | }, delay) 12 | } 13 | 14 | App.check_clock = (force = false) => { 15 | let placeholder 16 | let clock_format = App.get_setting(`clock_format`) 17 | 18 | if (clock_format) { 19 | let date = App.now() 20 | placeholder = dateFormat(date, clock_format) 21 | } 22 | else { 23 | placeholder = App.filter_placeholder 24 | } 25 | 26 | if (!force) { 27 | if (placeholder === App.last_filter_placeholder) { 28 | return 29 | } 30 | } 31 | 32 | let filters = DOM.els(`.mode_filter`) 33 | 34 | for (let el of filters) { 35 | el.placeholder = placeholder 36 | } 37 | 38 | App.last_filter_placeholder = placeholder 39 | } 40 | 41 | App.pick_clock_format = (e) => { 42 | let items = [] 43 | 44 | items.push({ 45 | text: `12 Hour`, 46 | action: () => { 47 | App.set_setting({setting: `clock_format`, value: `h:MM tt Z`}) 48 | App.refresh_setting_widgets([`clock_format`]) 49 | }, 50 | }) 51 | 52 | items.push({ 53 | text: `24 Hour`, 54 | action: () => { 55 | App.set_setting({setting: `clock_format`, value: `HH:MM Z`}) 56 | App.refresh_setting_widgets([`clock_format`]) 57 | }, 58 | }) 59 | 60 | App.show_context({e, items}) 61 | } -------------------------------------------------------------------------------- /js/main/close_button.js: -------------------------------------------------------------------------------- 1 | App.add_close_button = (item, side) => { 2 | if (item.mode !== `tabs`) { 3 | return 4 | } 5 | 6 | let cb_show 7 | let c_side 8 | 9 | if (item.tab_box) { 10 | cb_show = App.get_setting(`show_close_button_tab_box`) 11 | c_side = App.get_setting(`close_button_side_tab_box`) 12 | } 13 | else { 14 | cb_show = App.get_setting(`show_close_button`) 15 | c_side = App.get_setting(`close_button_side`) 16 | } 17 | 18 | if (cb_show === `never`) { 19 | return 20 | } 21 | 22 | if (side !== c_side) { 23 | return 24 | } 25 | 26 | let btn = DOM.create(`div`, `close_button ${c_side} item_node action`) 27 | btn.textContent = App.get_setting(`close_button_icon`) 28 | item.element.append(btn) 29 | } 30 | 31 | App.show_close_button_menu = (item, e) => { 32 | let menu = App.get_setting(`close_button_menu`) 33 | 34 | if (!menu.length) { 35 | return false 36 | } 37 | 38 | let items = App.custom_menu_items({ 39 | name: `close_button_menu`, 40 | item, 41 | }) 42 | 43 | let element = item?.element 44 | let compact = App.get_setting(`compact_close_button_menu`) 45 | App.show_context({items, e, element, compact}) 46 | return true 47 | } 48 | 49 | App.close_button_click = (item, e) => { 50 | let cmd = App.get_setting(`click_close_button`) 51 | App.run_command({cmd, item, from: `close_button`, e}) 52 | } 53 | 54 | App.close_button_double_click = (item, e) => { 55 | let cmd = App.get_setting(`double_click_close_button`) 56 | App.run_command({cmd, item, from: `close_button`, e}) 57 | } 58 | 59 | App.close_button_middle_click = (item, e) => { 60 | let cmd = App.get_setting(`middle_click_close_button`) 61 | App.run_command({cmd, item, from: `close_button`, e}) 62 | } -------------------------------------------------------------------------------- /js/main/closed.js: -------------------------------------------------------------------------------- 1 | App.setup_closed = () => { 2 | browser.sessions.onChanged.addListener(() => { 3 | if (App.active_mode === `closed`) { 4 | App.closed_changed = true 5 | } 6 | }) 7 | } 8 | 9 | App.get_closed = async () => { 10 | App.getting(`closed`) 11 | let results 12 | 13 | try { 14 | results = await browser.sessions.getRecentlyClosed({ 15 | maxResults: App.max_closed, 16 | }) 17 | } 18 | catch (err) { 19 | App.error(err) 20 | return [] 21 | } 22 | 23 | results = results.filter(x => x.tab) 24 | let tabs = results.map(x => x.tab) 25 | return tabs 26 | } 27 | 28 | App.closed_action = (args = {}) => { 29 | let def_args = { 30 | on_action: true, 31 | soft: false, 32 | } 33 | 34 | App.def_args(def_args, args) 35 | App.select_item({item: args.item, scroll: `nearest_smooth`, check_auto_scroll: true}) 36 | 37 | if (args.on_action) { 38 | App.on_action(`closed`) 39 | } 40 | 41 | App.focus_or_open_item(args.item, args.soft) 42 | } 43 | 44 | App.reopen_tab = async () => { 45 | let closed = await App.get_closed() 46 | 47 | if (closed && closed.length) { 48 | browser.sessions.restore(closed[0].sessionId) 49 | } 50 | } 51 | 52 | App.forget_closed = () => { 53 | let items = App.get_items(`closed`) 54 | 55 | App.show_confirm({ 56 | message: `Forget closed tabs? (${items.length})`, 57 | confirm_action: async () => { 58 | for (let item of items) { 59 | await browser.sessions.forgetClosedTab(item.window_id, item.session_id) 60 | } 61 | 62 | App.after_forget() 63 | }, 64 | }) 65 | } 66 | 67 | App.forget_closed_item = (item) => { 68 | let active = App.get_active_items({mode: `closed`, item}) 69 | 70 | App.show_confirm({ 71 | message: `Forget closed tabs? (${active.length})`, 72 | confirm_action: async () => { 73 | for (let item of active) { 74 | await browser.sessions.forgetClosedTab(item.window_id, item.session_id) 75 | } 76 | 77 | App.after_forget() 78 | }, 79 | force: active.length <= 1, 80 | }) 81 | } 82 | 83 | App.after_forget = () => { 84 | App.show_mode({mode: `closed`, force: true}) 85 | } -------------------------------------------------------------------------------- /js/main/combos.js: -------------------------------------------------------------------------------- 1 | App.cmd_combo_keys = () => { 2 | let keys = [`name`, `icon`] 3 | 4 | for (let i = 1; i <= App.command_combo_num; i++) { 5 | keys.push(`cmd_${i}`) 6 | } 7 | 8 | return keys 9 | } 10 | 11 | App.cmd_combo_widgets = () => { 12 | let obj = { 13 | name: `text`, 14 | icon: `text`, 15 | } 16 | 17 | for (let i = 1; i <= App.command_combo_num; i++) { 18 | obj[`cmd_${i}`] = `menu` 19 | } 20 | 21 | return obj 22 | } 23 | 24 | App.cmd_combo_labels = () => { 25 | let obj = { 26 | name: `Name`, 27 | icon: `Icon`, 28 | } 29 | 30 | for (let i = 1; i <= App.command_combo_num; i++) { 31 | obj[`cmd_${i}`] = `Command ${i}` 32 | } 33 | 34 | return obj 35 | } 36 | 37 | App.cmd_combo_sources = () => { 38 | let obj = {} 39 | 40 | for (let i = 1; i <= App.command_combo_num; i++) { 41 | obj[`cmd_${i}`] = () => { 42 | return App.cmdlist_single.slice(0) 43 | } 44 | } 45 | 46 | return obj 47 | } 48 | 49 | App.start_command_combos_addlist = () => { 50 | if (App.command_combos_addlist_ready) { 51 | return 52 | } 53 | 54 | let id = `settings_command_combos` 55 | let props = App.setting_props.command_combos 56 | let {popobj, regobj} = App.get_setting_addlist_objects() 57 | 58 | App.create_popup({...popobj, id: `addlist_${id}`, 59 | element: Addlist.register({...regobj, id, 60 | keys: App.cmd_combo_keys(), 61 | widgets: App.cmd_combo_widgets(), 62 | labels: App.cmd_combo_labels(), 63 | sources: App.cmd_combo_sources(), 64 | list_icon: (item) => { 65 | return item.icon || App.combo_icon 66 | }, 67 | list_text: (item) => { 68 | return item.name 69 | }, 70 | required: { 71 | name: true, 72 | }, 73 | tooltips: { 74 | icon: `Icon for this combo`, 75 | name: `Name of the combo`, 76 | }, 77 | title: props.name, 78 | })}) 79 | 80 | App.command_combos_addlist_ready = true 81 | } 82 | 83 | App.run_command_combo = async (combo, item, e) => { 84 | let cmds = [] 85 | 86 | for (let key in combo) { 87 | if (key.startsWith(`cmd_`)) { 88 | let value = combo[key] 89 | 90 | if (!value || (value === `none`)) { 91 | continue 92 | } 93 | 94 | cmds.push(value) 95 | } 96 | } 97 | 98 | if (!cmds.length) { 99 | return 100 | } 101 | 102 | let mode = item?.mode || App.active_mode 103 | let delay = App.get_setting(`command_combo_delay`) 104 | let last_selected = App.last_selected_date[mode] 105 | 106 | for (let cmd of cmds) { 107 | if (cmd.startsWith(`sleep_ms`)) { 108 | let ms = parseInt(cmd.split(`_`).pop()) 109 | await App.sleep(ms) 110 | continue 111 | } 112 | 113 | let lsd = App.last_selected_date[mode] 114 | 115 | if (lsd !== last_selected) { 116 | item = App.get_selected(mode) 117 | last_selected = lsd 118 | } 119 | 120 | await App.run_command({cmd, item, e}) 121 | 122 | if (delay > 0) { 123 | await App.sleep(delay) 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /js/main/containers.js: -------------------------------------------------------------------------------- 1 | App.get_contextual_identity = async (tab) => { 2 | try { 3 | if (tab.cookieStoreId && (tab.cookieStoreId !== `firefox-default`)) { 4 | return await browser.contextualIdentities.get(tab.cookieStoreId) 5 | } 6 | } 7 | catch (error) { 8 | App.error(error) 9 | } 10 | } 11 | 12 | App.get_container_tabs = (name) => { 13 | let items = App.get_items(`tabs`) 14 | return items.filter(x => x.container_name === name) 15 | } 16 | 17 | App.get_all_container_tabs = () => { 18 | let items = App.get_items(`tabs`) 19 | return items.filter(x => x.container_name) 20 | } 21 | 22 | App.tab_container_menu = (item, e) => { 23 | let items = App.tab_container_menu_items(item, e) 24 | App.show_context({items, e}) 25 | } 26 | 27 | App.tab_container_menu_items = (item, e) => { 28 | let items = [] 29 | 30 | items.push({ 31 | text: `Show`, 32 | icon: App.color_icon_square(item.container_color), 33 | action: () => { 34 | App.show_tab_list(`container`, e, item) 35 | }, 36 | }) 37 | 38 | items.push({ 39 | text: `Filter`, 40 | icon: App.color_icon_square(item.container_color), 41 | action: () => { 42 | App.filter_same_container(item) 43 | }, 44 | }) 45 | 46 | items.push({ 47 | text: `Open`, 48 | icon: App.get_setting(`container_icon`), 49 | action: () => { 50 | App.open_in_tab_container(item, e) 51 | }, 52 | }) 53 | 54 | return items 55 | } 56 | 57 | App.check_tab_container = async (tab) => { 58 | let ident = await App.get_contextual_identity(tab) 59 | 60 | if (ident) { 61 | tab.container_name = ident.name 62 | tab.container_color = ident.colorCode 63 | 64 | if (App.container_data[ident.name] === undefined) { 65 | App.container_data[ident.name] = {} 66 | } 67 | 68 | App.container_data[ident.name].color = ident.colorCode 69 | } 70 | } 71 | 72 | App.filter_same_container = (item) => { 73 | App.filter_container({mode: item.mode, container: item.container_name}) 74 | } 75 | 76 | App.show_filter_container_menu = (mode, e, show = false) => { 77 | let items = App.get_container_items(mode, show) 78 | let title_icon = App.get_setting(`container_icon`) 79 | let compact = App.get_setting(`compact_container_menu`) 80 | App.show_context({items, e, title: `Containers`, title_icon, compact}) 81 | } 82 | 83 | App.get_container_items = (mode, show) => { 84 | let items = [] 85 | let containers = [] 86 | 87 | for (let tab of App.get_items(`tabs`)) { 88 | if (tab.container_name) { 89 | if (!containers.includes(tab.container_name)) { 90 | containers.push(tab.container_name) 91 | } 92 | } 93 | } 94 | 95 | if (containers.length) { 96 | let icon = App.get_setting(`container_icon`) 97 | 98 | if (!show) { 99 | items.push({ 100 | icon, 101 | text: `All`, 102 | action: () => { 103 | App.filter_cmd(mode, `filter_tab_containers_all`, `containers_menu`) 104 | }, 105 | middle_action: () => { 106 | App.filter_container({mode, container: `all`, from: App.refine_string}) 107 | }, 108 | }) 109 | } 110 | 111 | for (let container of containers) { 112 | let icon = App.color_icon_square(App.container_data[container].color) 113 | 114 | items.push({ 115 | icon, 116 | text: container, 117 | action: (e) => { 118 | if (show) { 119 | App.show_tab_list(`container_${container}`, e) 120 | } 121 | else { 122 | App.filter_container({mode, container}) 123 | } 124 | }, 125 | middle_action: (e) => { 126 | if (show) { 127 | // 128 | } 129 | else { 130 | App.filter_container({mode, container, from: App.refine_string}) 131 | } 132 | }, 133 | }) 134 | } 135 | } 136 | else { 137 | items.push({ 138 | text: `No containers in use`, 139 | action: () => { 140 | App.alert(`Open containers in Firefox to use this feature`) 141 | }, 142 | }) 143 | } 144 | 145 | return items 146 | } 147 | 148 | App.get_all_containers = async () => { 149 | let containers = [] 150 | 151 | try { 152 | let data = await browser.contextualIdentities.query({}) 153 | 154 | for (let c of data) { 155 | containers.push({ 156 | id: c.cookieStoreId, 157 | name: c.name, 158 | color: c.colorCode, 159 | }) 160 | } 161 | 162 | return containers 163 | } 164 | catch (error) { 165 | App.error(error) 166 | } 167 | } 168 | 169 | App.open_in_tab_container = async (item, e) => { 170 | let new_tab_mode = App.get_setting(`new_tab_mode`) 171 | let containers = await App.get_all_containers() 172 | let active = App.get_active_items({mode: item.mode, item}) 173 | let items = [] 174 | let o_item 175 | 176 | if (new_tab_mode === `above`) { 177 | o_item = active[0] 178 | } 179 | else if (new_tab_mode === `below`) { 180 | o_item = active.at(-1) 181 | } 182 | else { 183 | o_item = active[0] 184 | } 185 | 186 | for (let c of containers) { 187 | items.push({ 188 | text: c.name, 189 | icon: App.color_icon_square(c.color), 190 | action: () => { 191 | for (let it of active) { 192 | App.create_new_tab({url: it.url, cookieStoreId: c.id}, o_item) 193 | } 194 | }, 195 | }) 196 | } 197 | 198 | let title = `Open In` 199 | let title_icon = App.get_setting(`container_icon`) 200 | let compact = App.get_setting(`compact_container_menu`) 201 | App.show_context({items, e, title, title_icon, compact}) 202 | } -------------------------------------------------------------------------------- /js/main/context.js: -------------------------------------------------------------------------------- 1 | App.setup_context = () => { 2 | NeedContext.min_width = `1rem` 3 | NeedContext.center_top = 50 4 | NeedContext.init() 5 | App.refresh_context() 6 | } 7 | 8 | App.refresh_context = () => { 9 | let autohide_delay = App.get_setting(`context_autohide_delay`) 10 | let autoclick_delay = App.get_setting(`context_autoclick_delay`) 11 | NeedContext.start_autohide(autohide_delay) 12 | NeedContext.start_autoclick(autoclick_delay) 13 | } 14 | 15 | App.show_context = (args = {}) => { 16 | if (!args.items) { 17 | return 18 | } 19 | 20 | clearInterval(App.autoclick_timeout) 21 | 22 | if (!args.items.length) { 23 | args.items.push({ 24 | text: `No items`, 25 | action: () => { 26 | if (args.no_items) { 27 | App.alert(args.no_items) 28 | } 29 | }, 30 | fake: true, 31 | }) 32 | } 33 | 34 | if (!App.get_setting(`context_titles`)) { 35 | args.title = undefined 36 | } 37 | 38 | args.autohide = App.get_setting(`context_autohide`) 39 | args.autoclick = App.get_setting(`context_autoclick`) 40 | NeedContext.show(args) 41 | } 42 | 43 | App.hide_context = () => { 44 | if (NeedContext.dragging) { 45 | return 46 | } 47 | 48 | NeedContext.hide() 49 | } 50 | 51 | App.context_open = () => { 52 | return NeedContext.open 53 | } 54 | 55 | App.show_generic_menu = (num, item, e) => { 56 | let items = App.custom_menu_items({ 57 | name: `generic_menu_${num}`, 58 | item, 59 | }) 60 | 61 | let element = item?.element 62 | let compact = App.get_setting(`compact_generic_menu_${num}`) 63 | App.show_context({items, e, element, compact}) 64 | } 65 | 66 | App.show_extra_menu = (item, e) => { 67 | let items = App.custom_menu_items({ 68 | name: `extra_menu`, 69 | item, e, 70 | }) 71 | 72 | let element = item?.element 73 | let compact = App.get_setting(`compact_extra_menu`) 74 | App.show_context({items, e, element, compact}) 75 | } 76 | 77 | App.show_empty_menu = (item, e) => { 78 | let name 79 | let mode = item?.mode || App.active_mode 80 | let mode_menu = App.get_setting(`empty_menu_${mode}`) 81 | 82 | if (mode_menu.length) { 83 | name = `empty_menu_${mode}` 84 | } 85 | else { 86 | let global = App.get_setting(`empty_menu`) 87 | 88 | if (global.length) { 89 | name = `empty_menu` 90 | } 91 | } 92 | 93 | if (!name) { 94 | return 95 | } 96 | 97 | let items = App.custom_menu_items({name, item}) 98 | let compact = App.get_setting(`compact_empty_menu`) 99 | App.show_context({items, e, compact}) 100 | } 101 | 102 | App.show_stuff_menu = (item, e) => { 103 | let items = App.custom_menu_items({name: `stuff_menu`, item}) 104 | let compact = App.get_setting(`compact_stuff_menu`) 105 | 106 | App.show_context({ 107 | e, 108 | items, 109 | title: `Stuff`, 110 | title_icon: App.shroom_icon, 111 | title_number: true, 112 | compact, 113 | }) 114 | } -------------------------------------------------------------------------------- /js/main/data.js: -------------------------------------------------------------------------------- 1 | App.export_data = (what, obj) => { 2 | App.show_textarea({ 3 | title: `Copy ${what} Data`, 4 | title_icon: App.data_icon, 5 | text: App.str(obj, true), 6 | }) 7 | } 8 | 9 | App.import_data = (what, action, value = ``) => { 10 | App.show_input({ 11 | value, 12 | title: `Paste ${what} Data`, 13 | title_icon: App.data_icon, 14 | button: `Import`, 15 | action: (text) => { 16 | if (!text.trim()) { 17 | return false 18 | } 19 | 20 | let json 21 | 22 | try { 23 | json = App.obj(text) 24 | } 25 | catch (err) { 26 | App.alert(`${err}`) 27 | return false 28 | } 29 | 30 | if (json) { 31 | App.show_confirm({ 32 | message: `Use this data?`, 33 | confirm_action: () => { 34 | action(json) 35 | }, 36 | }) 37 | } 38 | 39 | return true 40 | }, 41 | }) 42 | } -------------------------------------------------------------------------------- /js/main/dialog.js: -------------------------------------------------------------------------------- 1 | App.show_dialog = (args = {}) => { 2 | App.start_popups() 3 | 4 | if (App.popups.dialog.open) { 5 | return 6 | } 7 | 8 | DOM.el(`#dialog_message`).textContent = args.message 9 | let btns = DOM.el(`#dialog_buttons`) 10 | btns.innerHTML = `` 11 | 12 | for (let button of args.buttons) { 13 | let btn = DOM.create(`div`, `button`) 14 | btn.textContent = button[0] 15 | 16 | DOM.ev(btn, `click`, () => { 17 | App.popups.dialog.hide() 18 | button[1]() 19 | 20 | if (args.on_any_action) { 21 | args.on_any_action() 22 | } 23 | }) 24 | 25 | if (button[2]) { 26 | btn.classList.add(`button_2`) 27 | } 28 | 29 | btns.append(btn) 30 | button.element = btn 31 | } 32 | 33 | App.dialog_buttons = args.buttons 34 | 35 | App.dialog_on_dismiss = () => { 36 | if (args.on_dismiss) { 37 | args.on_dismiss() 38 | } 39 | 40 | if (args.on_any_action) { 41 | args.on_any_action() 42 | } 43 | } 44 | 45 | let focused_button 46 | 47 | if (args.focused_button !== undefined) { 48 | focused_button = args.focused_button 49 | } 50 | else { 51 | focused_button = args.buttons.length - 1 52 | } 53 | 54 | App.focus_dialog_button(focused_button) 55 | App.show_popup(`dialog`) 56 | } 57 | 58 | App.focus_dialog_button = (index) => { 59 | for (let [i, btn] of App.dialog_buttons.entries()) { 60 | if (i === index) { 61 | btn.element.classList.add(`hovered`) 62 | } 63 | else { 64 | btn.element.classList.remove(`hovered`) 65 | } 66 | } 67 | 68 | App.dialog_index = index 69 | } 70 | 71 | App.dialog_left = () => { 72 | if (App.dialog_index > 0) { 73 | App.focus_dialog_button(App.dialog_index - 1) 74 | } 75 | } 76 | 77 | App.dialog_right = () => { 78 | if (App.dialog_index < App.dialog_buttons.length - 1) { 79 | App.focus_dialog_button(App.dialog_index + 1) 80 | } 81 | } 82 | 83 | App.dialog_enter = () => { 84 | App.hide_popup(`dialog`) 85 | App.dialog_buttons[App.dialog_index][1]() 86 | } 87 | 88 | App.show_confirm = (args = {}) => { 89 | let def_args = { 90 | force: false, 91 | } 92 | 93 | App.def_args(def_args, args) 94 | 95 | if (args.force) { 96 | args.confirm_action() 97 | return 98 | } 99 | 100 | if (!args.cancel_action) { 101 | args.cancel_action = () => {} 102 | } 103 | 104 | let buttons = [ 105 | [`Cancel`, args.cancel_action, true], 106 | [`Confirm`, args.confirm_action], 107 | ] 108 | 109 | let on_dismiss = () => { 110 | if (args.cancel_action) { 111 | args.cancel_action() 112 | } 113 | } 114 | 115 | let on_any_action = () => { 116 | if (args.on_any_action) { 117 | args.on_any_action() 118 | } 119 | } 120 | 121 | App.show_dialog({ 122 | message: args.message, 123 | buttons, 124 | on_dismiss, 125 | on_any_action, 126 | }) 127 | 128 | App.play_sound(`effect_2`) 129 | } -------------------------------------------------------------------------------- /js/main/gestures.js: -------------------------------------------------------------------------------- 1 | App.setup_gestures = () => { 2 | App.refresh_gestures() 3 | let obj = {} 4 | 5 | for (let gesture of App.gestures) { 6 | obj[gesture] = (e) => { 7 | App.gesture_action(e, gesture) 8 | } 9 | } 10 | 11 | obj.default = (e) => { 12 | App.mouse_middle_action(e) 13 | } 14 | 15 | NiceGesture.start(App.main(), obj) 16 | } 17 | 18 | App.gesture_action = (e, gesture) => { 19 | if (App.screen_locked) { 20 | return 21 | } 22 | 23 | App.reset_triggers() 24 | let cmd = App.get_setting(`gesture_${gesture}`) 25 | App.run_command({cmd, from: `gesture`, e}) 26 | } 27 | 28 | App.refresh_gestures = () => { 29 | NiceGesture.enabled = App.get_setting(`gestures_enabled`) 30 | NiceGesture.threshold = App.get_setting(`gestures_threshold`) 31 | } 32 | 33 | App.settings_gestures = (el) => { 34 | let obj = { 35 | up: () => { 36 | App.settings_top() 37 | }, 38 | down: () => { 39 | App.settings_bottom() 40 | }, 41 | left: () => { 42 | App.show_prev_settings() 43 | }, 44 | right: () => { 45 | App.show_next_settings() 46 | }, 47 | default: () => { 48 | // 49 | }, 50 | } 51 | 52 | NiceGesture.start(el, obj) 53 | } 54 | 55 | App.generic_gestures = (el) => { 56 | let obj = { 57 | up: () => { 58 | App.scroll_to_top(el) 59 | }, 60 | down: () => { 61 | App.scroll_to_bottom(el) 62 | }, 63 | left: () => { 64 | // 65 | }, 66 | right: () => { 67 | // 68 | }, 69 | default: () => { 70 | // 71 | }, 72 | } 73 | 74 | NiceGesture.start(el, obj) 75 | } 76 | 77 | App.toggle_gestures = () => { 78 | let enabled = App.get_setting(`gestures_enabled`) 79 | App.set_setting({setting: `gestures_enabled`, value: !enabled}) 80 | App.toggle_message(`Gestures`, `gestures_enabled`) 81 | App.refresh_gestures() 82 | } -------------------------------------------------------------------------------- /js/main/gets.js: -------------------------------------------------------------------------------- 1 | App.get_edit = (item, prop, rule = true) => { 2 | let value = item[`custom_${prop}`] 3 | 4 | if ((value === undefined) && rule) { 5 | value = item[`rule_${prop}`] 6 | } 7 | 8 | if (value === undefined) { 9 | value = App.edit_default(prop) 10 | } 11 | 12 | return value 13 | } 14 | 15 | App.get_color = (item, rule = true) => { 16 | return App.get_edit(item, `color`, rule) 17 | } 18 | 19 | App.get_title = (item, rule = true) => { 20 | return App.get_edit(item, `title`, rule) 21 | } 22 | 23 | App.get_root = (item, rule = true) => { 24 | return App.get_edit(item, `root`, rule) 25 | } 26 | 27 | App.get_icon = (item, rule = true) => { 28 | return App.get_edit(item, `icon`, rule) 29 | } 30 | 31 | App.get_notes = (item, rule = true) => { 32 | return App.get_edit(item, `notes`, rule) 33 | } 34 | 35 | App.get_tags = (item, rule = true) => { 36 | return App.get_edit(item, `tags`, rule) 37 | } 38 | 39 | App.get_split_top = (item, rule = true) => { 40 | return App.get_edit(item, `split_top`, rule) 41 | } 42 | 43 | App.get_split_bottom = (item, rule = true) => { 44 | return App.get_edit(item, `split_bottom`, rule) 45 | } 46 | 47 | App.get_split = (item, what, rule = true) => { 48 | if (what === `top`) { 49 | return App.get_split_top(item, rule) 50 | } 51 | else if (what === `bottom`) { 52 | return App.get_split_bottom(item, rule) 53 | } 54 | } 55 | 56 | App.get_obfuscated = (item, rule = true) => { 57 | return App.get_edit(item, `obfuscated`, rule) 58 | } -------------------------------------------------------------------------------- /js/main/guides.js: -------------------------------------------------------------------------------- 1 | App.setting_guides = [ 2 | { 3 | title: `Importing settings`, 4 | text: `Settings can be imported by using JSON. 5 | 6 | You can import all settings or just some of them, the ones you need. 7 | 8 | This means other people can share their configuration with you. 9 | 10 | Or you can back up your settings and apply them on another computer.`, 11 | }, 12 | { 13 | title: `Type to filter lists`, 14 | text: `On any list, like command lists, you can type to filter the items. 15 | 16 | This is very important since it's much faster than scrolling down the lists to find items.`, 17 | }, 18 | { 19 | title: `Don't load unloaded tabs on click`, 20 | text: `To avoid loading unloaded tabs when clicking on them: 21 | 22 | Go to "Triggers", then change "Click Item (Tabs)" to "Soft Action". 23 | 24 | Now to load them, double clicking is required. 25 | 26 | Single click will only select them, not load them.`, 27 | }, 28 | ] -------------------------------------------------------------------------------- /js/main/history.js: -------------------------------------------------------------------------------- 1 | App.setup_history = () => { 2 | if (App.setup_history_ready) { 3 | return 4 | } 5 | 6 | browser.history.onVisited.addListener((info) => { 7 | App.debug(`History Visited`) 8 | 9 | if (App.active_mode === `history`) { 10 | App.history_changed = true 11 | } 12 | }) 13 | 14 | App.setup_history_ready = true 15 | } 16 | 17 | App.history_time = (deep = false) => { 18 | let months = App.get_setting(`history_max_months`) 19 | 20 | if (deep) { 21 | months = App.get_setting(`deep_history_max_months`) 22 | } 23 | 24 | return App.now() - (App.DAY * 30 * months) 25 | } 26 | 27 | App.get_history = async (query = ``, deep = false, by_what = `all`) => { 28 | App.getting(`history`) 29 | let results, max_items 30 | 31 | if (query && App.get_setting(`auto_deep_search_history`)) { 32 | deep = true 33 | } 34 | 35 | let normal_max = App.get_setting(`max_search_items_history`) 36 | let deep_max = App.get_setting(`deep_max_search_items_history`) 37 | let text = `` 38 | 39 | if (by_what.startsWith(`re`)) { 40 | max_items = Math.max(deep_max, 10 * 1000) 41 | } 42 | else { 43 | text = query 44 | 45 | if (deep) { 46 | max_items = deep_max 47 | } 48 | else { 49 | max_items = normal_max 50 | } 51 | } 52 | 53 | try { 54 | results = await browser.history.search({ 55 | text, 56 | maxResults: max_items, 57 | startTime: App.history_time(deep), 58 | }) 59 | } 60 | catch (err) { 61 | App.error(err) 62 | return [] 63 | } 64 | 65 | results.sort((a, b) => { 66 | return a.lastVisitTime > b.lastVisitTime ? -1 : 1 67 | }) 68 | 69 | App.last_history_query = query 70 | return results 71 | } 72 | 73 | App.history_action = (args = {}) => { 74 | let def_args = { 75 | on_action: true, 76 | soft: false, 77 | } 78 | 79 | App.def_args(def_args, args) 80 | App.select_item({item: args.item, scroll: `nearest_smooth`, check_auto_scroll: true}) 81 | 82 | if (args.on_action) { 83 | App.on_action(`history`) 84 | } 85 | 86 | App.focus_or_open_item(args.item, args.soft) 87 | } 88 | 89 | App.search_domain_history = (item) => { 90 | App.do_show_mode({ 91 | mode: `history`, 92 | reuse_filter: false, 93 | filter: item.hostname, 94 | }) 95 | } 96 | 97 | App.save_history_pick = () => { 98 | let value = App.get_filter().trim() 99 | 100 | if (!value) { 101 | return 102 | } 103 | 104 | let picks = App.history_picks 105 | picks = picks.filter(x => x !== value) 106 | picks.unshift(value) 107 | picks = picks.slice(0, App.max_history_picks) 108 | App.history_picks = picks 109 | App.stor_save_history_picks() 110 | let tb_mode = App.get_tab_box_mode() 111 | 112 | if ([`history`].includes(tb_mode)) { 113 | App.update_tab_box() 114 | } 115 | } 116 | 117 | App.forget_history_pick = (value) => { 118 | App.history_picks = App.history_picks.filter(x => x !== value) 119 | App.stor_save_history_picks() 120 | let tb_mode = App.get_tab_box_mode() 121 | 122 | if ([`history`].includes(tb_mode)) { 123 | App.update_tab_box() 124 | } 125 | } 126 | 127 | App.move_history_pick = (from, to) => { 128 | let picks = App.history_picks 129 | let from_index = picks.indexOf(from) 130 | let to_index = picks.indexOf(to) 131 | 132 | if ((from_index === -1) || (to_index === -1)) { 133 | return 134 | } 135 | 136 | picks.splice(from_index, 1) 137 | picks.splice(to_index, 0, from) 138 | App.history_picks = picks 139 | App.stor_save_history_picks() 140 | App.update_tab_box() 141 | } -------------------------------------------------------------------------------- /js/main/hover_button.js: -------------------------------------------------------------------------------- 1 | App.create_hover_button = (item, side) => { 2 | if (side !== App.get_setting(`hover_button_side`)) { 3 | return 4 | } 5 | 6 | let btn = DOM.create(`div`, `hover_button`) 7 | btn.textContent = App.get_setting(`hover_button_icon`) || App.command_icon 8 | item.element.append(btn) 9 | } 10 | 11 | App.show_hover_button_menu = (item, e) => { 12 | let name 13 | let mode = item?.mode || App.active_mode 14 | let mode_menu = App.get_setting(`hover_button_menu_${mode}`) 15 | 16 | if (mode_menu.length) { 17 | name = `hover_button_menu_${mode}` 18 | } 19 | else { 20 | let global = App.get_setting(`hover_button_menu`) 21 | 22 | if (global.length) { 23 | name = `hover_button_menu` 24 | } 25 | } 26 | 27 | if (!name) { 28 | return 29 | } 30 | 31 | let items = App.custom_menu_items({name, item}) 32 | let compact = App.get_setting(`compact_hover_button_menu`) 33 | App.show_context({items, e, compact}) 34 | } 35 | 36 | App.hover_button_middle_click = (item, e) => { 37 | let cmd = App.get_setting(`middle_click_hover_button`) 38 | App.run_command({cmd, item, from: `hover_button`, e}) 39 | } 40 | 41 | App.toggle_hover_button = (item, e) => { 42 | let sett = App.get_setting(`show_hover_button`) 43 | App.set_setting({setting: `show_hover_button`, value: !sett}) 44 | App.toggle_message(`Hover Btn`, `show_hover_button`) 45 | App.set_hover_button_vars() 46 | } -------------------------------------------------------------------------------- /js/main/input.js: -------------------------------------------------------------------------------- 1 | App.show_input = (args = {}) => { 2 | let def_args = { 3 | value: ``, 4 | bottom: false, 5 | left: false, 6 | wrap: false, 7 | } 8 | 9 | App.def_args(def_args, args) 10 | 11 | let on_enter = () => { 12 | if (!args.action) { 13 | return 14 | } 15 | 16 | let ans = args.action(DOM.el(`#textarea_text`).value.trim()) 17 | 18 | if (ans) { 19 | App.close_textarea() 20 | } 21 | } 22 | 23 | let buttons = [ 24 | { 25 | text: `Close`, 26 | action: () => { 27 | App.close_textarea() 28 | }, 29 | }, 30 | { 31 | text: `Clear`, 32 | action: () => { 33 | App.show_confirm({ 34 | message: `Clear text?`, 35 | confirm_action: () => { 36 | App.clear_textarea() 37 | }, 38 | }) 39 | }, 40 | }, 41 | { 42 | text: args.button, 43 | action: () => { 44 | on_enter() 45 | }, 46 | }, 47 | ] 48 | 49 | let on_dismiss 50 | 51 | if (args.autosave) { 52 | on_dismiss = () => { 53 | on_enter() 54 | } 55 | } 56 | 57 | App.show_textarea({ 58 | on_enter, 59 | title: args.title, 60 | title_icon: args.title_icon, 61 | left: args.left, 62 | bottom: args.bottom, 63 | wrap: args.wrap, 64 | text: args.value, 65 | readonly: false, 66 | on_dismiss, 67 | buttons, 68 | }) 69 | } -------------------------------------------------------------------------------- /js/main/lock_screen.js: -------------------------------------------------------------------------------- 1 | App.start_lock_screen = () => { 2 | if (App.check_ready(`lock_screen`)) { 3 | return 4 | } 5 | 6 | App.create_window({ 7 | id: `lock_screen`, 8 | setup: () => { 9 | let unlock = DOM.el(`#unlock_screen`) 10 | 11 | DOM.ev(unlock, `click`, () => { 12 | App.unlock_screen() 13 | }) 14 | }, 15 | after_show: () => { 16 | App.screen_locked = true 17 | let cmd = App.get_setting(`lock_screen_command`) 18 | App.run_command({cmd, from: `lock_screen`}) 19 | }, 20 | after_hide: () => { 21 | App.screen_locked = false 22 | let cmd = App.get_setting(`unlock_screen_command`) 23 | App.run_command({cmd, from: `lock_screen`}) 24 | }, 25 | colored_top: true, 26 | }) 27 | } 28 | 29 | App.lock_screen = () => { 30 | App.start_lock_screen() 31 | App.hide_window() 32 | 33 | let img_el = DOM.el(`#lock_screen_image`) 34 | 35 | if (App.get_setting(`empty_lock_screen`)) { 36 | DOM.hide(img_el) 37 | } 38 | else { 39 | let img_src = App.get_setting(`lock_screen_image`) 40 | 41 | if (!img_src) { 42 | img_src = `img/lock.jpg` 43 | } 44 | 45 | img_el.src = img_src 46 | DOM.show(img_el) 47 | } 48 | 49 | App.show_window(`lock_screen`) 50 | } 51 | 52 | App.unlock_screen = () => { 53 | let pw = App.get_setting(`lock_screen_password`) 54 | 55 | if (pw) { 56 | App.show_prompt({ 57 | password: true, 58 | placeholder: `Password`, 59 | on_submit: (ans) => { 60 | if (ans === pw) { 61 | App.hide_window() 62 | } 63 | }, 64 | }) 65 | 66 | return 67 | } 68 | 69 | App.hide_window() 70 | } -------------------------------------------------------------------------------- /js/main/main_menu.js: -------------------------------------------------------------------------------- 1 | App.create_main_button = (mode) => { 2 | let btn = DOM.create(`div`, `button main_button icon_button`, `${mode}_main_menu`) 3 | let click = App.get_cmd_name(`show_main_menu`) 4 | let rclick = App.get_cmd_name(`show_palette`) 5 | 6 | if (App.tooltips()) { 7 | btn.title = `Click: ${click}\nRight Click: ${rclick}` 8 | App.trigger_title(btn, `middle_click_main_button`) 9 | App.trigger_title(btn, `click_press_main_button`) 10 | App.trigger_title(btn, `middle_click_press_main_button`) 11 | App.trigger_title(btn, `wheel_up_main_button`) 12 | App.trigger_title(btn, `wheel_down_main_button`) 13 | } 14 | 15 | App.check_show_button(`main`, btn) 16 | App.set_main_menu_text(btn, mode) 17 | return btn 18 | } 19 | 20 | App.show_main_menu = (mode) => { 21 | let items = [] 22 | 23 | for (let m of App.modes) { 24 | let icon = App.mode_icon(m) 25 | let name = App.get_mode_name(m, true) 26 | 27 | // This could be done with cmds but the mouse action 28 | // and direct call to do show mode allows the permission prompts 29 | // to appear to access History and Bookmarks modes 30 | // It also allows more nuanced opts like 'selected' 31 | 32 | items.push({ 33 | icon, 34 | text: name, 35 | action: () => { 36 | if (m === `bookmarks`) { 37 | App.reset_bookmarks() 38 | } 39 | 40 | App.do_show_mode({mode: m, reuse_filter: false, force: true}) 41 | }, 42 | selected: m === mode, 43 | }) 44 | } 45 | 46 | App.sep(items) 47 | items.push(App.cmd_item({cmd: `show_settings`, middle: `export_settings`, short: true})) 48 | App.sep(items) 49 | items.push(App.cmd_item({cmd: `show_signals`, short: true})) 50 | items.push(App.cmd_item({cmd: `edit_global_notes`, short: true})) 51 | items.push(App.cmd_item({cmd: `show_stuff_menu`, short: true})) 52 | items.push(App.cmd_item({cmd: `show_about`, short: true})) 53 | App.sep(items) 54 | items.push(App.cmd_item({cmd: `lock_screen`, short: true})) 55 | items.push(App.cmd_item({cmd: `show_palette`, short: true})) 56 | App.sep(items) 57 | items.push(App.cmd_item({cmd: `focus_window_menu`, short: true})) 58 | 59 | let btn = DOM.el(`#${mode}_main_menu`) 60 | 61 | App.show_context({ 62 | element: btn, 63 | items, 64 | expand: true, 65 | margin: btn.clientHeight, 66 | }) 67 | } 68 | 69 | App.set_main_menu_text = (btn, mode, name = ``) => { 70 | let icon = App.mode_icon(mode) 71 | 72 | if (name) { 73 | name = name.substring(0, 12).trim() 74 | } 75 | else { 76 | name = App.get_mode_name(mode, true) 77 | } 78 | 79 | let value = App.button_text(icon, name) 80 | btn.innerHTML = `` 81 | btn.append(value) 82 | } 83 | 84 | App.main_button_middle_click = (e) => { 85 | let cmd = App.get_setting(`middle_click_main_button`) 86 | App.run_command({cmd, from: `main_menu`, e}) 87 | } -------------------------------------------------------------------------------- /js/main/menubutton.js: -------------------------------------------------------------------------------- 1 | const Menubutton = {} 2 | 3 | Menubutton.create = (args = {}) => { 4 | let def_args = { 5 | wrap: true, 6 | } 7 | 8 | App.def_args(def_args, args) 9 | 10 | if (!args.button) { 11 | args.button = DOM.create(`div`, `menubutton button`, args.id) 12 | } 13 | 14 | args.container = DOM.create(`div`, `menubutton_container`) 15 | args.button.draggable = true 16 | 17 | args.button.ondragstart = (e) => { 18 | e.dataTransfer.setData(`text/plain`, args.button.id) 19 | } 20 | 21 | let prev = DOM.create(`div`, `button arrow_button`) 22 | prev.textContent = `<` 23 | let next = DOM.create(`div`, `button arrow_button`) 24 | next.textContent = `>` 25 | 26 | DOM.ev(args.button, `click`, () => { 27 | args.show() 28 | }) 29 | 30 | DOM.ev(args.button, `auxclick`, (e) => { 31 | if (e.button === 1) { 32 | if (args.on_middle_click) { 33 | args.on_middle_click() 34 | } 35 | else { 36 | args.select_first() 37 | } 38 | } 39 | }) 40 | 41 | args.refresh_opts = () => { 42 | if (args.source) { 43 | args.opts = args.source() 44 | } 45 | } 46 | 47 | args.refresh_opts() 48 | 49 | args.set = (value, on_change = true) => { 50 | args.action(Menubutton.opt(args, value), on_change) 51 | } 52 | 53 | args.prev = () => { 54 | Menubutton.cycle(args, `prev`) 55 | } 56 | 57 | args.next = () => { 58 | Menubutton.cycle(args, `next`) 59 | } 60 | 61 | args.select_first = () => { 62 | args.refresh_opts() 63 | args.action(args.opts[0]) 64 | } 65 | 66 | args.action = (opt, on_change = true) => { 67 | if (!opt) { 68 | return 69 | } 70 | 71 | Menubutton.set_text(args, opt) 72 | 73 | if (on_change) { 74 | if (args.on_change) { 75 | args.on_change(args, opt) 76 | } 77 | } 78 | } 79 | 80 | args.show = () => { 81 | args.refresh_opts() 82 | let items = [] 83 | 84 | for (let opt of args.opts) { 85 | if (opt.text === App.separator_string) { 86 | App.sep(items) 87 | continue 88 | } 89 | 90 | items.push({ 91 | icon: opt.icon, 92 | text: opt.text, 93 | info: opt.info, 94 | image: opt.image, 95 | action: () => { 96 | args.action(opt) 97 | }, 98 | }) 99 | } 100 | 101 | let index = 0 102 | 103 | if (args.get_value) { 104 | let i = 0 105 | let value = args.get_value() 106 | 107 | for (let o of args.opts) { 108 | if (!o.value) { 109 | continue 110 | } 111 | 112 | if (o.value === value) { 113 | index = i 114 | break 115 | } 116 | 117 | i += 1 118 | } 119 | } 120 | 121 | App.show_context({ 122 | element: args.button, 123 | items, 124 | expand: true, 125 | index, 126 | margin: args.button.clientHeight, 127 | after_dismiss: args.after_dismiss, 128 | after_hide: args.after_hide, 129 | after_action: args.after_action, 130 | after_shift_action: args.after_shift_action, 131 | after_ctrl_action: args.after_ctrl_action, 132 | after_alt_action: args.after_alt_action, 133 | after_middle_action: args.after_middle_action, 134 | }) 135 | } 136 | 137 | args.first_set = false 138 | 139 | args.refresh_button = () => { 140 | if (args.selected !== undefined) { 141 | for (let opt of args.opts) { 142 | if (args.selected === opt.value) { 143 | Menubutton.set_text(args, opt) 144 | args.first_set = true 145 | break 146 | } 147 | } 148 | } 149 | } 150 | 151 | args.refresh_button() 152 | DOM.ev(prev, `click`, args.prev) 153 | DOM.ev(next, `click`, args.next) 154 | args.container.append(prev) 155 | args.container.append(next) 156 | args.button.after(args.container) 157 | prev.after(args.button) 158 | return args 159 | } 160 | 161 | Menubutton.set_text = (args, opt) => { 162 | args.button.innerHTML = `` 163 | 164 | if (opt.icon) { 165 | let icon = DOM.create(`div`, `menupanel_icon`) 166 | let icon_s = App.clone_if_node(opt.icon) 167 | icon.append(icon_s) 168 | args.button.append(icon) 169 | } 170 | 171 | let text = DOM.create(`div`) 172 | text.append(opt.text) 173 | args.button.append(text) 174 | args.value = opt.value 175 | } 176 | 177 | Menubutton.cycle = (args, dir) => { 178 | let waypoint = false 179 | let opts = args.opts.slice(0) 180 | 181 | if (dir === `prev`) { 182 | opts.reverse() 183 | } 184 | 185 | let opt 186 | 187 | if (args.wrap) { 188 | opt = opts[0] 189 | } 190 | 191 | for (let o of opts) { 192 | if (o.text === App.separator_string) { 193 | continue 194 | } 195 | 196 | if (waypoint) { 197 | opt = o 198 | break 199 | } 200 | 201 | if (o.value === args.value) { 202 | waypoint = true 203 | } 204 | } 205 | 206 | if (opt) { 207 | args.action(opt) 208 | } 209 | } 210 | 211 | Menubutton.opt = (args, value) => { 212 | for (let opt of args.opts) { 213 | if (opt.value === value) { 214 | return opt 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /js/main/messages.js: -------------------------------------------------------------------------------- 1 | App.setup_messages = () => { 2 | browser.runtime.onMessage.addListener(async (message) => { 3 | if (message.action === `mirror_settings`) { 4 | if (App.get_setting(`mirror_settings`)) { 5 | await App.stor_get_settings() 6 | App.refresh_settings() 7 | App.clear_show() 8 | } 9 | } 10 | else if (message.action === `mirror_edits`) { 11 | if (App.get_setting(`mirror_edits`)) { 12 | let item = App.get_item_by_id(`tabs`, message.id) 13 | App.check_tab_session([item], true) 14 | } 15 | } 16 | else if (message.action === `browser_command`) { 17 | App.run_browser_command(message.number) 18 | } 19 | else if (message.action === `popup_command`) { 20 | App.run_popup_command(message.number) 21 | } 22 | else if (message.action === `refresh_bookmarks`) { 23 | App.bookmarks_received = true 24 | App.bookmark_items_cache = message.items 25 | App.bookmark_folders_cache = message.folders 26 | 27 | if (message.show_mode) { 28 | App.do_show_mode({mode: `bookmarks`, force: true}) 29 | } 30 | } 31 | }) 32 | } -------------------------------------------------------------------------------- /js/main/mute.js: -------------------------------------------------------------------------------- 1 | App.mute_tab = async (id) => { 2 | try { 3 | await browser.tabs.update(id, {muted: true}) 4 | } 5 | catch (err) { 6 | App.error(err) 7 | } 8 | } 9 | 10 | App.unmute_tab = async (id) => { 11 | try { 12 | await browser.tabs.update(id, {muted: false}) 13 | } 14 | catch (err) { 15 | App.error(err) 16 | } 17 | } 18 | 19 | App.mute_tabs = (item) => { 20 | let items = [] 21 | 22 | for (let it of App.get_active_items({mode: `tabs`, item})) { 23 | if (!it.muted) { 24 | items.push(it) 25 | } 26 | } 27 | 28 | if (!items.length) { 29 | return 30 | } 31 | 32 | let force = App.check_warn(`warn_on_mute_tabs`, items) 33 | let ids = items.map(x => x.id) 34 | 35 | App.show_confirm({ 36 | message: `Mute items? (${ids.length})`, 37 | confirm_action: async () => { 38 | for (let id of ids) { 39 | App.mute_tab(id) 40 | } 41 | }, 42 | force, 43 | }) 44 | } 45 | 46 | App.unmute_tabs = (item) => { 47 | let items = [] 48 | 49 | for (let it of App.get_active_items({mode: `tabs`, item})) { 50 | if (it.muted) { 51 | items.push(it) 52 | } 53 | } 54 | 55 | if (!items.length) { 56 | return 57 | } 58 | 59 | let force = App.check_warn(`warn_on_unmute_tabs`, items) 60 | let ids = items.map(x => x.id) 61 | 62 | App.show_confirm({ 63 | message: `Unmute items? (${ids.length})`, 64 | confirm_action: async () => { 65 | for (let id of ids) { 66 | App.unmute_tab(id) 67 | } 68 | }, 69 | force, 70 | }) 71 | } 72 | 73 | App.toggle_mute_tabs = (item) => { 74 | let ids = [] 75 | let action 76 | 77 | for (let it of App.get_active_items({mode: `tabs`, item})) { 78 | if (!action) { 79 | if (it.muted) { 80 | action = `unmute` 81 | } 82 | else { 83 | action = `mute` 84 | } 85 | } 86 | 87 | if (action === `mute`) { 88 | if (it.muted) { 89 | continue 90 | } 91 | } 92 | else if (action === `unmute`) { 93 | if (!it.muted) { 94 | continue 95 | } 96 | } 97 | 98 | ids.push(it.id) 99 | } 100 | 101 | if (!ids.length) { 102 | return 103 | } 104 | 105 | for (let id of ids) { 106 | if (action === `mute`) { 107 | App.mute_tab(id) 108 | } 109 | else { 110 | App.unmute_tab(id) 111 | } 112 | } 113 | } 114 | 115 | App.mute_playing_tabs = () => { 116 | let items = App.get_playing_tabs() 117 | 118 | if (!items.length) { 119 | return 120 | } 121 | 122 | let force = App.check_warn(`warn_on_mute_tabs`, items) 123 | let ids = items.map(x => x.id) 124 | 125 | App.show_confirm({ 126 | message: `Mute items? (${ids.length})`, 127 | confirm_action: async () => { 128 | for (let id of ids) { 129 | App.mute_tab(id) 130 | } 131 | }, 132 | force, 133 | }) 134 | } 135 | 136 | App.mute_all_tabs = () => { 137 | App.change_all_tabs({ 138 | items: App.get_unmuted_tabs(), 139 | warn: `warn_on_mute_tabs`, 140 | message: `Mute items`, 141 | action: App.mute_tab, 142 | }) 143 | } 144 | 145 | App.unmute_all_tabs = () => { 146 | App.change_all_tabs({ 147 | items: App.get_muted_tabs(), 148 | warn: `warn_on_unmute_tabs`, 149 | message: `Unmute items`, 150 | action: App.unmute_tab, 151 | }) 152 | } -------------------------------------------------------------------------------- /js/main/notes.js: -------------------------------------------------------------------------------- 1 | App.edit_notes = (item) => { 2 | App.show_input({ 3 | title: `Tab Notes`, 4 | title_icon: App.notepad_icon, 5 | button: `Save`, 6 | action: (text) => { 7 | let notes = App.single_linebreak(text) 8 | let active = App.get_active_items({mode: item.mode, item}) 9 | 10 | for (let it of active) { 11 | if (it.rule_notes) { 12 | if (it.rule_notes === notes) { 13 | continue 14 | } 15 | } 16 | 17 | App.apply_edit({what: `notes`, item: it, value: notes, on_change: (value) => { 18 | App.custom_save(it.id, `notes`, value) 19 | }}) 20 | } 21 | 22 | return true 23 | }, 24 | value: App.get_notes(item), 25 | autosave: true, 26 | left: true, 27 | bottom: true, 28 | wrap: true, 29 | readonly: item.mode !== `tabs`, 30 | }) 31 | } 32 | 33 | App.remove_item_notes = (item, single = false) => { 34 | let active 35 | 36 | if (single) { 37 | active = [item] 38 | } 39 | else { 40 | active = App.get_active_items({mode: item.mode, item}) 41 | } 42 | 43 | if (active.length === 1) { 44 | let it = active[0] 45 | 46 | if (it.rule_notes && !it.custom_notes) { 47 | App.domain_rule_message() 48 | return 49 | } 50 | } 51 | 52 | App.remove_edits({ 53 | what: [`notes`], 54 | items: active, 55 | text: `notes`, 56 | force_ask: true, 57 | }) 58 | } 59 | 60 | App.remove_notes = (item) => { 61 | App.remove_item_notes(item, true) 62 | } 63 | 64 | App.edit_global_notes = () => { 65 | App.show_input({ 66 | title: `Global Notes`, 67 | title_icon: App.notepad_icon, 68 | button: `Save`, 69 | action: (text) => { 70 | App.notes = App.single_linebreak(text) 71 | App.stor_save_notes() 72 | return true 73 | }, 74 | value: App.notes, 75 | autosave: true, 76 | left: true, 77 | bottom: true, 78 | wrap: true, 79 | }) 80 | } 81 | 82 | App.get_noted_items = (mode) => { 83 | let items = [] 84 | 85 | for (let item of App.get_items(mode)) { 86 | if (App.get_notes(item)) { 87 | items.push(item) 88 | } 89 | } 90 | 91 | return items 92 | } -------------------------------------------------------------------------------- /js/main/obfuscate.js: -------------------------------------------------------------------------------- 1 | App.obfuscate_tabs = (item) => { 2 | let items = App.get_active_items({mode: item.mode, item}) 3 | let force = App.check_warn(`warn_on_obfuscate_tabs`, items) 4 | 5 | App.show_confirm({ 6 | message: `Obfuscate tabs? (${items.length})`, 7 | confirm_action: () => { 8 | for (let it of items) { 9 | App.obfuscate_tab(it) 10 | } 11 | }, 12 | force, 13 | }) 14 | } 15 | 16 | App.obfuscate_tab = (item) => { 17 | if (App.get_obfuscated(item)) { 18 | return 19 | } 20 | 21 | App.apply_edit({what: `obfuscated`, item, value: true, on_change: (value) => { 22 | App.custom_save(item.id, `obfuscated`, value) 23 | }}) 24 | } 25 | 26 | App.deobfuscate_tabs = (item) => { 27 | let items = App.get_active_items({mode: item.mode, item}) 28 | let force = App.check_warn(`warn_on_deobfuscate_tabs`, items) 29 | 30 | if (items.length === 1) { 31 | if (item.rule_obfuscated) { 32 | App.domain_rule_message() 33 | return 34 | } 35 | } 36 | 37 | App.show_confirm({ 38 | message: `Deobfuscate tabs? (${items.length})`, 39 | confirm_action: () => { 40 | for (let it of items) { 41 | App.deobfuscate_tab(it) 42 | } 43 | }, 44 | force, 45 | }) 46 | } 47 | 48 | App.deobfuscate_tab = (item) => { 49 | if (!App.get_obfuscated(item)) { 50 | return 51 | } 52 | 53 | App.apply_edit({what: `obfuscated`, item, value: false, on_change: (value) => { 54 | App.custom_save(item.id, `obfuscated`, value) 55 | }}) 56 | } 57 | 58 | App.toggle_obfuscate_tabs = (item) => { 59 | if (App.get_obfuscated(item)) { 60 | App.deobfuscate_tabs(item) 61 | } 62 | else { 63 | App.obfuscate_tabs(item) 64 | } 65 | } 66 | 67 | App.check_obfuscated = (item) => { 68 | if (App.get_obfuscated(item)) { 69 | item.element.classList.add(`obfuscated`) 70 | } 71 | else { 72 | item.element.classList.remove(`obfuscated`) 73 | } 74 | } 75 | 76 | App.obfuscate_text = (text) => { 77 | let symbol = App.get_setting(`obfuscate_symbol`) 78 | return text.replace(/./g, symbol) 79 | } -------------------------------------------------------------------------------- /js/main/other.js: -------------------------------------------------------------------------------- 1 | App.check_first_time = () => { 2 | if (!App.first_time.date) { 3 | App.show_intro_message() 4 | App.first_time.date = App.now() 5 | App.stor_save_first_time() 6 | } 7 | } 8 | 9 | App.show_intro_message = () => { 10 | let s = `Hi there 11 | The main menu is the top-left button 12 | Check out the settings 13 | I constantly experiment and change stuff, so expect things to break.` 14 | 15 | let text = App.periods(s) 16 | 17 | let buttons = [ 18 | { 19 | text: `About`, 20 | action: () => { 21 | App.close_textarea() 22 | App.show_about() 23 | }, 24 | }, 25 | { 26 | text: `Settings`, 27 | action: () => { 28 | App.close_textarea() 29 | App.show_settings_category(`general`) 30 | }, 31 | }, 32 | { 33 | text: `Close`, 34 | action: () => { 35 | App.close_textarea() 36 | }, 37 | }, 38 | ] 39 | 40 | let image = `img/grasshopper.png` 41 | 42 | App.show_textarea({title: `Welcome`, 43 | text, 44 | simple: true, 45 | buttons, 46 | align: `left`, 47 | image, 48 | }) 49 | } 50 | 51 | App.restart_extension = () => { 52 | browser.runtime.reload() 53 | } 54 | 55 | App.print_intro = () => { 56 | let d = App.now() 57 | let s = String.raw` 58 | //_____ __ 59 | @ )====// .\___ 60 | \#\_\__(_/_\\_/ 61 | / / \\ 62 | ` 63 | App.log(s.trim(), `green`) 64 | App.log(`Starting ${App.manifest.name} v${App.manifest.version}`) 65 | App.log(`${App.nice_date(d, true)} | ${d}`) 66 | } 67 | 68 | App.check_ready = (what) => { 69 | let s = `${what}_ready` 70 | 71 | if (App[s]) { 72 | return true 73 | } 74 | 75 | App[s] = true 76 | return false 77 | } 78 | 79 | App.reset_triggers = () => { 80 | App.reset_keyboard() 81 | App.reset_mouse() 82 | } 83 | 84 | App.button_text = (icon, text, bigger = false) => { 85 | let cls = `button_text` 86 | 87 | if (bigger) { 88 | cls += ` button_text_bigger` 89 | } 90 | 91 | let container = DOM.create(`div`, cls) 92 | 93 | if (icon) { 94 | let icon_el = DOM.create(`div`, `button_text_icon flex_row_center`) 95 | icon_el.append(icon) 96 | container.append(icon_el) 97 | } 98 | 99 | if (text) { 100 | let text_el = DOM.create(`div`, `button_text_text flex_row_center`) 101 | text_el.append(text) 102 | container.append(text_el) 103 | } 104 | 105 | return container 106 | } 107 | 108 | App.tooltip = (str) => { 109 | return App.clean_lines(App.single_space(str)) 110 | } 111 | 112 | App.periods = (str) => { 113 | return App.tooltip(str).split(`\n`).join(`. `) 114 | } 115 | 116 | App.ask_permission = async (what) => { 117 | let perm 118 | 119 | try { 120 | perm = await browser.permissions.request({permissions: [what]}) 121 | } 122 | catch (err) { 123 | perm = await browser.permissions.contains({permissions: [what]}) 124 | } 125 | 126 | return perm 127 | } 128 | 129 | App.permission_msg = (what) => { 130 | let s1 = `${what} permission is required.` 131 | let s2 = `Open the top left menu and click on the mode you want to enable` 132 | App.alert(`${s1} ${s2}`) 133 | } 134 | 135 | App.main = () => { 136 | return DOM.el(`#main`) 137 | } 138 | 139 | App.main_add = (cls) => { 140 | App.main().classList.add(cls) 141 | } 142 | 143 | App.main_remove = (cls) => { 144 | App.main().classList.remove(cls) 145 | } 146 | 147 | App.main_has = (cls) => { 148 | return App.main().classList.contains(cls) 149 | } 150 | 151 | App.open_sidebar = () => { 152 | browser.sidebarAction.open() 153 | } 154 | 155 | App.close_sidebar = () => { 156 | browser.sidebarAction.close() 157 | } 158 | 159 | App.toggle_sidebar = () => { 160 | browser.sidebarAction.toggle() 161 | } 162 | 163 | App.generate_password = () => { 164 | let password = App.random_string(App.password_length) 165 | 166 | App.show_textarea({ 167 | title: `Random Password`, 168 | text: password, 169 | simple: true, 170 | monospace: true, 171 | }) 172 | } 173 | 174 | App.play_sound = (name) => { 175 | if (!App.get_setting(`sound_effects`)) { 176 | return 177 | } 178 | 179 | let pname = `audio_player_${name}` 180 | 181 | if (!App[pname]) { 182 | App[pname] = new Audio(`audio/${name}.mp3`) 183 | } 184 | 185 | let player = App[pname] 186 | player.pause() 187 | player.currentTime = 0 188 | player.play() 189 | } 190 | 191 | App.check_caps = (text) => { 192 | if (App.get_setting(`all_caps`)) { 193 | text = text.toUpperCase() 194 | } 195 | 196 | return text 197 | } 198 | 199 | App.show_flashlight = () => { 200 | App.flashlight = DOM.create(`div`, ``, `flashlight`) 201 | document.body.appendChild(App.flashlight) 202 | App.flashlight_on = true 203 | 204 | DOM.ev(App.flashlight, `click`, () => { 205 | App.turn_flashlight_off() 206 | }) 207 | } 208 | 209 | App.turn_flashlight_off = () => { 210 | App.flashlight.remove() 211 | App.flashlight_on = false 212 | App.flashlight = undefined 213 | } 214 | 215 | App.check_show_button = (name, btn) => { 216 | let hide = !App.get_setting(`show_${name}_button`) 217 | 218 | if (hide) { 219 | DOM.hide(btn, 2) 220 | } 221 | else { 222 | DOM.show(btn, 2) 223 | } 224 | } 225 | 226 | App.tooltips = () => { 227 | return App.get_setting(`show_tooltips`) 228 | } 229 | 230 | App.toggle_message = (msg, setting) => { 231 | let sett = App.get_setting(setting) 232 | let what = sett ? `Enabled` : `Disabled` 233 | App.footer_message(`${msg} ${what}`) 234 | } -------------------------------------------------------------------------------- /js/main/palette.js: -------------------------------------------------------------------------------- 1 | App.start_palette = () => { 2 | if (App.check_ready(`palette`)) { 3 | return 4 | } 5 | 6 | App.create_popup({ 7 | id: `palette`, 8 | setup: () => { 9 | App.fill_palette() 10 | let container = DOM.el(`#palette_items`) 11 | 12 | DOM.ev(container, `click`, (e) => { 13 | App.palette_action(e.target) 14 | }) 15 | 16 | DOM.ev(`#palette_filter`, `input`, () => { 17 | App.filter_palette() 18 | }) 19 | }, 20 | }) 21 | 22 | App.filter_palette_debouncer = App.create_debouncer(() => { 23 | App.do_filter_palette() 24 | }, App.filter_delay_2) 25 | 26 | DOM.ev(`#palette_filter_clear`, `click`, () => { 27 | App.reset_generic_filter(`palette`) 28 | }) 29 | } 30 | 31 | App.show_palette = (prefilter = ``) => { 32 | App.start_palette() 33 | App.setup_popup(`palette`) 34 | let container = DOM.el(`#palette_items`) 35 | let filter = DOM.el(`#palette_filter`) 36 | let els = DOM.els(`.palette_item`, container) 37 | let active = App.get_active_items() 38 | let too_many = active.length > App.max_command_check_items 39 | let num = 0 40 | 41 | for (let el of els) { 42 | if (el.dataset.name.startsWith(`!`)) { 43 | DOM.hide(el) 44 | } 45 | else { 46 | DOM.show(el) 47 | } 48 | 49 | let command = App.get_command(el.dataset.command) 50 | 51 | if (too_many || App.check_command(command, {from: `palette`})) { 52 | DOM.show(el, 2) 53 | num += 1 54 | } 55 | else { 56 | DOM.hide(el, 2) 57 | } 58 | } 59 | 60 | filter.placeholder = `Command (${num})` 61 | App.hide_context() 62 | App.show_popup(`palette`) 63 | App.palette_select_first() 64 | container.scrollTop = 0 65 | filter.value = prefilter 66 | filter.focus() 67 | 68 | if (prefilter) { 69 | App.do_filter_palette() 70 | } 71 | } 72 | 73 | App.hide_palette = () => { 74 | App.hide_popup(`palette`) 75 | } 76 | 77 | App.palette_select = (el) => { 78 | let container = DOM.el(`#palette_items`) 79 | let els = DOM.els(`.palette_item`, container) 80 | 81 | for (let el of els) { 82 | el.classList.remove(`palette_selected`) 83 | } 84 | 85 | App.palette_selected = el 86 | App.palette_selected.classList.add(`palette_selected`) 87 | App.palette_selected.scrollIntoView({block: `nearest`}) 88 | } 89 | 90 | App.palette_item_hidden = (el) => { 91 | return DOM.class(el, [`hidden`, `hidden_2`]) 92 | } 93 | 94 | App.palette_select_first = () => { 95 | let container = DOM.el(`#palette_items`) 96 | let els = DOM.els(`.palette_item`, container) 97 | 98 | for (let el of els) { 99 | if (!App.palette_item_hidden(el)) { 100 | App.palette_select(el) 101 | break 102 | } 103 | } 104 | } 105 | 106 | App.palette_next = (reverse = false) => { 107 | let container = DOM.el(`#palette_items`) 108 | let els = DOM.els(`.palette_item`, container) 109 | 110 | if (els.length < 2) { 111 | return 112 | } 113 | 114 | let waypoint = false 115 | 116 | if (reverse) { 117 | els.reverse() 118 | } 119 | 120 | let first 121 | 122 | for (let el of els) { 123 | if (!App.palette_item_hidden(el)) { 124 | if (waypoint) { 125 | App.palette_select(el) 126 | return 127 | } 128 | 129 | if (!first) { 130 | first = el 131 | } 132 | } 133 | 134 | if (el === App.palette_selected) { 135 | waypoint = true 136 | } 137 | } 138 | 139 | if (first) { 140 | App.palette_select(first) 141 | } 142 | } 143 | 144 | App.palette_enter = () => { 145 | App.palette_action(App.palette_selected) 146 | } 147 | 148 | App.palette_action = (el) => { 149 | if (!el) { 150 | return 151 | } 152 | 153 | let item = DOM.parent(el, [`.palette_item`]) 154 | 155 | if (!item) { 156 | return 157 | } 158 | 159 | let cmd = item.dataset.command 160 | 161 | if (cmd) { 162 | App.hide_all_popups() 163 | App.update_command_history(cmd) 164 | App.fill_palette() 165 | App.run_command({cmd, from: `palette`}) 166 | } 167 | } 168 | 169 | App.fill_palette = () => { 170 | if (!App.palette_ready) { 171 | return 172 | } 173 | 174 | let container = DOM.el(`#palette_items`) 175 | container.innerHTML = `` 176 | let num = 0 177 | 178 | for (let cmd of App.command_palette_items) { 179 | if (cmd.skip_palette) { 180 | continue 181 | } 182 | 183 | let el = DOM.create(`div`, `palette_item action filter_item filter_text`) 184 | el.dataset.command = cmd.cmd 185 | el.dataset.name = cmd.name 186 | 187 | if (cmd.icon) { 188 | let icon = DOM.create(`div`, `palette_icon`) 189 | icon.append(cmd.icon) 190 | el.append(icon) 191 | } 192 | 193 | let name = DOM.create(`div`) 194 | name.append(cmd.name) 195 | el.append(name) 196 | el.title = `${cmd.info}\nKey: ${cmd.cmd}` 197 | container.append(el) 198 | num += 1 199 | } 200 | } 201 | 202 | App.palette_filter_focused = () => { 203 | return document.activeElement.id === `palette_filter` 204 | } 205 | 206 | App.clear_palette_filter = () => { 207 | if (App.filter_has_value(`palette`)) { 208 | App.reset_generic_filter(`palette`) 209 | } 210 | else { 211 | App.hide_all_popups() 212 | } 213 | } 214 | 215 | App.filter_palette = () => { 216 | App.filter_palette_debouncer.call() 217 | } 218 | 219 | App.do_filter_palette = () => { 220 | App.filter_palette_debouncer.cancel() 221 | App.palette_selected = undefined 222 | App.do_filter_2(`palette`) 223 | App.palette_select_first() 224 | } -------------------------------------------------------------------------------- /js/main/pinline.js: -------------------------------------------------------------------------------- 1 | App.setup_pinline = () => { 2 | App.pinline_debouncer = App.create_debouncer(() => { 3 | App.do_check_pinline() 4 | }, App.pinline_delay) 5 | } 6 | 7 | App.check_pinline = () => { 8 | App.pinline_debouncer.call() 9 | } 10 | 11 | App.do_check_pinline = () => { 12 | App.pinline_debouncer.cancel() 13 | let show = App.get_setting(`show_pinline`) 14 | App.debug(`Checking pinline`) 15 | App.remove_pinline() 16 | let tabs = App.divide_tabs(`visible`) 17 | let cls = `element tabs_element glowbox` 18 | 19 | if (show === `auto`) { 20 | if (!App.get_setting(`show_pinned_tabs`)) { 21 | if (!App.is_filtered(`tabs`)) { 22 | show = `never` 23 | } 24 | } 25 | } 26 | 27 | if (!App.tabs_normal()) { 28 | show = `never` 29 | } 30 | 31 | App.pinline_visible = true 32 | 33 | if (show === `never`) { 34 | App.pinline_visible = false 35 | } 36 | else if (show === `auto`) { 37 | if (!tabs.pinned_f.length || !tabs.normal_f.length) { 38 | App.pinline_visible = false 39 | } 40 | } 41 | else if (!tabs.pinned_f.length && !tabs.normal_f.length) { 42 | App.pinline_visible = false 43 | } 44 | 45 | if (!App.pinline_visible) { 46 | cls += ` hidden` 47 | } 48 | 49 | let pinline = DOM.create(`div`, cls, `pinline`) 50 | let icons = App.get_setting(`pinline_icons`) 51 | let n1 = tabs.pinned_f.length 52 | let n2 = tabs.normal_f.length 53 | let s1 = App.check_caps(App.plural(n1, `Pin`, `Pins`)) 54 | let s2 = App.check_caps(`Normal`) 55 | let left, right 56 | 57 | if (icons) { 58 | let pin_icon = App.get_setting(`pin_icon`) 59 | let normal_icon = App.get_setting(`normal_icon`) 60 | left = `${pin_icon} ${n1} ${s1}` 61 | right = `${normal_icon} ${n2} ${s2}` 62 | } 63 | else { 64 | left = `${n1} ${s1}` 65 | right = `${n2} ${s2}` 66 | } 67 | 68 | let sep = `  +  ` 69 | pinline.innerHTML = `${left}${sep}${right}` 70 | 71 | if (App.tooltips()) { 72 | pinline.title = `This is the Pinline.\nPinned tabs above. Normal tabs below` 73 | App.trigger_title(pinline, `click_pinline`) 74 | App.trigger_title(pinline, `middle_click_pinline`) 75 | App.trigger_title(pinline, `click_press_pinline`) 76 | App.trigger_title(pinline, `middle_click_press_pinline`) 77 | App.trigger_title(pinline, `wheel_up_pinline`) 78 | App.trigger_title(pinline, `wheel_down_pinline`) 79 | 80 | if (App.get_setting(`pinline_drag`)) { 81 | pinline.title += `\nDrag to define pin delimeter` 82 | } 83 | } 84 | 85 | if (App.get_setting(`pinline_drag`)) { 86 | pinline.draggable = true 87 | } 88 | 89 | if (tabs.pinned_f.length) { 90 | tabs.pinned_f.at(-1).element.after(pinline) 91 | } 92 | else if (tabs.normal_f.length) { 93 | tabs.normal_f.at(0).element.before(pinline) 94 | } 95 | } 96 | 97 | App.remove_pinline = () => { 98 | let pinline = DOM.el(`#pinline`) 99 | 100 | if (pinline) { 101 | pinline.remove() 102 | } 103 | } 104 | 105 | App.pinline_index = (only_visible = false) => { 106 | if (only_visible && !App.pinline_visible) { 107 | return -1 108 | } 109 | 110 | return DOM.els(`.tabs_element`).indexOf(DOM.el(`#pinline`)) 111 | } 112 | 113 | App.show_pinline_menu = (e) => { 114 | let items = App.custom_menu_items({ 115 | name: `pinline_menu`, 116 | }) 117 | 118 | let compact = App.get_setting(`compact_pinline_menu`) 119 | App.show_context({items, e, compact}) 120 | } 121 | 122 | App.pinline_click = (e) => { 123 | let cmd = App.get_setting(`click_pinline`) 124 | App.run_command({cmd, from: `pinline`, e}) 125 | } 126 | 127 | App.pinline_double_click = (e) => { 128 | let cmd = App.get_setting(`double_click_pinline`) 129 | App.run_command({cmd, from: `pinline`, e}) 130 | } 131 | 132 | App.pinline_middle_click = (e) => { 133 | let cmd = App.get_setting(`middle_click_pinline`) 134 | App.run_command({cmd, from: `pinline`, e}) 135 | } 136 | 137 | App.check_pinline_change = () => { 138 | let pinline_index = App.pinline_index() 139 | let to_pin = [] 140 | let to_unpin = [] 141 | 142 | for (let [i, item] of App.get_items(`tabs`).entries()) { 143 | if (i < pinline_index) { 144 | if (!item.pinned) { 145 | to_pin.push(item) 146 | } 147 | } 148 | else if (item.pinned) { 149 | to_unpin.push(item) 150 | } 151 | } 152 | 153 | for (let item of to_pin) { 154 | App.pin_tab(item.id) 155 | } 156 | 157 | for (let item of to_unpin) { 158 | App.unpin_tab(item.id) 159 | } 160 | } -------------------------------------------------------------------------------- /js/main/pins.js: -------------------------------------------------------------------------------- 1 | App.pin_tab = async (id) => { 2 | try { 3 | await browser.tabs.update(id, {pinned: true}) 4 | } 5 | catch (err) { 6 | App.error(err) 7 | } 8 | } 9 | 10 | App.unpin_tab = async (id) => { 11 | try { 12 | await browser.tabs.update(id, {pinned: false}) 13 | } 14 | catch (err) { 15 | App.error(err) 16 | } 17 | } 18 | 19 | App.pin_tabs = (item, force = false) => { 20 | let items = [] 21 | 22 | for (let it of App.get_active_items({mode: `tabs`, item})) { 23 | if (it.pinned) { 24 | continue 25 | } 26 | 27 | items.push(it) 28 | } 29 | 30 | if (!items.length) { 31 | return 32 | } 33 | 34 | if (!force) { 35 | force = App.check_warn(`warn_on_pin_tabs`, items) 36 | } 37 | 38 | let ids = items.map(x => x.id) 39 | 40 | App.show_confirm({ 41 | message: `Pin items? (${ids.length})`, 42 | confirm_action: async () => { 43 | for (let id of ids) { 44 | App.pin_tab(id) 45 | } 46 | }, 47 | force, 48 | }) 49 | } 50 | 51 | App.unpin_tabs = (item, force = false) => { 52 | let items = [] 53 | 54 | for (let it of App.get_active_items({mode: `tabs`, item})) { 55 | if (!it.pinned) { 56 | continue 57 | } 58 | 59 | items.push(it) 60 | } 61 | 62 | if (!items.length) { 63 | return 64 | } 65 | 66 | if (!force) { 67 | force = App.check_warn(`warn_on_unpin_tabs`, items) 68 | } 69 | 70 | let ids = items.map(x => x.id) 71 | 72 | App.show_confirm({ 73 | message: `Unpin items? (${ids.length})`, 74 | confirm_action: async () => { 75 | for (let id of ids) { 76 | App.unpin_tab(id) 77 | } 78 | }, 79 | force, 80 | }) 81 | } 82 | 83 | App.toggle_pin = (item) => { 84 | if (item.pinned) { 85 | App.unpin_tab(item.id) 86 | } 87 | else { 88 | App.pin_tab(item.id) 89 | } 90 | } 91 | 92 | App.toggle_pin_tabs = (item) => { 93 | let items = [] 94 | let action 95 | 96 | for (let it of App.get_active_items({mode: `tabs`, item})) { 97 | if (!action) { 98 | if (it.pinned) { 99 | action = `unpin` 100 | } 101 | else { 102 | action = `pin` 103 | } 104 | } 105 | 106 | if (action === `pin`) { 107 | if (it.pinned) { 108 | continue 109 | } 110 | } 111 | else if (action === `unpin`) { 112 | if (!it.pinned) { 113 | continue 114 | } 115 | } 116 | 117 | items.push(it) 118 | } 119 | 120 | if (!items.length) { 121 | return 122 | } 123 | 124 | let force = App.check_warn(`warn_on_pin_tabs`, items) 125 | let ids = items.map(x => x.id) 126 | let msg = `` 127 | 128 | if (action === `pin`) { 129 | msg = `Pin items?` 130 | } 131 | else { 132 | msg = `Unpin items?` 133 | } 134 | 135 | msg += ` (${ids.length})` 136 | 137 | App.show_confirm({ 138 | message: msg, 139 | confirm_action: async () => { 140 | for (let id of ids) { 141 | if (action === `pin`) { 142 | App.pin_tab(id) 143 | } 144 | else { 145 | App.unpin_tab(id) 146 | } 147 | } 148 | }, 149 | force, 150 | }) 151 | } 152 | 153 | App.get_last_pin_index = () => { 154 | let i = -1 155 | 156 | for (let item of App.get_items(`tabs`)) { 157 | if (item.pinned) { 158 | i += 1 159 | } 160 | else { 161 | return i 162 | } 163 | } 164 | 165 | return i 166 | } 167 | 168 | App.new_pin_tab = () => { 169 | App.open_new_tab({pinned: true}) 170 | } 171 | 172 | App.toggle_show_pins = () => { 173 | let og = App.get_setting(`show_pinned_tabs`) 174 | App.set_setting({setting: `show_pinned_tabs`, value: !og}) 175 | 176 | if (!og) { 177 | App.show_all_pins() 178 | } 179 | 180 | App.do_filter({mode: App.active_mode}) 181 | App.toggle_message(`Pins`, `show_pinned_tabs`) 182 | } 183 | 184 | App.get_first_pinned_tab = () => { 185 | let items = App.get_items(`tabs`) 186 | return items.find(x => x.pinned) 187 | } 188 | 189 | App.first_pinned_tab = () => { 190 | let first = App.get_first_pinned_tab() 191 | 192 | if (first) { 193 | App.tabs_action({item: first}) 194 | } 195 | } 196 | 197 | App.get_last_pinned_tab = () => { 198 | let items = App.get_items(`tabs`) 199 | return items.slice(0).reverse().find(x => x.pinned) 200 | } 201 | 202 | App.last_pinned_tab = () => { 203 | let last = App.get_last_pinned_tab() 204 | 205 | if (last) { 206 | App.tabs_action({item: last}) 207 | } 208 | } 209 | 210 | App.show_all_pins = () => { 211 | for (let item of App.get_items(`tabs`)) { 212 | if (item.pinned) { 213 | App.show_item_2(item) 214 | } 215 | } 216 | } 217 | 218 | App.pin_all_tabs = () => { 219 | App.change_all_tabs({ 220 | items: App.get_normal_tabs(), 221 | warn: `warn_on_pin_tabs`, 222 | message: `Pin items`, 223 | action: App.pin_tab, 224 | }) 225 | } 226 | 227 | App.unpin_all_tabs = () => { 228 | App.change_all_tabs({ 229 | items: App.get_pinned_tabs(), 230 | warn: `warn_on_unpin_tabs`, 231 | message: `Unpin items`, 232 | action: App.unpin_tab, 233 | }) 234 | } -------------------------------------------------------------------------------- /js/main/playing.js: -------------------------------------------------------------------------------- 1 | App.setup_playing = () => { 2 | App.check_playing_debouncer = App.create_debouncer((mode) => { 3 | App.do_check_playing(mode) 4 | }, App.check_playing_delay) 5 | } 6 | 7 | App.create_playing_button = (mode) => { 8 | let btn = DOM.create(`div`, `button icon_button playing_button hidden`, `playing_button_${mode}`) 9 | let rclick = App.get_cmd_name(`show_playing_tabs`) 10 | 11 | if (App.tooltips()) { 12 | App.trigger_title(btn, `click_playing_button`) 13 | btn.title += `\nRight Click: ${rclick}` 14 | App.trigger_title(btn, `middle_click_playing_button`) 15 | App.trigger_title(btn, `click_press_playing_button`) 16 | App.trigger_title(btn, `middle_click_press_playing_button`) 17 | App.trigger_title(btn, `wheel_up_playing_button`) 18 | App.trigger_title(btn, `wheel_down_playing_button`) 19 | } 20 | 21 | App.check_show_button(`playing`, btn) 22 | let icon = App.get_svg_icon(`speaker`) 23 | btn.append(icon) 24 | return btn 25 | } 26 | 27 | App.show_playing = (mode) => { 28 | App.playing = true 29 | DOM.show(`#playing_button_${mode}`) 30 | App.on_playing_change() 31 | } 32 | 33 | App.hide_playing = (mode) => { 34 | App.playing = false 35 | DOM.hide(`#playing_button_${mode}`) 36 | App.on_playing_change() 37 | } 38 | 39 | App.on_playing_change = () => { 40 | let tb_mode = App.get_tab_box_mode() 41 | 42 | if ([`playing`].includes(tb_mode)) { 43 | App.update_tab_box() 44 | } 45 | } 46 | 47 | App.check_playing = () => { 48 | App.check_playing_debouncer.call(App.active_mode) 49 | } 50 | 51 | App.do_check_playing = (mode = App.active_mode, force = false) => { 52 | let playing = App.get_playing_tabs() 53 | 54 | if (playing.length) { 55 | if (!App.playing || force) { 56 | App.show_playing(mode) 57 | App.check_tab_box_playing() 58 | } 59 | } 60 | else if (App.playing) { 61 | App.hide_playing(mode) 62 | } 63 | } 64 | 65 | App.get_playing_tabs = () => { 66 | return App.get_items(`tabs`).filter(x => x.playing) 67 | } 68 | 69 | App.playing_click = (e) => { 70 | let cmd = App.get_setting(`click_playing_button`) 71 | App.run_command({cmd, from: `playing`, e}) 72 | } 73 | 74 | App.playing_middle_click = (e) => { 75 | let cmd = App.get_setting(`middle_click_playing_button`) 76 | App.run_command({cmd, from: `playing`, e}) 77 | } -------------------------------------------------------------------------------- /js/main/popups.js: -------------------------------------------------------------------------------- 1 | App.create_popup = (args) => { 2 | let p = {} 3 | p.setup_done = false 4 | let popup = DOM.create(`div`, `popup_main hidden`, `popup_${args.id}`) 5 | let container = DOM.create(`div`, `popup_container`, `popup_${args.id}_container`) 6 | container.tabIndex = 0 7 | 8 | if (args.no_padding) { 9 | container.classList.add(`no_padding`) 10 | } 11 | 12 | if (args.element) { 13 | container.innerHTML = `` 14 | container.append(args.element) 15 | } 16 | else { 17 | container.innerHTML = App.get_template(args.id) 18 | } 19 | 20 | DOM.ev(popup, `click`, (e) => { 21 | if (e.target === popup) { 22 | p.dismiss() 23 | } 24 | }) 25 | 26 | DOM.ev(popup, `contextmenu`, (e) => { 27 | if (e.target === popup) { 28 | e.preventDefault() 29 | } 30 | }) 31 | 32 | popup.append(container) 33 | App.main().append(popup) 34 | p.element = popup 35 | 36 | p.setup = () => { 37 | if (args.setup && !p.setup_done) { 38 | args.setup() 39 | p.setup_done = true 40 | App.debug(`Popup Setup: ${args.id}`) 41 | } 42 | } 43 | 44 | p.show = () => { 45 | p.setup() 46 | DOM.show(p.element) 47 | container.focus() 48 | p.open = true 49 | } 50 | 51 | p.hide = (bypass = false) => { 52 | if (!p.open) { 53 | return 54 | } 55 | 56 | if (!bypass && args.on_hide) { 57 | args.on_hide(args.id) 58 | } 59 | else { 60 | DOM.hide(p.element) 61 | p.open = false 62 | 63 | if (args.after_hide) { 64 | args.after_hide(args.id) 65 | } 66 | } 67 | } 68 | 69 | p.dismiss = () => { 70 | App.popups[args.id].hide() 71 | 72 | if (args.on_dismiss) { 73 | args.on_dismiss() 74 | } 75 | } 76 | 77 | App.popups[args.id] = p 78 | } 79 | 80 | App.show_popup = (id) => { 81 | clearTimeout(App.alert_timeout) 82 | App.popups[id].show() 83 | App.popups[id].show_date = App.now() 84 | let open = App.open_popups() 85 | 86 | open.sort((a, b) => { 87 | return a.show_date < b.show_date ? -1 : 1 88 | }) 89 | 90 | let zindex = 99999 91 | 92 | for (let popup of open) { 93 | popup.element.style.zIndex = zindex 94 | zindex += 1 95 | } 96 | } 97 | 98 | App.setup_popup = (id) => { 99 | App.popups[id].setup() 100 | } 101 | 102 | App.start_popups = () => { 103 | if (App.check_ready(`popups`)) { 104 | return 105 | } 106 | 107 | App.create_popup({ 108 | id: `alert`, 109 | }) 110 | 111 | App.create_popup({ 112 | id: `textarea`, 113 | setup: () => { 114 | DOM.ev(`#textarea_copy`, `click`, () => { 115 | App.textarea_copy() 116 | }) 117 | 118 | DOM.ev(`#textarea_close`, `click`, () => { 119 | App.hide_popup(`textarea`) 120 | }) 121 | }, 122 | on_dismiss: () => { 123 | App.on_textarea_dismiss() 124 | }, 125 | no_padding: true, 126 | }) 127 | 128 | App.create_popup({ 129 | id: `dialog`, 130 | on_dismiss: () => { 131 | if (App.dialog_on_dismiss) { 132 | App.dialog_on_dismiss() 133 | } 134 | }, 135 | }) 136 | 137 | App.create_popup({ 138 | id: `prompt`, 139 | setup: () => { 140 | App.setup_prompt() 141 | }, 142 | on_dismiss: () => { 143 | App.on_prompt_dismiss() 144 | }, 145 | after_hide: () => { 146 | App.check_popup_command_close() 147 | }, 148 | }) 149 | } 150 | 151 | App.hide_all_popups = () => { 152 | clearTimeout(App.alert_timeout) 153 | 154 | for (let id of App.open_popup_list()) { 155 | App.popups[id].hide() 156 | } 157 | } 158 | 159 | App.hide_popup = (id, bypass = false) => { 160 | if (App.popups[id]) { 161 | App.popups[id].hide(bypass) 162 | } 163 | } 164 | 165 | App.open_popup_list = () => { 166 | let open = [] 167 | 168 | for (let id in App.popups) { 169 | if (App.popups[id].open) { 170 | open.push(id) 171 | } 172 | } 173 | 174 | return open 175 | } 176 | 177 | App.popup_is_open = (id, exact = true) => { 178 | for (let pid of App.open_popup_list()) { 179 | if (exact) { 180 | if (pid === id) { 181 | return true 182 | } 183 | } 184 | else if (pid.startsWith(id)) { 185 | return true 186 | } 187 | } 188 | 189 | return false 190 | } 191 | 192 | App.popup_open = () => { 193 | for (let key in App.popups) { 194 | if (App.popups[key].open) { 195 | return true 196 | } 197 | } 198 | 199 | return false 200 | } 201 | 202 | App.open_popups = () => { 203 | let open = [] 204 | 205 | for (let popup in App.popups) { 206 | if (App.popups[popup].open) { 207 | open.push(App.popups[popup]) 208 | } 209 | } 210 | 211 | return open 212 | } 213 | 214 | App.dismiss_popup = (id) => { 215 | App.popups[id].dismiss() 216 | } 217 | 218 | App.popup_mode = () => { 219 | let highest_z = 0 220 | let pmode 221 | 222 | for (let popup of App.open_popup_list()) { 223 | let z = parseInt(App.popups[popup].element.style.zIndex) 224 | 225 | if (z > highest_z) { 226 | highest_z = z 227 | pmode = popup 228 | } 229 | } 230 | 231 | return pmode 232 | } -------------------------------------------------------------------------------- /js/main/process.js: -------------------------------------------------------------------------------- 1 | App.process_info_list = (mode, info_list) => { 2 | let container = DOM.el(`#${mode}_container`) 3 | App[`${mode}_idx`] = 0 4 | 5 | if (!App.persistent_modes.includes(mode)) { 6 | App.clear_items(mode) 7 | } 8 | 9 | let items = App.get_items(mode) 10 | let exclude = [] 11 | 12 | if (mode === `bookmarks`) { 13 | if (App.bookmark_folders_enabled()) { 14 | if (App.get_setting(`bookmark_folders_above`)) { 15 | info_list.sort((a, b) => a.type === `folder` ? -1 : b.type === `folder`) 16 | } 17 | } 18 | } 19 | 20 | let zones_locked = App.zones_locked(mode) 21 | 22 | for (let info of info_list) { 23 | let item = App.process_info({ 24 | mode, 25 | info, 26 | exclude, 27 | list: true, 28 | add_parent: false, 29 | }) 30 | 31 | if (!item) { 32 | continue 33 | } 34 | 35 | if (item.header && zones_locked) { 36 | continue 37 | } 38 | 39 | if (mode !== `tabs`) { 40 | exclude.push(item.url) 41 | } 42 | 43 | items.push(item) 44 | container.append(item.element) 45 | } 46 | 47 | if (mode === `tabs`) { 48 | for (let item of items) { 49 | App.add_tab_parent(item) 50 | } 51 | } 52 | 53 | App.update_footer_count() 54 | App.do_check_pinline() 55 | 56 | if (mode === `tabs`) { 57 | App.check_tab_session() 58 | App.update_tab_box() 59 | } 60 | } 61 | 62 | App.process_info = (args = {}) => { 63 | let def_args = { 64 | exclude: [], 65 | list: false, 66 | add_parent: true, 67 | } 68 | 69 | App.def_args(def_args, args) 70 | 71 | if (!args.info) { 72 | return false 73 | } 74 | 75 | let special = false 76 | 77 | if (args.o_item) { 78 | if (!args.url) { 79 | args.info = {...args.o_item.original_data, ...args.info} 80 | } 81 | 82 | args.o_item.original_data = args.info 83 | } 84 | else if (args.mode === `bookmarks`) { 85 | if (args.info.type === `folder`) { 86 | args.info = {...args.info} 87 | args.info.url = `${App.bookmarks_folder_url}/${args.info.id}` 88 | args.info.favIconUrl = `img/folder.jpg` 89 | args.info.title = `Folder: ${args.info.title}` 90 | special = true 91 | } 92 | } 93 | 94 | let decoded_url 95 | 96 | if (args.info.url) { 97 | try { 98 | // Check if valid URL 99 | decoded_url = decodeURI(args.info.url) 100 | } 101 | catch (err) { 102 | return false 103 | } 104 | } 105 | 106 | let url = App.format_url(args.info.url || ``) 107 | 108 | if (args.exclude.includes(url)) { 109 | return false 110 | } 111 | 112 | let path = App.get_path(decoded_url) 113 | let protocol = App.get_protocol(url) 114 | let hostname = App.get_hostname(url) 115 | let title = args.info.title || `` 116 | let image = App.is_image(url) 117 | let video = App.is_video(url) 118 | let audio = App.is_audio(url) 119 | 120 | let item = { 121 | title, 122 | url, 123 | path, 124 | protocol, 125 | hostname, 126 | favicon: args.info.favIconUrl, 127 | mode: args.mode, 128 | window_id: args.info.windowId, 129 | session_id: args.info.sessionId, 130 | decoded_url, 131 | image, 132 | video, 133 | audio, 134 | special, 135 | is_item: true, 136 | header: false, 137 | } 138 | 139 | if (args.mode === `tabs`) { 140 | item.active = args.info.active 141 | item.pinned = args.info.pinned 142 | item.playing = args.info.audible 143 | item.muted = args.info.mutedInfo.muted 144 | item.unloaded = args.info.discarded 145 | item.last_access = args.info.lastAccessed 146 | item.status = args.info.status 147 | item.parent = args.info.openerTabId 148 | item.container_name = args.info.container_name 149 | item.container_color = args.info.container_color 150 | item.header = App.is_header_url(item.url) 151 | } 152 | else if (args.mode === `history`) { 153 | item.last_visit = args.info.lastVisitTime 154 | } 155 | else if (args.mode === `bookmarks`) { 156 | item.parent_id = args.info.parentId 157 | item.date_added = args.info.dateAdded 158 | item.type = args.info.type 159 | } 160 | 161 | App.check_rules(item) 162 | 163 | if (args.o_item) { 164 | args.o_item = Object.assign(args.o_item, item) 165 | App.refresh_item_element(args.o_item) 166 | App.refresh_tab_box_element(args.o_item) 167 | 168 | if (App.get_selected(args.mode) === args.o_item) { 169 | App.update_footer_info(args.o_item) 170 | } 171 | } 172 | else { 173 | if (!args.list) { 174 | if ((args.mode === `tabs`) && !item.active && item.parent) { 175 | item.unread = true 176 | } 177 | } 178 | 179 | item.original_data = args.info 180 | item.id = args.info.id || App[`${args.mode}_idx`] 181 | item.visible = true 182 | item.selected = false 183 | item.tab_box = false 184 | item.last_scroll = 0 185 | 186 | App.create_empty_item_element(item) 187 | 188 | if (App.get_setting(`fill_elements`)) { 189 | App.create_item_element(item) 190 | } 191 | 192 | if (args.mode === `tabs`) { 193 | if (args.add_parent) { 194 | App.add_tab_parent(item) 195 | } 196 | } 197 | 198 | App[`${args.mode}_idx`] += 1 199 | } 200 | 201 | return item 202 | } 203 | 204 | App.process_search_item = (info) => { 205 | info.path = App.get_path(info.url || `https://no.url`) 206 | } -------------------------------------------------------------------------------- /js/main/prompt.js: -------------------------------------------------------------------------------- 1 | App.setup_prompt = () => { 2 | DOM.ev(`#prompt_submit`, `click`, () => { 3 | App.prompt_submit() 4 | }) 5 | 6 | DOM.ev(`#prompt_clear`, `click`, () => { 7 | App.prompt_clear() 8 | }) 9 | 10 | DOM.ev(`#prompt_list`, `click`, (e) => { 11 | App.show_prompt_list() 12 | }) 13 | 14 | DOM.ev(`#prompt_fill`, `click`, (e) => { 15 | App.fill_prompt() 16 | }) 17 | 18 | DOM.el(`#prompt_list`).textContent = App.smiley_icon 19 | } 20 | 21 | App.show_prompt = (args = {}) => { 22 | let def_args = { 23 | value: ``, 24 | placeholder: ``, 25 | suggestions: [], 26 | list_submit: false, 27 | word_mode: false, 28 | unique_words: false, 29 | ignore_words: [], 30 | append: false, 31 | show_list: false, 32 | password: false, 33 | list: [], 34 | fill: ``, 35 | } 36 | 37 | App.def_args(def_args, args) 38 | App.start_popups() 39 | App.set_prompt_suggestions(args.suggestions) 40 | App.show_popup(`prompt`) 41 | let input = DOM.el(`#prompt_input`) 42 | input.value = args.value 43 | input.placeholder = args.placeholder 44 | let prompt_mode = App.get_setting(`prompt_mode`) 45 | 46 | if (args.password) { 47 | input.type = `password` 48 | } 49 | else { 50 | input.type = `text` 51 | } 52 | 53 | let list = DOM.el(`#prompt_list`) 54 | 55 | if (args.list.length) { 56 | DOM.show(list) 57 | } 58 | else { 59 | DOM.hide(list) 60 | } 61 | 62 | if (args.fill) { 63 | DOM.show(`#prompt_fill`) 64 | } 65 | else { 66 | DOM.hide(`#prompt_fill`) 67 | } 68 | 69 | App.prompt_fill = args.fill 70 | App.prompt_args = args 71 | input.focus() 72 | 73 | if (args.show_list) { 74 | App.show_prompt_list(`show_list`) 75 | } 76 | 77 | if ((prompt_mode === `highlight`) && !args.show_list) { 78 | input.select() 79 | } 80 | else { 81 | App.input_deselect(input) 82 | 83 | if (prompt_mode === `at_start`) { 84 | App.input_at_start(input) 85 | } 86 | else if (prompt_mode === `at_end`) { 87 | App.input_at_end(input) 88 | } 89 | } 90 | } 91 | 92 | App.prompt_submit = () => { 93 | let value = DOM.el(`#prompt_input`).value 94 | value = App.single_space(value).trim() 95 | App.hide_popup(`prompt`) 96 | App.prompt_args.on_submit(value) 97 | } 98 | 99 | App.on_prompt_dismiss = () => { 100 | if (App.prompt_args.on_dismiss) { 101 | App.prompt_args.on_dismiss() 102 | } 103 | } 104 | 105 | App.set_prompt_suggestions = (suggestions) => { 106 | let c = DOM.el(`#prompt_suggestions`) 107 | c.innerHTML = `` 108 | 109 | for (let suggestion of suggestions) { 110 | let option = DOM.create(`option`) 111 | option.innerHTML = suggestion 112 | c.append(option) 113 | } 114 | } 115 | 116 | App.prompt_clear = () => { 117 | let input = DOM.el(`#prompt_input`) 118 | input.value = `` 119 | input.focus() 120 | } 121 | 122 | App.show_prompt_list = (from = `click`) => { 123 | let args = App.prompt_args 124 | let input = DOM.el(`#prompt_input`) 125 | let valid = [] 126 | 127 | if (args.word_mode) { 128 | let words = input.value.split(` `).map(x => x.trim()) 129 | 130 | for (let item of args.list) { 131 | if (words.includes(item)) { 132 | continue 133 | } 134 | 135 | if (args.ignore_words.includes(item)) { 136 | continue 137 | } 138 | 139 | valid.push(item) 140 | } 141 | } 142 | else { 143 | for (let item of args.list) { 144 | if (input.value === item) { 145 | continue 146 | } 147 | 148 | valid.push(item) 149 | } 150 | } 151 | 152 | if (!valid.length) { 153 | if (from === `click`) { 154 | App.alert(`No more items to add`) 155 | } 156 | 157 | return 158 | } 159 | 160 | let items = [] 161 | 162 | for (let item of valid) { 163 | items.push({ 164 | text: item, 165 | action: () => { 166 | if (args.append) { 167 | input.value += ` ${item}` 168 | } 169 | else { 170 | input.value = item 171 | } 172 | 173 | input.value = App.single_space(input.value).trim() 174 | 175 | if (args.unique_words) { 176 | let words = input.value.split(` `).map(x => x.trim()) 177 | let unique = Array.from(new Set(words)) 178 | input.value = unique.join(` `) 179 | } 180 | 181 | if (args.list_submit) { 182 | App.prompt_submit() 183 | } 184 | }, 185 | }) 186 | } 187 | 188 | let btn = DOM.el(`#prompt_list`) 189 | 190 | App.show_context({ 191 | items, 192 | element: btn, 193 | after_hide: () => { 194 | input.focus() 195 | 196 | if (args.highlight) { 197 | input.select() 198 | } 199 | }, 200 | }) 201 | } 202 | 203 | // Fill the prompt with the fill value if any 204 | App.fill_prompt = () => { 205 | if (App.prompt_fill) { 206 | let input = DOM.el(`#prompt_input`) 207 | input.value = App.prompt_fill 208 | input.focus() 209 | } 210 | } -------------------------------------------------------------------------------- /js/main/recent_tabs.js: -------------------------------------------------------------------------------- 1 | App.setup_recent_tabs = () => { 2 | let delay = App.get_setting(`recent_tabs_delay`) 3 | 4 | App.empty_previous_tabs_debouncer = App.create_debouncer(() => { 5 | App.do_empty_previous_tabs() 6 | }, delay) 7 | } 8 | 9 | App.empty_previous_tabs = () => { 10 | App.empty_previous_tabs_debouncer.call() 11 | } 12 | 13 | App.do_empty_previous_tabs = () => { 14 | App.empty_previous_tabs_debouncer.cancel() 15 | App.previous_tabs = [] 16 | } 17 | 18 | App.get_recent_tabs = (args = {}) => { 19 | let def_args = { 20 | active: true, 21 | headers: false, 22 | max: 0, 23 | } 24 | 25 | App.def_args(def_args, args) 26 | let tabs = App.get_items(`tabs`).slice(0) 27 | 28 | tabs.sort((a, b) => { 29 | return a.last_access > b.last_access ? -1 : 1 30 | }) 31 | 32 | if (!args.active) { 33 | tabs = tabs.filter(x => !x.active) 34 | } 35 | 36 | if (!args.headers) { 37 | tabs = tabs.filter(x => !x.header) 38 | } 39 | 40 | if (args.max > 0) { 41 | tabs = tabs.slice(0, args.max) 42 | } 43 | 44 | return tabs 45 | } 46 | 47 | App.get_previous_tabs = () => { 48 | App.previous_tabs = App.get_recent_tabs() 49 | 50 | if (!App.get_setting(`jump_unloaded`)) { 51 | App.previous_tabs = App.previous_tabs.filter(x => !x.unloaded) 52 | } 53 | 54 | if (App.previous_tabs.length > 1) { 55 | let first_tab = App.previous_tabs.shift() 56 | App.previous_tabs.push(first_tab) 57 | } 58 | 59 | App.previous_tabs_index = -1 60 | } 61 | 62 | App.go_to_previous_tab = (reverse = false) => { 63 | if (!App.previous_tabs.length) { 64 | App.get_previous_tabs() 65 | } 66 | 67 | App.empty_previous_tabs() 68 | 69 | if (App.previous_tabs.length <= 1) { 70 | return 71 | } 72 | 73 | let items = App.previous_tabs.slice(0) 74 | 75 | if (reverse) { 76 | App.previous_tabs_index -= 1 77 | } 78 | else { 79 | App.previous_tabs_index += 1 80 | } 81 | 82 | if (App.previous_tabs_index > (items.length - 1)) { 83 | App.previous_tabs_index = 0 84 | } 85 | else if (App.previous_tabs_index < 0) { 86 | App.previous_tabs_index = items.length - 1 87 | } 88 | 89 | let prev = items[App.previous_tabs_index] 90 | 91 | if (prev) { 92 | App.tabs_action({item: prev, from: `previous`, scroll: `center_instant`}) 93 | } 94 | } -------------------------------------------------------------------------------- /js/main/restore.js: -------------------------------------------------------------------------------- 1 | App.check_restore = () => { 2 | if (App.get_setting(`auto_restore`) === `action`) { 3 | if (App.last_restore_date > 0) { 4 | if ((App.now() - App.last_restore_date) < App.restore_delay) { 5 | return 6 | } 7 | } 8 | 9 | App.last_restore_date = App.now() 10 | App.update_filter_history() 11 | return App.restore() 12 | } 13 | } 14 | 15 | App.start_auto_restore = () => { 16 | App.clear_restore() 17 | let d = App.get_setting(`auto_restore`) 18 | 19 | if ((d === `never`) || (d === `action`)) { 20 | return 21 | } 22 | 23 | let delay = App.parse_delay(d) 24 | 25 | App.restore_timeout = setTimeout(() => { 26 | App.restore() 27 | }, delay) 28 | } 29 | 30 | App.restore = () => { 31 | App.hide_context() 32 | App.hide_all_popups() 33 | 34 | if (!App.on_items()) { 35 | if (App.on_settings()) { 36 | App.hide_window() 37 | return false 38 | } 39 | 40 | App.hide_window() 41 | } 42 | 43 | let mode = App.active_mode 44 | 45 | if (mode !== App.main_mode()) { 46 | App.show_main_mode(mode) 47 | return true 48 | } 49 | else if (App.is_filtered(mode)) { 50 | App.filter_all(mode) 51 | return true 52 | } 53 | } 54 | 55 | App.clear_restore = () => { 56 | clearTimeout(App.restore_timeout) 57 | } -------------------------------------------------------------------------------- /js/main/root_urls.js: -------------------------------------------------------------------------------- 1 | App.edit_tab_root = (args = {}) => { 2 | let def_args = { 3 | root: ``, 4 | } 5 | 6 | App.def_args(def_args, args) 7 | let active = App.get_active_items({mode: args.item.mode, item: args.item}) 8 | let s = args.root ? `Edit root?` : `Remove root?` 9 | let force = App.check_warn(`warn_on_edit_tabs`, active) 10 | 11 | App.show_confirm({ 12 | message: `${s} (${active.length})`, 13 | confirm_action: () => { 14 | for (let it of active) { 15 | App.apply_edit({what: `root`, item: it, value: args.root, on_change: (value) => { 16 | App.custom_save(it.id, `root`, value) 17 | }}) 18 | } 19 | }, 20 | force, 21 | }) 22 | } 23 | 24 | App.edit_root_url = (item) => { 25 | let value = App.get_root(item) || item.hostname 26 | App.edit_prompt({what: `root`, item, value, url: true}) 27 | } 28 | 29 | App.get_all_roots = (include_rules = true) => { 30 | let roots = [] 31 | 32 | for (let item of App.get_items(`tabs`)) { 33 | if (item.custom_root) { 34 | if (!roots.includes(item.custom_root)) { 35 | roots.push(item.custom_root) 36 | } 37 | } 38 | 39 | if (include_rules) { 40 | if (item.rule_root) { 41 | if (!roots.includes(item.rule_root)) { 42 | roots.push(item.rule_root) 43 | } 44 | } 45 | } 46 | } 47 | 48 | return roots 49 | } 50 | 51 | App.go_to_root_url = (item, focus = false) => { 52 | let active = App.get_active_items({mode: item.mode, item}) 53 | 54 | for (let it of active) { 55 | it.root = App.get_root(it) 56 | 57 | if (!it.root) { 58 | continue 59 | } 60 | 61 | App.change_url(it, it.root) 62 | } 63 | 64 | if (focus && (active.length === 1)) { 65 | App.tabs_action({item}) 66 | } 67 | } 68 | 69 | App.remove_root_url = (item) => { 70 | let active = App.get_active_items({mode: item.mode, item}) 71 | 72 | if (active.length === 1) { 73 | let it = active[0] 74 | 75 | if (it.rule_root && !it.custom_root) { 76 | App.domain_rule_message() 77 | return 78 | } 79 | } 80 | 81 | App.remove_edits({what: [`root`], items: active, text: `roots`}) 82 | } 83 | 84 | App.root_possible = (item) => { 85 | let root = App.get_root(item) 86 | 87 | if (!root) { 88 | return false 89 | } 90 | 91 | return !App.urls_match(item.url, root) 92 | } 93 | 94 | App.item_has_root = (item) => { 95 | return Boolean(App.get_root(item)) 96 | } 97 | 98 | App.get_root_items = (mode) => { 99 | let items = [] 100 | 101 | for (let item of App.get_items(mode)) { 102 | if (App.get_root(item)) { 103 | items.push(item) 104 | } 105 | } 106 | 107 | return items 108 | } -------------------------------------------------------------------------------- /js/main/sort.js: -------------------------------------------------------------------------------- 1 | App.start_sort_tabs = () => { 2 | if (App.check_ready(`sort_tabs`)) { 3 | return 4 | } 5 | 6 | App.create_popup({ 7 | id: `sort_tabs`, 8 | setup: () => { 9 | DOM.ev(`#sort_tabs_button`, `click`, () => { 10 | App.sort_tabs_action() 11 | }) 12 | }, 13 | }) 14 | } 15 | 16 | App.sort_tabs = () => { 17 | App.start_sort_tabs() 18 | App.show_popup(`sort_tabs`) 19 | DOM.el(`#sort_tabs_pins`).checked = false 20 | DOM.el(`#sort_tabs_normal`).checked = true 21 | DOM.el(`#sort_tabs_reverse`).checked = false 22 | } 23 | 24 | App.do_sort_tabs = () => { 25 | function sort(list, reverse) { 26 | list.sort((a, b) => { 27 | if (a.hostname !== b.hostname) { 28 | if (reverse) { 29 | return a.hostname < b.hostname ? 1 : -1 30 | } 31 | 32 | return a.hostname > b.hostname ? 1 : -1 33 | } 34 | 35 | return a.title < b.title ? -1 : 1 36 | }) 37 | } 38 | 39 | let include_pins = DOM.el(`#sort_tabs_pins`).checked 40 | let include_normal = DOM.el(`#sort_tabs_normal`).checked 41 | 42 | if (!include_pins && !include_normal) { 43 | return 44 | } 45 | 46 | let items = App.get_items(`tabs`).slice(0) 47 | 48 | if (!items.length) { 49 | return 50 | } 51 | 52 | let normal = items.filter(x => !x.pinned) 53 | let pins = items.filter(x => x.pinned) 54 | let num = 0 55 | 56 | if (include_pins) { 57 | num += pins.length 58 | } 59 | 60 | if (include_normal) { 61 | num += normal.length 62 | } 63 | 64 | App.show_confirm({ 65 | message: `Sort tabs? (${num})`, 66 | confirm_action: async () => { 67 | let reverse = DOM.el(`#sort_tabs_reverse`).checked 68 | 69 | if (include_normal) { 70 | sort(normal, reverse) 71 | } 72 | 73 | if (include_pins) { 74 | sort(pins, reverse) 75 | } 76 | 77 | let all = [...pins, ...normal] 78 | App.tabs_locked = true 79 | 80 | for (let [i, item] of all.entries()) { 81 | await App.do_move_tab_index(item.id, i) 82 | } 83 | 84 | App.tabs_locked = false 85 | App.hide_all_popups() 86 | 87 | if (App.tabs_normal()) { 88 | App.clear_all_items() 89 | await App.do_show_mode({mode: `tabs`}) 90 | } 91 | }, 92 | }) 93 | } 94 | 95 | App.sort_tabs_action = () => { 96 | let sort_pins = DOM.el(`#sort_tabs_pins`).checked 97 | App.do_sort_tabs(sort_pins) 98 | } 99 | 100 | App.sort_selected_tabs = async (direction) => { 101 | let items = App.get_active_items({mode: `tabs`}) 102 | 103 | if (!items.length) { 104 | return 105 | } 106 | 107 | let index_top = App.get_item_index(`tabs`, items[0]) 108 | let new_items = items.slice(0) 109 | 110 | if (!App.tabs_in_same_place(new_items)) { 111 | return 112 | } 113 | 114 | if (direction === `asc`) { 115 | new_items.sort((a, b) => { 116 | return a.title.localeCompare(b.title) 117 | }) 118 | } 119 | else if (direction === `desc`) { 120 | new_items.sort((a, b) => { 121 | return b.title.localeCompare(a.title) 122 | }) 123 | } 124 | else if (direction === `reverse`) { 125 | new_items.reverse() 126 | } 127 | 128 | let force = App.check_warn(`warn_on_sort_tabs`, items) 129 | 130 | App.show_confirm({ 131 | message: `Sort tabs? (${new_items.length})`, 132 | confirm_action: async () => { 133 | for (let [i, item] of new_items.entries()) { 134 | let index = index_top + i 135 | await App.do_move_tab_index(item.id, index) 136 | } 137 | }, 138 | force, 139 | }) 140 | } 141 | 142 | App.tabs_normal = () => { 143 | return App.get_setting(`tab_sort`) === `normal` 144 | } 145 | 146 | App.tabs_recent = () => { 147 | return App.get_setting(`tab_sort`) === `recent` 148 | } 149 | 150 | App.toggle_tab_sort = () => { 151 | let value 152 | 153 | if (App.tabs_normal()) { 154 | value = `recent` 155 | } 156 | else if (App.tabs_recent()) { 157 | value = `normal` 158 | } 159 | 160 | App.set_setting({setting: `tab_sort`, value}) 161 | App.clear_items(`tabs`) 162 | App.do_show_mode({mode: `tabs`}) 163 | let name = App.capitalize(value) 164 | App.footer_message(`Sort: ${name}`) 165 | } -------------------------------------------------------------------------------- /js/main/step_back.js: -------------------------------------------------------------------------------- 1 | App.create_step_back_button = (mode) => { 2 | let btn = DOM.create(`div`, `step_back_button button icon_button`, `${mode}_back`) 3 | let rclick = App.get_cmd_name(`show_recent_tabs`) 4 | 5 | if (App.tooltips()) { 6 | App.trigger_title(btn, `click_step_back_button`) 7 | btn.title += `\nRight Click: ${rclick}` 8 | App.trigger_title(btn, `middle_click_step_back_button`) 9 | App.trigger_title(btn, `click_press_step_back_button`) 10 | App.trigger_title(btn, `middle_click_press_step_back_button`) 11 | App.trigger_title(btn, `wheel_up_step_back_button`) 12 | App.trigger_title(btn, `wheel_down_step_back_button`) 13 | } 14 | 15 | App.check_show_button(`step_back`, btn) 16 | btn.append(App.get_svg_icon(`back`)) 17 | return btn 18 | } 19 | 20 | App.step_back = (mode = App.active_mode, e = undefined) => { 21 | let active 22 | let tabs = mode === `tabs` 23 | 24 | if (tabs) { 25 | active = App.get_active_tab_item() 26 | } 27 | 28 | let item = App.get_selected(mode) 29 | let scroll = `center_smooth` 30 | let bookmarks = mode === `bookmarks` 31 | 32 | if (App.multiple_selected(mode)) { 33 | App.deselect({mode, select: `selected`, scroll}) 34 | } 35 | else if (tabs && active && active.visible && !active.selected) { 36 | App.select_item({item: active, scroll}) 37 | } 38 | else if (App.filter_has_value(mode)) { 39 | App.clear_filter(mode) 40 | } 41 | else if (App[`${mode}_filter_mode`] !== `all`) { 42 | App.filter_all(mode, `step_back`) 43 | } 44 | else if (item && item.element && !App.item_is_visible(item)) { 45 | App.select_item({item, scroll}) 46 | } 47 | else if (bookmarks && App.bookmarks_folder) { 48 | App.go_to_bookmarks_parent_folder() 49 | } 50 | else if (tabs && item && !item.active) { 51 | App.focus_current_tab(scroll) 52 | } 53 | else if (App.filter_is_focused(mode)) { 54 | App.unfocus_filter(mode) 55 | } 56 | else if (tabs) { 57 | if (App.get_setting(`step_back_recent`) && e && (e.key !== `Escape`)) { 58 | App.show_tab_list(`recent`, e) 59 | } 60 | else { 61 | App.go_to_previous_tab() 62 | } 63 | } 64 | else if (mode !== App.main_mode()) { 65 | App.show_main_mode() 66 | } 67 | else { 68 | App.unfocus_filter(mode) 69 | } 70 | } 71 | 72 | App.step_back_click = (e) => { 73 | let cmd = App.get_setting(`click_step_back_button`) 74 | App.run_command({cmd, from: `step_back`, e}) 75 | } 76 | 77 | App.step_back_middle_click = (e) => { 78 | let cmd = App.get_setting(`middle_click_step_back_button`) 79 | App.run_command({cmd, from: `step_back`, e}) 80 | } -------------------------------------------------------------------------------- /js/main/storage.js: -------------------------------------------------------------------------------- 1 | App.get_local_storage_old = (ls_name) => { 2 | let obj = null 3 | 4 | try { 5 | obj = App.obj(localStorage.getItem(ls_name)) 6 | } 7 | catch (err) { 8 | App.error(err) 9 | } 10 | 11 | return obj 12 | } 13 | 14 | App.get_local_storage = async (ls_name, fallback) => { 15 | let keys = {} 16 | keys[ls_name] = fallback 17 | let ans = await browser.storage.local.get(keys) 18 | return ans[ls_name] 19 | } 20 | 21 | App.save_local_storage = async (ls_name, obj) => { 22 | let keys = {} 23 | keys[ls_name] = obj 24 | await browser.storage.local.set(keys) 25 | } 26 | 27 | App.stor_compat_check = async () => { 28 | if (!App.stor_compat.length) { 29 | return 30 | } 31 | 32 | let checked = await App.get_local_storage(App.stor_compat_check_name, false) 33 | 34 | if (!checked) { 35 | App.debug(`Stor: Compat`) 36 | 37 | for (let item of App.stor_compat) { 38 | let obj = App.get_local_storage_old(item.old) 39 | 40 | if (obj !== null) { 41 | App.debug(`Stor: Converting ${item.old} to ${item.new}`) 42 | await App.save_local_storage(item.new, obj) 43 | 44 | try { 45 | localStorage.setItem(`${item.old}_backup`, App.str(obj)) 46 | } 47 | catch (err) { 48 | // Do nothing 49 | } 50 | 51 | localStorage.removeItem(item.old) 52 | } 53 | } 54 | 55 | await App.save_local_storage(App.stor_compat_check_name, true) 56 | } 57 | } 58 | 59 | App.stor_get_settings = async () => { 60 | App.settings = await App.get_local_storage(App.stor_settings_name, {}) 61 | App.check_settings() 62 | App.settings_done = true 63 | App.debug(`Stor: Got settings`) 64 | } 65 | 66 | App.stor_save_settings = async () => { 67 | App.debug(`Stor: Saving settings`) 68 | await App.save_local_storage(App.stor_settings_name, App.settings) 69 | } 70 | 71 | App.stor_get_command_history = async () => { 72 | App.command_history = await App.get_local_storage(App.stor_command_history_name, []) 73 | App.debug(`Stor: Got command history`) 74 | } 75 | 76 | App.stor_save_command_history = () => { 77 | App.debug(`Stor: Saving command history`) 78 | App.save_local_storage(App.stor_command_history_name, App.command_history) 79 | } 80 | 81 | App.stor_get_filter_history = async () => { 82 | App.filter_history = await App.get_local_storage(App.stor_filter_history_name, []) 83 | App.debug(`Stor: Got filter history`) 84 | } 85 | 86 | App.stor_save_filter_history = () => { 87 | App.debug(`Stor: Saving filter history`) 88 | App.save_local_storage(App.stor_filter_history_name, App.filter_history) 89 | } 90 | 91 | App.stor_get_tag_history = async () => { 92 | let def = [`jump`] 93 | App.tag_history = await App.get_local_storage(App.stor_tag_history_name, def) 94 | App.debug(`Stor: Got tag history`) 95 | } 96 | 97 | App.stor_save_tag_history = () => { 98 | App.debug(`Stor: Saving tag history`) 99 | App.save_local_storage(App.stor_tag_history_name, App.tag_history) 100 | } 101 | 102 | App.stor_get_title_history = async () => { 103 | App.title_history = await App.get_local_storage(App.stor_title_history_name, []) 104 | App.debug(`Stor: Got title history`) 105 | } 106 | 107 | App.stor_save_title_history = () => { 108 | App.debug(`Stor: Saving title history`) 109 | App.save_local_storage(App.stor_title_history_name, App.title_history) 110 | } 111 | 112 | App.stor_get_icon_history = async () => { 113 | App.icon_history = await App.get_local_storage(App.stor_icon_history_name, App.default_icons.slice(0)) 114 | App.debug(`Stor: Got icon history`) 115 | } 116 | 117 | App.stor_save_icon_history = () => { 118 | App.debug(`Stor: Saving icon history`) 119 | App.save_local_storage(App.stor_icon_history_name, App.icon_history) 120 | } 121 | 122 | App.stor_get_first_time = async () => { 123 | App.first_time = await App.get_local_storage(App.stor_first_time_name, {}) 124 | App.debug(`Stor: Got first time`) 125 | } 126 | 127 | App.stor_save_first_time = () => { 128 | App.debug(`Stor: Saving first_time`) 129 | App.save_local_storage(App.stor_first_time_name, App.first_time) 130 | } 131 | 132 | App.stor_get_notes = async () => { 133 | App.notes = await App.get_local_storage(App.stor_notes_name, ``) 134 | App.debug(`Stor: Got notes`) 135 | } 136 | 137 | App.stor_save_notes = () => { 138 | App.debug(`Stor: Saving notes`) 139 | App.save_local_storage(App.stor_notes_name, App.notes) 140 | } 141 | 142 | App.stor_get_bookmark_folder_picks = async () => { 143 | App.bookmark_folder_picks = await App.get_local_storage(App.stor_bookmark_folder_picks, []) 144 | App.debug(`Stor: Got bookmark folder picks`) 145 | } 146 | 147 | App.stor_save_bookmark_folder_picks = () => { 148 | App.debug(`Stor: Saving bookmark folder picks`) 149 | App.save_local_storage(App.stor_bookmark_folder_picks, App.bookmark_folder_picks) 150 | } 151 | 152 | App.stor_get_history_picks = async () => { 153 | App.history_picks = await App.get_local_storage(App.stor_history_picks, []) 154 | App.debug(`Stor: Got history picks`) 155 | } 156 | 157 | App.stor_save_history_picks = () => { 158 | App.debug(`Stor: Saving history picks`) 159 | App.save_local_storage(App.stor_history_picks, App.history_picks) 160 | } -------------------------------------------------------------------------------- /js/main/tab_list.js: -------------------------------------------------------------------------------- 1 | App.show_tab_list = (what, e, item) => { 2 | let tabs, title, title_icon 3 | 4 | if (what === `recent`) { 5 | let max = App.get_setting(`max_recent_tabs`) 6 | let active = App.get_setting(`recent_active`) 7 | tabs = App.get_recent_tabs({max, active}) 8 | title = `Recent` 9 | title_icon = App.get_setting(`tabs_mode_icon`) 10 | } 11 | else if (what === `pins`) { 12 | tabs = App.get_pinned_tabs() 13 | title = `Pinned` 14 | title_icon = App.get_setting(`pin_icon`) 15 | } 16 | else if (what === `playing`) { 17 | tabs = App.get_playing_tabs() 18 | title = `Playing` 19 | title_icon = App.get_setting(`playing_icon`) 20 | } 21 | else if (what === `nodes`) { 22 | tabs = App.get_tab_nodes(item) 23 | title = `Nodes` 24 | title_icon = App.get_setting(`node_icon`) 25 | } 26 | else if (what === `parent`) { 27 | tabs = [App.get_parent_item(item)] 28 | title = `Parent` 29 | title_icon = App.get_setting(`parent_icon`) 30 | } 31 | else if (what === `siblings`) { 32 | tabs = App.get_tab_siblings(item) 33 | title = `Siblings` 34 | title_icon = App.get_setting(`node_icon`) 35 | } 36 | else if (what === `domain`) { 37 | tabs = App.get_domain_tabs(item) 38 | title = `Domain` 39 | title_icon = App.settings_icons.filter 40 | } 41 | else if (what === `title`) { 42 | tabs = App.get_title_tabs(item) 43 | title = `Title` 44 | title_icon = App.settings_icons.filter 45 | } 46 | else if (what === `container`) { 47 | tabs = App.get_container_tabs(item.container_name) 48 | title = item.container_name 49 | title_icon = App.color_icon_square(item.container_color) 50 | } 51 | else if (what.startsWith(`container_`)) { 52 | let name = what.split(`_`)[1] 53 | tabs = App.get_container_tabs(name) 54 | title = name 55 | title_icon = App.color_icon_square(App.container_data[name].color) 56 | } 57 | else if (what.startsWith(`color_`)) { 58 | let color_id = what.split(`_`)[1] 59 | let color = App.get_color_by_id(color_id) 60 | 61 | if (!color) { 62 | return 63 | } 64 | 65 | tabs = App.get_color_tabs(color_id) 66 | title = color.name 67 | title_icon = App.color_icon(color_id) 68 | } 69 | else if (what.startsWith(`tag_`)) { 70 | let tag = what.split(`_`)[1] 71 | tabs = App.get_tag_tabs(tag) 72 | title = tag 73 | title_icon = App.get_setting(`tags_icon`) 74 | } 75 | else if (what.startsWith(`icon_`)) { 76 | let icon = what.split(`_`)[1] 77 | tabs = App.get_icon_tabs(icon) 78 | title = `Icon` 79 | title_icon = icon 80 | } 81 | 82 | let items = [] 83 | let playing_icon = App.get_setting(`playing_icon`) 84 | let muted_icon = App.get_setting(`muted_icon`) 85 | 86 | for (let tab of tabs) { 87 | let title = App.title(tab) 88 | let icon 89 | 90 | if (tab.muted) { 91 | icon = muted_icon 92 | } 93 | else if (tab.playing) { 94 | icon = playing_icon 95 | } 96 | 97 | let favicon = tab.favicon 98 | 99 | if (!favicon) { 100 | favicon = `img/favicon.jpg` 101 | } 102 | 103 | let obj = { 104 | image: favicon, 105 | icon, 106 | text: title, 107 | info: tab.url, 108 | action: async () => { 109 | await App.check_on_tabs() 110 | App.tabs_action({item: tab, from: `tab_list`}) 111 | }, 112 | middle_action: () => { 113 | App.close_tab_or_tabs(tab.id) 114 | }, 115 | context_action: (e) => { 116 | App.show_item_menu({item: tab, e}) 117 | }, 118 | icon_action: async (e, icon) => { 119 | if (tab.muted) { 120 | await App.unmute_tab(tab.id) 121 | 122 | if (!tab.playing) { 123 | icon.innerHTML = `` 124 | } 125 | else { 126 | icon.innerHTML = playing_icon 127 | } 128 | } 129 | else if (tab.playing) { 130 | await App.mute_tab(tab.id) 131 | icon.innerHTML = muted_icon 132 | } 133 | }, 134 | } 135 | 136 | if (tab.active) { 137 | obj.bold = true 138 | } 139 | 140 | items.push(obj) 141 | } 142 | 143 | App.show_context({ 144 | items, e, 145 | title, 146 | title_icon, 147 | middle_action_remove: true, 148 | title_number: true, 149 | }) 150 | } -------------------------------------------------------------------------------- /js/main/templates.js: -------------------------------------------------------------------------------- 1 | App.start_templates_addlist = () => { 2 | if (App.templates_addlist_ready) { 3 | return 4 | } 5 | 6 | let {popobj, regobj} = App.get_setting_addlist_objects() 7 | let id = `settings_templates` 8 | let props = App.setting_props.templates 9 | 10 | App.create_popup({...popobj, id: `addlist_${id}`, 11 | after_hide: () => { 12 | App.rules_item = undefined 13 | }, 14 | element: Addlist.register({...regobj, id, 15 | keys: [ 16 | `name`, 17 | `cmd_icon`, 18 | `color`, 19 | `title`, 20 | `icon`, 21 | `tags`, 22 | `root`, 23 | `notes`, 24 | `split_top`, 25 | `split_bottom`, 26 | ], 27 | pk: `name`, 28 | widgets: { 29 | name: `text`, 30 | cmd_icon: `text`, 31 | color: `menu`, 32 | title: `text`, 33 | root: `text`, 34 | icon: `text`, 35 | tags: `text`, 36 | notes: `textarea`, 37 | split_top: `checkbox`, 38 | split_bottom: `checkbox`, 39 | }, 40 | labels: { 41 | name: `Name`, 42 | cmd_icon: `Cmd Icon`, 43 | color: `Color`, 44 | title: `Title`, 45 | icon: `Icon`, 46 | tags: `Tags`, 47 | notes: `Notes`, 48 | split_top: `Split Top`, 49 | split_bottom: `Split Bottom`, 50 | root: `Root`, 51 | }, 52 | sources: { 53 | color: () => { 54 | return App.color_values() 55 | }, 56 | }, 57 | process: { 58 | root: (value) => { 59 | if (value) { 60 | return App.fix_url(value) 61 | } 62 | 63 | return value 64 | }, 65 | }, 66 | validate: (values) => { 67 | if (!values.name) { 68 | return false 69 | } 70 | 71 | if ( 72 | !values.color && 73 | !values.title && 74 | !values.root && 75 | !values.icon && 76 | !values.tags && 77 | !values.split_top && 78 | !values.split_bottom && 79 | !values.notes 80 | ) { 81 | return false 82 | } 83 | 84 | return true 85 | }, 86 | tooltips: { 87 | name: `Name of the template`, 88 | cmd_icon: `The icon for the command`, 89 | color: `Add this color to matches`, 90 | title: `Add this title to matches`, 91 | icon: `Add this icon to matches`, 92 | tags: `Add these tags to matches`, 93 | notes: `Add these notes to matches`, 94 | split_top: `Add a split top to matches`, 95 | split_bottom: `Add a split bottom to matches`, 96 | root: `Make this the root URL for matches`, 97 | }, 98 | list_icon: (item) => { 99 | return item.cmd_icon || App.template_icon 100 | }, 101 | list_text: (item) => { 102 | return item.name 103 | }, 104 | title: props.name, 105 | })}) 106 | 107 | App.templates_addlist_ready = true 108 | } 109 | 110 | App.apply_template = (template, item) => { 111 | function save(it, name) { 112 | let key = `custom_${name}` 113 | let value = template[name] || undefined 114 | 115 | if (value === undefined) { 116 | return 117 | } 118 | 119 | if (name === `tags`) { 120 | value = App.split_list(value) 121 | } 122 | 123 | it[key] = value 124 | browser.sessions.setTabValue(it.id, key, it[key]) 125 | } 126 | 127 | let active = App.get_active_items({mode: `tabs`, item}) 128 | 129 | if (!active.length) { 130 | return 131 | } 132 | 133 | for (let it of active) { 134 | save(it, `color`) 135 | save(it, `title`) 136 | save(it, `root`) 137 | save(it, `icon`) 138 | save(it, `tags`) 139 | save(it, `notes`) 140 | save(it, `split_top`) 141 | save(it, `split_bottom`) 142 | 143 | App.update_item({mode: `tabs`, id: it.id, info: it}) 144 | } 145 | } -------------------------------------------------------------------------------- /js/main/textarea.js: -------------------------------------------------------------------------------- 1 | App.show_textarea = (args = {}) => { 2 | let def_args = { 3 | simple: false, 4 | buttons: [], 5 | align: `center`, 6 | readonly: true, 7 | left: false, 8 | bottom: false, 9 | wrap: false, 10 | monospace: false, 11 | } 12 | 13 | App.def_args(def_args, args) 14 | args.text = args.text.trim() 15 | App.start_popups() 16 | let textarea = DOM.el(`#textarea_text`) 17 | let simplearea = DOM.el(`#textarea_simple`) 18 | let title = DOM.el(`#textarea_title`) 19 | 20 | if (args.title) { 21 | DOM.show(title) 22 | let text = args.title 23 | 24 | if (args.title_icon) { 25 | text = `${args.title_icon} ${text}` 26 | } 27 | 28 | title.textContent = text 29 | } 30 | else { 31 | DOM.hide(title) 32 | } 33 | 34 | if (args.simple) { 35 | DOM.hide(textarea) 36 | DOM.show(simplearea) 37 | simplearea.textContent = args.text 38 | 39 | if (args.monospace) { 40 | simplearea.classList.add(`monospace`) 41 | } 42 | else { 43 | simplearea.classList.remove(`monospace`) 44 | } 45 | } 46 | else { 47 | DOM.hide(simplearea) 48 | DOM.show(textarea) 49 | textarea.value = args.text 50 | 51 | if (args.readonly) { 52 | textarea.readOnly = true 53 | } 54 | else { 55 | textarea.readOnly = false 56 | } 57 | 58 | if (args.wrap) { 59 | textarea.classList.add(`pre_wrap`) 60 | } 61 | else { 62 | textarea.classList.remove(`pre_wrap`) 63 | } 64 | } 65 | 66 | let img = DOM.el(`#textarea_image`) 67 | 68 | if (args.image) { 69 | DOM.show(img) 70 | img.src = args.image 71 | } 72 | else { 73 | DOM.hide(img) 74 | } 75 | 76 | if (args.buttons.length) { 77 | DOM.hide(`#textarea_buttons`) 78 | DOM.show(`#textarea_custom_buttons`) 79 | let c = DOM.el(`#textarea_custom_buttons`) 80 | c.innerHTML = `` 81 | 82 | for (let btn of args.buttons) { 83 | let b = DOM.create(`div`, `button`) 84 | b.textContent = btn.text 85 | 86 | DOM.ev(b, `click`, () => { 87 | btn.action() 88 | }) 89 | 90 | c.append(b) 91 | } 92 | } 93 | else { 94 | DOM.hide(`#textarea_custom_buttons`) 95 | DOM.show(`#textarea_buttons`) 96 | } 97 | 98 | if (args.align === `center`) { 99 | DOM.el(`#textarea_simple`).classList.add(`center`) 100 | DOM.el(`#textarea_simple`).classList.remove(`left`) 101 | } 102 | else if (args.align === `left`) { 103 | DOM.el(`#textarea_simple`).classList.add(`left`) 104 | DOM.el(`#textarea_simple`).classList.remove(`center`) 105 | } 106 | 107 | App.textarea_args = args 108 | App.textarea_text = args.text 109 | App.show_popup(`textarea`) 110 | 111 | requestAnimationFrame(() => { 112 | App.focus_textarea(textarea) 113 | 114 | if (args.bottom) { 115 | App.cursor_at_end(textarea) 116 | } 117 | 118 | if (args.left) { 119 | App.scroll_to_left(textarea) 120 | } 121 | }) 122 | } 123 | 124 | App.textarea_copy = () => { 125 | App.close_textarea() 126 | App.copy_to_clipboard(App.textarea_text) 127 | } 128 | 129 | App.focus_textarea = (el) => { 130 | el.focus() 131 | el.selectionStart = 0 132 | el.selectionEnd = 0 133 | App.scroll_to_top(el) 134 | } 135 | 136 | App.close_textarea = () => { 137 | App.hide_popup(`textarea`) 138 | } 139 | 140 | App.clear_textarea = () => { 141 | let textarea = DOM.el(`#textarea_text`) 142 | textarea.value = `` 143 | App.focus_textarea(textarea) 144 | } 145 | 146 | App.on_textarea_dismiss = () => { 147 | if (App.textarea_args.on_dismiss) { 148 | App.textarea_args.on_dismiss() 149 | } 150 | } 151 | 152 | App.textarea_enter = () => { 153 | if (App.textarea_args.on_enter) { 154 | App.textarea_args.on_enter() 155 | } 156 | } -------------------------------------------------------------------------------- /js/main/titles.js: -------------------------------------------------------------------------------- 1 | App.title = (item) => { 2 | let title = App.get_title(item) || item.title || `` 3 | return App.check_caps(title) 4 | } 5 | 6 | App.edit_tab_title = (args = {}) => { 7 | let def_args = { 8 | title: ``, 9 | } 10 | 11 | App.def_args(def_args, args) 12 | let active = App.get_active_items({mode: args.item.mode, item: args.item}) 13 | let s = args.title ? `Edit title?` : `Remove title?` 14 | let force = App.check_warn(`warn_on_edit_tabs`, active) 15 | 16 | App.show_confirm({ 17 | message: `${s} (${active.length})`, 18 | confirm_action: () => { 19 | for (let it of active) { 20 | App.apply_edit({what: `title`, item: it, value: args.title, on_change: (value) => { 21 | App.custom_save(it.id, `title`, value) 22 | App.push_to_title_history([value]) 23 | }}) 24 | } 25 | }, 26 | force, 27 | }) 28 | } 29 | 30 | App.edit_title = (item, add_value = true) => { 31 | let value 32 | 33 | if (add_value) { 34 | let auto = App.get_setting(`edit_title_auto`) 35 | value = auto ? App.title(item) : `` 36 | } 37 | else { 38 | value = `` 39 | } 40 | 41 | App.edit_prompt({what: `title`, item, 42 | fill: item.title, value}) 43 | } 44 | 45 | App.push_to_title_history = (titles) => { 46 | if (!titles.length) { 47 | return 48 | } 49 | 50 | for (let title of titles) { 51 | if (!title) { 52 | continue 53 | } 54 | 55 | App.title_history = App.title_history.filter(x => x !== title) 56 | App.title_history.unshift(title) 57 | App.title_history = App.title_history.slice(0, App.title_history_max) 58 | } 59 | 60 | App.stor_save_title_history() 61 | } 62 | 63 | App.get_all_titles = (include_rules = true) => { 64 | let titles = [] 65 | 66 | for (let title of App.title_history) { 67 | if (!titles.includes(title)) { 68 | titles.push(title) 69 | } 70 | } 71 | 72 | for (let item of App.get_items(`tabs`)) { 73 | if (item.custom_title) { 74 | if (!titles.includes(item.custom_title)) { 75 | titles.push(item.custom_title) 76 | } 77 | } 78 | 79 | if (include_rules) { 80 | if (item.rule_title) { 81 | if (!titles.includes(item.rule_title)) { 82 | titles.push(item.rule_title) 83 | } 84 | } 85 | } 86 | } 87 | 88 | return titles 89 | } 90 | 91 | App.remove_item_title = (item) => { 92 | let active = App.get_active_items({mode: item.mode, item}) 93 | 94 | if (active.length === 1) { 95 | let it = active[0] 96 | 97 | if (it.rule_title && !it.custom_title) { 98 | App.domain_rule_message() 99 | return 100 | } 101 | } 102 | 103 | App.remove_edits({what: [`title`], items: active, text: `titles`}) 104 | } 105 | 106 | App.get_titled_items = (mode) => { 107 | let items = [] 108 | 109 | for (let item of App.get_items(mode)) { 110 | if (App.get_title(item)) { 111 | items.push(item) 112 | } 113 | } 114 | 115 | return items 116 | } -------------------------------------------------------------------------------- /js/main/toys.js: -------------------------------------------------------------------------------- 1 | App.locust_swarm = () => { 2 | App.stop_locust_swarm() 3 | 4 | let canvas = DOM.create(`canvas`) 5 | canvas.id = `canvas_locust_swarm` 6 | canvas.width = window.innerWidth 7 | canvas.height = window.innerHeight 8 | App.locust_swarm_canvas = canvas 9 | 10 | DOM.ev(canvas, `click`, () => { 11 | App.stop_locust_swarm() 12 | }) 13 | 14 | document.body.appendChild(canvas) 15 | let context = canvas.getContext(`2d`) 16 | canvas.width = document.body.offsetWidth 17 | let width = canvas.width 18 | let height = canvas.height 19 | context.fillStyle = `#000` 20 | context.fillRect(0, 0, width, height) 21 | let columns = Math.floor(width / 20) + 1 22 | let y_position = Array(columns).fill(0) 23 | let chars = [`🦗`, `🌿`] 24 | let delay = 50 25 | let num = 0 26 | let limit = 1000 27 | 28 | function random_char() { 29 | return chars[Math.floor(Math.random() * chars.length)] 30 | } 31 | 32 | function matrix() { 33 | context.fillStyle = `#0001` 34 | context.fillRect(0, 0, width, height) 35 | context.fillStyle = `#0f0` 36 | context.font = `15pt monospace` 37 | 38 | for (let [index, y] of y_position.entries()) { 39 | let text = random_char() 40 | let x = index * 20 41 | context.fillText(text, x, y) 42 | 43 | if (y > 100 + Math.random() * 10000) { 44 | y_position[index] = 0 45 | } 46 | else { 47 | y_position[index] = y + 20 48 | } 49 | } 50 | 51 | num += 1 52 | 53 | if (num >= limit) { 54 | App.stop_locust_swarm() 55 | } 56 | } 57 | 58 | if (!delay || (delay < 1)) { 59 | App.error(`Locust Swarm delay is invalid`) 60 | return 61 | } 62 | 63 | App.locust_swarm_on = true 64 | App.locust_swarm_interval = setInterval(matrix, delay) 65 | } 66 | 67 | App.stop_locust_swarm = () => { 68 | clearInterval(App.locust_swarm_interval) 69 | 70 | if (App.locust_swarm_canvas) { 71 | App.locust_swarm_canvas.remove() 72 | App.locust_swarm_canvas = undefined 73 | } 74 | 75 | App.locust_swarm_on = false 76 | } 77 | 78 | App.start_breathe_effect = () => { 79 | clearTimeout(App.breathe_effect_timeout) 80 | App.breathe_effect_on = !App.breathe_effect_on 81 | App.apply_theme() 82 | 83 | App.breathe_effect_timeout = setTimeout(() => { 84 | App.stop_breathe_effect() 85 | }, App.SECOND * 9) 86 | } 87 | 88 | App.stop_breathe_effect = () => { 89 | App.breathe_effect_on = false 90 | App.apply_theme() 91 | } -------------------------------------------------------------------------------- /js/main/tree.js: -------------------------------------------------------------------------------- 1 | App.add_tab_parent = (item) => { 2 | if (!item || !item.parent) { 3 | return 4 | } 5 | 6 | if (App.tab_tree[item.parent] === undefined) { 7 | let parent = App.get_item_by_id(`tabs`, item.parent) 8 | 9 | if (!parent) { 10 | return 11 | } 12 | 13 | App.tab_tree[item.parent] = {} 14 | App.tab_tree[item.parent].parent = parent 15 | App.tab_tree[item.parent].nodes = [] 16 | } 17 | 18 | if (!App.node_tab_already_in(item.parent, item)) { 19 | App.tab_tree[item.parent].nodes.push(item) 20 | App.update_tab_parent(item.parent) 21 | App.update_tab_nodes(App.tab_tree[item.parent].nodes) 22 | } 23 | } 24 | 25 | App.remove_tree_item = (item) => { 26 | if (item.id in App.tab_tree) { 27 | let nodes = App.tab_tree[item.id].nodes 28 | delete App.tab_tree[item.id] 29 | App.update_tab_nodes(nodes) 30 | } 31 | 32 | if (App.tab_tree[item.parent]) { 33 | let nodes = App.tab_tree[item.parent].nodes.filter(it => it !== item) 34 | nodes = nodes.filter(it => it) 35 | App.tab_tree[item.parent].nodes = nodes 36 | 37 | if (item.id in App.tab_tree) { 38 | delete App.tab_tree[item.id] 39 | } 40 | 41 | App.update_tab_parent(item.parent) 42 | App.update_tab_nodes(nodes) 43 | } 44 | } 45 | 46 | App.update_tab_parent = (id) => { 47 | let tree = App.tab_tree[id] 48 | 49 | if (tree.parent.element_ready) { 50 | App.check_item_icon(tree.parent) 51 | App.check_icons(tree.parent) 52 | } 53 | } 54 | 55 | App.update_tab_nodes = (nodes) => { 56 | for (let node of nodes) { 57 | if (node.element_ready) { 58 | App.check_item_icon(node) 59 | App.check_icons(node) 60 | } 61 | } 62 | } 63 | 64 | App.get_tab_nodes = (item) => { 65 | let tree = App.tab_tree[item.id] 66 | 67 | if (tree) { 68 | return tree.nodes 69 | } 70 | 71 | return [] 72 | } 73 | 74 | App.tab_has_nodes = (item) => { 75 | return App.get_tab_nodes(item).length > 0 76 | } 77 | 78 | App.tab_has_parent = (item) => { 79 | for (let id in App.tab_tree) { 80 | if (App.node_tab_already_in(id, item)) { 81 | return true 82 | } 83 | } 84 | 85 | return false 86 | } 87 | 88 | App.focus_parent_tab = (item) => { 89 | if (!item.parent) { 90 | return 91 | } 92 | 93 | let tree = App.tab_tree[item.parent] 94 | 95 | if (tree) { 96 | App.tabs_action({item: tree.parent}) 97 | } 98 | } 99 | 100 | App.close_node_tabs = (item) => { 101 | let nodes = App.get_tab_nodes(item) 102 | App.close_tabs({selection: nodes, title: `nodes`}) 103 | } 104 | 105 | App.get_parent_item = (item) => { 106 | let tree = App.tab_tree[item.parent] 107 | 108 | if (tree) { 109 | return tree.parent 110 | } 111 | } 112 | 113 | App.go_to_parent = (item) => { 114 | let parent = App.get_parent_item(item) 115 | 116 | if (item) { 117 | App.tabs_action({item: parent, from: `node`}) 118 | } 119 | } 120 | 121 | App.close_parent_tab = (item) => { 122 | let parent = App.get_parent_item(item) 123 | 124 | if (parent) { 125 | App.close_tabs({selection: [parent], title: `parent`}) 126 | } 127 | } 128 | 129 | App.node_tab_already_in = (id, item) => { 130 | let tree = App.tab_tree[id] 131 | 132 | for (let node of tree.nodes) { 133 | if (node.id === item.id) { 134 | return true 135 | } 136 | } 137 | 138 | return false 139 | } 140 | 141 | App.get_current_tab_nodes = () => { 142 | let item = App.get_selected(`tabs`) 143 | 144 | if (item) { 145 | return App.get_tab_nodes(item) 146 | } 147 | 148 | return [] 149 | } 150 | 151 | App.get_parent_tabs = () => { 152 | let items = [] 153 | 154 | for (let id in App.tab_tree) { 155 | items.push(App.tab_tree[id].parent) 156 | } 157 | 158 | return items 159 | } 160 | 161 | App.get_node_tabs = () => { 162 | let items = [] 163 | 164 | for (let id in App.tab_tree) { 165 | items = items.concat(App.tab_tree[id].nodes) 166 | } 167 | 168 | return items 169 | } 170 | 171 | App.filter_node_tab_siblings = (item) => { 172 | if (!item.parent) { 173 | return 174 | } 175 | 176 | App.filter_common({ 177 | name: `node`, 178 | full: `Node`, 179 | prop: item.parent, 180 | item, 181 | }) 182 | } 183 | 184 | App.get_tab_siblings = (item) => { 185 | let tree = App.tab_tree[item.parent] 186 | 187 | if (tree) { 188 | return tree.nodes 189 | } 190 | 191 | return [] 192 | } -------------------------------------------------------------------------------- /js/main/unloaded.js: -------------------------------------------------------------------------------- 1 | App.unload_tabs = (item, multiple = true, mode = `all`) => { 2 | let items = [] 3 | let active = false 4 | 5 | let used_items 6 | 7 | if (mode === `all`) { 8 | used_items = App.get_active_items({mode: `tabs`, item, multiple}) 9 | } 10 | else if (mode === `normal`) { 11 | used_items = App.get_normal_tabs() 12 | } 13 | else if (mode === `pinned`) { 14 | used_items = App.get_pinned_tabs() 15 | } 16 | 17 | for (let it of used_items) { 18 | if (it.unloaded) { 19 | continue 20 | } 21 | 22 | if (App.is_new_tab(it.url)) { 23 | continue 24 | } 25 | 26 | if (mode === `normal`) { 27 | if (it.pinned) { 28 | continue 29 | } 30 | } 31 | else if (mode === `pinned`) { 32 | if (!it.pinned) { 33 | continue 34 | } 35 | } 36 | 37 | if (it.active) { 38 | active = true 39 | } 40 | 41 | items.push(it) 42 | } 43 | 44 | if (!items.length) { 45 | return 46 | } 47 | 48 | let force = App.check_warn(`warn_on_unload_tabs`, items) 49 | let ids = items.map(x => x.id) 50 | 51 | App.show_confirm({ 52 | message: `Unload tabs? (${ids.length})`, 53 | confirm_action: async () => { 54 | if (active) { 55 | let succ = await App.get_tab_succ(items) 56 | 57 | if ((mode === `all`) && succ) { 58 | let method = `unload` 59 | await App.focus_tab({item: succ, scroll: `nearest_smooth`, method}) 60 | } 61 | else { 62 | await App.blank_tab() 63 | } 64 | } 65 | 66 | App.do_unload_tabs(ids) 67 | }, 68 | force, 69 | }) 70 | } 71 | 72 | App.unload_normal_tabs = (item) => { 73 | App.unload_tabs(item, true, `normal`) 74 | } 75 | 76 | App.unload_pinned_tabs = (item) => { 77 | App.unload_tabs(item, true, `pinned`) 78 | } 79 | 80 | App.do_unload_tabs = async (ids) => { 81 | try { 82 | await browser.tabs.discard(ids) 83 | } 84 | catch (err) { 85 | App.error(err) 86 | } 87 | } 88 | 89 | App.unload_other_tabs = (item) => { 90 | let items = [] 91 | 92 | function proc(include_pins) { 93 | if (!include_pins) { 94 | items = items.filter(x => !x.pinned) 95 | } 96 | 97 | let ids = items.map(x => x.id) 98 | 99 | App.show_confirm({ 100 | message: `Unload other tabs? (${ids.length})`, 101 | confirm_action: () => { 102 | App.do_unload_tabs(ids) 103 | }, 104 | }) 105 | } 106 | 107 | let active = App.get_active_items({mode: `tabs`, item}) 108 | 109 | if (!active.length) { 110 | return 111 | } 112 | 113 | for (let it of App.get_items(`tabs`)) { 114 | if (active.includes(it)) { 115 | continue 116 | } 117 | 118 | if (it.unloaded) { 119 | continue 120 | } 121 | 122 | items.push(it) 123 | } 124 | 125 | if (!items.length) { 126 | return 127 | } 128 | 129 | App.show_confirm({ 130 | message: `Include pins?`, 131 | confirm_action: () => { 132 | proc(true) 133 | }, 134 | cancel_action: () => { 135 | proc(false) 136 | }, 137 | }) 138 | } 139 | 140 | App.check_unloaded = (item) => { 141 | if (item.unloaded) { 142 | item.element.classList.add(`unloaded_tab`) 143 | } 144 | else { 145 | item.element.classList.remove(`unloaded_tab`) 146 | } 147 | } 148 | 149 | App.toggle_show_unloaded = () => { 150 | let og = App.get_setting(`show_unloaded_tabs`) 151 | App.set_setting({setting: `show_unloaded_tabs`, value: !og}) 152 | 153 | if (!og) { 154 | App.show_all_unloaded() 155 | } 156 | 157 | App.do_filter({mode: App.active_mode}) 158 | App.toggle_message(`Unloaded`, `show_unloaded_tabs`) 159 | } 160 | 161 | App.show_all_unloaded = () => { 162 | for (let item of App.get_items(`tabs`)) { 163 | if (item.unloaded) { 164 | App.show_item_2(item) 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /js/main/warns.js: -------------------------------------------------------------------------------- 1 | App.check_warn = (warn_setting, items) => { 2 | if (items.length >= App.get_setting(`max_warn_limit`)) { 3 | return false 4 | } 5 | 6 | let warn_empty = App.get_setting(`warn_on_empty_tabs`) 7 | 8 | if (!warn_empty) { 9 | if (items.every(App.is_empty_tab)) { 10 | return true 11 | } 12 | } 13 | 14 | let warn_on_action = App.get_setting(warn_setting) 15 | 16 | if (warn_on_action === `always`) { 17 | return false 18 | } 19 | else if (warn_on_action === `never`) { 20 | return true 21 | } 22 | else if (warn_on_action === `multiple`) { 23 | if (items.length > 1) { 24 | return false 25 | } 26 | } 27 | else if (warn_on_action === `special`) { 28 | if (items.length > 1) { 29 | return false 30 | } 31 | 32 | for (let item of items) { 33 | if (!warn_empty) { 34 | if (App.is_empty_tab(item)) { 35 | continue 36 | } 37 | } 38 | 39 | if (item.pinned && App.get_setting(`warn_special_pinned`)) { 40 | return false 41 | } 42 | 43 | if (item.playing && App.get_setting(`warn_special_playing`)) { 44 | return false 45 | } 46 | 47 | if (item.header && App.get_setting(`warn_special_header`)) { 48 | return false 49 | } 50 | 51 | if (item.unloaded && App.get_setting(`warn_special_unloaded`)) { 52 | return false 53 | } 54 | 55 | if (!item.header && App.get_setting(`warn_special_edited`)) { 56 | if (App.edited(item, false)) { 57 | return false 58 | } 59 | } 60 | } 61 | } 62 | 63 | return true 64 | } -------------------------------------------------------------------------------- /js/overrides.js: -------------------------------------------------------------------------------- 1 | // You can override default settings here 2 | // This might enable you to make your own "distro" 3 | // 4 | // For Example: 5 | // 6 | // App.setting_overrides = { 7 | // main_title: `This is a new title`, 8 | // item_height: `small`, 9 | // item_menu_tabs: [ 10 | // { 11 | // cmd: `go_to_bottom`, 12 | // }, 13 | // { 14 | // cmd: `page_up`, 15 | // }, 16 | // ], 17 | // } 18 | 19 | App.setting_overrides = {} 20 | 21 | // You can also override any other globals below: -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Grasshopper", 4 | "version": "6425", 5 | "description": "Godspeed You Tab Emperor", 6 | "author": "Merkoba", 7 | "permissions": [ 8 | "tabs", 9 | "sessions", 10 | "storage", 11 | "contextualIdentities", 12 | "cookies", 13 | "contextMenus" 14 | ], 15 | "optional_permissions": [ 16 | "history", 17 | "bookmarks", 18 | "clipboardRead" 19 | ], 20 | "icons": { 21 | "128": "img/icon128.png" 22 | }, 23 | "background": { 24 | "scripts": [ 25 | "background/background.js", 26 | "background/bookmarks_server.js" 27 | ], 28 | "persistent": true 29 | }, 30 | "browser_action": { 31 | "default_icon": { 32 | "128": "img/icon128.png" 33 | }, 34 | "default_title": "Grasshopper", 35 | "default_popup": "main.html?popup" 36 | }, 37 | "sidebar_action": { 38 | "default_title": "Grasshopper", 39 | "default_panel": "main.html?sidebar", 40 | "default_icon": "img/icon128.png" 41 | }, 42 | "commands": { 43 | "_execute_browser_action": { 44 | "suggested_key": { 45 | "default": "Ctrl+Space" 46 | }, 47 | "description": "Open the popup" 48 | }, 49 | "_execute_sidebar_action": { 50 | "suggested_key": { 51 | "default": "Ctrl+Shift+Space" 52 | }, 53 | "description": "Toggle the sidebar" 54 | }, 55 | "popup_tabs": { 56 | "description": "Open the popup in Tabs mode" 57 | }, 58 | "popup_history": { 59 | "description": "Open the popup in History mode" 60 | }, 61 | "popup_bookmarks": { 62 | "description": "Open the popup in Bookmarks mode" 63 | }, 64 | "popup_closed": { 65 | "description": "Open the popup in Closed mode" 66 | }, 67 | "browser_command_1": { 68 | "description": "Browser Command 1" 69 | }, 70 | "browser_command_2": { 71 | "description": "Browser Command 2" 72 | }, 73 | "browser_command_3": { 74 | "description": "Browser Command 3" 75 | }, 76 | "browser_command_4": { 77 | "description": "Browser Command 4" 78 | }, 79 | "browser_command_5": { 80 | "description": "Browser Command 5" 81 | }, 82 | "browser_command_6": { 83 | "description": "Browser Command 6" 84 | }, 85 | "browser_command_7": { 86 | "description": "Browser Command 7" 87 | }, 88 | "browser_command_8": { 89 | "description": "Browser Command 8" 90 | }, 91 | "browser_command_9": { 92 | "description": "Browser Command 9" 93 | }, 94 | "browser_command_10": { 95 | "description": "Browser Command 10" 96 | }, 97 | "popup_command_1": { 98 | "description": "Popup Command 1" 99 | }, 100 | "popup_command_2": { 101 | "description": "Popup Command 2" 102 | }, 103 | "popup_command_3": { 104 | "description": "Popup Command 3" 105 | }, 106 | "popup_command_4": { 107 | "description": "Popup Command 4" 108 | }, 109 | "popup_command_5": { 110 | "description": "Popup Command 5" 111 | }, 112 | "popup_command_6": { 113 | "description": "Popup Command 6" 114 | }, 115 | "popup_command_7": { 116 | "description": "Popup Command 7" 117 | }, 118 | "popup_command_8": { 119 | "description": "Popup Command 8" 120 | }, 121 | "popup_command_9": { 122 | "description": "Popup Command 9" 123 | }, 124 | "popup_command_10": { 125 | "description": "Popup Command 10" 126 | } 127 | }, 128 | "browser_specific_settings": { 129 | "gecko": { 130 | "id": "{23aee10d-1130-4694-80ef-428e287ba83d}" 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /more/signals/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from datetime import datetime 4 | from flask import Flask, request 5 | from flask_cors import CORS 6 | from pathlib import Path 7 | 8 | 9 | # ---------- 10 | 11 | 12 | # Main flask app 13 | app = Flask(__name__) 14 | 15 | # Allow cross-origin requests 16 | CORS(app) 17 | 18 | # The port to run the server on 19 | port = 5000 20 | 21 | # Enable debug mode 22 | debug = False 23 | 24 | # Your music player 25 | player = ["playerctl", "-p", "audacious"] 26 | 27 | # Delay to wait for metadata to update 28 | metadata_delay = "0.18" 29 | 30 | # Where to save tab backups 31 | backup_path = Path("~/.config/signals/backups").expanduser() 32 | 33 | # Seconds to seek forwards or backwards 34 | seek_time = 5 35 | 36 | 37 | # ---------- 38 | 39 | 40 | def run(args): 41 | subprocess.run(args) 42 | 43 | 44 | def output(args): 45 | return subprocess.run(args, capture_output=True) 46 | 47 | 48 | def get_arg(name): 49 | return request.json.get(name) 50 | 51 | 52 | def get_seconds(): 53 | return int(datetime.now().timestamp()) 54 | 55 | 56 | def sleep(secs): 57 | run(["sleep", secs]) 58 | 59 | 60 | def music(what): 61 | run([*player, *what]) 62 | 63 | 64 | def inc_volume(): 65 | run(["awesome-client", "Utils.increase_volume()"]) 66 | 67 | 68 | def dec_volume(): 69 | run(["awesome-client", "Utils.decrease_volume()"]) 70 | 71 | 72 | def max_volume(): 73 | run(["awesome-client", "Utils.max_volume()"]) 74 | 75 | 76 | def min_volume(): 77 | run(["awesome-client", "Utils.min_volume()"]) 78 | 79 | 80 | def get_metadata(what): 81 | result = output([*player, "metadata", "--format", what]) 82 | return result.stdout.decode("utf-8").strip() 83 | 84 | 85 | def metadata(): 86 | info = "" 87 | status = player_status() 88 | 89 | if status == "Playing": 90 | artist = get_metadata("{{artist}}") 91 | title = get_metadata("{{title}}") 92 | 93 | if artist and title: 94 | info = f"{artist} - {title}" 95 | elif artist: 96 | info = artist 97 | elif title: 98 | info = title 99 | 100 | return info 101 | 102 | 103 | def player_status(): 104 | result = output([*player, "status"]) 105 | return result.stdout.decode("utf-8").strip() 106 | 107 | 108 | def save_backup(what, data): 109 | tabs = get_arg(what) 110 | secs = get_seconds() 111 | name = f"{what}_{secs}.json" 112 | path = backup_path / name 113 | 114 | if not path.parent.exists(): 115 | path.parent.mkdir(parents=True) 116 | 117 | with path.open("w") as f: 118 | json.dump(tabs, f) 119 | 120 | 121 | def get_backup(what): 122 | text = "" 123 | 124 | if backup_path.parent.exists(): 125 | files = sorted(backup_path.glob(f"{what}_*.json")) 126 | 127 | if files: 128 | with files[-1].open() as f: 129 | line = json.load(f) 130 | text = json.dumps(line, indent=2) 131 | 132 | return text 133 | 134 | 135 | # ---------- 136 | 137 | 138 | @app.route("/music-play", methods=["POST"]) 139 | def music_play(): 140 | music(["play-pause"]) 141 | return "ok" 142 | 143 | 144 | @app.route("/music-next", methods=["POST"]) 145 | def music_next(): 146 | music(["next"]) 147 | sleep(metadata_delay) 148 | return metadata() 149 | 150 | 151 | @app.route("/music-prev", methods=["POST"]) 152 | def music_prev(): 153 | music(["previous"]) 154 | sleep(metadata_delay) 155 | return metadata() 156 | 157 | 158 | @app.route("/volume-up", methods=["POST"]) 159 | def volume_up(): 160 | inc_volume() 161 | return "ok" 162 | 163 | 164 | @app.route("/volume-down", methods=["POST"]) 165 | def volume_down(): 166 | dec_volume() 167 | return "ok" 168 | 169 | 170 | @app.route("/volume-max", methods=["POST"]) 171 | def volume_max(): 172 | max_volume() 173 | return "ok" 174 | 175 | 176 | @app.route("/volume-min", methods=["POST"]) 177 | def volume_min(): 178 | min_volume() 179 | return "ok" 180 | 181 | 182 | @app.route("/music-np", methods=["GET"]) 183 | def music_np(): 184 | return metadata() 185 | 186 | 187 | @app.route("/music-seek-forwards", methods=["POST"]) 188 | def music_seek_f(): 189 | music(["position", f"{seek_time}+"]) 190 | return "ok" 191 | 192 | 193 | @app.route("/music-seek-backwards", methods=["POST"]) 194 | def music_seek_b(): 195 | music(["position", f"{seek_time}-"]) 196 | return "ok" 197 | 198 | 199 | # ------------ 200 | 201 | 202 | @app.route("/post-backup-tabs", methods=["POST"]) 203 | def post_backup_tabs(): 204 | msg = "" 205 | 206 | if request.content_type == "application/json": 207 | data = get_arg("tabs") 208 | 209 | if data: 210 | save_backup("tabs", data) 211 | msg = "Tabs Saved" 212 | 213 | if not msg: 214 | msg = "You sent nothing" 215 | 216 | return msg 217 | 218 | 219 | @app.route("/get-backup-tabs", methods=["GET"]) 220 | def get_backup_backup(): 221 | msg = get_backup("tabs") 222 | 223 | if not msg: 224 | msg = "No Tabs" 225 | 226 | return msg 227 | 228 | 229 | @app.route("/post-backup-settings", methods=["POST"]) 230 | def post_backup_settings(): 231 | msg = "" 232 | 233 | if request.content_type == "application/json": 234 | data = get_arg("settings") 235 | 236 | if data: 237 | save_backup("settings", data) 238 | msg = "Settings Saved" 239 | 240 | if not msg: 241 | msg = "You sent nothing" 242 | 243 | return msg 244 | 245 | 246 | @app.route("/get-backup-settings", methods=["GET"]) 247 | def get_backups_settings(): 248 | msg = get_backup("settings") 249 | 250 | if not msg: 251 | msg = "No Settings" 252 | 253 | return msg 254 | 255 | 256 | @app.route("/active-test", methods=["POST"]) 257 | def active_test(): 258 | msg = "" 259 | 260 | if request.content_type == "application/json": 261 | data = get_arg("active") 262 | print(data) 263 | msg = f"URL: {data["url"]} | Pinned: {data["pinned"]}" 264 | 265 | if not msg: 266 | msg = "You sent nothing" 267 | 268 | return msg 269 | 270 | 271 | # ---------- 272 | 273 | 274 | if __name__ == "__main__": 275 | app.run(host="0.0.0.0", port=port, debug=debug) 276 | -------------------------------------------------------------------------------- /more/signals/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask == 3.1.0 2 | Flask-Cors == 5.0.0 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grasshopper", 3 | "version": "1.0.0", 4 | "description": "Advanced Tab Manager For Firefox", 5 | "main": "eslint.config.js", 6 | "author": "madprops", 7 | "license": "GPL-3.0-only", 8 | "scripts": { 9 | "lint": "eslint --cache -c eslint.config.mjs", 10 | "fix": "eslint --fix -c eslint.config.mjs" 11 | }, 12 | "devDependencies": { 13 | "eslint": "~9.11.1", 14 | "globals": "~15.9.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /utils/bundle.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | def blue(text) 5 | puts "\e[34m#{text}\e[0m" 6 | end 7 | 8 | def bundle(what) 9 | directory = "js/#{what}" 10 | content = [] 11 | 12 | Dir.glob(File.join(directory, "*.js")).each do |file| 13 | content << File.read(file) 14 | end 15 | 16 | bundle = content.join("\n\n") 17 | output = File.join("js", "bundle.#{what}.js") 18 | File.open(output, 'w') { |file| file.write(bundle) } 19 | blue("Bundled #{what} to #{output}") 20 | end 21 | 22 | bundle("libs") 23 | bundle("main") -------------------------------------------------------------------------------- /utils/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export NODE_OPTIONS="--no-warnings" 3 | 4 | # Only check files that have changed recently 5 | last_tag=$(git describe --tags --abbrev=0) 6 | 7 | # Pick one 8 | # files=$(git diff --name-only $last_tag HEAD -- '*.js') 9 | files=$(git ls-files -- "*.js") 10 | files=$(echo $files | tr " " "\n" | grep -v "/libs/" | tr "\n" " ") 11 | 12 | if [ -n "$files" ]; then 13 | npm run --silent lint $files 14 | fi -------------------------------------------------------------------------------- /utils/dups.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def find_duplicate_functions(file_path) 4 | content = File.read(file_path) 5 | function_pattern = /App\.(\w+)\s*=\s*\(.*?\)\s*=>/ 6 | functions = Hash.new(0) 7 | 8 | content.scan(function_pattern) do |match| 9 | function_name = match[0] 10 | functions[function_name] += 1 11 | end 12 | 13 | duplicates = functions.select { |_, count| count > 1 } 14 | 15 | if not duplicates.empty? 16 | duplicates.each { |name, count| puts "#{name}: #{count} times" } 17 | end 18 | end 19 | 20 | file_path = "js/bundle.main.js" 21 | find_duplicate_functions(file_path) -------------------------------------------------------------------------------- /utils/fix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | files=$(git ls-files -- "*.js") 3 | files=$(echo $files | tr " " "\n" | grep -v "/libs/" | tr "\n" " ") 4 | 5 | if [ -n "$files" ]; then 6 | npm run --silent fix $files 7 | fi -------------------------------------------------------------------------------- /utils/header.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # Define the directory containing the JavaScript files 5 | directory = "js/main" 6 | 7 | # Ensure the directory exists 8 | unless Dir.exist?(directory) 9 | puts "Directory does not exist." 10 | exit 11 | end 12 | 13 | # Define the content to be added at the top of each file 14 | new_header = "/* Top line */" 15 | 16 | # Iterate over each JavaScript file in the directory 17 | Dir.glob(File.join(directory, "*.js")).each do |file| 18 | # Read the original content of the file 19 | original_content = File.read(file) 20 | 21 | # Split the content into lines 22 | lines = original_content.lines 23 | 24 | # Check if the first line is a comment and remove it 25 | if lines[0].strip.start_with?("/*") 26 | lines.shift 27 | end 28 | 29 | # Remove leading empty lines 30 | lines.shift while lines.first.strip.empty? 31 | 32 | # Prepend the new header 33 | lines.unshift(new_header + "\n\n") 34 | 35 | # Join the lines back into a single string 36 | new_content = lines.join 37 | 38 | # Write the new content back to the file 39 | File.write(file, new_content) 40 | 41 | puts "Updated file: #{file}" 42 | end 43 | 44 | puts "All JavaScript files have been updated." -------------------------------------------------------------------------------- /utils/remove_header.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # Define the directory containing the JavaScript files 5 | directory = "js/main" 6 | 7 | # Ensure the directory exists 8 | unless Dir.exist?(directory) 9 | puts "Directory does not exist." 10 | exit 11 | end 12 | 13 | # Define the content to be added at the top of each file 14 | new_header = "/* global App, DOM, browser, dateFormat, Addlist, AColorPicker, Menubutton, jdenticon, ColorLib, NiceGesture, NeedContext */" 15 | 16 | # Iterate over each JavaScript file in the directory 17 | Dir.glob(File.join(directory, "*.js")).each do |file| 18 | # Read the original content of the file 19 | original_content = File.read(file) 20 | 21 | # Split the content into lines 22 | lines = original_content.lines 23 | 24 | # Check if the first line is a comment and remove it 25 | if lines[0].strip.start_with?("/*") 26 | lines.shift 27 | end 28 | 29 | # Remove leading empty lines 30 | lines.shift while lines.first.strip.empty? 31 | 32 | # Join the lines back into a single string 33 | new_content = lines.join 34 | 35 | # Write the new content back to the file 36 | File.write(file, new_content) 37 | 38 | puts "Updated file: #{file}" 39 | end 40 | 41 | puts "All JavaScript files have been updated." -------------------------------------------------------------------------------- /utils/replace.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # Check if the correct number of arguments is provided 5 | if ARGV.length != 2 6 | puts "Usage: ruby replace_script.rb " 7 | exit 8 | end 9 | 10 | # Retrieve command-line arguments 11 | search_string = ARGV[0] 12 | replace_string = ARGV[1] 13 | 14 | # Iterate over all JavaScript files in the directory 15 | Dir.glob(File.join('js/main', '**', '*.js')).each do |file_path| 16 | # Read the file content 17 | file_content = File.read(file_path) 18 | 19 | # Replace the search_string with the replace_string 20 | updated_content = file_content.gsub(search_string, replace_string) 21 | 22 | # Write the updated content back to the file 23 | File.write(file_path, updated_content) 24 | 25 | puts "Updated: #{file_path}" 26 | end -------------------------------------------------------------------------------- /utils/search.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | context=${2:-0} 3 | 4 | goldie -p=main.html -a "$1" -C="$context" 5 | goldie -p=css/style.css -a "$1" -C="$context" 6 | goldie -p=js/init.js -a "$1" -C="$context" 7 | goldie -p=js/app.js -a "$1" -C="$context" 8 | cd js/main && goldie -a "$1" -C="$context" -------------------------------------------------------------------------------- /utils/stats.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $total_lines = 0 4 | $total_size = 0 5 | $total_files = 0 6 | $separator = "\e[36m | \e[0m" 7 | 8 | def get_lines(path) 9 | lines = 0 10 | File.foreach(path) { lines += 1 } 11 | return lines 12 | end 13 | 14 | def get_size(path) 15 | return File.size(path) / 1024.0 16 | end 17 | 18 | def count_subdir(path) 19 | lines = 0 20 | size = 0 21 | files = 0 22 | 23 | Dir.chdir(path) do 24 | Dir.glob("*").each do |file| 25 | lines += get_lines(file) 26 | size += get_size(file) 27 | files += 1 28 | end 29 | end 30 | 31 | return lines, size.round(1), files 32 | end 33 | 34 | def count_file(path) 35 | lines = get_lines(path) 36 | size = get_size(path) 37 | return lines, size.round(1) 38 | end 39 | 40 | def print_files(files) 41 | word = files == 1 ? "file" : "files" 42 | return "#{files} #{word}" 43 | end 44 | 45 | def show(path, name = nil) 46 | is_subdir = path.split(".").length == 1 47 | 48 | if is_subdir 49 | lines, size, files = count_subdir(path) 50 | else 51 | lines, size = count_file(path) 52 | files = 1 53 | end 54 | 55 | $total_lines += lines 56 | $total_size += size 57 | $total_files += files 58 | 59 | if name == nil 60 | name = path 61 | end 62 | 63 | msg = [ 64 | "\e[34m#{name}\e[0m", 65 | "#{lines} lines", 66 | "#{size} KB", 67 | ] 68 | 69 | if files > 1 70 | msg.push("#{print_files(files)}") 71 | end 72 | 73 | puts msg.join($separator) 74 | end 75 | 76 | def total 77 | msg = [ 78 | "\e[32mTotal\e[0m", 79 | "#{$total_lines} lines", 80 | "#{$total_size.round(1)} KB", 81 | "#{print_files($total_files)}", 82 | ] 83 | 84 | puts msg.join($separator) 85 | end 86 | 87 | def intro 88 | puts "\e[32mGrasshopper Stats\e[0m 🦗\n" 89 | end 90 | 91 | intro() 92 | 93 | show("js/main") 94 | show("js/libs") 95 | show("js/app.js", "app.js") 96 | show("js/init.js", "init.js") 97 | show("background") 98 | show("main.html") 99 | show("more/signals", "signals") 100 | show("utils") 101 | show("css") 102 | 103 | total() -------------------------------------------------------------------------------- /utils/stylecheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npx stylelint -c stylelint.config.mjs css/style.css 4 | if [ $? -ne 0 ]; then 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /utils/tag.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "git" 3 | require "json" 4 | file = File.read("manifest.json") 5 | manifest = JSON.parse(file) 6 | version = manifest["version"] 7 | name = "v#{version}" 8 | repo = Git.open(".") 9 | repo.add_tag(name) 10 | repo.push("origin", name) 11 | puts "Created tag: #{name}" -------------------------------------------------------------------------------- /utils/zip.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "json" 3 | require "fileutils" 4 | 5 | # Read the manifest 6 | file = File.read("manifest.json") 7 | manifest = JSON.parse(file) 8 | version = manifest["version"].gsub(".", "_") 9 | name = manifest["name"].downcase.split.join("_") 10 | 11 | # Delete the old zip file 12 | old_name = Dir.glob("#{name}*.zip").first 13 | 14 | if old_name 15 | if File.exist?(old_name) 16 | File.delete(old_name) 17 | puts "Removed #{old_name}" 18 | end 19 | end 20 | 21 | # Create the new zip file 22 | new_name = "#{name}_v#{version}.zip" 23 | `zip -r #{new_name} * -x "*.zip" "node_modules/*" "package-lock.json" ".eslintcache" ".directory"` 24 | puts "Created #{new_name}" --------------------------------------------------------------------------------