├── .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
├── data
└── nouns.txt
├── eslint.config.mjs
├── fonts
├── AnticSlab-Regular.ttf
├── Bitcount-Regular.ttf
└── 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
├── content.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
│ ├── clusters.js
│ ├── colors.js
│ ├── combos.js
│ ├── command_props.js
│ ├── commands.js
│ ├── containers.js
│ ├── context.js
│ ├── custom_commands.js
│ ├── data.js
│ ├── datastore.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
│ ├── idle_tabs.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_count.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
│ ├── words.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/4431b8ad259b4301dc7f0fc75987319da5b7e419/audio/effect_1.mp3
--------------------------------------------------------------------------------
/audio/effect_2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/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 | })
159 |
160 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
161 | if (request.action === `boost_tab`) {
162 | browser.tabs.executeScript(request.tab_id, {
163 | file: `js/content.js`,
164 | })
165 | }
166 | })
--------------------------------------------------------------------------------
/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 | body.mirror_horizontal #needcontext-container {
7 | transform: scaleX(-1) !important;
8 | }
9 |
10 | body.mirror_vertical #needcontext-container {
11 | transform: scaleY(-1) !important;
12 | }
13 |
14 | #needcontext-container {
15 | background-color: var(--background_color) !important;
16 | color: var(--text_color) !important;
17 | border-color: var(--alt_color_2) !important;
18 | font-size: var(--font_size) !important;
19 | font-family: var(--font) !important;
20 | }
21 |
22 | .no_rounded #needcontext-container {
23 | border-radius: 0 !important;
24 | }
25 |
26 | .needcontext-item-selected {
27 | background-color: var(--alt_color_1) !important;
28 | }
29 |
30 | .needcontext-item-selected:active {
31 | background-color: var(--alt_color_1) !important;
32 | }
33 |
34 | #needcontext-title {
35 | background-color: var(--alt_color_0) !important;
36 | color: var(--text_color) !important;
37 | }
--------------------------------------------------------------------------------
/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/AnticSlab-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/fonts/AnticSlab-Regular.ttf
--------------------------------------------------------------------------------
/fonts/Bitcount-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/fonts/Bitcount-Regular.ttf
--------------------------------------------------------------------------------
/fonts/NovaSquare-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/fonts/NovaSquare-Regular.ttf
--------------------------------------------------------------------------------
/header/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Header
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
45 |
46 |
47 |
48 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/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/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_1.jpg
--------------------------------------------------------------------------------
/img/backgrounds/background_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_2.jpg
--------------------------------------------------------------------------------
/img/backgrounds/background_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_3.jpg
--------------------------------------------------------------------------------
/img/backgrounds/background_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_4.jpg
--------------------------------------------------------------------------------
/img/backgrounds/background_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_5.jpg
--------------------------------------------------------------------------------
/img/backgrounds/background_6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/backgrounds/background_6.jpg
--------------------------------------------------------------------------------
/img/favicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/favicon.jpg
--------------------------------------------------------------------------------
/img/folder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/folder.jpg
--------------------------------------------------------------------------------
/img/grasshopper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/grasshopper.png
--------------------------------------------------------------------------------
/img/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/icon128.png
--------------------------------------------------------------------------------
/img/info.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/info.jpg
--------------------------------------------------------------------------------
/img/leaf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/leaf.jpg
--------------------------------------------------------------------------------
/img/lock.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madprops/grasshopper/4431b8ad259b4301dc7f0fc75987319da5b7e419/img/lock.jpg
--------------------------------------------------------------------------------
/js/content.js:
--------------------------------------------------------------------------------
1 | if (!window._grasshopper_boosted_) {
2 | let resize_timeout
3 |
4 | window.addEventListener(`resize`, () => {
5 | clearTimeout(resize_timeout)
6 |
7 | resize_timeout = setTimeout(() => {
8 | browser.runtime.sendMessage({action: `fullscreen_change`})
9 | }, 1000)
10 | })
11 |
12 | window._grasshopper_boosted_ = true
13 |
14 | // eslint-disable-next-line no-console
15 | console.info(`🟢 Grasshopper: Content Script Loaded`) //
16 | }
--------------------------------------------------------------------------------
/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.stor_get_palette_history()
23 | await App.stor_get_datastore()
24 | await App.check_init_mode()
25 |
26 | App.make_tab_box_modes()
27 | App.setup_commands()
28 | App.setup_tabs()
29 | App.setup_closed()
30 | App.setup_settings()
31 | App.setup_tab_box()
32 | App.setup_active_trace()
33 | App.setup_tab_count()
34 | App.setup_keyboard()
35 | App.setup_window()
36 | App.setup_gestures()
37 | App.setup_filter()
38 | App.setup_modes()
39 | App.setup_scroll()
40 | App.setup_items()
41 | App.setup_theme()
42 | App.setup_drag()
43 | App.setup_favorites()
44 | App.setup_playing()
45 | App.setup_messages()
46 | App.setup_mouse()
47 | App.do_apply_theme()
48 | App.setup_pinline()
49 | App.setup_footer()
50 | App.setup_recent_tabs()
51 | App.setup_context()
52 | App.build_shell()
53 | App.mouse_inside_check()
54 | App.resolve_icons()
55 |
56 | await App.clear_show()
57 |
58 | App.init_tab_box()
59 | App.make_window_visible()
60 | App.check_first_time()
61 | App.start_clock()
62 | App.start_main_title()
63 | App.check_init_commands()
64 | App.start_signal_intervals()
65 | App.start_idle_tabs_check()
66 | App.start_progressive_fill()
67 |
68 | App.start_date = App.now()
69 | }
70 |
71 | 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/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 | trace.textContent = n
37 | DOM.show(trace)
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 | message = message.toString()
5 | let text = App.make_html_safe(message)
6 | text = text.replace(/\n/g, `
`)
7 | msg.innerHTML = text
8 | App.show_popup(`alert`)
9 |
10 | if (autohide_delay > 0) {
11 | App.alert_timeout = setTimeout(() => {
12 | App.hide_popup(`alert`)
13 | }, autohide_delay)
14 | }
15 | }
16 |
17 | App.alert_autohide = (message, force = false) => {
18 | if (!force) {
19 | if (!App.get_setting(`show_feedback`)) {
20 | App.footer_message(message)
21 | return
22 | }
23 | }
24 |
25 | App.alert(message, App.alert_autohide_delay)
26 | }
--------------------------------------------------------------------------------
/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 enabled = App.get_setting(`clock_enabled`)
17 | let format = App.get_setting(`clock_format`)
18 |
19 | if (enabled && format) {
20 | let date = App.now()
21 | placeholder = dateFormat(date, format)
22 | }
23 | else {
24 | placeholder = App.get_filter_placeholder()
25 | }
26 |
27 | if (!force) {
28 | if (placeholder === App.last_filter_placeholder) {
29 | return
30 | }
31 | }
32 |
33 | let filters = DOM.els(`.mode_filter`)
34 |
35 | for (let el of filters) {
36 | el.placeholder = placeholder
37 | }
38 |
39 | App.last_filter_placeholder = placeholder
40 | }
41 |
42 | App.pick_clock_format = (e) => {
43 | let items = []
44 |
45 | items.push({
46 | text: `12 Hour`,
47 | action: () => {
48 | App.set_setting({setting: `clock_format`, value: `h:MM tt Z`})
49 | App.refresh_setting_widgets([`clock_format`])
50 | },
51 | })
52 |
53 | items.push({
54 | text: `24 Hour`,
55 | action: () => {
56 | App.set_setting({setting: `clock_format`, value: `HH:MM Z`})
57 | App.refresh_setting_widgets([`clock_format`])
58 | },
59 | })
60 |
61 | App.show_context({e, items})
62 | }
--------------------------------------------------------------------------------
/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 | }
63 |
64 | App.close_button_ctrl_click = (item, e) => {
65 | let cmd = App.get_setting(`ctrl_click_close_button`)
66 | App.run_command({cmd, item, from: `close_button`, e})
67 | }
68 |
69 | App.close_button_shift_click = (item, e) => {
70 | let cmd = App.get_setting(`shift_click_close_button`)
71 | App.run_command({cmd, item, from: `close_button`, e})
72 | }
73 |
74 | App.close_button_ctrl_shift_click = (item, e) => {
75 | let cmd = App.get_setting(`ctrl_shift_click_close_button`)
76 | App.run_command({cmd, item, from: `close_button`, e})
77 | }
78 |
79 | App.close_button_ctrl_middle_click = (item, e) => {
80 | let cmd = App.get_setting(`ctrl_middle_click_close_button`)
81 | App.run_command({cmd, item, from: `close_button`, e})
82 | }
83 |
84 | App.close_button_shift_middle_click = (item, e) => {
85 | let cmd = App.get_setting(`shift_middle_click_close_button`)
86 | App.run_command({cmd, item, from: `close_button`, e})
87 | }
88 |
89 | App.close_button_ctrl_shift_middle_click = (item, e) => {
90 | let cmd = App.get_setting(`ctrl_shift_middle_click_close_button`)
91 | App.run_command({cmd, item, from: `close_button`, e})
92 | }
--------------------------------------------------------------------------------
/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/clusters.js:
--------------------------------------------------------------------------------
1 | App.get_tab_clusters = (remove_first = false) => {
2 | let items = App.get_items(`tabs`)
3 |
4 | let grouped = items
5 | .reduce((acc, item) => {
6 | let hostname = item.hostname || `unknown`
7 | let existing = acc.find(group => (group.length > 0) && (group[0].hostname === hostname))
8 |
9 | if (existing) {
10 | existing.push(item)
11 | }
12 | else {
13 | acc.push([item])
14 | }
15 |
16 | return acc
17 | }, [])
18 | .filter(group => group.length >= App.get_setting(`min_cluster_size`))
19 |
20 | if (remove_first) {
21 | grouped = grouped.map(group => {
22 | if (group.length > 1) {
23 | return group.slice(1)
24 | }
25 |
26 | return []
27 | }).filter(group => group.length > 0)
28 | }
29 |
30 | let tabs = grouped.flat()
31 | return App.remove_headers(tabs)
32 | }
33 |
34 | App.show_tab_clusters = (e) => {
35 | App.show_tab_list(`clusters`, e)
36 | }
--------------------------------------------------------------------------------
/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.find_container_by_name = (containers, name = ``) => {
170 | name = name.trim().toLowerCase()
171 |
172 | for (let container of containers) {
173 | if (container.name.toLowerCase() === name) {
174 | return container
175 | }
176 | }
177 | }
178 |
179 | App.open_in_tab_container = async (item, e, name = ``) => {
180 | let new_tab_mode = App.get_setting(`new_tab_mode`)
181 | let containers = await App.get_all_containers()
182 | let active = App.get_active_items({mode: item.mode, item})
183 | let o_item
184 |
185 | if (!active.length) {
186 | return
187 | }
188 |
189 | if (new_tab_mode === `above`) {
190 | o_item = active[0]
191 | }
192 | else if (new_tab_mode === `below`) {
193 | o_item = active.at(-1)
194 | }
195 | else {
196 | o_item = active[0]
197 | }
198 |
199 | if (name) {
200 | let target_container = App.find_container_by_name(containers, name)
201 |
202 | if (!target_container) {
203 | App.alert(`No container named ${name} found`)
204 | return
205 | }
206 |
207 | for (let it of active) {
208 | App.create_new_tab({url: it.url, cookieStoreId: target_container.id}, o_item)
209 | }
210 |
211 | return
212 | }
213 |
214 | let items = []
215 |
216 | for (let c of containers) {
217 | items.push({
218 | text: c.name,
219 | icon: App.color_icon_square(c.color),
220 | action: () => {
221 | for (let it of active) {
222 | App.create_new_tab({url: it.url, cookieStoreId: c.id}, o_item)
223 | }
224 | },
225 | })
226 | }
227 |
228 | if (!items.length) {
229 | App.alert(`No containers available`)
230 | return
231 | }
232 |
233 | let title = `Open In`
234 | let title_icon = App.get_setting(`container_icon`)
235 | let compact = App.get_setting(`compact_container_menu`)
236 | App.show_context({items, e, title, title_icon, compact})
237 | }
--------------------------------------------------------------------------------
/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/custom_commands.js:
--------------------------------------------------------------------------------
1 | App.custom_command_actions = {
2 | go_to_zone: (cmd, items, e) => {
3 | let zone = App.get_zone_by_title(cmd.argument)
4 |
5 | if (!zone) {
6 | return
7 | }
8 |
9 | App.go_to_zone(zone)
10 | },
11 | move_to_zone_top: (cmd, items, e) => {
12 | let zone = App.get_zone_by_title(cmd.argument)
13 |
14 | if (!zone || !items.length) {
15 | return
16 | }
17 |
18 | App.move_to_zone(zone, items[0], `top`)
19 | },
20 | move_to_zone_bottom: (cmd, items, e) => {
21 | let zone = App.get_zone_by_title(cmd.argument)
22 |
23 | if (!zone || !items.length) {
24 | return
25 | }
26 |
27 | App.move_to_zone(zone, items[0], `bottom`)
28 | },
29 | edit_title: (cmd, items, e) => {
30 | for (let it of items) {
31 | App.edit_title_directly(it, cmd.argument)
32 | }
33 | },
34 | add_tag: (cmd, items, e) => {
35 | for (let it of items) {
36 | App.add_tag_string(it, cmd.argument)
37 | }
38 | },
39 | append_note: (cmd, items, e) => {
40 | for (let it of items) {
41 | App.append_note(it, cmd.argument)
42 | }
43 | },
44 | prepend_note: (cmd, items, e) => {
45 | for (let it of items) {
46 | App.prepend_note(it, cmd.argument)
47 | }
48 | },
49 | append_global_note: (cmd, items, e) => {
50 | App.append_global_note(cmd.argument)
51 | },
52 | prepend_global_note: (cmd, items, e) => {
53 | App.prepend_global_note(cmd.argument)
54 | },
55 | open_in_container: (cmd, items, e) => {
56 | App.open_in_tab_container(items[0], e, cmd.argument)
57 | },
58 | }
59 |
60 | App.custom_command_items = [
61 | {text: `Go To Zone`, value: `go_to_zone`, icon: App.zone_icon},
62 | {text: `Move To Zone (Top)`, value: `move_to_zone_top`, icon: App.zone_icon},
63 | {text: `Move To Zone (Bottom)`, value: `move_to_zone_bottom`, icon: App.zone_icon},
64 | {text: `Edit Title`, value: `edit_title`, icon: App.notepad_icon},
65 | {text: `Add Tag`, value: `add_tag`, icon: App.tag_icon},
66 | {text: `Append Note`, value: `append_note`, icon: App.notepad_icon},
67 | {text: `Prepend Note`, value: `prepend_note`, icon: App.notepad_icon},
68 | {text: `Append Global Note`, value: `append_global_note`, icon: App.notepad_icon},
69 | {text: `Prepend Global Note`, value: `prepend_global_note`, icon: App.notepad_icon},
70 | {text: `Open In Container`, value: `open_in_container`, icon: App.container_icon},
71 | ]
72 |
73 | App.start_custom_commands_addlist = () => {
74 | if (App.custom_commands_addlist_ready) {
75 | return
76 | }
77 |
78 | let id = `settings_custom_commands`
79 | let props = App.setting_props.custom_commands
80 | let {popobj, regobj} = App.get_setting_addlist_objects()
81 |
82 | App.create_popup({...popobj, id: `addlist_${id}`,
83 | element: Addlist.register({...regobj, id,
84 | keys: [`name`, `icon`, `action`, `argument`],
85 | widgets: {
86 | name: `text`,
87 | icon: `text`,
88 | action: `menu`,
89 | argument: `text`,
90 | },
91 | labels: {
92 | name: `Name`,
93 | icon: `Icon`,
94 | action: `Action`,
95 | argument: `Argument`,
96 | },
97 | required: {
98 | name: true,
99 | action: true,
100 | argument: false,
101 | },
102 | list_icon: (item) => {
103 | return item.icon
104 | },
105 | list_text: (item) => {
106 | return item.name
107 | },
108 | tooltips: {
109 | icon: `Icon for this command`,
110 | name: `Name of the command`,
111 | action: `Type of action to run`,
112 | argument: `Argument for the action`,
113 | },
114 | sources: {
115 | action: () => {
116 | return App.custom_command_items
117 | },
118 | },
119 | title: props.name,
120 | })})
121 |
122 | App.custom_commands_addlist_ready = true
123 | }
124 |
125 | App.run_custom_command = (cmd, item, e) => {
126 | if (!cmd.argument) {
127 | return
128 | }
129 |
130 | let action = App.custom_command_actions[cmd.action]
131 |
132 | if (action) {
133 | let items = App.get_active_items({mode: item.mode, item})
134 | action(cmd, items, e)
135 | }
136 | }
--------------------------------------------------------------------------------
/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/datastore.js:
--------------------------------------------------------------------------------
1 | App.datastore_prompt = () => {
2 | App.show_prompt({
3 | placeholder: `Text`,
4 | on_submit: (text) => {
5 | App.add_to_datastore(text)
6 | },
7 | })
8 | }
9 |
10 | App.add_to_datastore = (value, type = `note`) => {
11 | if (!value) {
12 | return
13 | }
14 |
15 | if (typeof value !== `string`) {
16 | value = App.str(value, true)
17 | }
18 |
19 | if (!value.trim()) {
20 | return
21 | }
22 |
23 | if (value.length > App.datastore_max_text) {
24 | App.footer_message(`Datastore: Text is too long`)
25 | App.error_sound()
26 | return
27 | }
28 |
29 | let obj = {
30 | type,
31 | value,
32 | date: App.now(),
33 | }
34 |
35 | if (!App.datastore.items) {
36 | App.datastore.items = []
37 | }
38 |
39 | App.remove_from_list(App.datastore.items, value)
40 | App.datastore.items.push(obj)
41 |
42 | if (App.datastore.items.length > App.datastore_max) {
43 | App.datastore.items = App.datastore.items.slice(-App.datastore_max)
44 | }
45 |
46 | App.stor_save_datastore()
47 | App.footer_message(`Datastore: Added`)
48 | App.action_sound()
49 | }
50 |
51 | App.get_datastore_items = () => {
52 | return App.datastore.items || []
53 | }
54 |
55 | App.clear_datastore = () => {
56 | let items = App.get_datastore_items()
57 |
58 | if (!items.length) {
59 | App.alert(`Datastore is empty`)
60 | return
61 | }
62 |
63 | App.show_confirm(
64 | {
65 | message: `Clear the datastore?`,
66 | confirm_action: () => {
67 | App.datastore.items = []
68 | App.stor_save_datastore()
69 | App.alert_autohide(`Datastore cleared`)
70 | },
71 | },
72 | )
73 | }
74 |
75 | App.browse_datastore = () => {
76 | let items = App.get_datastore_items()
77 | let ds_items = []
78 |
79 | for (let item of items) {
80 | if (!item.value) {
81 | continue
82 | }
83 |
84 | ds_items.push({
85 | text: `${item.type}: ${item.value.substring(0, 20).trim()}`,
86 | action: () => {
87 | let c_items = [
88 | {
89 | text: `View`,
90 | action: () => {
91 | App.show_textarea({title: `Datastore Item `, text: item.value})
92 | },
93 | },
94 | ]
95 |
96 | if (item.value) {
97 | if ([`settings`, `theme`].includes(item.type)) {
98 | c_items.push({
99 | text: `Import`,
100 | action: () => {
101 | App.import_settings(item.value)
102 | },
103 | })
104 | }
105 | else if ([`urls`].includes(item.type)) {
106 | c_items.push({
107 | text: `Open`,
108 | action: () => {
109 | App.open_tab_urls(item.value)
110 | },
111 | })
112 | }
113 | else if ([`note`].includes(item.type)) {
114 | c_items.push({
115 | text: `Apply (Tab)`,
116 | action: () => {
117 | let it = App.get_selected(`tabs`)
118 |
119 | if (it) {
120 | App.apply_notes(it, item.value)
121 | }
122 | },
123 | })
124 |
125 | c_items.push({
126 | text: `Append (Tab)`,
127 | action: () => {
128 | let it = App.get_selected(`tabs`)
129 |
130 | if (it) {
131 | App.append_note(it, item.value)
132 | }
133 | },
134 | })
135 |
136 | c_items.push({
137 | text: `Prepend (Tab)`,
138 | action: () => {
139 | let it = App.get_selected(`tabs`)
140 |
141 | if (it) {
142 | App.prepend_note(it, item.value)
143 | }
144 | },
145 | })
146 |
147 | c_items.push({
148 | text: `Apply (Global)`,
149 | action: () => {
150 | App.apply_global_notes(item.value)
151 | },
152 | })
153 |
154 | c_items.push({
155 | text: `Append (Global)`,
156 | action: () => {
157 | App.append_global_note(item.value)
158 | },
159 | })
160 |
161 | c_items.push({
162 | text: `Prepend (Global)`,
163 | action: () => {
164 | App.prepend_global_note(item.value)
165 | },
166 | })
167 | }
168 | }
169 |
170 | if (c_items.length === 1) {
171 | c_items[0].action()
172 | }
173 | else {
174 | App.show_context({
175 | title: `Action`,
176 | title_icon: App.action_icon,
177 | items: c_items,
178 | })
179 | }
180 | },
181 | })
182 | }
183 |
184 | ds_items.reverse()
185 |
186 | App.show_context({
187 | title: `Datastore (${items.length})`,
188 | title_icon: App.data_icon,
189 | items: ds_items,
190 | })
191 | }
192 |
193 | App.datastore_settings = () => {
194 | let data = App.get_settings_snapshot(``)
195 | App.add_to_datastore(data, `settings`)
196 | }
197 |
198 | App.datastore_theme = () => {
199 | let data = App.get_settings_snapshot(`theme`)
200 | App.add_to_datastore(data, `theme`)
201 | }
202 |
203 | App.datastore_urls = () => {
204 | let data = App.get_url_list()
205 | App.add_to_datastore(data, `urls`)
206 | }
207 |
208 | App.export_datastore = () => {
209 | App.export_data(`Datastore`, App.datastore)
210 | }
211 |
212 | App.import_datastore = () => {
213 | App.import_data(`Datastore`, (obj) => {
214 | App.datastore = obj
215 | App.stor_save_datastore()
216 | App.footer_message(`Datastore: Imported`)
217 | App.action_sound()
218 | })
219 | }
--------------------------------------------------------------------------------
/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.action_sound()
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 | {
29 | title: `Datastore backups`,
30 | text: `Using the "Datastore Settings" command you can save snapshots of the current settings in the datastore.
31 |
32 | Then you can use "Browse Datastore" to restore previous snapshots.`,
33 | },
34 | {
35 | title: `Find & Return`,
36 | text: `'Find' allows you to search for a specific setting.
37 |
38 | 'Return' opens the last category visited, at the correct scrolling position.`,
39 | },
40 | ]
--------------------------------------------------------------------------------
/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/idle_tabs.js:
--------------------------------------------------------------------------------
1 | App.idle_tabs_enabled = () => {
2 | return App.get_setting(`idle_tabs_check`)
3 | }
4 |
5 | App.get_idle_tabs_delay = () => {
6 | return App.get_setting(`idle_tabs_delay`)
7 | }
8 |
9 | App.start_idle_tabs_check = () => {
10 | if (!App.idle_tabs_enabled()) {
11 | App.stop_idle_tabs_timeout()
12 | return
13 | }
14 |
15 | App.start_idle_tabs_timeout()
16 | }
17 |
18 | App.start_idle_tabs_timeout = () => {
19 | App.idle_tabs_timeout = setTimeout(() => {
20 | App.do_idle_tabs_check()
21 | }, App.idle_tabs_delay)
22 | }
23 |
24 | App.stop_idle_tabs_timeout = () => {
25 | clearTimeout(App.idle_tabs_timeout)
26 | }
27 |
28 | App.do_idle_tabs_check = () => {
29 | if (!App.idle_tabs_enabled()) {
30 | return
31 | }
32 |
33 | let delay = App.get_idle_tabs_delay()
34 | let tabs = App.get_items(`tabs`)
35 |
36 | for (let tab of tabs) {
37 | if (!tab.activated) {
38 | continue
39 | }
40 |
41 | App.do_idle_tab_check(tab, App.now(), delay)
42 | }
43 |
44 | App.start_idle_tabs_timeout()
45 | }
46 |
47 | App.check_idle_tab = (tab) => {
48 | let delay = App.get_idle_tabs_delay()
49 | App.do_idle_tab_check(tab, App.now(), delay)
50 | }
51 |
52 | App.do_idle_tab_check = (tab, now, delay) => {
53 | let diff = now - tab.last_access || 0
54 | let mins = Math.floor(diff / App.MINUTE)
55 |
56 | if (mins >= delay) {
57 | if (!tab.idle) {
58 | tab.idle = true
59 | App.update_item({mode: `tabs`, id: tab.id, info: tab})
60 | }
61 | }
62 | else if (tab.idle) {
63 | tab.idle = false
64 | App.update_item({mode: `tabs`, id: tab.id, info: tab})
65 | }
66 | }
67 |
68 | App.tab_is_idle = (tab) => {
69 | return tab.idle && !tab.unloaded && !tab.header
70 | }
--------------------------------------------------------------------------------
/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 | let text_el = DOM.el(`#lock_screen_text`)
50 | let num_words = App.get_setting(`lock_screen_words`)
51 |
52 | if (num_words > 0) {
53 | let words = App.get_random_words(num_words)
54 | text_el.textContent = words.join(` `)
55 | DOM.show(text_el)
56 | }
57 | else {
58 | DOM.hide(text_el)
59 | }
60 |
61 | clearTimeout(App.words_autohide_timeout)
62 | let words_autohide = App.get_setting(`lock_screen_words_autohide`)
63 |
64 | if (words_autohide > 0) {
65 | App.words_autohide_timeout = setTimeout(() => {
66 | DOM.hide(text_el)
67 | }, words_autohide * 1000)
68 | }
69 |
70 | App.show_window(`lock_screen`)
71 | }
72 |
73 | App.unlock_screen = () => {
74 | let pw = App.get_setting(`lock_screen_password`)
75 |
76 | if (pw) {
77 | App.show_prompt({
78 | password: true,
79 | placeholder: `Password`,
80 | on_submit: (ans) => {
81 | if (ans === pw) {
82 | App.hide_window()
83 | }
84 | },
85 | })
86 |
87 | return
88 | }
89 |
90 | App.hide_window()
91 | }
--------------------------------------------------------------------------------
/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: `browse_datastore`, short: true}))
52 | items.push(App.cmd_item({cmd: `show_stuff_menu`, short: true}))
53 | items.push(App.cmd_item({cmd: `show_about`, short: true}))
54 | App.sep(items)
55 | items.push(App.cmd_item({cmd: `lock_screen`, short: true}))
56 | items.push(App.cmd_item({cmd: `show_palette`, short: true}))
57 | App.sep(items)
58 |
59 | items.push({text: `💉 Booster`, direct_action: () => {
60 | App.booster_shot()
61 | }, info: `Enable scripts to improve the experience`})
62 |
63 | App.sep(items)
64 | items.push(App.cmd_item({cmd: `focus_window_menu`, short: true}))
65 |
66 | let btn = DOM.el(`#${mode}_main_menu`)
67 |
68 | App.show_context({
69 | element: btn,
70 | items,
71 | expand: true,
72 | margin: btn.clientHeight,
73 | })
74 | }
75 |
76 | App.set_main_menu_text = (btn, mode, name = ``) => {
77 | let icon = App.mode_icon(mode)
78 |
79 | if (name) {
80 | name = name.substring(0, 12).trim()
81 | }
82 | else {
83 | name = App.get_mode_name(mode, true)
84 | }
85 |
86 | let value = App.button_text(icon, name)
87 | btn.innerHTML = ``
88 | btn.append(value)
89 | }
90 |
91 | App.main_button_middle_click = (e) => {
92 | let cmd = App.get_setting(`middle_click_main_button`)
93 | App.run_command({cmd, from: `main_menu`, e})
94 | }
--------------------------------------------------------------------------------
/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 | else if (message.action === `fullscreen_change`) {
32 | App.fix_scroll()
33 | }
34 | })
35 | }
--------------------------------------------------------------------------------
/js/main/mute.js:
--------------------------------------------------------------------------------
1 | App.mute_tab = async (id) => {
2 | try {
3 | await App.update_tab(id, {muted: true})
4 | }
5 | catch (err) {
6 | App.error(err)
7 | }
8 | }
9 |
10 | App.unmute_tab = async (id) => {
11 | try {
12 | await App.update_tab(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.set_notes(it, notes)
18 | }
19 |
20 | return true
21 | },
22 | value: App.get_notes(item),
23 | autosave: true,
24 | left: true,
25 | bottom: true,
26 | wrap: true,
27 | readonly: item.mode !== `tabs`,
28 | })
29 | }
30 |
31 | App.remove_item_notes = (item, single = false) => {
32 | let active
33 |
34 | if (single) {
35 | active = [item]
36 | }
37 | else {
38 | active = App.get_active_items({mode: item.mode, item})
39 | }
40 |
41 | if (active.length === 1) {
42 | let it = active[0]
43 |
44 | if (it.rule_notes && !it.custom_notes) {
45 | App.domain_rule_message()
46 | return
47 | }
48 | }
49 |
50 | App.remove_edits({
51 | what: [`notes`],
52 | items: active,
53 | text: `notes`,
54 | force_ask: true,
55 | })
56 | }
57 |
58 | App.remove_notes = (item) => {
59 | App.remove_item_notes(item, true)
60 | }
61 |
62 | App.edit_global_notes = () => {
63 | App.show_input({
64 | title: `Global Notes`,
65 | title_icon: App.notepad_icon,
66 | button: `Save`,
67 | action: (text) => {
68 | App.set_global_notes(text)
69 | return true
70 | },
71 | value: App.notes,
72 | autosave: true,
73 | left: true,
74 | bottom: true,
75 | wrap: true,
76 | })
77 | }
78 |
79 | App.get_noted_items = (mode) => {
80 | let items = []
81 |
82 | for (let item of App.get_items(mode)) {
83 | if (App.get_notes(item)) {
84 | items.push(item)
85 | }
86 | }
87 |
88 | return items
89 | }
90 |
91 | App.set_notes = (item, notes) => {
92 | App.apply_edit({
93 | what: `notes`,
94 | item,
95 | value: notes,
96 | on_change: (value) => {
97 | App.custom_save(item.id, `notes`, value)
98 | },
99 | })
100 | }
101 |
102 | App.apply_notes = (item, notes) => {
103 | let items = App.get_active_items(item)
104 |
105 | for (let it of items) {
106 | App.set_notes(it, notes)
107 | }
108 | }
109 |
110 | App.set_global_notes = (notes) => {
111 | App.notes = App.single_linebreak(notes)
112 | App.stor_save_notes()
113 | }
114 |
115 | App.apply_global_notes = (notes) => {
116 | App.set_global_notes(notes)
117 | App.footer_message(`Notes set`)
118 | App.action_sound()
119 | }
120 |
121 | App.add_note = (type, item, note = ``) => {
122 | function action(note) {
123 | if (!note) {
124 | return
125 | }
126 |
127 | let items = App.get_active_items(item)
128 |
129 | for (let it of items) {
130 | let current
131 |
132 | if (type.includes(`global`)) {
133 | current = App.notes
134 | }
135 | else {
136 | current = App.get_notes(it)
137 | }
138 |
139 | let new_notes
140 |
141 | if (type.startsWith(`append`)) {
142 | new_notes = current ? `${current}\n${note}` : note
143 | }
144 | else if (type.startsWith(`prepend`)) {
145 | new_notes = current ? `${note}\n${current}` : note
146 | }
147 |
148 | if (type.includes(`global`)) {
149 | App.set_global_notes(new_notes)
150 | }
151 | else {
152 | App.set_notes(it, new_notes)
153 | }
154 |
155 | App.footer_message(`Note added`)
156 | App.action_sound()
157 | }
158 | }
159 |
160 | if (note) {
161 | action(note)
162 | }
163 | else {
164 | App.show_prompt({
165 | placeholder: `Note`,
166 | on_submit: (text) => {
167 | action(text)
168 | },
169 | })
170 | }
171 | }
172 |
173 | App.append_note = (item, note = ``) => {
174 | App.add_note(`append`, item, note)
175 | }
176 |
177 | App.prepend_note = (item, note = ``) => {
178 | App.add_note(`prepend`, item, note)
179 | }
180 |
181 | App.append_global_note = (note = ``) => {
182 | App.add_note(`append_global`, undefined, note)
183 | }
184 |
185 | App.prepend_global_note = (note = ``) => {
186 | App.add_note(`prepend_global`, undefined, note)
187 | }
--------------------------------------------------------------------------------
/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/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 App.update_tab(id, {pinned: true})
4 | }
5 | catch (err) {
6 | App.error(err)
7 | }
8 | }
9 |
10 | App.unpin_tab = async (id) => {
11 | try {
12 | await App.update_tab(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 | boosted: false,
68 | }
69 |
70 | App.def_args(def_args, args)
71 |
72 | if (!args.info) {
73 | return false
74 | }
75 |
76 | let special = false
77 |
78 | if (args.o_item) {
79 | if (!args.url) {
80 | args.info = {...args.o_item.original_data, ...args.info}
81 | }
82 |
83 | args.o_item.original_data = args.info
84 | }
85 | else if (args.mode === `bookmarks`) {
86 | if (args.info.type === `folder`) {
87 | args.info = {...args.info}
88 | args.info.url = `${App.bookmarks_folder_url}/${args.info.id}`
89 | args.info.favIconUrl = `img/folder.jpg`
90 | args.info.title = `Folder: ${args.info.title}`
91 | special = true
92 | }
93 | }
94 |
95 | let decoded_url
96 |
97 | if (args.info.url) {
98 | try {
99 | // Check if valid URL
100 | decoded_url = decodeURI(args.info.url)
101 | }
102 | catch (err) {
103 | return false
104 | }
105 | }
106 |
107 | let url = App.format_url(args.info.url || ``)
108 |
109 | if (args.exclude.includes(url)) {
110 | return false
111 | }
112 |
113 | let path = App.get_path(decoded_url)
114 | let protocol = App.get_protocol(url)
115 | let hostname = App.get_hostname(url)
116 | let title = App.get_item_title(args)
117 | let image = App.is_image(url)
118 | let video = App.is_video(url)
119 | let audio = App.is_audio(url)
120 |
121 | let item = {
122 | title,
123 | url,
124 | path,
125 | protocol,
126 | hostname,
127 | favicon: args.info.favIconUrl,
128 | mode: args.mode,
129 | window_id: args.info.windowId,
130 | session_id: args.info.sessionId,
131 | decoded_url,
132 | image,
133 | video,
134 | audio,
135 | special,
136 | is_item: true,
137 | header: false,
138 | }
139 |
140 | if (args.mode === `tabs`) {
141 | item.header = App.is_header_url(item.url)
142 | item.active = args.info.active
143 | item.pinned = args.info.pinned
144 | item.playing = args.info.audible
145 | item.muted = args.info.mutedInfo.muted
146 | item.unloaded = args.info.discarded
147 | item.last_access = args.info.lastAccessed
148 | item.status = args.info.status
149 | item.parent = args.info.openerTabId
150 | item.container_name = args.info.container_name
151 | item.container_color = args.info.container_color
152 | item.boosted = args.info.boosted
153 | }
154 | else if (args.mode === `history`) {
155 | item.last_visit = args.info.lastVisitTime
156 | }
157 | else if (args.mode === `bookmarks`) {
158 | item.parent_id = args.info.parentId
159 | item.date_added = args.info.dateAdded
160 | item.type = args.info.type
161 | }
162 |
163 | App.check_rules(item)
164 |
165 | if (args.o_item) {
166 | args.o_item = Object.assign(args.o_item, item)
167 | App.refresh_item_element(args.o_item)
168 | App.refresh_tab_box_element(args.o_item)
169 |
170 | if (App.get_selected(args.mode) === args.o_item) {
171 | App.update_footer_info(args.o_item)
172 | }
173 | }
174 | else {
175 | if (!args.list) {
176 | if ((args.mode === `tabs`) && !item.active && item.parent) {
177 | item.unread = true
178 | }
179 | }
180 |
181 | item.original_data = args.info
182 | item.id = args.info.id || App[`${args.mode}_idx`]
183 | item.visible = true
184 | item.selected = false
185 | item.tab_box = false
186 | item.last_scroll = 0
187 | item.activated = false
188 |
189 | App.create_empty_item_element(item)
190 | let fill = App.get_setting(`fill_elements`)
191 |
192 | if (fill === `instant`) {
193 | App.create_item_element(item)
194 | }
195 |
196 | if (args.mode === `tabs`) {
197 | if (args.add_parent) {
198 | App.add_tab_parent(item)
199 | }
200 | }
201 |
202 | App[`${args.mode}_idx`] += 1
203 | }
204 |
205 | return item
206 | }
207 |
208 | App.process_search_item = (info) => {
209 | info.path = App.get_path(info.url || `https://no.url`)
210 | }
--------------------------------------------------------------------------------
/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_palette_history = async () => {
92 | App.palette_history = await App.get_local_storage(App.stor_palette_history_name, [])
93 | App.debug(`Stor: Got palette history`)
94 | }
95 |
96 | App.stor_save_palette_history = () => {
97 | App.debug(`Stor: Saving palette history`)
98 | App.save_local_storage(App.stor_palette_history_name, App.palette_history)
99 | }
100 |
101 | App.stor_get_tag_history = async () => {
102 | let def = [`jump`]
103 | App.tag_history = await App.get_local_storage(App.stor_tag_history_name, def)
104 | App.debug(`Stor: Got tag history`)
105 | }
106 |
107 | App.stor_save_tag_history = () => {
108 | App.debug(`Stor: Saving tag history`)
109 | App.save_local_storage(App.stor_tag_history_name, App.tag_history)
110 | }
111 |
112 | App.stor_get_title_history = async () => {
113 | App.title_history = await App.get_local_storage(App.stor_title_history_name, [])
114 | App.debug(`Stor: Got title history`)
115 | }
116 |
117 | App.stor_save_title_history = () => {
118 | App.debug(`Stor: Saving title history`)
119 | App.save_local_storage(App.stor_title_history_name, App.title_history)
120 | }
121 |
122 | App.stor_get_icon_history = async () => {
123 | App.icon_history = await App.get_local_storage(App.stor_icon_history_name, App.default_icons.slice(0))
124 | App.debug(`Stor: Got icon history`)
125 | }
126 |
127 | App.stor_save_icon_history = () => {
128 | App.debug(`Stor: Saving icon history`)
129 | App.save_local_storage(App.stor_icon_history_name, App.icon_history)
130 | }
131 |
132 | App.stor_get_first_time = async () => {
133 | App.first_time = await App.get_local_storage(App.stor_first_time_name, {})
134 | App.debug(`Stor: Got first time`)
135 | }
136 |
137 | App.stor_save_first_time = () => {
138 | App.debug(`Stor: Saving first_time`)
139 | App.save_local_storage(App.stor_first_time_name, App.first_time)
140 | }
141 |
142 | App.stor_get_notes = async () => {
143 | App.notes = await App.get_local_storage(App.stor_notes_name, ``)
144 | App.debug(`Stor: Got notes`)
145 | }
146 |
147 | App.stor_save_notes = () => {
148 | App.debug(`Stor: Saving notes`)
149 | App.save_local_storage(App.stor_notes_name, App.notes)
150 | }
151 |
152 | App.stor_get_bookmark_folder_picks = async () => {
153 | App.bookmark_folder_picks = await App.get_local_storage(App.stor_bookmark_folder_picks, [])
154 | App.debug(`Stor: Got bookmark folder picks`)
155 | }
156 |
157 | App.stor_save_bookmark_folder_picks = () => {
158 | App.debug(`Stor: Saving bookmark folder picks`)
159 | App.save_local_storage(App.stor_bookmark_folder_picks, App.bookmark_folder_picks)
160 | }
161 |
162 | App.stor_get_history_picks = async () => {
163 | App.history_picks = await App.get_local_storage(App.stor_history_picks, [])
164 | App.debug(`Stor: Got history picks`)
165 | }
166 |
167 | App.stor_save_history_picks = () => {
168 | App.debug(`Stor: Saving history picks`)
169 | App.save_local_storage(App.stor_history_picks, App.history_picks)
170 | }
171 |
172 | App.stor_get_datastore = async () => {
173 | App.datastore = await App.get_local_storage(App.stor_datastore, {})
174 | App.debug(`Stor: Got datastore`)
175 | }
176 |
177 | App.stor_save_datastore = () => {
178 | App.debug(`Stor: Saving datastore`)
179 | App.save_local_storage(App.stor_datastore, App.datastore)
180 | }
--------------------------------------------------------------------------------
/js/main/tab_count.js:
--------------------------------------------------------------------------------
1 | App.setup_tab_count = () => {
2 | App.update_tab_count_debouncer = App.create_debouncer(() => {
3 | App.do_update_tab_count()
4 | }, App.update_tab_count_delay)
5 | }
6 |
7 | App.update_tab_count = () => {
8 | App.update_tab_count_debouncer.call()
9 | }
10 |
11 | App.do_update_tab_count = () => {
12 | App.update_tab_count_debouncer.cancel()
13 |
14 | if (!App.get_setting(`tab_count`)) {
15 | return
16 | }
17 |
18 | let domains = App.group_tabs_by_domain()
19 |
20 | for (let domain in domains) {
21 | let items = domains[domain]
22 |
23 | if (!items.length) {
24 | return
25 | }
26 |
27 | let num = items.length
28 |
29 | for (let item of items) {
30 | if (!item.element) {
31 | continue
32 | }
33 |
34 | let count = DOM.el(`.tab_count`, item.element)
35 |
36 | if (count) {
37 | if (num > 1) {
38 | count.textContent = num
39 | DOM.show(count)
40 | }
41 | else {
42 | DOM.hide(count)
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | App.create_tab_count = () => {
50 | return DOM.create(`div`, `tab_count item_node hidden`)
51 | }
--------------------------------------------------------------------------------
/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 | else if (what === `clusters`) {
82 | tabs = App.get_tab_clusters()
83 | title = `Clusters`
84 | title_icon = App.cluster_icon
85 | }
86 |
87 | let items = []
88 | let playing_icon = App.get_setting(`playing_icon`)
89 | let muted_icon = App.get_setting(`muted_icon`)
90 |
91 | for (let tab of tabs) {
92 | let title = App.title(tab)
93 | let icon
94 |
95 | if (tab.muted) {
96 | icon = muted_icon
97 | }
98 | else if (tab.playing) {
99 | icon = playing_icon
100 | }
101 |
102 | let favicon = tab.favicon
103 |
104 | if (!favicon) {
105 | favicon = `img/favicon.jpg`
106 | }
107 |
108 | let obj = {
109 | image: favicon,
110 | icon,
111 | text: title,
112 | info: tab.url,
113 | action: async () => {
114 | await App.check_on_tabs()
115 | App.tabs_action({item: tab, from: `tab_list`})
116 | },
117 | middle_action: () => {
118 | App.close_tab_or_tabs(tab.id)
119 | },
120 | context_action: (e) => {
121 | App.show_item_menu({item: tab, e})
122 | },
123 | icon_action: async (e, icon) => {
124 | if (tab.muted) {
125 | await App.unmute_tab(tab.id)
126 |
127 | if (!tab.playing) {
128 | icon.innerHTML = ``
129 | }
130 | else {
131 | icon.innerHTML = playing_icon
132 | }
133 | }
134 | else if (tab.playing) {
135 | await App.mute_tab(tab.id)
136 | icon.innerHTML = muted_icon
137 | }
138 | },
139 | }
140 |
141 | if (tab.active) {
142 | obj.bold = true
143 | }
144 |
145 | items.push(obj)
146 | }
147 |
148 | App.show_context({
149 | items, e,
150 | title,
151 | title_icon,
152 | middle_action_remove: true,
153 | title_number: true,
154 | })
155 | }
--------------------------------------------------------------------------------
/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 | }
117 |
118 | App.edit_title_directly = (item, value) => {
119 | let obj = {}
120 | obj.title = value
121 | obj.item = item
122 | App.edit_tab_title(obj)
123 | }
124 |
125 | App.get_item_title = (args) => {
126 | let title = args.info.title || ``
127 |
128 | if (!App.get_setting(`show_protocol`)) {
129 | title = App.remove_protocol(title)
130 | }
131 |
132 | return title
133 | }
--------------------------------------------------------------------------------
/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 | }
92 |
93 | App.mirror = (what) => {
94 | if (App.mirror_mode === what) {
95 | App.mirror_mode = `none`
96 | }
97 | else {
98 | App.mirror_mode = what
99 | }
100 |
101 | App.body_remove(`mirror_horizontal`)
102 | App.body_remove(`mirror_vertical`)
103 | App.body_add(`mirror_${App.mirror_mode}`)
104 | }
--------------------------------------------------------------------------------
/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 (App.get_obfuscated(item) && App.get_setting(`warn_special_obfuscated`)) {
56 | return false
57 | }
58 |
59 | if (!item.header && App.get_setting(`warn_special_edited`)) {
60 | if (App.edited(item, false, [`obfuscated`])) {
61 | return false
62 | }
63 | }
64 | }
65 | }
66 |
67 | return true
68 | }
--------------------------------------------------------------------------------
/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": "6502",
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 | "activeTab",
20 | ""
21 | ],
22 | "icons": {
23 | "128": "img/icon128.png"
24 | },
25 | "background": {
26 | "scripts": [
27 | "background/background.js",
28 | "background/bookmarks_server.js"
29 | ],
30 | "persistent": true
31 | },
32 | "browser_action": {
33 | "default_icon": {
34 | "128": "img/icon128.png"
35 | },
36 | "default_title": "Grasshopper",
37 | "default_popup": "main.html?popup"
38 | },
39 | "sidebar_action": {
40 | "default_title": "Grasshopper",
41 | "default_panel": "main.html?sidebar",
42 | "default_icon": "img/icon128.png",
43 | "open_at_install": false
44 | },
45 | "commands": {
46 | "_execute_browser_action": {
47 | "suggested_key": {
48 | "default": "Ctrl+Space"
49 | },
50 | "description": "Open the popup"
51 | },
52 | "_execute_sidebar_action": {
53 | "suggested_key": {
54 | "default": "Ctrl+Shift+Space"
55 | },
56 | "description": "Toggle the sidebar"
57 | },
58 | "popup_tabs": {
59 | "description": "Open the popup in Tabs mode"
60 | },
61 | "popup_history": {
62 | "description": "Open the popup in History mode"
63 | },
64 | "popup_bookmarks": {
65 | "description": "Open the popup in Bookmarks mode"
66 | },
67 | "popup_closed": {
68 | "description": "Open the popup in Closed mode"
69 | },
70 | "browser_command_1": {
71 | "description": "Browser Command 1"
72 | },
73 | "browser_command_2": {
74 | "description": "Browser Command 2"
75 | },
76 | "browser_command_3": {
77 | "description": "Browser Command 3"
78 | },
79 | "browser_command_4": {
80 | "description": "Browser Command 4"
81 | },
82 | "browser_command_5": {
83 | "description": "Browser Command 5"
84 | },
85 | "browser_command_6": {
86 | "description": "Browser Command 6"
87 | },
88 | "browser_command_7": {
89 | "description": "Browser Command 7"
90 | },
91 | "browser_command_8": {
92 | "description": "Browser Command 8"
93 | },
94 | "browser_command_9": {
95 | "description": "Browser Command 9"
96 | },
97 | "browser_command_10": {
98 | "description": "Browser Command 10"
99 | },
100 | "popup_command_1": {
101 | "description": "Popup Command 1"
102 | },
103 | "popup_command_2": {
104 | "description": "Popup Command 2"
105 | },
106 | "popup_command_3": {
107 | "description": "Popup Command 3"
108 | },
109 | "popup_command_4": {
110 | "description": "Popup Command 4"
111 | },
112 | "popup_command_5": {
113 | "description": "Popup Command 5"
114 | },
115 | "popup_command_6": {
116 | "description": "Popup Command 6"
117 | },
118 | "popup_command_7": {
119 | "description": "Popup Command 7"
120 | },
121 | "popup_command_8": {
122 | "description": "Popup Command 8"
123 | },
124 | "popup_command_9": {
125 | "description": "Popup Command 9"
126 | },
127 | "popup_command_10": {
128 | "description": "Popup Command 10"
129 | }
130 | },
131 | "browser_specific_settings": {
132 | "gecko": {
133 | "id": "{23aee10d-1130-4694-80ef-428e287ba83d}"
134 | }
135 | }
136 | }
--------------------------------------------------------------------------------
/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/" | grep -v "words.js" | 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}"
--------------------------------------------------------------------------------