├── .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 | 
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 |
39 |
40 |
41 |
42 |
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}"
--------------------------------------------------------------------------------