├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_report.yml ├── LICENSE ├── README.md └── src ├── _locales └── en │ └── messages.json ├── css ├── chromium │ ├── chrome_shared.css │ ├── text_defaults.css │ └── widgets.css ├── options.css ├── popup.css └── ui │ ├── fade.css │ ├── generate-alert.css │ ├── hover.css │ └── toggle.css ├── html ├── options.html └── popup.html ├── img ├── extras │ ├── ChromeWebStore_Badge_v2_206x58_C.png │ ├── chrome.png │ └── urli.png ├── font-awesome │ ├── black │ │ ├── check.png │ │ ├── filter.png │ │ ├── gear.png │ │ ├── info-circle.png │ │ ├── keyboard-o.png │ │ ├── mouse-pointer.png │ │ ├── pencil-square.png │ │ ├── question-circle.png │ │ ├── times.png │ │ └── window-restore.png │ ├── blue │ │ ├── minus-circle.png │ │ └── plus-circle.png │ ├── green │ │ ├── check-circle.png │ │ ├── chevron-circle-left.png │ │ └── chevron-circle-right.png │ ├── orange │ │ ├── clock-o.png │ │ ├── pause-circle.png │ │ ├── play-circle.png │ │ └── refresh.png │ ├── purple │ │ ├── cloud-download.png │ │ ├── exclamation-circle.png │ │ └── flask.png │ ├── red │ │ └── times-circle.png │ └── yellow │ │ └── star.png └── icons │ ├── dark │ ├── 128.png │ ├── 16.png │ ├── 24.png │ ├── 32.png │ └── 48.png │ ├── light │ ├── 16.png │ ├── 24.png │ └── 32.png │ ├── rainbow │ ├── 16.png │ ├── 24.png │ └── 32.png │ └── urli │ ├── 16.png │ ├── 24.png │ └── 32.png ├── js ├── action.js ├── auto.js ├── background.js ├── download.js ├── increment-decrement.js ├── next-prev.js ├── options.js ├── permissions.js ├── popup.js ├── shortcuts.js └── ui.js └── manifest.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_report.yml: -------------------------------------------------------------------------------- 1 | name: Issue Report 2 | description: Report a problem, request a feature, ask a question, or get general help with an issue. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for taking the time to fill out this report. 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Description 12 | description: What is this issue about? The more detailed you can be, the better! You can describe what you tried and what you expected to happen. You can also upload any screenshots or images here to help explain the issue (but please do not upload images of the mascot). 13 | placeholder: Describe the issue 14 | validations: 15 | required: true 16 | - type: input 17 | id: url 18 | attributes: 19 | label: URL 20 | description: If applicable, could you provide an example URL that relates to the issue? However, please do not post NSFW (Not Safe For Work) URLs. You may enter "N/A" if not applicable. 21 | placeholder: "Example: https://www.example.com/this/specific/url.html" 22 | validations: 23 | required: true 24 | - type: input 25 | id: version 26 | attributes: 27 | label: Version 28 | description: What version of the app are you using? Please enter an actual number (do not enter "Latest" or "Current"). 29 | placeholder: "Example: 1" 30 | validations: 31 | required: true 32 | - type: input 33 | id: browser 34 | attributes: 35 | label: Browser 36 | description: What browser and browser version are you using? Please enter an actual number (do not enter "Latest" or "Current"). 37 | placeholder: "Example: Chrome 100" 38 | validations: 39 | required: true 40 | - type: input 41 | id: os 42 | attributes: 43 | label: OS 44 | description: What operating system are you using? 45 | placeholder: "Example: Windows 10" 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: device 50 | attributes: 51 | label: Device 52 | description: What type of device are you using? 53 | options: 54 | - 55 | - PC 56 | - Phone 57 | - Other 58 | validations: 59 | required: true 60 | - type: checkboxes 61 | id: adherence 62 | attributes: 63 | label: Adherence 64 | description: Please confirm that you've read the [Sticky](https://github.com/sixcious/url-incrementer/issues/20) before posting your issue or question. 65 | options: 66 | - label: I have read the [Sticky](https://github.com/sixcious/url-incrementer/issues/20) 67 | required: true 68 | validations: 69 | required: true 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Roy Six 2 | 3 | LICENSE TBD. Please contact the copyright holders if you have any questions. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 8 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 9 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 10 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 11 | SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URL Incrementer 2 | URL Incrementer 3 | 4 | ## Available For 5 | Google Chrome 6 |       7 | Microsoft Edge 8 |       9 | Mozilla Firefox 10 | 11 |

12 | 13 | 14 | ### Important Note About Version 6 (September 2024) 15 | In order to update URLI to Manifest V3 (MV3), the permissions level needed to be increased to ``. Please see this [GitHub Issue](https://github.com/sixcious/url-incrementer/issues/17) for more information. Thank you for your understanding. 16 | 17 |










18 | 19 | ## Features 20 | - 4 Actions: Increment URL, Next Link, Click Element, URL List 21 | - Keyboard, Mouse, and Context Menu Shortcuts 22 | - 1-Click Increment Decrement Buttons 23 | - Advanced Incrementing Features: Multi, Error Skip, Date Time, Decimal Number, Roman Numeral, Custom Base, and Alphanumeric (Base 2-36, includes Hexadecimal) 24 | - Auto Incrementing 25 | - Save URLs: Save settings for your favorite URLs and URLI will always remember them the next time you visit 26 | - Shuffle URLs: Make it fun and randomize the next pages you see! 27 | - Options: Change how URLI pre-selects the number to increment... and more 28 | - Toolkit: Generate Links, Open Tabs, Crawl URLs (Special Mode Required) 29 | 30 | #### Feature Notes 31 | - Firefox only: Local file:// URLs may not increment due to a bug in Firefox (Bug 1266960) 32 | - Mapping shortcut keys to mouse buttons with 3rd party apps like Logitech Gaming Software is not officially supported and may only work if you use Logitech's "Multikey Macro" option 33 | 34 | ## Documentation 35 | - [Help Guide](https://github.com/sixcious/url-incrementer/wiki) 36 | - [Version History](https://github.com/sixcious/url-incrementer/wiki/Version-History) 37 | 38 | ## Permissions Justification 39 | `Read and change all your data on the websites you visit`- URLI needs to request this permission so that it can offer its advanced internal shortcuts, auto incrementing, and for the content script to load on each page you want to incrememt. 40 | 41 | ## Privacy Policy 42 | URL Incrementer does *not* track you. It does *not* use analytic services. It does *not* collect or transmit any data from your device or computer. All your data is stored locally on your device. Your data is *your* data. 43 | 44 | ## Contributing 45 | Thank you for considering to contribute! The best way you can help me is to leave a review on the [Chrome Web Store](https://chromewebstore.google.com/detail/url-incrementer/hjgllnccfndbjbedlecgdedlikohgbko/reviews), [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/url-incrementer/hnndkchemmjdlodgpcnojbmadckbieek), or [Mozilla Firefox Add-ons](https://addons.mozilla.org/firefox/addon/url-incrementer/). I really appreciate your support. 46 | 47 | ## License 48 | View License 49 | 50 | ## Copyright 51 | URLI, a URL Incrementer 52 | Copyright © 2011-2020 Roy Six 53 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "URL Incrementer", 4 | "description": "Extension name." 5 | }, 6 | "short_name": { 7 | "message": "URLI", 8 | "description": "Extension short name." 9 | }, 10 | "description": { 11 | "message": "Increment [+] a URL or go to the Next [>] Page; Supports Auto- and Download-Incrementing", 12 | "description": "Extension description." 13 | }, 14 | "title": { 15 | "message": "URL Incrementer", 16 | "description": "Extension title (tool tip) shown when hovered." 17 | }, 18 | 19 | "POPUP": { "message": "----- POPUP -----" }, 20 | "setup_h3_label": { "message": "Select the part of the URL to Increment [+] or Decrement [-]" }, 21 | "url_label": { "message": "URL" }, 22 | "leading_zeros_pad_label": { "message": "Pad With Leading Zeros" }, 23 | "accept_button": { "message": "Accept" }, 24 | "cancel_button": { "message": "Cancel" }, 25 | "options_button_span": { "message": "Options" }, 26 | "auto_toggle_span": { "message": "AUTO" }, 27 | "download_toggle_span": { "message": "DL" }, 28 | "auto_label": { "message": "Auto" }, 29 | "auto_action_label": { "message": "Action"}, 30 | "auto_action_option_increment": { "message": "Increment [+]" }, 31 | "auto_action_option_decrement": { "message": "Decrement [-]" }, 32 | "auto_action_option_next": { "message": "Next [>]" }, 33 | "auto_action_option_prev": { "message": "Prev [<]" }, 34 | "auto_times_label": { "message": "Times"}, 35 | "auto_seconds_label": { "message": "Seconds" }, 36 | "auto_wait_label": { "message": "Wait for the page to fully load" }, 37 | "auto_badge_label": { "message": "Show times left in the icon badge" }, 38 | "auto_eta_label": { "message": "ETA: " }, 39 | "auto_eta_day": { "message": "Over a Day (You sure about this?!)" }, 40 | "auto_eta_hour": { "message": " Hour " }, 41 | "auto_eta_hours": { "message": " Hours " }, 42 | "auto_eta_minute": { "message": " Minute " }, 43 | "auto_eta_minutes": { "message": " Minutes " }, 44 | "auto_eta_second": { "message": " Second " }, 45 | "auto_eta_seconds": { "message": " Seconds " }, 46 | "auto_eta_tbd": { "message": "TBD" }, 47 | "auto_eta_done": { "message": "Done!" }, 48 | "download_label": { "message": "Download" }, 49 | "download_strategy_label": { "message": "Strategy" }, 50 | "download_strategy_option_extensions": { "message": "Download these file extension(s)" }, 51 | "download_strategy_option_tags": { "message": "Download these tag(s)" }, 52 | "download_strategy_option_attributes": { "message": "Download these attribute(s)" }, 53 | "download_strategy_option_selector": { "message": "Use a custom JavaScript selector" }, 54 | "download_strategy_option_page": { "message": "Download the web page itself" }, 55 | "download_optional_label": { "message": "Optional Filters" }, 56 | "download_includes_label": { "message": "URL Includes" }, 57 | "download_excludes_label": { "message": "URL Excludes" }, 58 | "download_min_mb_label": { "message": "Min File Size" }, 59 | "download_max_mb_label": { "message": "Max File Size" }, 60 | "download_megabytes_label": { "message": "MB" }, 61 | "download_preview_set": { "message": "Set to download " }, 62 | "download_preview_outof": { "message": " out of " }, 63 | "download_preview_urls": { "message": " URLs" }, 64 | "download_preview_thumb_label": { "message": "Thumb" }, 65 | "download_preview_extension_label": { "message": "Ext" }, 66 | "download_preview_tag_label": { "message": "Tag" }, 67 | "download_preview_attribute_label": { "message": "Atr" }, 68 | "download_preview_url_label": { "message": "URL" }, 69 | "download_preview_compressed_label": { "message": "Compressed View" }, 70 | "download_preview_blocked": { "message": "Sorry, please wait a few more seconds ... scanning this page for URLs is either taking a really long time or URLI is not allowed to check for downloads on this page (e.g. chrome:// pages). Please try disabling and re-enabling Download again in the Options to make sure URLI has permissions." }, 71 | "download_preview_noresults": { "message": "Sorry, no results were found." }, 72 | "download_help_extensions_label": { "message": "File Extensions Help" }, 73 | "download_help_tags_label": { "message": "Tags Help" }, 74 | "download_help_attributes_label": { "message": "Attributes Help" }, 75 | "download_help_selector_label": { "message": "Selector Help" }, 76 | "download_help_page_label": { "message": "Web Page Help" }, 77 | "download_help_extensions_title": { "message": "These are the file extensions found on the page based on the URLs. If the URL doesn't contain the extension in it (e.g. .jpg), URLI won't find it. Important: Since these extensions are based only on the URL Strings (not the actual files), it's not guaranteed to be the file's true type." }, 78 | "download_help_tags_title": { "message": "These are the HTML tags (e.g. ) that were found on the page." }, 79 | "download_help_attributes_title": { "message": "These are the HTML tag attributes (e.g. ] Prev [<] with Auto, please first enable Enhanced Mode in Options to give URLI permissions" }, 94 | "auto_times_invalid_error": { "message": "Auto Times must be between 1 and 100000" }, 95 | "auto_seconds_invalid_error": { "message": "Auto Seconds must be between 1 and 3600" }, 96 | "auto_download_seconds_error": { "message": "When using Auto with Download, seconds must be at least 5" }, 97 | "download_enabled_error": { "message": "Please first enable Download in Options to give URLI permissions" }, 98 | 99 | "OPTIONS": { "message": "----- OPTIONS -----" }, 100 | "help_quick_shortcuts": { "message": "If Quick is enabled, you can Increment [+] or perform other actions instantly without having to enter Setup"}, 101 | "chrome_shortcuts_h3": { "message": "Chrome Shortcuts" }, 102 | "chrome_shortcuts_enable_button": { "message": "Enable Chrome Shortcuts" }, 103 | "chrome_shortcuts_quick_enable_label": { "message": "Enable Quick Chrome Shortcuts" }, 104 | "chrome_shortcuts_p1": { "message": "URLI is currently set to use Chrome Shortcuts." }, 105 | "chrome_shortcuts_p2": { "message": "Chrome Shortcuts support limited keys, but you can enable URLI's Internal Shortcuts to use any key or mouse button you want! (requires permissions)" }, 106 | "chrome_shortcuts_button": { "message": "Configure Chrome Shortcuts" }, 107 | "internal_shortcuts_h3": { "message": "Internal Shortcuts" }, 108 | "internal_shortcuts_enable_button": { "message": "Enable Internal Shortcuts" }, 109 | "internal_shortcuts_p": { "message": "URLI is currently set to use its internal shortcuts! Click on the text boxes and press the key(s) you'd like to set. Alt, Ctrl, Shift, and Meta keys can be used. Left/Middle/Right Mouse Buttons can also be set as shortcuts."}, 110 | "key_quick_enable_label": {"message": "Enable Quick Keyboard Shortcuts" }, 111 | "mouse_quick_enable_label": {"message": "Enable Quick Mouse Button Shortcuts" }, 112 | "key_label": { "message": "Key" }, 113 | "mouse_label": { "message": "Mouse" }, 114 | "key_increment_label": { "message": "Increment [+]" }, 115 | "key_decrement_label": { "message": "Decrement [-]" }, 116 | "key_next_label": { "message": "Next [>]" }, 117 | "key_prev_label": { "message": "Prev [<]" }, 118 | "key_clear_label": { "message": "Clear [x]" }, 119 | "key_auto_label": { "message": "Auto Pause" }, 120 | "key_notset_option": { "message": "(Not set)" }, 121 | "mouse_notset_option": { "message": "(Not set)" }, 122 | "mouse_left_option": { "message": "Left" }, 123 | "mouse_middle_option": { "message": "Middle" }, 124 | "mouse_right_option": { "message": "Right" }, 125 | "one_click_buttons_h3": { "message": "1-Click Increment Decrement Buttons" }, 126 | "one_click_buttons_p": { "message": "Accessing URLI's buttons requires 2 clicks, but you can add 1-Click buttons to your toolbar! They consume 0 Chrome memory when inactive! It's a win-win!" }, 127 | "one_click_buttons_increment": { "message": "1-Click URL Increment Button for URLI (Chrome Web Store)" }, 128 | "one_click_buttons_decrement": { "message": "1-Click URL Decrement Button for URLI (Chrome Web Store)" }, 129 | "icon_settings_h3": { "message": "Icon Settings" }, 130 | "icon_color_label": { "message": "Icon Color (Toolbar)" }, 131 | "icon_color_label_dark": { "message": "Dark" }, 132 | "icon_color_label_light": { "message": "Light" }, 133 | "icon_color_label_rainbow": { "message": "Rainbow" }, 134 | "icon_color_label_urli": { "message": "URLI" }, 135 | "icon_feedback_enable_label": { "message": "Enable icon feedback when incrementing or from other actions" }, 136 | "popup_settings_h3": { "message": "Popup User Interface Settings" }, 137 | "popup_button_size_label": { "message": "Button Size" }, 138 | "popup_animations_enable_label": { "message": "Enable button click animations (Try it!)" }, 139 | "popup_open_setup_label": { "message": "When Popup UI opens, automatically jump to URL Setup" }, 140 | "popup_settings_can_overwrite_label": { "message": "Allow Popup UI settings to overwrite these settings" }, 141 | "increment_decrement_settings_h3": { "message": "Increment Decrement Settings" }, 142 | "increment_decrement_settings_p": { "message": "Change the way URLI pre-selects the number to increment and other options. You can set a couple of these options in the Popup UI too." }, 143 | "selection_option_prefixes": { "message": "Select numbers in the URL with prefixes like page=1" }, 144 | "selection_option_lastnumber": { "message": "Select the last number in the URL" }, 145 | "selection_option_firstnumber": { "message": "Select the first number in the URL" }, 146 | "selection_option_custom": { "message": "Use a custom JavaScript regular expression" }, 147 | "selection_custom_url_label": { "message": "Test URL String (Required)" }, 148 | "selection_custom_pattern_label": { "message": "Pattern" }, 149 | "selection_custom_flags_label": { "message": "Flags" }, 150 | "selection_custom_group_label": { "message": "Capturing Group (Usually 0)" }, 151 | "selection_custom_index_label": { "message": "Index Position (Usually 0)" }, 152 | "selection_custom_test_button": { "message": "Test" }, 153 | "selection_custom_save_button": { "message": "Save" }, 154 | "selection_custom_help_label": { "message": "For more testing and help, use" }, 155 | "selection_custom_match_error": { "message": "Match not found" }, 156 | "selection_custom_group_error": { "message": "Group must be 0 or higher" }, 157 | "selection_custom_index_error": { "message": "Index must be 0 or higher" }, 158 | "selection_custom_matchgroup_error": { "message": "Match found, but not in group entered" }, 159 | "selection_custom_matchindex_error": { "message": "Match found, but not in index entered" }, 160 | "selection_custom_matchnotalphanumeric_error": { "message": "is not alphanumeric (try adjusting index)" }, 161 | "selection_custom_test_success": { "message": "Success" }, 162 | "selection_custom_save_success": { "message": "Saved" }, 163 | "leading_zeros_label": { "message": "Leading Zeros" }, 164 | "leading_zeros_pad_by_detection_label": { "message": "Pad By Detection (Recommended)" }, 165 | "error_skip_what": { "message": "What's Error Skipping?" }, 166 | "error_skip_description": { "message": "URLI can check if the next URL will return an HTTP error code (like 404) and increment again, skipping it up to 100 times. If an error is encountered, URLI's icon will flash with the error code (or flash \"RED\" for redirects). Set it to 0 to disable it. Important: This will make a request to the server each time to check the status code, and setting this value too high might cause the server to issue a \"Too Many Requests\" response. A value of 10 or less should be reasonably OK. Also, Please Note: Using this with Auto or the Popup/1-Click Buttons requires Enhanced Mode." }, 167 | "next_prev_settings_h3": { "message": "Next Prev Settings" }, 168 | "next_prev_settings_p": { "message": "Some URLs may not have numbers, so URLI can increment using Next Prev links on the page. To use Next Prev with Auto, enable Enhanced Mode below." }, 169 | "next_prev_links_priority_label": { "message": "Links" }, 170 | "next_prev_links_priority_attributes_option": { "message": "Prefer links with attributes like \"rel=next\"" }, 171 | "next_prev_links_priority_innerHTML_option": { "message": "Prefer links with inner HTML like \"Next\" in them" }, 172 | "next_prev_domain_label": { "message": "Domain" }, 173 | "next_prev_same_domain_policy_enable_label": { "message": "Only consider same-domain links" }, 174 | "next_prev_popup_buttons_label": { "message": "Buttons" }, 175 | "next_prev_popup_buttons_description_label": { "message": "Show Next Prev buttons (Requires Enhanced Permissions)" }, 176 | "auto_settings_h3": { "message": "Auto Settings" }, 177 | "auto_settings_h3_alt": { "message": "Auto" }, 178 | "auto_settings_p": { "message": "Relax and let URLI auto-increment! Start Auto anytime by toggling it in the Popup UI. Supports multiple tabs at the same time and pause/resume." }, 179 | "download_settings_h3": { "message": "Download Settings" }, 180 | "download_settings_h3_alt": { "message": "Download" }, 181 | "download_experimental_label": { "message": "EXPERIMENTAL" }, 182 | "download_enable_button": { "message": "Enable Download" }, 183 | "download_disable_button": { "message": "Disable Download" }, 184 | "download_disable_p": { "message": "This isn't just any downloader! Use it with Auto to create an Auto Incrementer Downloader! Click the button to enable (grant permissions)." }, 185 | "download_enable_p": { "message": "Download is now enabled! Start Downloading anytime by toggling the \"DL\" in the Popup UI." }, 186 | "enhanced_mode_h3": { "message": "Enhanced Mode" }, 187 | "enhanced_mode_enable_button": { "message": "Enable Enhanced Mode" }, 188 | "enhanced_mode_disable_button": { "message": "Disable Enhanced Mode" }, 189 | "enhanced_mode_enable_p": { "message": "Enhanced Mode is now enabled! You can now use Next/Prev and Error Skipping with Auto and Popup/1-Click Buttons." }, 190 | "enhanced_mode_disable_p": { "message": "Enhanced Mode lets you use both Next/Prev and Error Skipping with Auto and Popup/1-Click Buttons. Click the button to enable (grant permissions)." }, 191 | "about_h3": { "message": "About" }, 192 | "version_span": { "message": "Version" }, 193 | "website_a": { "message": "Website - Source Code on GitHub" }, 194 | "email_a": { "message": "Contact - Please Send Feedback or Report Bugs"}, 195 | "reset_options_button" : { "message": "Reset Options" }, 196 | "reset_options_message" : { "message": "Options Reset!" }, 197 | "urli_thank_you_rainbow": { "message": "Thank you for using URLI, a URL Incrementer" }, 198 | "urli_thank_you_wish": { "message": "URLI loves incrementing for you and wishes you an Incrementful Day!" }, 199 | "urli_click_malfunctioning": { "message" : "i'M m4lfUnCt10n1Ng!" }, 200 | "special_thanks_label": { "message": "Credits & Special Thanks" }, 201 | "special_thanks_content": { "message": "NickMWPrince & Gopi P. (AUTO Concept), Coolio Wolfus (Ver 1.x Testing), Eric C. (Alphanumeric Idea), Adam C. & Will (User Feedback), FontAwesome (Icons), Hover.css (Animations), @mallendeo (Toggles), ZURB Foundation (Styles), Mike West (Dialogs), regex101.com (Regex Test), httpstat.us (Error Skipping Test), URL Flipper (The First), Stack Overflow Users (Internal Code), Icon Bunny (Rainbow color palette), ..." }, 202 | "special_thanks_you": { "message": "... but most of all you for using URLI!" }, 203 | 204 | "SHARED": { "message": "----- SHARED -----" }, 205 | "selection_label": { "message": "Selection" }, 206 | "interval_label": { "message": "Interval" }, 207 | "base_label": { "message": "Base" }, 208 | "base_option_default": { "message": "(Default)" }, 209 | "base_option_2": { "message": "2 Binary [0-1]" }, 210 | "base_option_3": { "message": "3 Ternary [0-2]" }, 211 | "base_option_4": { "message": "4 Quaternary [0-3]" }, 212 | "base_option_5": { "message": "5 Quinary [0-4]" }, 213 | "base_option_6": { "message": "6 Senary [0-5]" }, 214 | "base_option_7": { "message": "7 Septenary [0-6]" }, 215 | "base_option_8": { "message": "8 Octal [0-7]" }, 216 | "base_option_9": { "message": "9 Nonary [0-8]" }, 217 | "base_option_10": { "message": "10 Decimal [0-9]" }, 218 | "base_option_11": { "message": "11 Undecimal [0-9][A]" }, 219 | "base_option_12": { "message": "12 Duodecimal [0-9][A-B]" }, 220 | "base_option_13": { "message": "13 Tridecimal [0-9][A-C]" }, 221 | "base_option_14": { "message": "14 Tetradecimal [0-9][A-D]" }, 222 | "base_option_15": { "message": "15 Pentadecimal [0-9][A-E]" }, 223 | "base_option_16": { "message": "16 Hexadecimal [0-9][A-F]" }, 224 | "base_option_17": { "message": "17 Septendecimal [0-9][A-G]" }, 225 | "base_option_18": { "message": "18 Octodecimal [0-9][A-H]" }, 226 | "base_option_19": { "message": "19 Nonadecimal [0-9][A-I]" }, 227 | "base_option_20": { "message": "20 Vigesimal [0-9][A-J]" }, 228 | "base_option_21": { "message": "21 Unvigesimal [0-9][A-K]" }, 229 | "base_option_22": { "message": "22 Duovigesimal [0-9][A-L]" }, 230 | "base_option_23": { "message": "23 Trivigesimal [0-9][A-M]" }, 231 | "base_option_24": { "message": "24 Tetravigesimal [0-9][A-N]" }, 232 | "base_option_25": { "message": "25 Pentavigesimal [0-9][A-O]" }, 233 | "base_option_26": { "message": "26 Hexavigesimal [0-9][A-P]" }, 234 | "base_option_27": { "message": "27 Heptavigesimal [0-9][A-Q]" }, 235 | "base_option_28": { "message": "28 Octovigesimal [0-9][A-R]" }, 236 | "base_option_29": { "message": "29 Nonavigesimal [0-9][A-S]" }, 237 | "base_option_30": { "message": "30 Trigesimal [0-9][A-T]" }, 238 | "base_option_31": { "message": "31 Unotrigesimal [0-9][A-U]" }, 239 | "base_option_32": { "message": "32 Duotrigesimal [0-9][A-V]" }, 240 | "base_option_33": { "message": "33 Tritrigesimal [0-9][A-W]" }, 241 | "base_option_34": { "message": "34 Tetratrigesimal [0-9][A-X]" }, 242 | "base_option_35": { "message": "35 Pentatrigesimal [0-9][A-Y]" }, 243 | "base_option_36": { "message": "36 Hexatrigesimal [0-9][A-Z]" }, 244 | "base_case_lowercase_label": { "message": "Lowercase [a-z]" }, 245 | "base_case_uppercase_label": { "message": "Uppercase [A-Z]" }, 246 | "error_skip_label": { "message": "Error Skip" }, 247 | "error_codes_404_label": { "message": "404 Page Not Found" }, 248 | "error_codes_3XX_label": { "message": "3XX Redirects" }, 249 | "error_codes_4XX_label": { "message": "4XX Client Errors" }, 250 | "error_codes_5XX_label": { "message": "5XX Server Errors" }, 251 | "error_codes_custom_enabled_label": { "message": "Use Custom HTTP Status Codes" }, 252 | "download_experimental_disclaimer": { "message": "Remember: Download is still an experimental feature! Please only use it for small and simple downloading tasks while incrementing." }, 253 | "download_settings_disclaimer": { "message": "Before downloading, please open your Browser Settings and make sure \"Ask where to save each file\" is unchecked and you set a Download location!" } 254 | } -------------------------------------------------------------------------------- /src/css/chromium/chrome_shared.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. */ 4 | 5 | /* This file holds CSS that should be shared, in theory, by all user-visible 6 | * chrome:// pages. */ 7 | 8 | @import url(text_defaults.css); 9 | @import url(widgets.css); 10 | 11 | 12 | /* Prevent CSS from overriding the hidden property. */ 13 | [hidden] { 14 | display: none !important; 15 | } 16 | 17 | html { 18 | height: 100%; /* For printing. */ 19 | } 20 | 21 | html.loading * { 22 | -webkit-transition-delay: 0ms !important; 23 | -webkit-transition-duration: 0ms !important; 24 | } 25 | 26 | body { 27 | cursor: default; 28 | margin: 0; 29 | } 30 | 31 | p { 32 | line-height: 1.8em; 33 | } 34 | 35 | h1, 36 | h2, 37 | h3 { 38 | -webkit-user-select: none; 39 | font-weight: normal; 40 | /* Makes the vertical size of the text the same for all fonts. */ 41 | line-height: 1; 42 | } 43 | 44 | h1 { 45 | font-size: 1.5em; 46 | } 47 | 48 | h2 { 49 | font-size: 1.3em; 50 | margin-bottom: 0.4em; 51 | } 52 | 53 | h3 { 54 | color: black; 55 | font-size: 1.2em; 56 | margin-bottom: 0.8em; 57 | } 58 | 59 | a { 60 | color: rgb(17, 85, 204); 61 | text-decoration: underline; 62 | } 63 | 64 | a:active { 65 | color: rgb(5, 37, 119); 66 | } 67 | 68 | /* Elements that need to be LTR even in an RTL context, but should align 69 | * right. (Namely, URLs, search engine names, etc.) 70 | */ 71 | html[dir='rtl'] .weakrtl { 72 | direction: ltr; 73 | text-align: right; 74 | } 75 | 76 | /* Input fields in search engine table need to be weak-rtl. Since those input 77 | * fields are generated for all cr.ListItem elements (and we only want weakrtl 78 | * on some), the class needs to be on the enclosing div. 79 | */ 80 | html[dir='rtl'] div.weakrtl input { 81 | direction: ltr; 82 | text-align: right; 83 | } 84 | 85 | html[dir='rtl'] .favicon-cell.weakrtl { 86 | -webkit-padding-end: 22px; 87 | -webkit-padding-start: 0; 88 | } 89 | 90 | /* weakrtl for selection drop downs needs to account for the fact that 91 | * Webkit does not honor the text-align attribute for the select element. 92 | * (See Webkit bug #40216) 93 | */ 94 | html[dir='rtl'] select.weakrtl { 95 | direction: rtl; 96 | } 97 | 98 | html[dir='rtl'] select.weakrtl option { 99 | direction: ltr; 100 | } 101 | 102 | /* WebKit does not honor alignment for text specified via placeholder attribute. 103 | * This CSS is a workaround. Please remove once WebKit bug is fixed. 104 | * https://bugs.webkit.org/show_bug.cgi?id=63367 105 | */ 106 | html[dir='rtl'] input.weakrtl::-webkit-input-placeholder, 107 | html[dir='rtl'] .weakrtl input::-webkit-input-placeholder { 108 | direction: rtl; 109 | } 110 | -------------------------------------------------------------------------------- /src/css/chromium/text_defaults.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2014 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. */ 4 | 5 | /* This file is dynamically processed by a C++ data source handler to fill in 6 | * some per-platform/locale styles that dramatically alter the page. This is 7 | * done to reduce flicker, as JS may not run before the page is rendered. 8 | * 9 | * There are two ways to include this stylesheet: 10 | * 1. via its chrome://resources/ URL in HTML, i.e.: 11 | * 12 | * 13 | * 14 | * 2. via the webui::AppendWebUICSSTextDefaults() method to directly append it 15 | * to an HTML string. 16 | * Otherwise its placeholders won't be expanded. */ 17 | 18 | html { 19 | direction: ltr; 20 | } 21 | 22 | body { 23 | font-family: 'Segoe UI', Tahoma, sans-serif; 24 | font-size: 75%; 25 | } 26 | 27 | button { 28 | font-family: 'Segoe UI', Tahoma, sans-serif; 29 | } 30 | -------------------------------------------------------------------------------- /src/css/chromium/widgets.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. */ 4 | 5 | /* This file defines styles for form controls. The order of rule blocks is 6 | * important as there are some rules with equal specificity that rely on order 7 | * as a tiebreaker. These are marked with OVERRIDE. */ 8 | 9 | /* Default state **************************************************************/ 10 | 11 | :-webkit-any(button, 12 | input[type='button'], 13 | input[type='submit']):not(.custom-appearance), 14 | select, 15 | input[type='checkbox'], 16 | input[type='radio'] { 17 | -webkit-appearance: none; 18 | -webkit-user-select: none; 19 | background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); 20 | border: 1px solid rgba(0, 0, 0, 0.25); 21 | border-radius: 2px; 22 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), 23 | inset 0 1px 2px rgba(255, 255, 255, 0.75); 24 | color: #444; 25 | font: inherit; 26 | margin: 0 1px 0 0; 27 | outline: none; 28 | text-shadow: 0 1px 0 rgb(240, 240, 240); 29 | } 30 | 31 | :-webkit-any(button, 32 | input[type='button'], 33 | input[type='submit']):not(.custom-appearance), 34 | select { 35 | min-height: 2em; 36 | min-width: 4em; 37 | /* The following platform-specific rule is necessary to get adjacent 38 | * buttons, text inputs, and so forth to align on their borders while also 39 | * aligning on the text's baselines. */ 40 | padding-bottom: 1px; 41 | } 42 | 43 | :-webkit-any(button, 44 | input[type='button'], 45 | input[type='submit']):not(.custom-appearance) { 46 | -webkit-padding-end: 10px; 47 | -webkit-padding-start: 10px; 48 | } 49 | 50 | select { 51 | -webkit-appearance: none; 52 | -webkit-padding-end: 20px; 53 | -webkit-padding-start: 6px; 54 | /* OVERRIDE */ 55 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), 56 | -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); 57 | background-position: right center; 58 | background-repeat: no-repeat; 59 | } 60 | 61 | html[dir='rtl'] select { 62 | background-position: center left; 63 | } 64 | 65 | input[type='checkbox'] { 66 | height: 13px; 67 | position: relative; 68 | vertical-align: middle; 69 | width: 13px; 70 | } 71 | 72 | input[type='radio'] { 73 | /* OVERRIDE */ 74 | border-radius: 100%; 75 | height: 15px; 76 | position: relative; 77 | vertical-align: middle; 78 | width: 15px; 79 | } 80 | 81 | /* TODO(estade): add more types here? */ 82 | input[type='number'], 83 | input[type='password'], 84 | input[type='search'], 85 | input[type='text'], 86 | input[type='url'], 87 | input:not([type]), 88 | textarea { 89 | border: 1px solid #bfbfbf; 90 | border-radius: 2px; 91 | box-sizing: border-box; 92 | color: #444; 93 | font: inherit; 94 | margin: 0; 95 | /* Use min-height to accommodate addditional padding for touch as needed. */ 96 | min-height: 2em; 97 | padding: 3px; 98 | outline: none; 99 | /* For better alignment between adjacent buttons and inputs. */ 100 | padding-bottom: 4px; 101 | } 102 | 103 | input[type='search'] { 104 | -webkit-appearance: textfield; 105 | /* NOTE: Keep a relatively high min-width for this so we don't obscure the end 106 | * of the default text in relatively spacious languages (i.e. German). */ 107 | min-width: 160px; 108 | } 109 | 110 | /* Remove when https://bugs.webkit.org/show_bug.cgi?id=51499 is fixed. 111 | * TODO(dbeam): are there more types that would benefit from this? */ 112 | input[type='search']::-webkit-textfield-decoration-container { 113 | direction: inherit; 114 | } 115 | 116 | /* Checked ********************************************************************/ 117 | 118 | input[type='checkbox']:checked::before { 119 | -webkit-user-select: none; 120 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAAAAaElEQVR4Xl3PIQoCQQCF4Y8JW42D1bDZ4iVEjDbxFpstYhC7eIVBZHkXFGw734sv/TqDQQ8Xb1udja/I8igeIm7Aygj2IpoKTGZnVRNxAHYi4iPiDlA9xX+aNQDFySziqDN6uSp6y7ofEMwZ05uUZRkAAAAASUVORK5CYII=); 121 | background-size: 100% 100%; 122 | content: ''; 123 | display: block; 124 | height: 100%; 125 | width: 100%; 126 | } 127 | 128 | input[type='radio']:checked::before { 129 | background-color: #666; 130 | border-radius: 100%; 131 | bottom: 3px; 132 | content: ''; 133 | display: block; 134 | left: 3px; 135 | position: absolute; 136 | right: 3px; 137 | top: 3px; 138 | } 139 | 140 | /* Hover **********************************************************************/ 141 | 142 | :enabled:hover:-webkit-any( 143 | select, 144 | input[type='checkbox'], 145 | input[type='radio'], 146 | :-webkit-any( 147 | button, 148 | input[type='button'], 149 | input[type='submit']):not(.custom-appearance)) { 150 | background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); 151 | border-color: rgba(0, 0, 0, 0.3); 152 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), 153 | inset 0 1px 2px rgba(255, 255, 255, 0.95); 154 | color: black; 155 | } 156 | 157 | :enabled:hover:-webkit-any(select) { 158 | /* OVERRIDE */ 159 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), 160 | -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); 161 | } 162 | 163 | /* Active *********************************************************************/ 164 | 165 | :enabled:active:-webkit-any( 166 | select, 167 | input[type='checkbox'], 168 | input[type='radio'], 169 | :-webkit-any( 170 | button, 171 | input[type='button'], 172 | input[type='submit']):not(.custom-appearance)) { 173 | background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); 174 | box-shadow: none; 175 | text-shadow: none; 176 | } 177 | 178 | :enabled:active:-webkit-any(select) { 179 | /* OVERRIDE */ 180 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), 181 | -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); 182 | } 183 | 184 | /* Disabled *******************************************************************/ 185 | 186 | :disabled:-webkit-any( 187 | button, 188 | input[type='button'], 189 | input[type='submit']):not(.custom-appearance), 190 | select:disabled { 191 | background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); 192 | border-color: rgba(80, 80, 80, 0.2); 193 | box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), 194 | inset 0 1px 2px rgba(255, 255, 255, 0.75); 195 | color: #aaa; 196 | } 197 | 198 | select:disabled { 199 | /* OVERRIDE */ 200 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAASklEQVQY02P4z0AMRGZGMaShwCisyhITG/4jw8RErMr+KyxYiFC0YOF/BeyWIikEKWLA4Ta4QogiPMpACt82QRThVQYUYYWz4BAAGr6Ii6kEPacAAAAASUVORK5CYII=), 201 | -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); 202 | } 203 | 204 | input:disabled:-webkit-any([type='checkbox'], 205 | [type='radio']) { 206 | opacity: .75; 207 | } 208 | 209 | input:disabled:-webkit-any([type='password'], 210 | [type='search'], 211 | [type='text'], 212 | [type='url'], 213 | :not([type])) { 214 | color: #999; 215 | } 216 | 217 | /* Focus **********************************************************************/ 218 | 219 | :enabled:focus:-webkit-any( 220 | select, 221 | input[type='checkbox'], 222 | input[type='number'], 223 | input[type='password'], 224 | input[type='radio'], 225 | input[type='search'], 226 | input[type='text'], 227 | input[type='url'], 228 | input:not([type]), 229 | :-webkit-any( 230 | button, 231 | input[type='button'], 232 | input[type='submit']):not(.custom-appearance)) { 233 | /* OVERRIDE */ 234 | -webkit-transition: border-color 200ms; 235 | /* We use border color because it follows the border radius (unlike outline). 236 | * This is particularly noticeable on mac. */ 237 | border-color: rgb(77, 144, 254); 238 | outline: none; 239 | } 240 | 241 | /* Action links ***************************************************************/ 242 | 243 | [is='action-link'] { 244 | cursor: pointer; 245 | display: inline-block; 246 | text-decoration: none; 247 | } 248 | 249 | [is='action-link']:hover { 250 | text-decoration: underline; 251 | } 252 | 253 | [is='action-link']:active { 254 | color: rgb(5, 37, 119); 255 | text-decoration: underline; 256 | } 257 | 258 | [is='action-link'][disabled] { 259 | color: #999; 260 | cursor: default; 261 | pointer-events: none; 262 | text-decoration: none; 263 | } 264 | 265 | [is='action-link'].no-outline { 266 | outline: none; 267 | } 268 | 269 | /* Checkbox/radio helpers ****************************************************** 270 | * 271 | * .checkbox and .radio classes wrap labels. Checkboxes and radios should use 272 | * these classes with the markup structure: 273 | * 274 | *
275 | * 279 | *
280 | */ 281 | 282 | :-webkit-any(.checkbox, .radio) label { 283 | /* Don't expand horizontally: . */ 284 | align-items: center; 285 | display: inline-flex; 286 | padding-bottom: 7px; 287 | padding-top: 7px; 288 | } 289 | 290 | :-webkit-any(.checkbox, .radio) label input { 291 | flex-shrink: 0; 292 | } 293 | 294 | :-webkit-any(.checkbox, .radio) label input ~ span { 295 | -webkit-margin-start: 0.6em; 296 | -webkit-user-select: none; 297 | /* Make sure long spans wrap at the same horizontal position they start. */ 298 | display: block; 299 | } 300 | 301 | :-webkit-any(.checkbox, .radio) label:hover { 302 | color: black; 303 | } 304 | 305 | label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span { 306 | color: #999; 307 | } 308 | 309 | extensionview { 310 | display: inline-block; 311 | height: 300px; 312 | width: 300px; 313 | } -------------------------------------------------------------------------------- /src/css/options.css: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | /* Options CSS is minimal because Options is set to use Chrome style */ 8 | 9 | @import url("ui/fade.css"); 10 | @import url("ui/hover.css"); 11 | @import url("ui/generate-alert.css"); 12 | 13 | body { 14 | min-width: 415px; 15 | } 16 | 17 | h3 { 18 | display: inline; 19 | user-select: initial; 20 | } 21 | 22 | section:not(:last-child) { 23 | margin-bottom: 40px; 24 | } 25 | 26 | div.heading { 27 | border-bottom: 1px solid #333333; 28 | padding-bottom: 6px; 29 | margin-bottom: 12px; 30 | } 31 | 32 | div.heading.selection-first { 33 | margin-bottom: 22px; 34 | } 35 | 36 | div.heading button { 37 | float: right; 38 | position: relative; 39 | top: -8px; 40 | } 41 | 42 | div.table { 43 | display: table; 44 | } 45 | 46 | div.row { 47 | display: table-row; 48 | } 49 | 50 | div.column { 51 | display: table-cell; 52 | padding: 0 10px 10px 0; 53 | } 54 | 55 | input[type="number"] { 56 | width: 52px; 57 | } 58 | 59 | img { 60 | vertical-align: text-bottom; 61 | } 62 | 63 | input[type="image"] { 64 | display: inline-block; 65 | vertical-align: middle; 66 | } 67 | 68 | input[type="image"]:focus { 69 | outline: none; 70 | } 71 | 72 | .key-input { 73 | width: 180px; 74 | } 75 | 76 | .tooltip img { 77 | display: inline-block; 78 | margin-left: 4px; 79 | } 80 | 81 | /* icon and popup */ 82 | 83 | .icon-color.radio, #popup-button-size-div { 84 | margin-bottom: 24px; 85 | } 86 | 87 | .icon-color.radio label:not(:last-child) { 88 | margin-right: 16px; 89 | } 90 | 91 | .icon-color.radio label:first-child { 92 | margin-right: 0; 93 | display: block; 94 | } 95 | 96 | .icon-color.radio label > input { 97 | margin-right: 3px; 98 | } 99 | 100 | #popup-button-size-label { 101 | display: block; 102 | margin-bottom: 5px; 103 | } 104 | 105 | #popup-button-size-img { 106 | vertical-align: top; 107 | cursor: pointer; 108 | } 109 | 110 | /* error-skip */ 111 | 112 | #error-skip.column { 113 | padding-bottom: 0; 114 | } 115 | 116 | #error-skip-checkboxes { 117 | margin-top: 8px; 118 | } 119 | 120 | #error-skip-checkboxes div.column { 121 | padding: 4px 4px 4px 0; 122 | } 123 | 124 | #error-skip-checkboxes label { 125 | padding: 0; 126 | } 127 | 128 | #error-skip-description { 129 | display: inline; 130 | border-bottom: 1px dotted #333; 131 | margin-left: 4px; 132 | padding-bottom: 2px; 133 | font-style: italic; 134 | } 135 | 136 | /* selection-custom */ 137 | 138 | #selection-custom { 139 | margin: 10px 0; 140 | } 141 | 142 | #selection-custom-url-textarea { 143 | width: 100%; 144 | color: #777777; 145 | word-break: break-all; 146 | max-width: 300px; 147 | max-height: 90px; 148 | min-width: 300px; 149 | min-height: 41px; 150 | } 151 | 152 | #selection-custom-pattern-input { 153 | width: 120px; 154 | margin: 0 4px; 155 | } 156 | 157 | #selection-custom-flags-input { 158 | width: 52px; 159 | } 160 | 161 | /* download (experimental and img alignment for cloud and flask */ 162 | 163 | #download-experimental { 164 | font-size: 0.9em; 165 | font-style: italic; 166 | display: inline-block; 167 | margin-left: 6px; 168 | } 169 | 170 | #download-cloud-download { 171 | margin-bottom: -1px; 172 | } 173 | 174 | #download-experimental-flask { 175 | margin-bottom: 1px; 176 | } 177 | 178 | /* about */ 179 | 180 | #about-floats-div { 181 | margin-bottom: 17px; 182 | } 183 | 184 | #about-floats-div #name-version-website-div { 185 | float: left; 186 | } 187 | 188 | #about-floats-div #reset-options-div { 189 | float: right; 190 | } 191 | 192 | #about-floats-div .clear-both { 193 | clear: both; 194 | } 195 | 196 | /* urli-thank-you */ 197 | 198 | #urli-thank-you { 199 | width: 100%; 200 | vertical-align: top; 201 | padding-top: 2px; 202 | } 203 | 204 | #urli-thank-you-rainbow { 205 | font-size: 1.1em; 206 | margin-bottom: 8px; 207 | } 208 | 209 | #urli-thank-you-rainbow span { 210 | padding: 0 2px; 211 | } 212 | 213 | #urli-thank-you-wish { 214 | font-style: italic; 215 | } 216 | 217 | /* special thanks */ 218 | 219 | #special-thanks { 220 | margin-top: 0.75em; 221 | } 222 | 223 | #special-thanks-content, #special-thanks-you { 224 | font-size: 0.75em; 225 | font-style: italic; 226 | } 227 | 228 | #special-thanks-you { 229 | font-weight: bold; 230 | text-align: center; 231 | } 232 | 233 | #copyright { 234 | font-size: 0.75em; 235 | text-align: right; 236 | margin-top: 8px; 237 | } 238 | 239 | /* heading border-bottom-color overrides */ 240 | 241 | #increment-decrement-settings div.heading { 242 | border-bottom-color: #1779BA; 243 | } 244 | 245 | #next-prev-settings div.heading { 246 | border-bottom-color: #05854D; 247 | } 248 | 249 | #auto-settings div.heading { 250 | border-bottom-color: #FF6600; 251 | } 252 | 253 | #download-settings div.heading { 254 | border-bottom-color: rebeccapurple; 255 | } 256 | 257 | #enhanced-mode div.heading { 258 | border-bottom-color: #EEBB22; 259 | } 260 | 261 | /* heading h3 color overrides */ 262 | 263 | #increment-decrement-settings div.heading h3 { 264 | color: #1779BA; 265 | } 266 | 267 | #next-prev-settings div.heading h3 { 268 | color: #05854D; 269 | } 270 | 271 | #auto-settings div.heading h3 { 272 | color: #FF6600; 273 | } 274 | 275 | #download-settings div.heading h3, #download-experimental { 276 | color: rebeccapurple; 277 | } 278 | 279 | #enhanced-mode div.heading h3 { 280 | color: #EEBB22; 281 | } 282 | 283 | /* Colors derived from Rainbow color palette by Icon Bunny @ Iconscout https://iconscout.com/icon/rainbow-97 */ 284 | .rainbow-underline:after { 285 | content: ''; 286 | display: block; 287 | margin-top: 2px; 288 | height: 2px; 289 | background: linear-gradient( 290 | to right, 291 | /*#1779BA 0%, #1779BA 16%, #FF6600 16%, #FF6600 32%, #FFCC22 32%, #FFCC22 48%, #05854D 48%, #05854D 64%, #FF6600 64%, #FF6600 80%, #663399 80%, #663399 100%*/ 292 | /*#EE5555 0%, #EE5555 16%, #FF6600 16%, #FF6600 32%, #FFCC22 32%, #FFCC22 48%, #66BB6B 48%, #66BB6B 64%, #44A5F5 64%, #44A5F5 80%, #663399 80%, #663399 100%*/ 293 | #44A5F5 0%, #44A5F5 24%, #EE5555 25%, #EE5555 49%, #FFCC22 50%, #FFCC22 74%, #66BB6B 75%, #66BB6B 100% 294 | /*#44A5F5 0%, #44A5F5 20%, #EE5555 20%, #EE5555 40%, #FFCC22 40%, #FFCC22 60%, #66BB6B 60%, #66BB6B 80%, #663399 80%, #663399 100%*/ 295 | ); 296 | } 297 | 298 | /* Overrides generateAlert overlay styling so alert is displayed in gap space */ 299 | #options .overlay { 300 | top: 80%; /* initial */ 301 | } -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | /* Popup CSS relies on Chromium CSS */ 8 | 9 | @import url("chromium/chrome_shared.css"); 10 | @import url("ui/generate-alert.css"); 11 | @import url("ui/hover.css"); 12 | @import url("ui/fade.css"); 13 | @import url("ui/toggle.css"); 14 | 15 | /* chromium/chrome-shared.css Overrides */ 16 | html { 17 | height: auto; 18 | } 19 | 20 | /* Unfortunately, we need to have user-select on checkboxes due to select URL bug causing issues when checkboxes are selected */ 21 | h3, :-webkit-any(.checkbox, .radio) label input ~ span { 22 | -webkit-user-select: initial; 23 | } 24 | 25 | :-webkit-any(.checkbox, .radio) label input ~ span{ 26 | -webkit-margin-start: 0.3em; 27 | } 28 | 29 | /* Native elements styles */ 30 | 31 | body { 32 | color: #333333; 33 | background: #FFFFFF; 34 | } 35 | 36 | * { 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | div.table { 42 | display: table; 43 | } 44 | 45 | div.row { 46 | display: table-row; 47 | } 48 | 49 | div.column { 50 | display: table-cell; 51 | } 52 | 53 | /* Controls CSS */ 54 | 55 | #controls { 56 | margin: 5px; 57 | } 58 | 59 | #controls input[type="image"] { 60 | vertical-align: middle; /* Matches hover.css styling */ 61 | padding: 8px; 62 | } 63 | 64 | #controls input[type="image"]:not(:last-child) { 65 | margin-right: 10px; 66 | } 67 | 68 | #controls input[type="image"]:focus { 69 | outline: none; 70 | } 71 | 72 | #controls input[type="image"].disabled { 73 | opacity: 0.2; 74 | } 75 | 76 | /* Setup CSS */ 77 | 78 | #setup { 79 | min-width: 512px; 80 | margin: 12px; 81 | } 82 | 83 | #setup h3 { 84 | font-weight: bold; 85 | /* font-style: italic;*/ 86 | padding-bottom: 4px; 87 | margin-bottom: 0.6em; 88 | } 89 | 90 | #setup h3 { 91 | color: #333333; 92 | border-bottom: 1px solid #333333; 93 | } 94 | 95 | #setup input[type="number"] { 96 | width: 52px; 97 | display: block; 98 | } 99 | 100 | #setup select { 101 | display: block; 102 | } 103 | 104 | #setup textarea { 105 | width: 100%; 106 | color: #777777; 107 | word-break: break-all; 108 | max-width: 512px; 109 | max-height: 90px; 110 | min-width: 512px; 111 | min-height: 41px; 112 | } 113 | 114 | #setup img { 115 | vertical-align: bottom; 116 | } 117 | 118 | #setup div.column { 119 | padding-top: 7px; 120 | } 121 | 122 | #properties { 123 | margin-bottom: 16px; 124 | } 125 | 126 | #setup #properties div.column:not(:last-child) { 127 | padding-right: 27px; 128 | } 129 | 130 | #setup div.column > label, .label-display-block { 131 | display: block; 132 | margin-bottom: 2px; 133 | } 134 | 135 | #setup #leading-zeros-pad-label { 136 | font-size: 0.9em; /* 1em fits OK, but worried about this breaking into a second line so smaller font-size just in case */ 137 | } 138 | 139 | #setup #setup-buttons { 140 | text-align: center; 141 | position: relative; /* need position relative height auto for position absolute elements auto/download toggles and options button */ 142 | height: auto; 143 | } 144 | 145 | #setup #setup-buttons > button:not(:last-child) { 146 | margin-right: 12px; 147 | } 148 | 149 | #options-button { 150 | position: absolute; 151 | right: 4px; 152 | bottom: 3px; 153 | font-size: 1.05em; 154 | font-weight: bold; 155 | cursor: pointer; 156 | } 157 | 158 | #options-button img { 159 | margin-right: -2px; 160 | } 161 | 162 | #setup #extra-toggles { 163 | position: absolute; 164 | left: 0; 165 | bottom: -6px; 166 | } 167 | 168 | #setup #extra-toggles .checkbox > label:not(:last-child) { 169 | margin-right: 8px; 170 | } 171 | 172 | #setup #auto, #setup #download { 173 | margin-bottom: 24px; 174 | } 175 | 176 | #auto-eta, #download-experimental { 177 | font-size: 0.8em; 178 | font-weight: bold; 179 | font-style: italic; 180 | position: absolute; 181 | right: 12px; 182 | margin-top: -10px; 183 | } 184 | 185 | #auto-eta { 186 | color: #FF6600; 187 | } 188 | 189 | #download-experimental { 190 | color: rebeccapurple; 191 | } 192 | 193 | #setup #auto div.column:not(:last-child), #setup #download div.column:not(:last-child) { 194 | padding-right: 30px; 195 | } 196 | 197 | #setup #auto #auto-wait-badge { 198 | vertical-align: bottom; 199 | } 200 | 201 | #setup #auto #auto-wait-badge .checkbox label { 202 | padding: 2px 0; 203 | } 204 | 205 | #setup div.row.two div.column { 206 | padding-top: 12px; 207 | } 208 | 209 | #auto-toggle-input.tgl-ios:checked + .tgl-btn { 210 | background: #FF6600; 211 | } 212 | 213 | #download-toggle-input.tgl-ios:checked + .tgl-btn { 214 | background: rebeccapurple; 215 | } 216 | 217 | #auto-toggle span, #download-toggle span { 218 | font-size: 1.05em; 219 | font-weight: bold; 220 | font-style: italic; 221 | margin-left: 3px; 222 | } 223 | 224 | #auto-toggle span { 225 | color: #FF6600; 226 | } 227 | 228 | #download-toggle span { 229 | color: rebeccapurple; 230 | } 231 | 232 | #auto-toggle-img { 233 | margin-bottom: -1px; 234 | } 235 | 236 | /* cloud-download ireregular padding overrides */ 237 | #controls input[type="image"]#download-input { 238 | padding: 4px 8px; 239 | } 240 | 241 | #setup img#download-h3-cloud-download, #setup img#auto-h3-refresh { 242 | margin-bottom: -2px; 243 | } 244 | 245 | #setup #download-toggle { 246 | vertical-align: bottom; 247 | } 248 | 249 | #setup h3#setup-h3 { 250 | color: #333333; 251 | border-bottom-color: #333333; 252 | } 253 | 254 | #setup h3#auto-h3 { 255 | color: #FF6600; 256 | border-bottom-color: #FF6600; 257 | } 258 | 259 | #setup h3#download-h3 { 260 | color: rebeccapurple; 261 | border-bottom-color: rebeccapurple; 262 | margin: 0; 263 | } 264 | 265 | #auto div.column, #download div.column { 266 | padding-top: 3px; 267 | } 268 | 269 | /* download-setup (download-strategy and download-optional columns) */ 270 | 271 | #download-setup.table { 272 | width: 100%; 273 | border-bottom: 1px solid rebeccapurple; 274 | } 275 | 276 | div.column#download-strategy, div.column#download-optional { 277 | padding-top: 8px; 278 | padding-bottom: 12px; 279 | width: 50%; 280 | position: relative; /* for position absolute help */ 281 | } 282 | 283 | #download-strategy-help, #download-optional-help { 284 | font-weight: bold; 285 | font-size: 0.9em; 286 | position: absolute; 287 | right: 8px; 288 | top: 4px; 289 | border-bottom: 1px dotted #333333; 290 | padding-bottom: 1px; 291 | } 292 | 293 | div.column#download-optional { 294 | border-left: 1px solid rebeccapurple; 295 | padding-left: 12px; 296 | } 297 | 298 | #download-optional-label-div { 299 | font-weight: bold; 300 | font-style: italic; 301 | } 302 | 303 | #download-strategy-sub-options { 304 | height: 100%; 305 | min-height: 60px; 306 | } 307 | 308 | #download-extensions label, #download-tags label, #download-attributes label { 309 | display: inline-block; /* keeps extension next to checkbox on same line without breaking */ 310 | margin-right: 8px; 311 | margin-bottom: 4px; 312 | } 313 | 314 | #download-extensions label span, #download-tags label span, #download-attributes label span { 315 | padding-left: 3px; 316 | } 317 | 318 | #download-strategy-select { 319 | margin-bottom: 8px; 320 | } 321 | 322 | #download-selector-input { 323 | width: 90%; 324 | margin-top: 4px; 325 | } 326 | 327 | #download-includes-input, #download-excludes-input { 328 | width: 108px; 329 | } 330 | 331 | #download-min-mb input[type="number"], #download-max-mb input[type="number"] { /* Keep "MB" next to input */ 332 | display: inline-block; 333 | margin-right: 1px; 334 | } 335 | 336 | /* download-preview-heading and download-preview-checkboxes */ 337 | 338 | #download-preview-heading { 339 | margin: 6px 0; 340 | } 341 | 342 | #download-preview-heading-title { 343 | font-weight: bold; 344 | } 345 | 346 | #download-preview-heading-title .success { 347 | color: #05854D; 348 | } 349 | 350 | #download-preview-heading-title .error { 351 | color: #E6003E; 352 | } 353 | 354 | #download-preview-checkboxes { 355 | text-align: right; 356 | } 357 | 358 | #download-preview-checkboxes label:not(:last-child) { 359 | margin-right: 8px; 360 | } 361 | 362 | /* ZURB Foundation table styling inspired/derived */ 363 | 364 | table { 365 | border-collapse: collapse; 366 | border-radius: 0; 367 | margin-bottom: 1rem; 368 | max-width: 512px; 369 | max-height: 226px; 370 | display: block; 371 | overflow: auto; 372 | word-break: break-all; 373 | } 374 | 375 | table thead, table tbody { 376 | border: 1px solid #f1f1f1; 377 | background-color: #fefefe; 378 | } 379 | 380 | table thead { 381 | background: #f8f8f8; 382 | color: #0a0a0a; 383 | } 384 | 385 | table thead tr { 386 | background: transparent; 387 | } 388 | 389 | table thead tr th { 390 | font-weight: bold; 391 | text-align: left; 392 | } 393 | 394 | table thead th, table tbody th, table tbody td { 395 | padding: 0.25rem 0.312rem 0.312rem; 396 | } 397 | 398 | table tbody tr:nth-child(even) { 399 | border-bottom: 0; 400 | background-color: #f1f1f1; 401 | } 402 | 403 | table tr:nth-of-type(even):hover { 404 | background-color: #ececec; 405 | } 406 | 407 | table tbody tr:hover { 408 | background-color: #f9f9f9; 409 | } 410 | 411 | /* table download-preview specific class styles */ 412 | 413 | table th.check, td.check { min-width: 20px; } 414 | table th.count, td.count { min-width: 24px; } 415 | table th.thumb, td.thumb { min-width: 42px; } table td.thumb img, table td.thumb video { max-width: 40px; max-height: 40px; } 416 | table th.extension, td.extension { min-width: 34px; } 417 | table th.tag, td.tag { min-width: 36px; } 418 | table th.attribute, td.attribute { min-width: 34px; } 419 | table th.url, td.url { min-width: 34px; } 420 | table tr.unselected td.check img { opacity: 0.1; } /* unselected should have the check be really low opacity */ -------------------------------------------------------------------------------- /src/css/ui/fade.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Fade CSS 3 | * Fade-In/Out Animation CSS 4 | * TODO: make fade-out-max-height work better 5 | */ 6 | 7 | @keyframes fade-in { 8 | from { opacity: 0; transform: scale(0); } 9 | to { opacity: 1; transform: scale(1); } 10 | } 11 | 12 | @keyframes fade-in-max-height { 13 | from { max-height: 0; } 14 | to { max-height: 1000px; } 15 | } 16 | 17 | @keyframes fade-out { 18 | from { opacity: 1; transform: scale(1); } 19 | to { opacity: 0; transform: scale(0); } 20 | } 21 | 22 | @keyframes fade-out-max-height { 23 | from { max-height: 1000px; } 24 | to { max-height: 0; } 25 | } 26 | 27 | .fade-in { 28 | animation: fade-in 0.3s ease-out, fade-in-max-height 0.6s ease-out; 29 | } 30 | 31 | .fade-out { 32 | animation: fade-out 0.3s ease-out, fade-out-max-height 0.3s ease-out; 33 | } 34 | 35 | .display-block { 36 | display: block; 37 | } 38 | 39 | .display-none { 40 | display: none; 41 | } -------------------------------------------------------------------------------- /src/css/ui/generate-alert.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Generate Alert CSS 3 | * Generates an alert box (e.g. to show error messages) using an overlay 4 | * Derived from the sample Google extension, "Proxy Settings" by Mike West 5 | */ 6 | 7 | .overlay { 8 | display: block; 9 | text-align: center; 10 | position: fixed; 11 | left: 50%; 12 | top: 50%; 13 | width: 240px; 14 | padding: 10px; 15 | margin: -40px 0 0 -120px; 16 | opacity: 0; 17 | background: rgba(0, 0, 0, 0.75); 18 | border-radius: 5px; 19 | color: #FFFFFF; 20 | font-size: 1.2em; 21 | transition: all 1.0s ease; 22 | transform: scale(0); 23 | } 24 | 25 | .overlay ul { 26 | list-style-type: none; 27 | margin: 0; 28 | padding: 0; 29 | } 30 | 31 | .overlay-visible { 32 | opacity: 1; 33 | transform: scale(1); 34 | } -------------------------------------------------------------------------------- /src/css/ui/hover.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Hover.css (http://ianlunn.github.io/Hover/) 3 | * Version: 2.3.1 4 | * Author: Ian Lunn @IanLunn 5 | * Author URL: http://ianlunn.co.uk/ 6 | * Github: https://github.com/IanLunn/Hover 7 | 8 | * Hover.css Copyright Ian Lunn 2017. Generated with Sass. 9 | */ 10 | 11 | /* Grow */ 12 | .hvr-grow { 13 | display: inline-block; 14 | vertical-align: middle; 15 | -webkit-transform: perspective(1px) translateZ(0); 16 | transform: perspective(1px) translateZ(0); 17 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 18 | -webkit-transition-duration: 0.3s; 19 | transition-duration: 0.3s; 20 | -webkit-transition-property: transform; 21 | transition-property: transform; 22 | } 23 | .hvr-grow:hover/*, .hvr-grow:focus, .hvr-grow:active*/ { 24 | -webkit-transform: scale(1.1); 25 | transform: scale(1.1); 26 | } 27 | 28 | /* Push */ 29 | @-webkit-keyframes hvr-push { 30 | 50% { 31 | -webkit-transform: scale(0.8); 32 | transform: scale(0.8); 33 | } 34 | 100% { 35 | -webkit-transform: scale(1); 36 | transform: scale(1); 37 | } 38 | } 39 | @keyframes hvr-push { 40 | 50% { 41 | -webkit-transform: scale(0.8); 42 | transform: scale(0.8); 43 | } 44 | 100% { 45 | -webkit-transform: scale(1); 46 | transform: scale(1); 47 | } 48 | } 49 | .hvr-push { 50 | display: inline-block; 51 | vertical-align: middle; 52 | -webkit-transform: perspective(1px) translateZ(0); 53 | transform: perspective(1px) translateZ(0); 54 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 55 | } 56 | .hvr-push-click/*.hvr-push:hover, .hvr-push:focus, .hvr-push:active */ { 57 | -webkit-animation-name: hvr-push; 58 | animation-name: hvr-push; 59 | -webkit-animation-duration: 0.3s; 60 | animation-duration: 0.3s; 61 | -webkit-animation-timing-function: linear; 62 | animation-timing-function: linear; 63 | -webkit-animation-iteration-count: 1; 64 | animation-iteration-count: 1; 65 | } 66 | 67 | /* Buzz Out */ 68 | @-webkit-keyframes hvr-buzz-out { 69 | 10% { 70 | -webkit-transform: translateX(3px) rotate(2deg); 71 | transform: translateX(3px) rotate(2deg); 72 | } 73 | 20% { 74 | -webkit-transform: translateX(-3px) rotate(-2deg); 75 | transform: translateX(-3px) rotate(-2deg); 76 | } 77 | 30% { 78 | -webkit-transform: translateX(3px) rotate(2deg); 79 | transform: translateX(3px) rotate(2deg); 80 | } 81 | 40% { 82 | -webkit-transform: translateX(-3px) rotate(-2deg); 83 | transform: translateX(-3px) rotate(-2deg); 84 | } 85 | 50% { 86 | -webkit-transform: translateX(2px) rotate(1deg); 87 | transform: translateX(2px) rotate(1deg); 88 | } 89 | 60% { 90 | -webkit-transform: translateX(-2px) rotate(-1deg); 91 | transform: translateX(-2px) rotate(-1deg); 92 | } 93 | 70% { 94 | -webkit-transform: translateX(2px) rotate(1deg); 95 | transform: translateX(2px) rotate(1deg); 96 | } 97 | 80% { 98 | -webkit-transform: translateX(-2px) rotate(-1deg); 99 | transform: translateX(-2px) rotate(-1deg); 100 | } 101 | 90% { 102 | -webkit-transform: translateX(1px) rotate(0); 103 | transform: translateX(1px) rotate(0); 104 | } 105 | 100% { 106 | -webkit-transform: translateX(-1px) rotate(0); 107 | transform: translateX(-1px) rotate(0); 108 | } 109 | } 110 | @keyframes hvr-buzz-out { 111 | 10% { 112 | -webkit-transform: translateX(3px) rotate(2deg); 113 | transform: translateX(3px) rotate(2deg); 114 | } 115 | 20% { 116 | -webkit-transform: translateX(-3px) rotate(-2deg); 117 | transform: translateX(-3px) rotate(-2deg); 118 | } 119 | 30% { 120 | -webkit-transform: translateX(3px) rotate(2deg); 121 | transform: translateX(3px) rotate(2deg); 122 | } 123 | 40% { 124 | -webkit-transform: translateX(-3px) rotate(-2deg); 125 | transform: translateX(-3px) rotate(-2deg); 126 | } 127 | 50% { 128 | -webkit-transform: translateX(2px) rotate(1deg); 129 | transform: translateX(2px) rotate(1deg); 130 | } 131 | 60% { 132 | -webkit-transform: translateX(-2px) rotate(-1deg); 133 | transform: translateX(-2px) rotate(-1deg); 134 | } 135 | 70% { 136 | -webkit-transform: translateX(2px) rotate(1deg); 137 | transform: translateX(2px) rotate(1deg); 138 | } 139 | 80% { 140 | -webkit-transform: translateX(-2px) rotate(-1deg); 141 | transform: translateX(-2px) rotate(-1deg); 142 | } 143 | 90% { 144 | -webkit-transform: translateX(1px) rotate(0); 145 | transform: translateX(1px) rotate(0); 146 | } 147 | 100% { 148 | -webkit-transform: translateX(-1px) rotate(0); 149 | transform: translateX(-1px) rotate(0); 150 | } 151 | } 152 | .hvr-buzz-out { 153 | display: inline-block; 154 | vertical-align: middle; 155 | -webkit-transform: perspective(1px) translateZ(0); 156 | transform: perspective(1px) translateZ(0); 157 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 158 | } 159 | .hvr-buzz-out-click { 160 | -webkit-animation-name: hvr-buzz-out; 161 | animation-name: hvr-buzz-out; 162 | -webkit-animation-duration: 0.75s; 163 | animation-duration: 0.75s; 164 | -webkit-animation-timing-function: linear; 165 | animation-timing-function: linear; 166 | -webkit-animation-iteration-count: 1; 167 | animation-iteration-count: 1; 168 | } 169 | 170 | /* Skew Forward */ 171 | .hvr-skew-forward { 172 | display: inline-block; 173 | vertical-align: middle; 174 | -webkit-transform: perspective(1px) translateZ(0); 175 | transform: perspective(1px) translateZ(0); 176 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 177 | -webkit-transition-duration: 0.3s; 178 | transition-duration: 0.3s; 179 | -webkit-transition-property: transform; 180 | transition-property: transform; 181 | -webkit-transform-origin: 0 100%; 182 | transform-origin: 0 100%; 183 | } 184 | .hvr-skew-forward:hover/*, .hvr-skew-forward:focus, .hvr-skew-forward:active*/ { 185 | -webkit-transform: skew(-10deg); 186 | transform: skew(-10deg); 187 | } -------------------------------------------------------------------------------- /src/css/ui/toggle.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Toggle Buttons CSS 3 | * @mallendeo 4 | * https://codepen.io/mallendeo/pen/eLIiG 5 | */ 6 | 7 | .tgl { 8 | display: none; 9 | } 10 | .tgl, .tgl:after, .tgl:before, .tgl *, .tgl *:after, .tgl *:before, .tgl + .tgl-btn { 11 | box-sizing: border-box; 12 | } 13 | .tgl::-moz-selection, .tgl:after::-moz-selection, .tgl:before::-moz-selection, .tgl *::-moz-selection, .tgl *:after::-moz-selection, .tgl *:before::-moz-selection, .tgl + .tgl-btn::-moz-selection { 14 | background: none; 15 | } 16 | .tgl::selection, .tgl:after::selection, .tgl:before::selection, .tgl *::selection, .tgl *:after::selection, .tgl *:before::selection, .tgl + .tgl-btn::selection { 17 | background: none; 18 | } 19 | .tgl + .tgl-btn { 20 | outline: 0; 21 | display: block; 22 | width: 4em; 23 | height: 2em; 24 | position: relative; 25 | cursor: pointer; 26 | -webkit-user-select: none; 27 | -moz-user-select: none; 28 | -ms-user-select: none; 29 | user-select: none; 30 | } 31 | .tgl + .tgl-btn:after, .tgl + .tgl-btn:before { 32 | position: relative; 33 | display: block; 34 | content: ""; 35 | width: 50%; 36 | height: 100%; 37 | } 38 | .tgl + .tgl-btn:after { 39 | left: 0; 40 | } 41 | .tgl + .tgl-btn:before { 42 | display: none; 43 | } 44 | .tgl:checked + .tgl-btn:after { 45 | left: 50%; 46 | } 47 | 48 | .tgl-ios + .tgl-btn { 49 | background: #fbfbfb; 50 | border-radius: 2em; 51 | padding: 2px; 52 | transition: all .4s ease; 53 | border: 1px solid #e8eae9; 54 | } 55 | .tgl-ios + .tgl-btn:after { 56 | border-radius: 2em; 57 | background: #fbfbfb; 58 | transition: left 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), padding 0.3s ease, margin 0.3s ease; 59 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 0 rgba(0, 0, 0, 0.08); 60 | } 61 | .tgl-ios + .tgl-btn:hover:after { 62 | will-change: padding; 63 | } 64 | .tgl-ios + .tgl-btn:active { 65 | box-shadow: inset 0 0 0 2em #e8eae9; 66 | } 67 | .tgl-ios + .tgl-btn:active:after { 68 | padding-right: .8em; 69 | } 70 | .tgl-ios:checked + .tgl-btn { 71 | background: #86d993; 72 | } 73 | .tgl-ios:checked + .tgl-btn:active { 74 | box-shadow: none; 75 | } 76 | .tgl-ios:checked + .tgl-btn:active:after { 77 | margin-left: -.8em; 78 | } 79 | 80 | /* URLI Custom Toggle Button Overrides: */ 81 | .tgl + .tgl-btn { 82 | width: 2.5em; 83 | height: 1.25em; 84 | background: #AAAAAA; 85 | } 86 | .tgl-ios:checked + .tgl-btn { 87 | background: #4285f4; 88 | } 89 | 90 | .tgl + .tgl-btn:after, .tgl + .tgl-btn:before { 91 | width: 63%; 92 | height: 175%; 93 | top: -38%; 94 | } 95 | 96 | .tgl-ios + .tgl-btn:after { 97 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(0, 0, 0, 0.08); 98 | } 99 | 100 | .tgl + .tgl-btn:after { 101 | left: -3px; 102 | } 103 | 104 | .tgl-btn { 105 | margin-left: 5px; 106 | } -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | URL Incrementer Popup 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /src/img/extras/ChromeWebStore_Badge_v2_206x58_C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/extras/ChromeWebStore_Badge_v2_206x58_C.png -------------------------------------------------------------------------------- /src/img/extras/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/extras/chrome.png -------------------------------------------------------------------------------- /src/img/extras/urli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/extras/urli.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/check.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/filter.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/gear.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/info-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/info-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/keyboard-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/keyboard-o.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/mouse-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/mouse-pointer.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/pencil-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/pencil-square.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/question-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/question-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/times.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/times.png -------------------------------------------------------------------------------- /src/img/font-awesome/black/window-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/black/window-restore.png -------------------------------------------------------------------------------- /src/img/font-awesome/blue/minus-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/blue/minus-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/blue/plus-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/blue/plus-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/green/check-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/green/check-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/green/chevron-circle-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/green/chevron-circle-left.png -------------------------------------------------------------------------------- /src/img/font-awesome/green/chevron-circle-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/green/chevron-circle-right.png -------------------------------------------------------------------------------- /src/img/font-awesome/orange/clock-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/orange/clock-o.png -------------------------------------------------------------------------------- /src/img/font-awesome/orange/pause-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/orange/pause-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/orange/play-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/orange/play-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/orange/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/orange/refresh.png -------------------------------------------------------------------------------- /src/img/font-awesome/purple/cloud-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/purple/cloud-download.png -------------------------------------------------------------------------------- /src/img/font-awesome/purple/exclamation-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/purple/exclamation-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/purple/flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/purple/flask.png -------------------------------------------------------------------------------- /src/img/font-awesome/red/times-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/red/times-circle.png -------------------------------------------------------------------------------- /src/img/font-awesome/yellow/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/font-awesome/yellow/star.png -------------------------------------------------------------------------------- /src/img/icons/dark/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/dark/128.png -------------------------------------------------------------------------------- /src/img/icons/dark/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/dark/16.png -------------------------------------------------------------------------------- /src/img/icons/dark/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/dark/24.png -------------------------------------------------------------------------------- /src/img/icons/dark/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/dark/32.png -------------------------------------------------------------------------------- /src/img/icons/dark/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/dark/48.png -------------------------------------------------------------------------------- /src/img/icons/light/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/light/16.png -------------------------------------------------------------------------------- /src/img/icons/light/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/light/24.png -------------------------------------------------------------------------------- /src/img/icons/light/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/light/32.png -------------------------------------------------------------------------------- /src/img/icons/rainbow/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/rainbow/16.png -------------------------------------------------------------------------------- /src/img/icons/rainbow/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/rainbow/24.png -------------------------------------------------------------------------------- /src/img/icons/rainbow/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/rainbow/32.png -------------------------------------------------------------------------------- /src/img/icons/urli/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/urli/16.png -------------------------------------------------------------------------------- /src/img/icons/urli/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/urli/24.png -------------------------------------------------------------------------------- /src/img/icons/urli/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixcious/url-incrementer/b6efb45812bf691a6f74058eb0f4b4ca92ce807a/src/img/icons/urli/32.png -------------------------------------------------------------------------------- /src/js/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Action = function () { 10 | 11 | /** 12 | * Performs the instance's action. 13 | * 14 | * @param instance the instance for this tab 15 | * @param action the action (e.g. increment or decrement) 16 | * @param caller String indicating who called this function (e.g. command, popup, content script) 17 | * @param callback the function callback (optional) 18 | * @public 19 | */ 20 | function performAction(instance, action, caller, callback) { 21 | //console.log("URLI.Action.performAction() - instance=" + instance + ", action=" + action + ", caller=" + caller); 22 | let actionPerformed = false; 23 | // Handle AUTO 24 | if (instance.autoEnabled) { 25 | // Get the most recent instance from Background in case auto has been paused 26 | instance = URLI.Background.getInstance(instance.tabId); 27 | // Handle autoTimes 28 | if (!instance.autoPaused) { 29 | if (instance.autoAction === action) { 30 | instance.autoTimes--; 31 | } else if ((instance.autoTimes < instance.autoTimesOriginal) && 32 | ((instance.autoAction === "increment" || instance.autoAction === "decrement") && (action === "increment" || action === "decrement")) || 33 | ((instance.autoAction === "next" || instance.autoAction === "prev") && (action === "next" || action === "prev"))) { 34 | instance.autoTimes++; 35 | } 36 | } 37 | // Prevents a rare race condition: 38 | // If the user tries to manually perform the auto action when times is at 0 but before the page has loaded and auto has cleared itself 39 | if (instance.autoTimes < 0) { 40 | //console.log("URLI.Action.performAction() - auto rare race condition encountered, about to clear. instance.autoTimes=" + instance.autoTimes); 41 | actionPerformed = clear(instance, action, caller, callback); 42 | return; 43 | } 44 | } 45 | // Handle DOWNLOAD 46 | if (instance.downloadEnabled) { 47 | // If download enabled auto not enabled, send a message to the popup to update the download preview (if it's open) 48 | if (!instance.autoEnabled && (["increment", "decrement", "next", "prev"].includes(action))) { 49 | chrome.tabs.onUpdated.addListener(URLI.Background.tabUpdatedListener); 50 | } 51 | } 52 | // Perform Action 53 | switch (action) { 54 | case "increment": 55 | case "decrement": 56 | if ((instance.errorSkip > 0 && (instance.errorCodes && instance.errorCodes.length > 0) || (instance.errorCodesCustomEnabled && instance.errorCodesCustom && instance.errorCodesCustom.length > 0)) && (!(caller === "popupClickActionButton" || caller === "auto" || caller === "externalExtension") || instance.enhancedMode)) { 57 | actionPerformed = incrementDecrementSkipErrors(instance, action, caller, callback); 58 | } else { 59 | actionPerformed = incrementDecrement(instance, action, caller, callback); 60 | } 61 | break; 62 | case "next": 63 | case "prev": 64 | actionPerformed = nextPrev(instance, action, caller, callback); 65 | break; 66 | case "clear": 67 | actionPerformed = clear(instance, action, caller, callback); 68 | break; 69 | case "auto": // the auto action is always a pause or resume 70 | actionPerformed = auto(instance, action, caller, callback); 71 | break; 72 | case "download": 73 | actionPerformed = download(instance, action, caller, callback); 74 | break; 75 | default: 76 | break; 77 | } 78 | // Icon Feedback if action was performed and other conditions are met (e.g. we don't show feedback if auto is enabled) 79 | if (actionPerformed && !(instance.autoEnabled || caller === "popupClearBeforeSet" || caller === "tabRemovedListener")) { 80 | chrome.storage.sync.get(null, function(items) { 81 | if (items.iconFeedbackEnabled) { 82 | URLI.Background.setBadge(instance.tabId, action, true); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | /** 89 | * Performs an increment or decrement action. 90 | * 91 | * @param instance the instance for this tab 92 | * @param action the action (increment or decrement) 93 | * @param caller String indicating who called this function (e.g. command, popup, content script) 94 | * @param callback the function callback (optional) 95 | * @private 96 | */ 97 | function incrementDecrement(instance, action, caller, callback) { 98 | let actionPerformed = false; 99 | // If URLI didn't find a selection, we can't increment or decrement 100 | if (instance.selection !== "" && instance.selectionStart >= 0) { 101 | actionPerformed = true; 102 | const urlProps = URLI.IncrementDecrement.modifyURL(action, instance.url, instance.selection, instance.selectionStart, instance.interval, instance.base, instance.baseCase, instance.leadingZeros); 103 | instance.url = urlProps.urlmod; 104 | instance.selection = urlProps.selectionmod; 105 | chrome.tabs.update(instance.tabId, {url: instance.url}); 106 | if (instance.enabled) { // Don't store Quick Instances (Instance is never enabled in quick mode) 107 | URLI.Background.setInstance(instance.tabId, instance); 108 | } 109 | chrome.runtime.sendMessage({greeting: "updatePopupInstance", instance: instance}); 110 | } 111 | return actionPerformed; 112 | } 113 | 114 | /** 115 | * Performs an increment or decrement action while also skipping errors. 116 | * 117 | * @param instance the instance for this tab 118 | * @param action the action (increment or decrement) 119 | * @param caller String indicating who called this function (e.g. command, popup, content script) 120 | * @param callback the function callback (optional) 121 | * @private 122 | */ 123 | function incrementDecrementSkipErrors(instance, action, caller, callback) { 124 | let actionPerformed = false; 125 | // If URLI didn't find a selection, we can't increment or decrement 126 | if (instance.selection !== "" && instance.selectionStart >= 0) { 127 | actionPerformed = true; 128 | //console.log("URLI.Action.incrementDecrementSkipErrors() - performing error skipping, about to execute increment-decrement.js script..."); 129 | chrome.tabs.executeScript(instance.tabId, { 130 | file: "js/increment-decrement.js", 131 | runAt: "document_start" 132 | }, function () { 133 | // This covers a very rare case where the user might be trying to increment the domain and where we lose permissions to execute the script. Fallback to doing a normal increment/decrement operation 134 | if (chrome.runtime.lastError) { 135 | //console.log("URLI.Action.incrementDecrementSkipErrors() - chrome.runtime.lastError.message:" + chrome.runtime.lastError.message); 136 | return incrementDecrement(instance, action, caller, callback); 137 | } 138 | const code = "URLI.IncrementDecrement.modifyURLAndSkipErrors(" + 139 | JSON.stringify(action) + ", " + 140 | JSON.stringify(instance) + ", " + 141 | JSON.parse(instance.errorSkip) + ");"; 142 | // No callback because this will be executing async code and then sending a message back to the background 143 | chrome.tabs.executeScript(instance.tabId, {code: code, runAt: "document_start"}); 144 | }); 145 | } 146 | return actionPerformed; 147 | } 148 | 149 | /** 150 | * Performs a next or prev action. 151 | * 152 | * @param instance the instance for this tab 153 | * @param action the action (e.g. next or prev) 154 | * @param caller String indicating who called this function (e.g. command, popup, content script) 155 | * @param callback the function callback (optional) 156 | * @private 157 | */ 158 | function nextPrev(instance, action, caller, callback) { 159 | let actionPerformed = true; 160 | chrome.tabs.executeScript(instance.tabId, {file: "js/next-prev.js", runAt: "document_end"}, function() { 161 | const code = "URLI.NextPrev.findNextPrevURL(" + 162 | JSON.stringify(action) + ", " + 163 | JSON.stringify(instance.nextPrevLinksPriority) + ", " + 164 | JSON.parse(instance.nextPrevSameDomainPolicy) + ");"; 165 | chrome.tabs.executeScript(instance.tabId, {code: code, runAt: "document_end"}, function(results) { 166 | if (results && results[0]) { 167 | const url = results[0]; 168 | chrome.tabs.update(instance.tabId, {url: url}); 169 | if (instance.autoEnabled && (instance.autoAction === "next" || instance.autoAction === "prev")) { 170 | //console.log("URLI.Action.nextPrev() - setting instance in background"); 171 | instance.url = url; 172 | URLI.Background.setInstance(instance.tabId, instance); 173 | } 174 | chrome.runtime.sendMessage({greeting: "updatePopupInstance", instance: instance}); 175 | } 176 | }); 177 | }); 178 | return actionPerformed; 179 | } 180 | 181 | /** 182 | * Performs a clear action. 183 | * 184 | * @param instance the instance for this tab 185 | * @param action the action (clear) 186 | * @param caller String indicating who called this function (e.g. command, popup, content script) 187 | * @param callback the function callback (optional) 188 | * @private 189 | */ 190 | function clear(instance, action, caller, callback) { 191 | let actionPerformed = false; 192 | // Prevents a clear badge from displaying if there is no instance (e.g. in quick shortcuts mode) 193 | if (instance.enabled || instance.autoEnabled || instance.downloadEnabled) { 194 | actionPerformed = true; 195 | } 196 | URLI.Background.deleteInstance(instance.tabId); 197 | if (caller !== "popupClearBeforeSet") { // Don't remove key/mouse listeners if popup clear before set 198 | chrome.storage.sync.get(null, function (items) { 199 | if (items.permissionsInternalShortcuts && items.keyEnabled && !items.keyQuickEnabled) { 200 | chrome.tabs.sendMessage(instance.tabId, {greeting: "removeKeyListener"}); 201 | } 202 | if (items.permissionsInternalShortcuts && items.mouseEnabled && !items.mouseQuickEnabled) { 203 | chrome.tabs.sendMessage(instance.tabId, {greeting: "removeMouseListener"}); 204 | } 205 | }); 206 | } 207 | if (instance.autoEnabled) { 208 | URLI.Auto.stopAutoTimer(instance, caller); 209 | } 210 | // for callers like popup that still need the instance, disable all states and reset autoTimes 211 | instance.enabled = instance.downloadEnabled = instance.autoEnabled = instance.autoPaused = false; 212 | instance.autoTimes = instance.autoTimesOriginal; 213 | if (callback) { 214 | callback(instance); 215 | } else { 216 | chrome.runtime.sendMessage({greeting: "updatePopupInstance", instance: instance}); 217 | } 218 | return actionPerformed; 219 | } 220 | 221 | /** 222 | * Performs an auto action (pause or resume only). 223 | * 224 | * @param instance the instance for this tab 225 | * @param action the action (auto pause/resume) 226 | * @param caller String indicating who called this function (e.g. command, popup, content script) 227 | * @param callback the function callback (optional) 228 | * @private 229 | */ 230 | function auto(instance, action, caller, callback) { 231 | let actionPerformed = false; 232 | if (instance && instance.autoEnabled) { 233 | URLI.Auto.pauseOrResumeAutoTimer(instance); 234 | instance = URLI.Background.getInstance(instance.tabId); // Get the updated pause or resume state set by auto 235 | if (callback) { 236 | callback(instance); 237 | } else { 238 | chrome.runtime.sendMessage({greeting: "updatePopupInstance", instance: instance}); 239 | } 240 | actionPerformed = true; 241 | } 242 | return actionPerformed; 243 | } 244 | 245 | /** 246 | * Performs a download action. 247 | * 248 | * @param instance the instance for this tab 249 | * @param action the action (download) 250 | * @param caller String indicating who called this function (e.g. command, popup, content script) 251 | * @param callback the function callback (optional) 252 | * @private 253 | */ 254 | function download(instance, action, caller, callback) { 255 | let actionPerformed = false; 256 | if (instance.downloadEnabled) { 257 | actionPerformed = true; 258 | chrome.tabs.executeScript(instance.tabId, {file: "js/download.js", runAt: "document_end"}, function() { 259 | const code = "URLI.Download.findDownloadURLs(" + 260 | JSON.stringify(instance.downloadStrategy) + ", " + 261 | JSON.stringify(instance.downloadExtensions) + ", " + 262 | JSON.stringify(instance.downloadTags) + ", " + 263 | JSON.stringify(instance.downloadAttributes) + ", " + 264 | JSON.stringify(instance.downloadSelector) + ", " + 265 | JSON.stringify(instance.downloadIncludes) + ", " + 266 | JSON.stringify(instance.downloadExcludes) + ");"; 267 | chrome.tabs.executeScript(instance.tabId, {code: code, runAt: "document_end"}, function (results) { 268 | if (results && results[0]) { 269 | const downloads = results[0]; 270 | for (let download of downloads) { 271 | //console.log("URLI.Action.download() - downloading url=" + download.url + " ... "); 272 | chrome.downloads.download({url: download.url}, function(downloadId) { 273 | chrome.downloads.search({id: downloadId}, function(results) { 274 | const downloadItem = results ? results[0] : undefined; 275 | if (downloadItem) { 276 | if (downloadItem.totalBytes > 0 && ( 277 | (!isNaN(instance.downloadMinMB) && instance.downloadMinMB > 0 ? (instance.downloadMinMB * 1048576) > downloadItem.totalBytes : false) || 278 | (!isNaN(instance.downloadMaxMB) && instance.downloadMaxMB > 0 ? (instance.downloadMaxMB * 1048576) < downloadItem.totalBytes : false) 279 | )) { 280 | //console.log("URLI.Action.download() - canceling download because downloadItem.totalbytes=" + downloadItem.totalBytes + " and instance.MinMB=" + (instance.downloadMinMB * 1048576) + " or instance.MaxMB=" + (instance.downloadMaxMB * 1048576)); 281 | chrome.downloads.cancel(downloadId); 282 | } 283 | } 284 | }); 285 | }); 286 | } 287 | } 288 | if (callback) { 289 | callback(instance); 290 | } 291 | }); 292 | }); 293 | } 294 | return actionPerformed; 295 | } 296 | 297 | // Return Public Functions 298 | return { 299 | performAction: performAction 300 | }; 301 | }(); -------------------------------------------------------------------------------- /src/js/auto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Auto = function () { 10 | 11 | // The instance auto timers in Background memory 12 | // Note: We have a separate map to store all the instances' auto timers instead of storing each auto timer in the 13 | // instance itself because the AutoTimer is a function and can't be JSON parsed/stringified 14 | const autoTimers = new Map(); 15 | 16 | // A boolean flag indicating if we have added the tabs.on.updated autoListener (to prevent adding multiple listeners) 17 | let autoListenerAdded = false; 18 | 19 | /** 20 | * Starts the auto timer for the instance by doing all the necessary start-up work (convenience method). 21 | * 22 | * @param instance the instance to start an auto timer for 23 | * @public 24 | */ 25 | function startAutoTimer(instance) { 26 | clearAutoTimeout(instance); 27 | setAutoTimeout(instance); 28 | addAutoListener(); 29 | URLI.Background.setBadge(instance.tabId, "auto", false); 30 | } 31 | 32 | /** 33 | * Stops the auto timer for the instance by doing all the necessary stopping work (convenience method). 34 | * 35 | * @param instance the instance's auto timer to stop 36 | * @param caller the caller asking to stop the auto timer (to determine how to set the badge) 37 | * @public 38 | */ 39 | function stopAutoTimer(instance, caller) { 40 | clearAutoTimeout(instance); 41 | removeAutoListener(); 42 | // Don't need to set the badge if the tab is being removed 43 | if (caller !== "tabRemovedListener") { 44 | // Don't set the clear badge if popup is just updating the instance (ruins auto badge if auto is re-set) 45 | if (caller !== "popupClearBeforeSet") { 46 | URLI.Background.setBadge(instance.tabId, "clear", true); 47 | } else { 48 | URLI.Background.setBadge(instance.tabId, "default", false); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Pauses or resumes the instance's auto timer. If the instance is paused, it resumes or vice versa. 55 | * 56 | * @param instance the instance's auto timer to pause or resume 57 | * @public 58 | */ 59 | function pauseOrResumeAutoTimer(instance) { 60 | const autoTimer = instance ? autoTimers.get(instance.tabId) : undefined; 61 | if (autoTimer) { 62 | if (!instance.autoPaused) { 63 | autoTimer.pause(); 64 | instance.autoPaused = true; 65 | URLI.Background.setBadge(instance.tabId, "autopause", false); 66 | } else { 67 | autoTimer.resume(); 68 | instance.autoPaused = false; 69 | if (instance.autoBadge === "times") { 70 | URLI.Background.setBadge(instance.tabId, "autotimes", false, instance.autoTimes + ""); 71 | } else { 72 | URLI.Background.setBadge(instance.tabId, "auto", false); 73 | } 74 | } 75 | URLI.Background.setInstance(instance.tabId, instance); // necessary: update instance.autoPaused boolean state 76 | autoTimers.set(instance.tabId, autoTimer); // necessary? update autoTimers paused state 77 | } 78 | } 79 | 80 | /** 81 | * Sets the instance's auto timeout and then performs the auto action after the time has elapsed. 82 | * 83 | * @param instance the instance's timeout to set 84 | * @private 85 | */ 86 | function setAutoTimeout(instance) { 87 | const autoTimer = new URLI.AutoTimer(function() { 88 | if (instance.downloadEnabled) { 89 | URLI.Action.performAction(instance, "download", "auto", function(instance) { 90 | URLI.Action.performAction(instance, instance.autoAction, "auto"); 91 | }); 92 | } else { 93 | URLI.Action.performAction(instance, instance.autoAction, "auto"); 94 | } 95 | }, instance.autoSeconds * 1000); 96 | autoTimers.set(instance.tabId, autoTimer); 97 | } 98 | 99 | /** 100 | * Clears the instance's auto timeout and deletes the auto timer from the map. 101 | * 102 | * @param instance the instance's timeout to clear 103 | * @private 104 | */ 105 | function clearAutoTimeout(instance) { 106 | const autoTimer = instance ? autoTimers.get(instance.tabId) : undefined; 107 | if (autoTimer) { 108 | autoTimer.clear(); 109 | autoTimers.delete(instance.tabId); 110 | } 111 | } 112 | 113 | /** 114 | * Sets the instance's auto timer wait state. This is used when the option "Wait for the page to fully load" is 115 | * checked. During the small window when the tab is still loading, this will not let pause/resume restart the timer. 116 | * 117 | * @param instance the instance's timeout to clear 118 | * @param wait boolean indicating whether the timer should wait (true) or not wait (false) 119 | * @private 120 | */ 121 | function setAutoWait(instance, wait) { 122 | const autoTimer = instance ? autoTimers.get(instance.tabId) : undefined; 123 | if (autoTimer) { 124 | autoTimer.setWait(wait); 125 | autoTimers.set(instance.tabId, autoTimer); 126 | } 127 | } 128 | 129 | /** 130 | * Adds the auto listener (only if there isn't one already). 131 | * 132 | * @private 133 | */ 134 | function addAutoListener() { 135 | if (!autoListenerAdded) { 136 | chrome.tabs.onUpdated.addListener(autoListener); 137 | autoListenerAdded = true; 138 | } 139 | } 140 | 141 | /** 142 | * Removes the auto listener (only if there isn't any other instance that still has auto enabled). 143 | * 144 | * @private 145 | */ 146 | function removeAutoListener() { 147 | if (![...URLI.Background.getInstances().values()].some(instance => instance && instance.autoEnabled)) { 148 | chrome.tabs.onUpdated.removeListener(autoListener); 149 | autoListenerAdded = false; 150 | } 151 | } 152 | 153 | /** 154 | * The chrome.tabs.onUpdated auto listener that fires every time a tab is updated. 155 | * Decides whether or not to set the autoTimeout based on the instance's current properties. 156 | * Also decides when it is time to delete the instance when the auto times count has reached 0. 157 | * 158 | * @param tabId the tab ID 159 | * @param changeInfo the status (either complete or loading) 160 | * @param tab the tab object 161 | * @private 162 | */ 163 | function autoListener(tabId, changeInfo, tab) { 164 | //console.log("URLI.Auto.autoListener() - the chrome.tabs.onUpdated auto listener is on!"); 165 | // Cache loading and complete for maybe a small performance gain since we need to check multiple times? 166 | const loading = changeInfo.status === "loading", 167 | complete = changeInfo.status === "complete"; 168 | // We only care about loading and complete statuses 169 | if (!loading && !complete) { 170 | return; 171 | } 172 | const instance = URLI.Background.getInstance(tabId); 173 | // If auto is enabled for this instance 174 | if (instance && instance.autoEnabled) { 175 | // Loading Only: 176 | if (loading) { 177 | // If autoWait is on, we set the wait boolean to true in case the user tries to pause/resume (e.g. start) the timeout while the tab is loading 178 | if (instance.autoWait) { 179 | setAutoWait(instance, true); 180 | } 181 | // Set the "AUTO" Browser Action Badge as soon as we can (loading). This needs to be done each time the tab is updated 182 | if (instance.autoPaused) { 183 | URLI.Background.setBadge(tabId, "autopause", false); 184 | } 185 | else if (instance.autoBadge === "times") { 186 | URLI.Background.setBadge(tabId, "autotimes", false, (instance.autoTimes) + ""); 187 | } else { 188 | URLI.Background.setBadge(tabId, "auto", false); 189 | } 190 | } 191 | // Complete Only: 192 | if (complete) { 193 | // If download enabled, send a message to the popup to update the download preview (if it's open) 194 | // Note: Do NOT send this message at Loading because it doesn't refresh properly sometimes (even though the download script runs at document_end) 195 | if (instance.downloadEnabled) { 196 | chrome.runtime.sendMessage({greeting: "updatePopupDownloadPreview", instance: instance}); 197 | } 198 | } 199 | // AutoWait (Complete or Loading) : 200 | if (instance.autoWait ? complete : loading) { 201 | // If autoWait is on, we now set the wait boolean to false indicating a pause/resume (e.g. start) can start the timeout 202 | if (instance.autoWait) { 203 | setAutoWait(instance, false); 204 | } 205 | // If the auto instance was paused, this is almost considered a no-op 206 | if (instance.autoPaused) { 207 | // Clear the instance if auto is paused but the times count is at 0 or less (TODO: is this really needed, we need to treat paused differently?) 208 | if (instance.autoTimes <= 0) { 209 | URLI.Action.performAction(instance, "clear", "auto"); 210 | } 211 | } 212 | // If autoTimes is still greater than 0, set the auto timeout, else clear the instance 213 | // Note: Remember, the first time Auto is already done via Popup calling setAutoTimeout() 214 | else if (instance.autoTimes > 0) { 215 | clearAutoTimeout(instance); // Prevents adding multiple timeouts (e.g. if user manually navigated the auto tab) 216 | setAutoTimeout(instance); 217 | } else { 218 | // Note: clearing will clearAutoTimeout and removeAutoListener, so we don't have to do it here 219 | URLI.Action.performAction(instance, "clear", "auto"); 220 | } 221 | } 222 | } else if (complete) { // Removes any stray auto listeners that may possibly exist 223 | removeAutoListener(); 224 | } 225 | } 226 | 227 | // Return Public Functions 228 | return { 229 | startAutoTimer: startAutoTimer, 230 | stopAutoTimer: stopAutoTimer, 231 | pauseOrResumeAutoTimer: pauseOrResumeAutoTimer 232 | }; 233 | }(); 234 | 235 | /** 236 | * The AutoTimer that contains the internal setTimeout with pause and resume capabilities. 237 | * It also contains a "wait" state to keep it from setting a timeout before the page has fully loaded, 238 | * if the user checked the "Wait for the page to fully load" checkbox. 239 | * 240 | * This function is based on code written by Tim Down @ stackoverflow.com. 241 | * 242 | * @param callback the function callback 243 | * @param delay the delay for the timeout 244 | * @see https://stackoverflow.com/a/3969760 245 | */ 246 | URLI.AutoTimer = function (callback, delay) { 247 | 248 | let timerId, start, remaining = delay, wait = false; 249 | 250 | this.pause = function() { 251 | window.clearTimeout(timerId); 252 | remaining -= Date.now() - start; 253 | remaining = remaining < 0 || wait ? delay : remaining; 254 | //console.log("URLI.AutoTimer.pause() - timerId=" + timerId + " start=" + start + " delay=" + delay + " remaining=" + remaining + " wait=" + wait); 255 | }; 256 | 257 | this.resume = function() { 258 | start = Date.now(); 259 | window.clearTimeout(timerId); 260 | timerId = wait ? timerId : window.setTimeout(callback, remaining); 261 | //console.log("URLI.AutoTimer.resume() - timerId=" + timerId + " start=" + start + " delay=" + delay + " remaining=" + remaining + " wait=" + wait); 262 | }; 263 | 264 | this.clear = function() { 265 | window.clearTimeout(timerId); 266 | }; 267 | 268 | this.setWait = function(wait_) { 269 | wait = wait_; 270 | }; 271 | 272 | this.resume(); 273 | }; -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Background = function () { 10 | 11 | // The storage default values 12 | // Note: Storage.set can only set top-level JSON objects, do not use nested JSON objects (instead, prefix keys that should be grouped together) 13 | const STORAGE_DEFAULT_VALUES = { 14 | /* permissions */ "permissionsInternalShortcuts": false, "permissionsDownload": false, "permissionsEnhancedMode": false, 15 | /* icon */ "iconColor": "dark", "iconFeedbackEnabled": false, 16 | /* popup */ "popupButtonSize": 32, "popupAnimationsEnabled": true, "popupOpenSetup": true, "popupSettingsCanOverwrite": true, 17 | /* shortcuts */ "quickEnabled": true, 18 | /* key */ "keyEnabled": true, "keyQuickEnabled": true, "keyIncrement": [6, "ArrowUp"], "keyDecrement": [6, "ArrowDown"], "keyNext": [6, "ArrowRight"], "keyPrev": [6, "ArrowLeft"], "keyClear": [6, "KeyX"], "keyAuto": [6, "KeyA"], 19 | /* mouse */ "mouseEnabled": false, "mouseQuickEnabled": false, "mouseIncrement": -1, "mouseDecrement": -1, "mouseNext": -1, "mousePrev": -1, "mouseClear": -1, "mouseAuto": -1, 20 | /* incdec */ "selectionPriority": "prefixes", "interval": 1, "leadingZerosPadByDetection": true, "base": 10, "baseCase": "lowercase", "errorSkip": 0, "errorCodes": ["404", "", "", ""], "errorCodesCustomEnabled": false, "errorCodesCustom": [], "selectionCustom": { "url": "", "pattern": "", "flags": "", "group": 0, "index": 0 }, 21 | /* nextprev */ "nextPrevLinksPriority": "attributes", "nextPrevSameDomainPolicy": true, "nextPrevPopupButtons": false, 22 | /* auto */ "autoAction": "increment", "autoTimes": 10, "autoSeconds": 5, "autoWait": true, "autoBadge": "times", 23 | /* download */ "downloadStrategy": "extensions", "downloadExtensions": [], "downloadTags": [], "downloadAttributes": [], "downloadSelector": "", "downloadIncludes": [], "downloadExcludes": [], "downloadMinMB": null, "downloadMaxMB": null, "downloadPreview": ["thumb", "extension", "tag", "compressed"], 24 | /* fun */ "urli": "loves incrementing for you" 25 | }, 26 | 27 | // The browser action badges that will be displayed against the extension icon 28 | BROWSER_ACTION_BADGES = { 29 | "increment": { "text": "+", "backgroundColor": "#1779BA" }, 30 | "decrement": { "text": "-", "backgroundColor": "#1779BA" }, 31 | "next": { "text": ">", "backgroundColor": "#05854D" }, 32 | "prev": { "text": "<", "backgroundColor": "#05854D" }, 33 | "clear": { "text": "X", "backgroundColor": "#FF0000" }, 34 | "auto": { "text": "AUTO", "backgroundColor": "#FF6600" }, 35 | "autotimes": { "text": "", "backgroundColor": "#FF6600" }, 36 | "autopause": { "text": "❚❚", "backgroundColor": "#FF6600" }, 37 | "download": { "text": "DL", "backgroundColor": "#663399" }, 38 | "skip": { "text": "", "backgroundColor": "#000028" }, //"#FFCC22" }, 39 | "default": { "text": "", "backgroundColor": [0,0,0,0] } 40 | }, 41 | 42 | // The individual tab instances in Background memory 43 | // Note: We never save instances in storage due to URLs being a privacy concern 44 | instances = new Map(); 45 | 46 | /** 47 | * Gets the storage default values (SDV). 48 | * 49 | * @return the storage default values (SDV) 50 | * @public 51 | */ 52 | function getSDV() { 53 | return STORAGE_DEFAULT_VALUES; 54 | } 55 | 56 | /** 57 | * Gets all instances. 58 | * 59 | * @return {Map} the tab instances 60 | * @public 61 | */ 62 | function getInstances() { 63 | return instances; 64 | } 65 | 66 | /** 67 | * Gets the instance. 68 | * 69 | * @param tabId the tab id to lookup this instance by 70 | * @return instance the tab's instance 71 | * @public 72 | */ 73 | function getInstance(tabId) { 74 | return instances.get(tabId); 75 | } 76 | 77 | /** 78 | * Sets the instance. 79 | * 80 | * @param tabId the tab id to lookup this instance by 81 | * @param instance the instance to set 82 | * @public 83 | */ 84 | function setInstance(tabId, instance) { 85 | instances.set(tabId, instance); 86 | } 87 | 88 | /** 89 | * Deletes the instance. 90 | * 91 | * @param tabId the tab id to lookup this instance by 92 | * @public 93 | */ 94 | function deleteInstance(tabId) { 95 | instances.delete(tabId); 96 | } 97 | 98 | /** 99 | * Builds an instance with default values. 100 | * 101 | * @param tab the tab properties (id, url) to set this instance with 102 | * @param items the storage items to help build a default instance 103 | * @return instance the newly built instance 104 | * @public 105 | */ 106 | function buildInstance(tab, items) { 107 | const selectionProps = URLI.IncrementDecrement.findSelection(tab.url, items.selectionPriority, items.selectionCustom); 108 | return { 109 | "enabled": false, "autoEnabled": false, "downloadEnabled": false, "autoPaused": false, "enhancedMode": items.permissionsEnhancedMode, 110 | "tabId": tab.id, "url": tab.url, 111 | "selection": selectionProps.selection, "selectionStart": selectionProps.selectionStart, 112 | "leadingZeros": items.leadingZerosPadByDetection && selectionProps.selection.charAt(0) === '0' && selectionProps.selection.length > 1, 113 | "interval": items.interval, 114 | "base": items.base, "baseCase": items.baseCase, 115 | "errorSkip": items.errorSkip, "errorCodes": items.errorCodes, "errorCodesCustomEnabled": items.errorCodesCustomEnabled, "errorCodesCustom": items.errorCodesCustom, 116 | "nextPrevLinksPriority": items.nextPrevLinksPriority, "nextPrevSameDomainPolicy": items.nextPrevSameDomainPolicy, 117 | "autoAction": items.autoAction, "autoTimesOriginal": items.autoTimes, "autoTimes": items.autoTimes, "autoSeconds": items.autoSeconds, "autoWait": items.autoWait, "autoBadge": items.autoBadge, 118 | "downloadStrategy": items.downloadStrategy, "downloadExtensions": items.downloadExtensions, "downloadTags": items.downloadTags, "downloadAttributes": items.downloadAttributes, "downloadSelector": items.downloadSelector, 119 | "downloadIncludes": items.downloadIncludes, "downloadExcludes": items.downloadExcludes, 120 | "downloadMinMB": items.downloadMinMB, "downloadMaxMB": items.downloadMaxMB, 121 | "downloadPreview": items.downloadPreview 122 | }; 123 | } 124 | 125 | /** 126 | * Sets the browser action badge for this tabId. Can either be temporary or for an indefinite time. Note that when the tab is updated, the browser removes the badge. 127 | * 128 | * @param tabId the tab ID to set this badge to 129 | * @param badge the badge key to set from BROWSER_ACTION_BADGES 130 | * @param temporary boolean indicating whether the badge should be displayed temporarily 131 | * @param text (optional) the text to use instead of the the badge text 132 | * @param backgroundColor (optional) the backgroundColor to use instead of the badge backgroundColor 133 | * @public 134 | */ 135 | function setBadge(tabId, badge, temporary, text, backgroundColor) { 136 | chrome.browserAction.setBadgeText({text: text ? text : BROWSER_ACTION_BADGES[badge].text, tabId: tabId}); 137 | chrome.browserAction.setBadgeBackgroundColor({color: backgroundColor ? backgroundColor : BROWSER_ACTION_BADGES[badge].backgroundColor, tabId: tabId}); 138 | if (temporary) { 139 | setTimeout(function () { 140 | chrome.browserAction.setBadgeText({text: BROWSER_ACTION_BADGES["default"].text, tabId: tabId}); 141 | chrome.browserAction.setBadgeBackgroundColor({color: BROWSER_ACTION_BADGES["default"].backgroundColor, tabId: tabId}); 142 | }, 2000); 143 | } 144 | } 145 | 146 | /** 147 | * Listen for installation changes and do storage/extension initialization work. 148 | * 149 | * @param details the installation details 150 | * @public 151 | */ 152 | function installedListener(details) { 153 | // New Installations: Setup storage and open Options Page in a new tab 154 | if (details.reason === "install") { 155 | //console.log("URLI.Background.installedListener() - details.reason === install"); 156 | chrome.storage.sync.clear(function() { 157 | chrome.storage.sync.set(STORAGE_DEFAULT_VALUES, function() { 158 | chrome.runtime.openOptionsPage(); 159 | }); 160 | }); 161 | } 162 | // Update Installations Version 5.2 and Below: Reset storage and re-save old increment values and remove all permissions for a clean slate 163 | else if (details.reason === "update" && details.previousVersion <= "5.2") { 164 | //console.log("URLI.Background.installedListener() - details.reason === update, previousVersion <= 5.2, actual previousVersion=" + details.previousVersion); 165 | chrome.storage.sync.get(null, function(olditems) { 166 | chrome.storage.sync.clear(function() { 167 | chrome.storage.sync.set(STORAGE_DEFAULT_VALUES, function() { 168 | chrome.storage.sync.set({ 169 | "selectionPriority": olditems && olditems.selectionPriority ? olditems.selectionPriority : STORAGE_DEFAULT_VALUES.selectionPriority, 170 | "interval": olditems && olditems.interval ? olditems.interval : STORAGE_DEFAULT_VALUES.interval, 171 | "leadingZerosPadByDetection": olditems && olditems.leadingZerosPadByDetection ? olditems.leadingZerosPadByDetection : STORAGE_DEFAULT_VALUES.leadingZerosPadByDetection, 172 | "base": olditems && olditems.base ? olditems.base : STORAGE_DEFAULT_VALUES.base, 173 | "baseCase": olditems && olditems.baseCase ? olditems.baseCase : STORAGE_DEFAULT_VALUES.baseCase, 174 | "selectionCustom": olditems && olditems.selectionCustom ? olditems.selectionCustom : STORAGE_DEFAULT_VALUES.selectionCustom 175 | }); 176 | }); 177 | }); 178 | }); 179 | if (chrome.declarativeContent) { 180 | chrome.declarativeContent.onPageChanged.removeRules(undefined); 181 | } 182 | chrome.permissions.remove({ permissions: ["declarativeContent", "downloads"], origins: [""]}); 183 | } 184 | } 185 | 186 | /** 187 | * Listen for requests from chrome.runtime.sendMessage (e.g. Content Scripts). 188 | * 189 | * @param request the request containing properties to parse (e.g. greeting message) 190 | * @param sender the sender who sent this message, with an identifying tabId 191 | * @param sendResponse the optional callback function (e.g. for a reply back to the sender) 192 | * @public 193 | */ 194 | function messageListener(request, sender, sendResponse) { 195 | //console.log("URLI.Background.messageListener() - request=" + request + " sender=" + sender); 196 | switch (request.greeting) { 197 | case "getInstance": 198 | sendResponse({instance: URLI.Background.getInstance(sender.tab.id)}); 199 | break; 200 | case "performAction": 201 | chrome.storage.sync.get(null, function(items) { 202 | let instance = getInstance(sender.tab.id); 203 | if (!instance && request.action !== "auto") { 204 | instance = buildInstance(sender.tab, items); 205 | } 206 | if (instance) { 207 | URLI.Action.performAction(instance, request.action, "shortcuts.js"); 208 | } 209 | }); 210 | break; 211 | case "incrementDecrementSkipErrors": 212 | if (request.instance) { 213 | chrome.tabs.update(request.instance.tabId, {url: request.instance.url}); 214 | if (request.instance.enabled) { // Don't store Quick Instances (Instance is never enabled in quick mode) 215 | URLI.Background.setInstance(request.instance.tabId, request.instance); 216 | } 217 | chrome.runtime.sendMessage({greeting: "updatePopupInstance", instance: request.instance}); 218 | } 219 | break; 220 | case "setBadgeSkipErrors": 221 | if (request.errorCode && request.instance && !request.instance.autoEnabled) { 222 | setBadge(sender.tab.id, "skip", true, request.errorCode + ""); 223 | } 224 | break; 225 | default: 226 | break; 227 | } 228 | sendResponse({}); 229 | } 230 | 231 | /** 232 | * Listen for external requests from external extensions: Increment Button and Decrement Button for URLI. 233 | * 234 | * @param request the request containing properties to parse (e.g. greeting message) 235 | * @param sender the sender who sent this message, with an identifying tabId 236 | * @param sendResponse the optional callback function (e.g. for a reply back to the sender) 237 | * @public 238 | */ 239 | function messageExternalListener(request, sender, sendResponse) { 240 | //console.log("URLI.Background.messageExternalListener() - request.action=" + request.action + " sender.id=" + sender.id); 241 | const URL_INCREMENT_BUTTON_EXTENSION_ID = "decebmdlceenceecblpfjanoocfcmjai", 242 | URL_DECREMENT_BUTTON_EXTENSION_ID = "nnmjbfglinmjnieblelacmlobabcenfk"; 243 | if (sender && (sender.id === URL_INCREMENT_BUTTON_EXTENSION_ID || sender.id === URL_DECREMENT_BUTTON_EXTENSION_ID)) { 244 | switch (request.greeting) { 245 | case "performAction": 246 | chrome.storage.sync.get(null, function(items) { 247 | let instance = getInstance(request.tab.id); 248 | if (!instance && request.action !== "auto") { 249 | instance = buildInstance(request.tab, items); 250 | } 251 | if (instance && (request.action === "increment" || request.action === "decrement")) { 252 | URLI.Action.performAction(instance, request.action, "externalExtension"); 253 | } 254 | }); 255 | break; 256 | default: 257 | break; 258 | } 259 | sendResponse({}); 260 | } 261 | } 262 | 263 | /** 264 | * Listen for commands (Browser Extension shortcuts) and perform the command's action. 265 | * 266 | * @param command the shortcut command that was performed 267 | * @public 268 | */ 269 | function commandListener(command) { 270 | if (command === "increment" || command === "decrement" || command === "next" || command === "prev" || command === "auto" || command === "clear") { 271 | chrome.storage.sync.get(null, function(items) { 272 | if (!items.permissionsInternalShortcuts) { 273 | chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) { 274 | if (tabs && tabs[0]) { // for example, tab may not exist if command is called while in popup window 275 | let instance = getInstance(tabs[0].id); 276 | if ((command === "increment" || command === "decrement" || command === "next" || command === "prev") && (items.quickEnabled || (instance && instance.enabled)) || 277 | (command === "auto" && instance && instance.autoEnabled) || 278 | (command === "clear" && instance && (instance.enabled || instance.autoEnabled || instance.downloadEnabled))) { 279 | if (!instance && items.quickEnabled) { 280 | instance = buildInstance(tabs[0], items); 281 | } 282 | URLI.Action.performAction(instance, command, "command"); 283 | } 284 | } 285 | }); 286 | } 287 | }); 288 | } 289 | } 290 | 291 | /** 292 | * Listen for when tabs are removed and clear the instances if they exist. 293 | * 294 | * @param tabId the tab ID 295 | * @param removeInfo information about how the tab is being removed (e.g. window closed) 296 | * @public 297 | */ 298 | function tabRemovedListener(tabId, removeInfo) { 299 | const instance = URLI.Background.getInstance(tabId); 300 | if (instance) { 301 | URLI.Action.performAction(instance, "clear", "tabRemovedListener"); 302 | } 303 | } 304 | 305 | /** 306 | * The chrome.tabs.onUpdated listener that is temporarily added (then removed) for certain events. 307 | * 308 | * @param tabId the tab ID 309 | * @param changeInfo the status (either complete or loading) 310 | * @param tab the tab object 311 | * @public 312 | */ 313 | function tabUpdatedListener(tabId, changeInfo, tab) { 314 | //console.log("URLI.Background.tabUpdatedListener() - the chrome.tabs.onUpdated download preview listener is on!"); 315 | if (changeInfo.status === "complete") { 316 | const instance = URLI.Background.getInstance(tabId); 317 | // If download enabled auto not enabled, send a message to the popup to update the download preview (if it's open) 318 | if (instance && instance.downloadEnabled && !instance.autoEnabled) { 319 | chrome.runtime.sendMessage({greeting: "updatePopupDownloadPreview", instance: instance}); 320 | } 321 | chrome.tabs.onUpdated.removeListener(tabUpdatedListener); 322 | } 323 | } 324 | 325 | /** 326 | * The extension's background startup listener that is run the first time the extension starts. 327 | * For example, when Chrome is started, when the extension is installed or updated, or when the 328 | * extension is re-enabled after being disabled. 329 | * 330 | * Ensures the toolbar icon and declarativeContent rules are set (due to Chrome sometimes not re-setting them). 331 | * 332 | * @public 333 | */ 334 | function startupListener() { 335 | //console.log("URLI.Background.startupListener()"); 336 | chrome.storage.sync.get(null, function(items) { 337 | // Ensure the chosen toolbar icon is set 338 | if (items && ["dark", "light", "rainbow", "urli"].includes(items.iconColor)) { 339 | //console.log("URLI.Background.startupListener() - setting chrome.browserAction.setIcon() to " + items.iconColor); 340 | chrome.browserAction.setIcon({ 341 | path : { 342 | "16": "/img/icons/" + items.iconColor + "/16.png", 343 | "24": "/img/icons/" + items.iconColor + "/24.png", 344 | "32": "/img/icons/" + items.iconColor + "/32.png" 345 | } 346 | }); 347 | } 348 | // Ensure Internal Shortcuts declarativeContent rule is added 349 | // The declarativeContent rule sometimes gets lost when the extension is updated or when the extension is enabled after being disabled 350 | if (items && items.permissionsInternalShortcuts) { 351 | if (chrome.declarativeContent) { 352 | chrome.declarativeContent.onPageChanged.getRules(undefined, function(rules) { 353 | let shortcutsjsRule = false; 354 | for (let rule of rules) { 355 | if (rule.actions[0].js[0] === "js/shortcuts.js") { 356 | //console.log("URLI.Background.startupListener() - internal shortcuts enabled, found shortcuts.js rule!"); 357 | shortcutsjsRule = true; 358 | break; 359 | } 360 | } 361 | if (!shortcutsjsRule) { 362 | //console.log("URLI.Background.startupListener() - oh no, something went wrong. internal shortcuts enabled, but shortcuts.js rule not found!"); 363 | chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { 364 | chrome.declarativeContent.onPageChanged.addRules([{ 365 | conditions: [new chrome.declarativeContent.PageStateMatcher()], 366 | actions: [new chrome.declarativeContent.RequestContentScript({js: ["js/shortcuts.js"]})] 367 | }], function(rules) { 368 | //console.log("URLI.Background.startupListener() - successfully added declarativeContent rules:" + rules); 369 | }); 370 | }); 371 | } 372 | }); 373 | } 374 | } 375 | }); 376 | } 377 | 378 | // Return Public Functions 379 | return { 380 | getSDV: getSDV, 381 | getInstances: getInstances, 382 | getInstance: getInstance, 383 | setInstance: setInstance, 384 | deleteInstance: deleteInstance, 385 | buildInstance: buildInstance, 386 | setBadge: setBadge, 387 | installedListener: installedListener, 388 | messageListener: messageListener, 389 | messageExternalListener: messageExternalListener, 390 | commandListener: commandListener, 391 | tabRemovedListener: tabRemovedListener, 392 | tabUpdatedListener: tabUpdatedListener, 393 | startupListener: startupListener 394 | }; 395 | }(); 396 | 397 | // Background Listeners 398 | chrome.runtime.onInstalled.addListener(URLI.Background.installedListener); 399 | chrome.runtime.onMessage.addListener(URLI.Background.messageListener); 400 | chrome.runtime.onMessageExternal.addListener(URLI.Background.messageExternalListener); 401 | chrome.commands.onCommand.addListener(URLI.Background.commandListener); 402 | chrome.tabs.onRemoved.addListener(URLI.Background.tabRemovedListener); 403 | URLI.Background.startupListener(); -------------------------------------------------------------------------------- /src/js/download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Download = function () { 10 | 11 | // A list of all attributes that can contain URLs (Note the following URL attributes are deprecated in HTML5: background, classid, codebase, longdesc, profile) 12 | // List derived from Daniel DiPaolo @ stackoverflow.com @see https://stackoverflow.com/a/2725168 13 | const URL_ATTRIBUTES = ["action", "cite", "data", "formaction", "href", "icon", "manifest", "poster", "src", "usemap", "style"]; 14 | 15 | /** 16 | * Finds the current page URL and all URLs, extensions, tags, and attributes on the page to build a 17 | * download preview. 18 | * 19 | * @returns {*} results, the array of all URLs items, all extensions, all tags, and all attributes 20 | * @public 21 | */ 22 | function previewDownloadURLs() { 23 | const pageURL = findPageURL(), 24 | allURLs = findDownloadURLs("all"), 25 | allExtensions = findProperties(allURLs, "extension"), 26 | allTags = findProperties(allURLs, "tag"), 27 | allAttributes = findProperties(allURLs, "attribute"); 28 | return { "pageURL": pageURL, "allURLs": allURLs, "allExtensions": allExtensions, "allTags": allTags, "allAttributes": allAttributes } 29 | } 30 | 31 | /** 32 | * Finds all URLs by a specific strategy. Strategies can be "all", "extensions", "tags", "attributes", "selector", 33 | * or "page". This is the controller method that hands off the work to a lower-level method that actually parses 34 | * the elements using the selector. 35 | * 36 | * @param strategy the download strategy to employ 37 | * @param extensions (optional) if strategy is extensions: the file extensions to check for 38 | * @param tags (optional) if strategy is tags: the HTML tags (e.g. ) to check for 39 | * @param attributes (optional) if strategy is attributes: the HTML tag attributes (e.g. src, href) to check for 40 | * @param selector (optional) if strategy is selector: the CSS selectors to use in querySelectorAll() 41 | * @param includes (optional) the array of Strings that must be included in the URLs 42 | * @param excludes (optional) the array of Strings that must be excluded from the URLs 43 | * @returns {*} results, the array of results 44 | * @public 45 | */ 46 | function findDownloadURLs(strategy, extensions, tags, attributes, selector, includes, excludes) { 47 | let results = [], 48 | selectorbuilder = ""; 49 | try { 50 | switch (strategy) { 51 | case "all": 52 | case "extensions": // Noticed issues with using a selectorbuilder based on the extensions so go with all for this for now 53 | for (let urlattribute of URL_ATTRIBUTES) { 54 | selectorbuilder += (selectorbuilder !== "" ? "," : "") + "[" + urlattribute + "]"; 55 | } 56 | results = findDownloadURLsBySelector(strategy, extensions, tags, attributes, selectorbuilder, includes, excludes); 57 | break; 58 | case "tags": 59 | for (let tag of tags) { 60 | selectorbuilder += (selectorbuilder !== "" ? "," : "") + tag; 61 | } 62 | results = findDownloadURLsBySelector(strategy, extensions, tags, attributes, selectorbuilder, includes, excludes); 63 | break; 64 | case "attributes": 65 | for (let attribute of attributes) { 66 | selectorbuilder += (selectorbuilder !== "" ? "," : "") + "[" + attribute + "]"; 67 | } 68 | results = findDownloadURLsBySelector(strategy, extensions, tags, attributes, selectorbuilder, includes, excludes); 69 | break; 70 | case "selector": 71 | results = findDownloadURLsBySelector(strategy, extensions, tags, attributes, selector, includes, excludes); 72 | break; 73 | case "page": 74 | results = findPageURL(includes, excludes); 75 | break; 76 | default: 77 | results = []; 78 | break; 79 | } 80 | } catch (e) { 81 | //console.log("URLI.Download.findDownloadURLs() - exception caught:" + e); 82 | results = []; 83 | } 84 | return results; 85 | } 86 | 87 | /** 88 | * Finds all URLs that match the specified strategy and applicable parameters. Performs a query on the page's elements 89 | * and checks each element to see if it passes the strategy's rules. 90 | * 91 | * @param strategy the download strategy to employ 92 | * @param extensions (optional) if strategy is extensions: the file extensions to check for 93 | * @param tags (optional) if strategy is tags: the HTML tags (e.g. ) to check for 94 | * @param attributes (optional) if strategy is attributes: the HTML tag attributes (e.g. src, href) to check for 95 | * @param selector (optional) if strategy is selector: the CSS selectors to use in querySelectorAll() 96 | * @param includes (optional) the array of Strings that must be included in the URLs 97 | * @param excludes (optional) the array of Strings that must be excluded from the URLs 98 | * @returns {*} results, the array of results 99 | * @private 100 | */ 101 | function findDownloadURLsBySelector(strategy, extensions, tags, attributes, selector, includes, excludes) { 102 | const items = new Map(), // return value, we use a Map to avoid potential duplicate URLs 103 | elements = document.querySelectorAll(selector); 104 | let url = "", 105 | extension = "", 106 | attribute = "", 107 | tag = ""; 108 | //console.log("URLI.Download.findDownloadURLsBySelector() - found " + elements.length + " element(s)"); 109 | for (let element of elements) { 110 | for (let urlattribute of URL_ATTRIBUTES) { 111 | if (element[urlattribute]) { 112 | if (urlattribute === "style") { 113 | url = extractURLFromStyle(element.style); 114 | } else { 115 | url = element[urlattribute]; 116 | } 117 | if (url && doesIncludeOrExclude(url, includes, true) && doesIncludeOrExclude(url, excludes, false)) { 118 | extension = findExtension(url); 119 | // Special Restriction (Extensions) 120 | if (strategy === "extensions" && (!extension || !extensions.includes(extension))) { 121 | continue; 122 | } 123 | tag = element.tagName ? element.tagName.toLowerCase() : ""; 124 | // Special Restriction (Tags) 125 | if (strategy === "tags" && (!tag || !tags.includes(tag))) { 126 | continue; 127 | } 128 | attribute = urlattribute; 129 | // Special Restriction (Attributes) 130 | if (strategy === "attributes" && (!attribute || !attributes.includes(attribute))) { 131 | continue; 132 | } 133 | items.set(url + "", {"url": url, "extension": extension, "tag": tag, "attribute": attribute}); 134 | } 135 | } 136 | } 137 | } 138 | return [...items.values()]; // Convert Map values into Array for return value back (Map/Set can't be used) 139 | } 140 | 141 | /** 142 | * Finds the current web page's URL. 143 | * 144 | * @param includes (optional) the array of Strings that must be included in the URL 145 | * @param excludes (optional) the array of Strings that must be excluded from the URL 146 | * @returns {*} results, the array of results 147 | * @private 148 | */ 149 | function findPageURL(includes, excludes) { 150 | const url = document.location.href, 151 | extension = findExtension(url); 152 | if (url && doesIncludeOrExclude(url, includes, true) && doesIncludeOrExclude(url, excludes, false)) { 153 | return [{"url": url, "extension": extension, "tag": "", "attribute": ""}]; 154 | } else { 155 | return []; 156 | } 157 | } 158 | 159 | /** 160 | * Finds all the unique properties (extensions, tags, or attributes) from the collection of items. 161 | * 162 | * @param items the items to check 163 | * @param property the property to check (e.g. "extension", "tag", "attribute") 164 | * @returns {Array} the unique properties sorted 165 | * @private 166 | */ 167 | function findProperties(items, property) { 168 | const properties = new Set(); 169 | if (items) { 170 | for (let item of items) { 171 | if (item && item[property]) { 172 | properties.add(item[property]); 173 | } 174 | } 175 | } 176 | return [...properties].sort(); 177 | } 178 | 179 | /** 180 | * Determines if the URL includes or excludes the terms. 181 | * 182 | * @param url the url to check against 183 | * @param terms the terms to check 184 | * @param doesInclude boolean indicating if this is an includes or excludes check 185 | * @returns {boolean} true if the url includes or excludes the terms 186 | * @private 187 | */ 188 | function doesIncludeOrExclude(url, terms, doesInclude) { 189 | let does = true; 190 | if (terms && terms.length > 0) { 191 | for (let term of terms) { 192 | if (term && doesInclude ? !url.includes(term) : url.includes(term)) { 193 | does = false; 194 | break; 195 | } 196 | } 197 | } 198 | return does; 199 | } 200 | 201 | /** 202 | * Finds the file extension from a URL String. 203 | * Regex to find a file extension from a URL is by SteeBono @ stackoverflow.com 204 | * 205 | * @param url the URL to parse 206 | * @returns {string} the file extension (if found) 207 | * @see https://stackoverflow.com/a/42841283 208 | * @private 209 | */ 210 | function findExtension(url) { 211 | let extension = ""; 212 | if (url && url.length > 0) { 213 | const regex = /.+\/{2}.+\/{1}.+(\.\w+)\?*.*/, 214 | group = 1, 215 | urlquestion = url.substring(0, url.indexOf("?")), 216 | urlhash = !urlquestion ? url.substring(0, url.indexOf("#")) : undefined, 217 | match = regex.exec(urlquestion ? urlquestion : urlhash ? urlhash : url ? url : ""); 218 | if (match && match[group]) { 219 | extension = match[group].slice(1); // Remove the . (e.g. .jpeg becomes jpeg) 220 | if (!isValidExtension(extension)) { // If extension is not valid, throw it out 221 | extension = ""; 222 | } 223 | } 224 | } 225 | return extension; 226 | } 227 | 228 | /** 229 | * Determines if a potential file extension is valid. 230 | * Arbitrary rules: Extensions must be alphanumeric and under 8 characters 231 | * 232 | * @param extension the extension to check 233 | * @returns {boolean} true if the extension is valid, false if not 234 | * @private 235 | */ 236 | function isValidExtension(extension) { 237 | return extension && extension.trim() !== "" && /^[a-z0-9]+$/i.test(extension) && extension.length <= 8; 238 | } 239 | 240 | /** 241 | * Finds the URL from a CSS style. 242 | * Regex to find the URL from a CSS style is by Alex Z @ stackoverflow.com 243 | * Style properties that can have URLs is by Chad Scira et all @ stackoverflow.com 244 | * 245 | * @param style the CSS style 246 | * @returns {string} the URL extracted from the style, if it exists 247 | * @see https://stackoverflow.com/a/34166861 248 | * @see https://stackoverflow.com/q/24730939 249 | * @private 250 | */ 251 | function extractURLFromStyle(style) { 252 | let url = ""; 253 | if (style) { 254 | const URL_STYLE_PROPERTIES = ["background", "background-image", "list-style", "list-style-image", "content", "cursor", "play-during", "cue", "cue-after", "cue-before", "border-image", "border-image-source", "mask", "mask-image", "@import", "@font-face"], 255 | regex = /\s*url\s*\(\s*(?:'(\S*?)'|"(\S*?)"|((?:\\\s|\\\)|\\\"|\\\'|\S)*?))\s*\)/i; 256 | for (let property of URL_STYLE_PROPERTIES) { 257 | if (style[property]) { 258 | const match = regex.exec(style[property]); 259 | url = match ? match[2] ? match[2] : "" : ""; // TODO: Check other groups from this regex? 260 | if (url) { 261 | //console.log("URLI.Download.extractURLFromStyle() - style property=" + property + ", style[property]=" + style[property] + ", and url=" + url); 262 | break; 263 | } 264 | } 265 | } 266 | } 267 | return url; 268 | } 269 | 270 | // Return Public Functions 271 | return { 272 | previewDownloadURLs: previewDownloadURLs, 273 | findDownloadURLs: findDownloadURLs 274 | }; 275 | }(); -------------------------------------------------------------------------------- /src/js/increment-decrement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.IncrementDecrement = function () { 10 | 11 | /** 12 | * Finds a selection in the url to increment or decrement depending on the preference. 13 | * 14 | * "Prefixes" Preference: 15 | * Looks for terms and common prefixes that come before numbers, such as 16 | * page=, pid=, p=, next=, =, and /. Example URLs with prefixes (= and /): 17 | * http://www.google.com?page=1234 18 | * http://www.google.com/1234 19 | * 20 | * "Last Number" Preference: 21 | * Uses the last number in the url. 22 | * 23 | * "First Number": Preference: 24 | * Uses the first number in the url. 25 | * 26 | * If no numbers exist in the URL, returns an empty selection. 27 | * 28 | * @param url the url to find the selection in 29 | * @param preference the preferred strategy to use to find the selection 30 | * @param custom the JSON object with custom regular expression parameters 31 | * @return JSON object {selection, selectionStart} 32 | * @public 33 | */ 34 | function findSelection(url, preference, custom) { 35 | // Regular Expressions: 36 | // Lookbehind is only supported in Chrome 62+ so using convoluted alternatives, lookbehinds are enclosed in comments below 37 | const repag = /page=\d+/, // RegExp to find a number with "page=" TODO: replace with lookbehind regex /(?<=page)=(\d+)/ 38 | reter = /(?:(p|id|next)=\d+)/, // RegExp to find numbers with common terms like "id=" TODO: replace with lookbehind regex /(?<=pid|p|next|id)=(\d+)/ 39 | repre = /(?:[=\/]\d+)(?!.*[=\/]\d+)/, // RegExp to find the last number with a prefix (= or /) TODO: Don't capture the = or / so substring(1) is no longer needed 40 | relas = /\d+(?!.*\d+)/, // RegExg to find the last number in the url 41 | refir = /\d+/, // RegExg to find the first number in the url 42 | recus = preference === "custom" && custom ? new RegExp(custom.pattern, custom.flags) : undefined, // RegExp Custom (if set by user) TODO: Validate custom regex with current url for alphanumeric selection 43 | // Matches: 44 | mapag = repag.exec(url), 45 | mater = reter.exec(url), 46 | mapre = repre.exec(url), 47 | malas = relas.exec(url), 48 | mafir = refir.exec(url), 49 | macus = recus ? recus.exec(url) : undefined; 50 | //console.log("URLI.IncrementDecrement.findSelection() - matches: pag=" + mapag + ", ter=" + mater + ", pre=" + mapre + ", las=" + malas + ", fir=" + mafir + ", cus=" + macus); 51 | return preference === "prefixes" ? 52 | mapag ? {selection: mapag[0].substring(5), selectionStart: mapag.index + 5} : 53 | mater ? {selection: mater[0].substring(mater[1].length + 1), selectionStart: mater.index + mater[1].length + 1} : 54 | mapre ? {selection: mapre[0].substring(1), selectionStart: mapre.index + 1} : 55 | malas ? {selection: malas[0], selectionStart: malas.index} : 56 | {selection: "", selectionStart: -1} : 57 | preference === "lastnumber" ? 58 | malas ? {selection: malas[0], selectionStart: malas.index} : 59 | {selection: "", selectionStart: -1} : 60 | preference === "firstnumber" ? 61 | mafir ? {selection: mafir[0], selectionStart: mafir.index} : 62 | {selection: "", selectionStart: -1} : 63 | preference === "custom" ? 64 | macus && macus[custom.group] ? {selection: macus[custom.group].substring(custom.index), selectionStart: macus.index + custom.index} : 65 | mapag ? {selection: mapag[0].substring(5), selectionStart: mapag.index + 5} : 66 | mater ? {selection: mater[0].substring(mater[1].length), selectionStart: mater.index + mater[1].length} : 67 | mapre ? {selection: mapre[0].substring(1), selectionStart: mapre.index + 1} : 68 | malas ? {selection: malas[0], selectionStart: malas.index} : 69 | {selection: "", selectionStart: -1} : 70 | {selection: "", selectionStart: -1}; 71 | } 72 | 73 | /** 74 | * Modifies the URL by either incrementing or decrementing the specified 75 | * selection. 76 | * 77 | * @param action the action to perform (increment or decrement) 78 | * @param url the URL that will be modified 79 | * @param selection the selected part in the URL to modify 80 | * @param selectionStart the starting index of the selection in the URL 81 | * @param interval the amount to increment or decrement 82 | * @param base the base to use (the supported base range is 2-36) 83 | * @param baseCase the case to use for letters (lowercase or uppercase) 84 | * @param leadingZeros if true, pad with leading zeros, false don't pad 85 | * @return JSON object {urlmod: modified url, selectionmod: modified selection} 86 | * @public 87 | */ 88 | function modifyURL(action, url, selection, selectionStart, interval, base, baseCase, leadingZeros) { 89 | let urlmod, 90 | selectionmod, 91 | selectionint = parseInt(selection, base); // parseInt base range is 2-36 92 | // Increment or decrement the selection; if decrement is negative, set to 0 (low bound) 93 | selectionmod = action === "increment" ? (selectionint + interval).toString(base) : 94 | action === "decrement" ? (selectionint - interval >= 0 ? selectionint - interval : 0).toString(base) : 95 | ""; 96 | if (leadingZeros && selection.length > selectionmod.length) { // Leading 0s 97 | selectionmod = "0".repeat(selection.length - selectionmod.length) + selectionmod; 98 | } 99 | if (/[a-z]/i.test(selectionmod)) { // If Alphanumeric, convert case 100 | selectionmod = baseCase === "lowercase" ? selectionmod.toLowerCase() : baseCase === "uppercase" ? selectionmod.toUpperCase() : selectionmod; 101 | } 102 | // Append: part 1 of the URL + modified selection + part 2 of the URL 103 | urlmod = url.substring(0, selectionStart) + selectionmod + url.substring(selectionStart + selection.length); 104 | return {urlmod: urlmod, selectionmod: selectionmod}; 105 | } 106 | 107 | /** 108 | * Modifies the URL by either incrementing or decrementing the specified 109 | * selection and performs error skipping. 110 | * 111 | * @param action the action to perform (increment or decrement) 112 | * @param instance the instance containing the URL properties 113 | * @param errorSkipRemaining the number of times left to skip while performing this action 114 | * @param errorCodeEncountered whether or not an error code has been encountered yet while performing this action 115 | * @public 116 | */ 117 | function modifyURLAndSkipErrors(action, instance, errorSkipRemaining, errorCodeEncountered) { 118 | //console.log("URLI.IncrementDecrement.modifyURLAndSkipErrors() - instance.errorCodes=" + instance.errorCodes +", instance.errorCodesCustomEnabled=" + instance.errorCodesCustomEnabled + ", instance.errorCodesCustom=" + instance.errorCodesCustom + ", errorSkipRemaining=" + errorSkipRemaining); 119 | const origin = document.location.origin, 120 | urlOrigin = new URL(instance.url).origin, 121 | urlProps = modifyURL(action, instance.url, instance.selection, instance.selectionStart, instance.interval, instance.base, instance.baseCase, instance.leadingZeros); 122 | instance.url = urlProps.urlmod; 123 | instance.selection = urlProps.selectionmod; 124 | // We check that the current page's origin matches the instance's URL origin as we otherwise cannot use fetch due to CORS 125 | if (origin === urlOrigin && errorSkipRemaining > 0) { 126 | fetch(urlProps.urlmod, { method: "HEAD", credentials: "same-origin" }).then(function(response) { 127 | if (response && response.status && 128 | ((instance.errorCodes && ( 129 | (instance.errorCodes.includes("404") && response.status === 404) || 130 | (instance.errorCodes.includes("3XX") && ((response.status >= 300 && response.status <= 399) || response.redirected)) || // Note: 301,302,303,307,308 return response.status of 200 and must be checked by response.redirected 131 | (instance.errorCodes.includes("4XX") && response.status >= 400 && response.status <= 499) || 132 | (instance.errorCodes.includes("5XX") && response.status >= 500 && response.status <= 599))) || 133 | (instance.errorCodesCustomEnabled && instance.errorCodesCustom && 134 | (instance.errorCodesCustom.includes(response.status + "") || (response.redirected && ["301", "302", "303", "307", "308"].some(redcode => instance.errorCodesCustom.includes(redcode))))))) { // response.status + "" because custom array stores string inputs 135 | //console.log("URLI.IncrementDecrement.modifyURLAndSkipErrors() - skipping this URL because response.status was in errorCodes or response.redirected, response.status=" + response.status); 136 | // setBadgeSkipErrors, but only need to send message the first time an errorCode is encountered 137 | if (!errorCodeEncountered) { 138 | chrome.runtime.sendMessage({greeting: "setBadgeSkipErrors", "errorCode": response.redirected ? "RED" : response.status, "instance": instance}); 139 | } 140 | // Recursively call this method again to perform the action again and skip this URL, decrementing errorSkipRemaining and setting errorCodeEncountered to true 141 | modifyURLAndSkipErrors(action, instance, errorSkipRemaining - 1, true); 142 | } else { 143 | //console.log("URLI.IncrementDecrement.modifyURLAndSkipErrors() - not attempting to skip this URL because response.status=" + response.status + " and it was not in errorCodes. aborting and updating tab"); 144 | chrome.runtime.sendMessage({greeting: "incrementDecrementSkipErrors", "instance": instance}); 145 | } 146 | }).catch(e => { 147 | //console.log("URLI.IncrementDecrement.modifyURLAndSkipErrors() - a fetch() exception was caught:" + e); 148 | chrome.runtime.sendMessage({greeting: "setBadgeSkipErrors", "errorCode": "ERR", "instance": instance}); 149 | chrome.runtime.sendMessage({greeting: "incrementDecrementSkipErrors", "instance": instance}); 150 | }); 151 | } else { 152 | //console.log("URLI.IncrementDecrement.modifyURLAndSkipErrors() - " + (origin !== urlOrigin ? "the instance's URL origin does not match this page's URL origin" : "we have exhausted the errorSkip attempts") + ". aborting and updating tab "); 153 | chrome.runtime.sendMessage({greeting: "incrementDecrementSkipErrors", "instance": instance}); 154 | } 155 | } 156 | 157 | // Return Public Functions 158 | return { 159 | findSelection: findSelection, 160 | modifyURL: modifyURL, 161 | modifyURLAndSkipErrors: modifyURLAndSkipErrors 162 | }; 163 | }(); -------------------------------------------------------------------------------- /src/js/next-prev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.NextPrev = function () { 10 | 11 | // Keywords are ordered in priority 12 | // startsWithExcludes helps better prioritize some keywords (e.g. we prefer an "includes" "prev" over a "startsWith" "back") 13 | const keywords = { 14 | "next": ["next", "forward", "次", ">", ">", "new"], 15 | "prev": ["prev", "previous", "前", "<", "<", "‹", "back", "old"], 16 | "startsWithExcludes": [">", ">", "new", "<", "<", "‹", "back", "old"] 17 | }, 18 | // urls store important, attributes, and innerHTML links that were found 19 | urls = { 20 | "important": { "relAttribute": new Map() }, 21 | "attributes": { "equals": new Map(), "startsWith": new Map(), "includes": new Map() }, 22 | "innerHTML": { "equals": new Map(), "startsWith": new Map(), "includes": new Map() } 23 | }; 24 | 25 | /** 26 | * Finds the next or prev URL. 27 | * 28 | * @param direction the direction to go: next or prev 29 | * @param priority the link priority to use: attributes or innerHTML 30 | * @param sameDomain whether to enforce the same domain policy 31 | * @return {string} the next or prev url 32 | * @public 33 | */ 34 | function findNextPrevURL(direction, priority, sameDomain) { 35 | const priority2 = priority === "attributes" ? "innerHTML" : "attributes", 36 | algorithms = [ // note: the order matters, the highest priority algorithms are first when they are iterated below 37 | { "priority": "important", "subpriority": "relAttribute" }, 38 | { "priority": priority, "subpriority": "equals" }, 39 | { "priority": priority2, "subpriority": "equals" }, 40 | { "priority": priority, "subpriority": "startsWith" }, 41 | { "priority": priority2, "subpriority": "startsWith" }, 42 | { "priority": priority, "subpriority": "includes" }, 43 | { "priority": priority2, "subpriority": "includes" } 44 | ]; 45 | buildURLs(direction, sameDomain); 46 | for (let algorithm of algorithms) { 47 | const url = traverseResults(algorithm.priority, algorithm.subpriority, keywords[direction]); 48 | if (url) { return url; } 49 | } 50 | return ""; 51 | } 52 | 53 | /** 54 | * Traverses the urls results object to see if a URL was found. 55 | * e.g. urls[attributes][equals][nextKeyword] 56 | * 57 | * @param priority the link priority to use: attributes or innerHTML 58 | * @param subpriority the sub priority to use: equals, startsWith, includes 59 | * @param keywords the ordered list of keywords sorted in priority 60 | * @return {string} the url (if found) 61 | * @private 62 | */ 63 | function traverseResults(priority, subpriority, keywords) { 64 | let url = ""; 65 | for (let keyword of keywords) { 66 | if (urls[priority][subpriority].has(keyword)) { 67 | url = urls[priority][subpriority].get(keyword); 68 | //console.log("URLI.NextPrev.traverseResults() - a next/prev Link was found:" + priority + " - " + subpriority + " - " + keyword + " - " + url); 69 | break; 70 | } 71 | } 72 | return url; 73 | } 74 | 75 | /** 76 | * Builds the urls results object by parsing all link and anchor elements. 77 | * 78 | * @param direction the direction to go: next or prev 79 | * @param sameDomain whether to enforce the same domain policy 80 | * @private 81 | */ 82 | function buildURLs(direction, sameDomain) { 83 | // Note: The following DOM elements contain links: link, a, area, and base 84 | const links = document.getElementsByTagName("link"), 85 | anchors = document.links, // document.links includes all anchor and area elements 86 | hostname = document.location.hostname; 87 | parseElements(direction, links, hostname, sameDomain); 88 | parseElements(direction, anchors, hostname, sameDomain); 89 | } 90 | 91 | /** 92 | * Parses the elements by examining if their attributes or innerHTML contain 93 | * next or prev keywords in them. 94 | * 95 | * @param direction the direction to go: next or prev 96 | * @param elements the DOM elements to parse: links or anchors 97 | * @param hostname the document's hostname used to verify if URLs are in the same domain 98 | * @param sameDomain whether to enforce the same domain policy 99 | * @private 100 | */ 101 | function parseElements(direction, elements, hostname, sameDomain) { 102 | for (let element of elements) { 103 | if (!element.href) { 104 | continue; 105 | } 106 | try { // Check if URL is in same domain if enabled, wrap in try/catch in case of exceptions with URL object 107 | const url = new URL(element.href); 108 | if (sameDomain && url.hostname !== hostname) { 109 | continue; 110 | } 111 | } catch (e) { 112 | continue; 113 | } 114 | parseText(direction, "innerHTML", element.href, element.innerHTML.trim().toLowerCase(), ""); 115 | for (let attribute of element.attributes) { 116 | parseText(direction, "attributes", element.href, attribute.nodeValue.trim().toLowerCase(), attribute.nodeName.toLowerCase()); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Parses an element's text for keywords that might indicate a next or prev 123 | * link. Adds the link to the urls map if a match is found. 124 | * 125 | * @param direction the direction to go: next or prev 126 | * @param type the type of element text: attributes or innerHTML 127 | * @param href the URL to set this link to 128 | * @param text the element's text to parse keywords from 129 | * @param attribute attribute's node name if it's needed 130 | * @private 131 | */ 132 | function parseText(direction, type, href, text, attribute) { 133 | // Iterate over this direction's keywords and build out the urls object's maps 134 | for (let keyword of keywords[direction]) { 135 | if (type === "attributes" && attribute === "rel" && text === keyword) { // important e.g. rel="next" or rel="prev" 136 | urls.important.relAttribute.set(keyword, href); 137 | } else if (text === keyword) { 138 | urls[type].equals.set(keyword, href); 139 | } else if (text.startsWith(keyword) && keywords.startsWithExcludes.indexOf(keyword) < 0) { // startsWithExcludes 140 | urls[type].startsWith.set(keyword, href); 141 | } else if (text.includes(keyword)) { 142 | urls[type].includes.set(keyword, href); 143 | } 144 | } 145 | } 146 | 147 | // Return Public Functions 148 | return { 149 | findNextPrevURL: findNextPrevURL 150 | }; 151 | }(); -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Options = function () { 10 | 11 | const DOM = {}, // Map to cache DOM elements: key=id, value=element 12 | FLAG_KEY_NONE = 0x0, // 0000 13 | FLAG_KEY_ALT = 0x1, // 0001 14 | FLAG_KEY_CTRL = 0x2, // 0010 15 | FLAG_KEY_SHIFT = 0x4, // 0100 16 | FLAG_KEY_META = 0x8, // 1000 17 | KEY_MODIFIER_CODE_ARRAY = [ // An array of the KeyboardEvent.code modifiers (used in the case of an assigned shortcut only being a key modifier, e.g. just the Shift key for Increment) 18 | "Alt", "AltLeft", "AltRight", 19 | "Control", "ControlLeft", "ControlRight", 20 | "Shift", "ShiftLeft", "ShiftRight", 21 | "Meta", "MetaLeft", "MetaRight" 22 | ], 23 | NUMBERS = ["oN3", "tW0", "thR33", "f0uR", "f1V3", "s1X", "s3VeN", "e1GhT", "n1N3", "t3N"], 24 | FACES = ["≧☉_☉≦", "(⌐■_■)♪", "(ᵔᴥᵔ)", "◉_◉", "(+__X)"]; 25 | 26 | let key = [0,""], // Reusable key to stores the key's event modifiers [0] and code [1] 27 | timeout = undefined; // Reusable global timeout for input changes to fire after the user stops typing 28 | 29 | /** 30 | * Loads the DOM content needed to display the options page. 31 | * 32 | * DOMContentLoaded will fire when the DOM is loaded. Unlike the conventional 33 | * "load", it does not wait for images and media. 34 | * 35 | * @public 36 | */ 37 | function DOMContentLoaded() { 38 | const ids = document.querySelectorAll("[id]"), 39 | i18ns = document.querySelectorAll("[data-i18n]"); 40 | // Cache DOM elements 41 | for (let element of ids) { 42 | DOM["#" + element.id] = element; 43 | } 44 | // Set i18n (internationalization) text from messages.json 45 | for (let element of i18ns) { 46 | element[element.dataset.i18n] = chrome.i18n.getMessage(element.id.replace(/-/g, '_').replace(/\*.*/, '')); 47 | } 48 | // Add Event Listeners to the DOM elements 49 | DOM["#internal-shortcuts-enable-button"].addEventListener("click", function() { URLI.Permissions.requestPermissions("internalShortcuts", function(granted) { if (granted) { populateValuesFromStorage("internalShortcuts"); } }) }); 50 | DOM["#chrome-shortcuts-enable-button"].addEventListener("click", function() { URLI.Permissions.removePermissions("internalShortcuts", function(removed) { if (removed) { populateValuesFromStorage("internalShortcuts"); } }) }); 51 | DOM["#chrome-shortcuts-quick-enable-input"].addEventListener("change", function () { chrome.storage.sync.set({"quickEnabled": this.checked}); }); 52 | DOM["#chrome-shortcuts-button"].addEventListener("click", function() { chrome.tabs.update({url: "chrome://extensions/shortcuts"}); }); 53 | DOM["#key-quick-enable-input"].addEventListener("change", function () { chrome.storage.sync.set({"keyQuickEnabled": this.checked}); }); 54 | DOM["#mouse-quick-enable-input"].addEventListener("change", function () { chrome.storage.sync.set({"mouseQuickEnabled": this.checked}); }); 55 | DOM["#key-increment-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 56 | DOM["#key-decrement-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 57 | DOM["#key-next-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 58 | DOM["#key-prev-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 59 | DOM["#key-clear-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 60 | DOM["#key-auto-input"].addEventListener("keydown", function (event) { setKey(event); writeInput(this, key); }); 61 | DOM["#key-increment-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyIncrement": key}, function() { setKeyEnabled(); }); }); 62 | DOM["#key-decrement-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyDecrement": key}, function() { setKeyEnabled(); }); }); 63 | DOM["#key-next-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyNext": key}, function() { setKeyEnabled(); }); }); 64 | DOM["#key-prev-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyPrev": key}, function() { setKeyEnabled(); }); }); 65 | DOM["#key-clear-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyClear": key}, function() { setKeyEnabled(); }); }); 66 | DOM["#key-auto-input"].addEventListener("keyup", function () { chrome.storage.sync.set({"keyAuto": key}, function() { setKeyEnabled(); }); }); 67 | DOM["#key-increment-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyIncrement": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-increment-input"], []); }); 68 | DOM["#key-decrement-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyDecrement": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-decrement-input"], []); }); 69 | DOM["#key-next-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyNext": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-next-input"], []); }); 70 | DOM["#key-prev-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyPrev": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-prev-input"], []); }); 71 | DOM["#key-clear-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyClear": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-clear-input"], []); }); 72 | DOM["#key-auto-clear-input"].addEventListener("click", function () { chrome.storage.sync.set({"keyAuto": []}, function() { setKeyEnabled(); }); writeInput(DOM["#key-auto-input"], []); }); 73 | DOM["#mouse-increment-select"].addEventListener("change", function() { chrome.storage.sync.set({"mouseIncrement": +this.value}, function() { setMouseEnabled(); }); }); 74 | DOM["#mouse-decrement-select"].addEventListener("change", function() { chrome.storage.sync.set({"mouseDecrement": +this.value}, function() { setMouseEnabled(); }); }); 75 | DOM["#mouse-next-select"].addEventListener("change", function() { chrome.storage.sync.set({"mouseNext": +this.value}, function() { setMouseEnabled(); }); }); 76 | DOM["#mouse-prev-select"].addEventListener("change", function() { chrome.storage.sync.set({"mousePrev": +this.value}, function() { setMouseEnabled(); }); }); 77 | DOM["#mouse-clear-select"].addEventListener("change", function() { chrome.storage.sync.set({"mouseClear": +this.value}, function() { setMouseEnabled(); }); }); 78 | DOM["#mouse-auto-select"].addEventListener("change", function() { chrome.storage.sync.set({"mouseAuto": +this.value}, function() { setMouseEnabled(); }); }); 79 | DOM["#icon-color-radio-dark"].addEventListener("change", changeIconColor); 80 | DOM["#icon-color-radio-light"].addEventListener("change", changeIconColor); 81 | DOM["#icon-color-radio-rainbow"].addEventListener("change", changeIconColor); 82 | DOM["#icon-color-radio-urli"].addEventListener("change", changeIconColor); 83 | DOM["#icon-feedback-enable-input"].addEventListener("change", function () { chrome.storage.sync.set({"iconFeedbackEnabled": this.checked}); }); 84 | DOM["#popup-button-size-input"].addEventListener("change", function () { if (+this.value >= 16 && +this.value <= 64) { chrome.storage.sync.set({"popupButtonSize": +this.value}); 85 | DOM["#popup-button-size-img"].style = "width:" + (+this.value) + "px; height:" + (+this.value) + "px;"; } }); 86 | DOM["#popup-button-size-img"].addEventListener("click", function () { if (DOM["#popup-animations-enable-input"].checked) { URLI.UI.clickHoverCss(this, "hvr-push-click"); } }); 87 | DOM["#popup-animations-enable-input"].addEventListener("change", function () { chrome.storage.sync.set({"popupAnimationsEnabled": this.checked}); 88 | DOM["#popup-button-size-img"].className = this.checked ? "hvr-grow" : "" }); 89 | DOM["#popup-settings-can-overwrite-input"].addEventListener("change", function () { chrome.storage.sync.set({"popupSettingsCanOverwrite": this.checked}); }); 90 | DOM["#popup-open-setup-input"].addEventListener("change", function () { chrome.storage.sync.set({"popupOpenSetup": this.checked}); }); 91 | DOM["#selection-select"].addEventListener("change", function() { DOM["#selection-custom"].className = this.value === "custom" ? "display-block fade-in" : "display-none"; chrome.storage.sync.set({"selectionPriority": this.value}); }); 92 | DOM["#selection-custom-save-button"].addEventListener("click", function () { customSelection("save"); }); 93 | DOM["#selection-custom-test-button"].addEventListener("click", function() { customSelection("test"); }); 94 | DOM["#interval-input"].addEventListener("change", function () { chrome.storage.sync.set({"interval": +this.value > 0 ? +this.value : 1}); }); 95 | DOM["#leading-zeros-pad-by-detection-input"].addEventListener("change", function() { chrome.storage.sync.set({ "leadingZerosPadByDetection": this.checked}); }); 96 | DOM["#base-select"].addEventListener("change", function() { DOM["#base-case"].className = +this.value > 10 ? "display-block fade-in" : "display-none"; chrome.storage.sync.set({"base": +this.value}); }); 97 | DOM["#base-case-lowercase-input"].addEventListener("change", function() { chrome.storage.sync.set({"baseCase": this.value}); }); 98 | DOM["#base-case-uppercase-input"].addEventListener("change", function() { chrome.storage.sync.set({"baseCase": this.value}); }); 99 | DOM["#error-skip-input"].addEventListener("change", function() { if (+this.value >= 0 && +this.value <= 100) { chrome.storage.sync.set({"errorSkip": +this.value }); } }); 100 | DOM["#error-codes-404-input"].addEventListener("change", updateErrorCodes); 101 | DOM["#error-codes-3XX-input"].addEventListener("change", updateErrorCodes); 102 | DOM["#error-codes-4XX-input"].addEventListener("change", updateErrorCodes); 103 | DOM["#error-codes-5XX-input"].addEventListener("change", updateErrorCodes); 104 | DOM["#error-codes-custom-enabled-input"].addEventListener("change", function() { chrome.storage.sync.set({"errorCodesCustomEnabled": this.checked}); DOM["#error-codes-custom"].className = this.checked ? "display-block fade-in" : "display-none"; }); 105 | DOM["#error-codes-custom-input"].addEventListener("input", updateErrorCodesCustom); 106 | DOM["#enhanced-mode-enable-button"].addEventListener("click", function() { URLI.Permissions.requestPermissions("enhancedMode", function(granted) { if (granted) { populateValuesFromStorage("enhancedMode"); } }) }); 107 | DOM["#enhanced-mode-disable-button"].addEventListener("click", function() { URLI.Permissions.removePermissions("enhancedMode", function(removed) { if (removed) { populateValuesFromStorage("enhancedMode"); } }) }); 108 | DOM["#next-prev-links-priority-select"].addEventListener("change", function () { chrome.storage.sync.set({"nextPrevLinksPriority": this.value}); }); 109 | DOM["#next-prev-same-domain-policy-enable-input"].addEventListener("change", function() { chrome.storage.sync.set({"nextPrevSameDomainPolicy": this.checked}); }); 110 | DOM["#next-prev-popup-buttons-input"].addEventListener("change", function() { chrome.storage.sync.set({"nextPrevPopupButtons": this.checked}); }); 111 | DOM["#download-enable-button"].addEventListener("click", function() { URLI.Permissions.requestPermissions("download", function(granted) { if (granted) { populateValuesFromStorage("download"); } }) }); 112 | DOM["#download-disable-button"].addEventListener("click", function() { URLI.Permissions.removePermissions("download", function(removed) { if (removed) { populateValuesFromStorage("download"); } }) }); 113 | DOM["#urli-input"].addEventListener("click", clickURLI); 114 | DOM["#reset-options-button"].addEventListener("click", resetOptions); 115 | DOM["#manifest-name"].textContent = chrome.runtime.getManifest().name; 116 | DOM["#manifest-version"].textContent = chrome.runtime.getManifest().version; 117 | // Populate all values from storage 118 | populateValuesFromStorage("all"); 119 | } 120 | 121 | /** 122 | * Populates the options form values from the extension storage. 123 | * 124 | * @param values which values to populate, e.g. "all" for all or "xyz" for only xyz values (with fade-in effect) 125 | * @private 126 | */ 127 | function populateValuesFromStorage(values) { 128 | chrome.storage.sync.get(null, function(items) { 129 | if (values === "all" || values === "internalShortcuts") { 130 | DOM["#chrome-shortcuts"].className = !items.permissionsInternalShortcuts ? values === "internalShortcuts" ? "display-block fade-in" : "display-block" : "display-none"; 131 | DOM["#internal-shortcuts"].className = items.permissionsInternalShortcuts ? values === "internalShortcuts" ? "display-block fade-in" : "display-block" : "display-none"; 132 | } 133 | if (values === "all" || values === "enhancedMode") { 134 | DOM["#enhanced-mode-disable-button"].className = items.permissionsEnhancedMode ? values === "enhancedMode" ? "display-block fade-in" : "display-block" : "display-none"; 135 | DOM["#enhanced-mode-enable-button"].className = !items.permissionsEnhancedMode ? values === "enhancedMode" ? "display-block fade-in" : "display-block" : "display-none"; 136 | DOM["#enhanced-mode-enable"].className = items.permissionsEnhancedMode ? values === "enhancedMode" ? "display-block fade-in" : "display-block" : "display-none"; 137 | DOM["#enhanced-mode-disable"].className = !items.permissionsEnhancedMode ? values === "enhancedMode" ? "display-block fade-in" : "display-block" : "display-none"; 138 | } 139 | if (values === "all" || values === "download") { 140 | DOM["#download-disable-button"].className = items.permissionsDownload ? values === "download" ? "display-block fade-in" : "display-block" : "display-none"; 141 | DOM["#download-enable-button"].className = !items.permissionsDownload ? values === "download" ? "display-block fade-in" : "display-block" : "display-none"; 142 | DOM["#download-settings-enable"].className = items.permissionsDownload ? values === "download" ? "display-block fade-in" : "display-block" : "display-none"; 143 | DOM["#download-settings-disable"].className = !items.permissionsDownload ? values === "download" ? "display-block fade-in" : "display-block" : "display-none"; 144 | } 145 | if (values === "all") { 146 | DOM["#chrome-shortcuts-quick-enable-input"].checked = items.quickEnabled; 147 | DOM["#key-quick-enable-input"].checked = items.keyQuickEnabled; 148 | DOM["#mouse-quick-enable-input"].checked = items.mouseQuickEnabled; 149 | DOM["#key-enable-img"].className = items.keyEnabled ? "display-inline" : "display-none"; 150 | DOM["#mouse-enable-img"].className = items.mouseEnabled ? "display-inline" : "display-none"; 151 | writeInput(DOM["#key-increment-input"], items.keyIncrement); 152 | writeInput(DOM["#key-decrement-input"], items.keyDecrement); 153 | writeInput(DOM["#key-next-input"], items.keyNext); 154 | writeInput(DOM["#key-prev-input"], items.keyPrev); 155 | writeInput(DOM["#key-clear-input"], items.keyClear); 156 | writeInput(DOM["#key-auto-input"], items.keyAuto); 157 | DOM["#mouse-increment-select"].value = items.mouseIncrement; 158 | DOM["#mouse-decrement-select"].value = items.mouseDecrement; 159 | DOM["#mouse-next-select"].value = items.mouseNext; 160 | DOM["#mouse-prev-select"].value = items.mousePrev; 161 | DOM["#mouse-clear-select"].value = items.mouseClear; 162 | DOM["#mouse-auto-select"].value = items.mouseAuto; 163 | DOM["#icon-color-radio-" + items.iconColor].checked = true; 164 | DOM["#icon-feedback-enable-input"].checked = items.iconFeedbackEnabled; 165 | DOM["#popup-button-size-input"].value = items.popupButtonSize; 166 | DOM["#popup-button-size-img"].style = "width:" + items.popupButtonSize + "px; height:" + items.popupButtonSize + "px;"; 167 | DOM["#popup-button-size-img"].className = items.popupAnimationsEnabled ? "hvr-grow" : ""; 168 | DOM["#popup-animations-enable-input"].checked = items.popupAnimationsEnabled; 169 | DOM["#popup-open-setup-input"].checked = items.popupOpenSetup; 170 | DOM["#popup-settings-can-overwrite-input"].checked = items.popupSettingsCanOverwrite; 171 | DOM["#selection-select"].value = items.selectionPriority; 172 | DOM["#selection-custom"].className = items.selectionPriority === "custom" ? "display-block" : "display-none"; 173 | DOM["#selection-custom-url-textarea"].value = items.selectionCustom.url; 174 | DOM["#selection-custom-pattern-input"].value = items.selectionCustom.pattern; 175 | DOM["#selection-custom-flags-input"].value = items.selectionCustom.flags; 176 | DOM["#selection-custom-group-input"].value = items.selectionCustom.group; 177 | DOM["#selection-custom-index-input"].value = items.selectionCustom.index; 178 | DOM["#interval-input"].value = items.interval; 179 | DOM["#leading-zeros-pad-by-detection-input"].checked = items.leadingZerosPadByDetection; 180 | DOM["#base-select"].value = items.base; 181 | DOM["#base-case"].className = items.base > 10 ? "display-block" : "display-none"; 182 | DOM["#base-case-lowercase-input"].checked = items.baseCase === "lowercase"; 183 | DOM["#base-case-uppercase-input"].checked = items.baseCase === "uppercase"; 184 | DOM["#error-skip-input"].value = items.errorSkip; 185 | DOM["#error-codes-404-input"].checked = items.errorCodes.includes("404"); 186 | DOM["#error-codes-3XX-input"].checked = items.errorCodes.includes("3XX"); 187 | DOM["#error-codes-4XX-input"].checked = items.errorCodes.includes("4XX"); 188 | DOM["#error-codes-5XX-input"].checked = items.errorCodes.includes("5XX"); 189 | DOM["#error-codes-custom-enabled-input"].checked = items.errorCodesCustomEnabled; 190 | DOM["#error-codes-custom"].className = items.errorCodesCustomEnabled ? "display-block" : "display-none"; 191 | DOM["#error-codes-custom-input"].value = items.errorCodesCustom; 192 | DOM["#next-prev-links-priority-select"].value = items.nextPrevLinksPriority; 193 | DOM["#next-prev-same-domain-policy-enable-input"].checked = items.nextPrevSameDomainPolicy; 194 | DOM["#next-prev-popup-buttons-input"].checked = items.nextPrevPopupButtons; 195 | } 196 | }); 197 | } 198 | 199 | /** 200 | * Changes the extension icon color in the browser's toolbar (browserAction). 201 | * 202 | * @private 203 | */ 204 | function changeIconColor() { 205 | // Possible values may be: dark, light, rainbow, or urli 206 | chrome.browserAction.setIcon({ 207 | path : { 208 | "16": "/img/icons/" + this.value + "/16.png", 209 | "24": "/img/icons/" + this.value + "/24.png", 210 | "32": "/img/icons/" + this.value + "/32.png" 211 | } 212 | }); 213 | chrome.storage.sync.set({"iconColor": this.value}); 214 | } 215 | 216 | /** 217 | * Sets the enabled state of key shortcuts. 218 | * 219 | * @private 220 | */ 221 | function setKeyEnabled() { 222 | chrome.storage.sync.get(null, function(items) { 223 | const enabled = items.keyIncrement.length !== 0 || items.keyDecrement.length !== 0 || items.keyNext.length !== 0 || items.keyPrev.length !== 0 || items.keyClear.length !== 0 || items.keyAuto.length !== 0; 224 | chrome.storage.sync.set({"keyEnabled": enabled}, function() { 225 | DOM["#key-enable-img"].className = enabled ? "display-inline" : "display-none"; 226 | }); 227 | }); 228 | } 229 | 230 | /** 231 | * Sets the enabled state of mouse button shortcuts. 232 | * 233 | * @private 234 | */ 235 | function setMouseEnabled() { 236 | chrome.storage.sync.get(null, function(items) { 237 | const enabled = items.mouseIncrement !== -1 || items.mouseDecrement !== -1 || items.mouseNext !== -1 || items.mousePrev !== -1 || items.mouseClear !== -1 || items.mouseAuto !== -1; 238 | chrome.storage.sync.set({"mouseEnabled": enabled}, function() { 239 | DOM["#mouse-enable-img"].className = enabled ? "display-inline" : "display-none"; 240 | }); 241 | }); 242 | } 243 | 244 | /** 245 | * Sets the key that was pressed on a keydown event. This is needed afterwards 246 | * to write the key to the input value and save the key to storage on keyup. 247 | * 248 | * @param event the key event fired 249 | * @private 250 | */ 251 | function setKey(event) { 252 | // Set key [0] as the event modifiers OR'd together and [1] as the event key code 253 | key = [ 254 | (event.altKey ? FLAG_KEY_ALT : FLAG_KEY_NONE) | // 0001 255 | (event.ctrlKey ? FLAG_KEY_CTRL : FLAG_KEY_NONE) | // 0010 256 | (event.shiftKey ? FLAG_KEY_SHIFT : FLAG_KEY_NONE) | // 0100 257 | (event.metaKey ? FLAG_KEY_META : FLAG_KEY_NONE), // 1000 258 | event.code 259 | ]; 260 | } 261 | 262 | /** 263 | * Writes the key(s) that were pressed to the text input. 264 | * 265 | * @param input the input to write to 266 | * @param key the key object to write 267 | * @private 268 | */ 269 | function writeInput(input, key) { 270 | // Write the input value based on the key event modifier bits and key code 271 | // Note1: KeyboardEvent.code will output the text-representation of the key code, e.g. the key "A" would output "KeyA" 272 | // Note2: If the key code is in the KEY_MODIFIER_CODE_ARRAY (e.g. Alt, Ctrl), it is not written a second time 273 | let text = ""; 274 | if (!key || key.length === 0) { text = chrome.i18n.getMessage("key_notset_option"); } 275 | else { 276 | if ((key[0] & FLAG_KEY_ALT)) { text += "Alt + "; } 277 | if ((key[0] & FLAG_KEY_CTRL) >> 1) { text += "Ctrl + "; } 278 | if ((key[0] & FLAG_KEY_SHIFT) >> 2) { text += "Shift + "; } 279 | if ((key[0] & FLAG_KEY_META) >> 3) { text += "Meta + "; } 280 | if (key[1] && !KEY_MODIFIER_CODE_ARRAY.includes(key[1])) { text += key[1]; } 281 | } 282 | input.value = text; 283 | } 284 | 285 | /** 286 | * Updates the error codes for error skip by examining if each checkbox is checked (on change event). 287 | * 288 | * @private 289 | */ 290 | function updateErrorCodes() { 291 | chrome.storage.sync.set({"errorCodes": 292 | [DOM["#error-codes-404-input"].checked ? DOM["#error-codes-404-input"].value : "", 293 | DOM["#error-codes-3XX-input"].checked ? DOM["#error-codes-3XX-input"].value : "", 294 | DOM["#error-codes-4XX-input"].checked ? DOM["#error-codes-4XX-input"].value : "", 295 | DOM["#error-codes-5XX-input"].checked ? DOM["#error-codes-5XX-input"].value : ""] 296 | }); 297 | } 298 | 299 | /** 300 | * This function is called as the user is typing in the error code custom text input. 301 | * We don't want to call chrome.storage after each key press, as it's an expensive procedure, so we set a timeout delay. 302 | * 303 | * @private 304 | */ 305 | function updateErrorCodesCustom() { 306 | //console.log("URLI.Options.updateErrorCodesCustom() - about to clearTimeout and setTimeout"); 307 | clearTimeout(timeout); 308 | timeout = setTimeout(function() { chrome.storage.sync.set({ 309 | "errorCodesCustom": DOM["#error-codes-custom-input"].value ? DOM["#error-codes-custom-input"].value.replace(/\s+/g, "").split(",").filter(Boolean) : [] 310 | })}, 1000); 311 | } 312 | 313 | /** 314 | * Validates the custom selection regular expression fields and then performs the desired action. 315 | * 316 | * @param action the action to perform (test or save) 317 | * @private 318 | */ 319 | function customSelection(action) { 320 | const url = DOM["#selection-custom-url-textarea"].value, 321 | pattern = DOM["#selection-custom-pattern-input"].value, 322 | flags = DOM["#selection-custom-flags-input"].value, 323 | group = +DOM["#selection-custom-group-input"].value, 324 | index = +DOM["#selection-custom-index-input"].value; 325 | let regexp, 326 | matches, 327 | selection, 328 | selectionStart; 329 | try { 330 | regexp = new RegExp(pattern, flags); 331 | matches = regexp.exec(url); 332 | if (!pattern || !matches) { 333 | throw chrome.i18n.getMessage("selection_custom_match_error"); 334 | } 335 | if (group < 0) { 336 | throw chrome.i18n.getMessage("selection_custom_group_error"); 337 | } 338 | if (index < 0) { 339 | throw chrome.i18n.getMessage("selection_custom_index_error"); 340 | } 341 | if (!matches[group]) { 342 | throw chrome.i18n.getMessage("selection_custom_matchgroup_error"); 343 | } 344 | selection = matches[group].substring(index); 345 | if (!selection || selection === "") { 346 | throw chrome.i18n.getMessage("selection_custom_matchindex_error"); 347 | } 348 | selectionStart = matches.index + index; 349 | if (selectionStart > url.length || selectionStart + selection.length > url.length) { 350 | throw chrome.i18n.getMessage("selection_custom_matchindex_error"); 351 | } 352 | if (!/^[a-z0-9]+$/i.test(url.substring(selectionStart, selectionStart + selection.length))) { 353 | throw url.substring(selectionStart, selectionStart + selection.length) + " " + chrome.i18n.getMessage("selection_custom_matchnotalphanumeric_error"); 354 | } 355 | } catch (e) { 356 | DOM["#selection-custom-message-span"].textContent = e; 357 | return; 358 | } 359 | if (action === "test") { 360 | DOM["#selection-custom-message-span"].textContent = chrome.i18n.getMessage("selection_custom_test_success"); 361 | DOM["#selection-custom-url-textarea"].setSelectionRange(selectionStart, selectionStart + selection.length); 362 | DOM["#selection-custom-url-textarea"].focus(); 363 | } else if (action === "save") { 364 | DOM["#selection-custom-message-span"].textContent = chrome.i18n.getMessage("selection_custom_save_success"); 365 | chrome.storage.sync.set({"selectionCustom": { "url": url, "pattern": pattern, "flags": flags, "group": group, "index": index }}); 366 | } 367 | } 368 | 369 | /** 370 | * Resets the options by clearing the storage and setting it with the default storage values, removing any extra 371 | * permissions, and finally re-populating the options input values from storage again. 372 | * 373 | * @private 374 | */ 375 | function resetOptions() { 376 | chrome.runtime.getBackgroundPage(function(backgroundPage) { 377 | chrome.storage.sync.clear(function() { 378 | chrome.storage.sync.set(backgroundPage.URLI.Background.getSDV(), function() { 379 | //console.log("URLI.Options.resetOptions() - removing all permissions..."); 380 | URLI.Permissions.removeAllPermissions(); 381 | changeIconColor.call(DOM["#icon-color-radio-dark"]); 382 | populateValuesFromStorage("all"); 383 | URLI.UI.generateAlert([chrome.i18n.getMessage("reset_options_message")]); 384 | }); 385 | }); 386 | }); 387 | } 388 | 389 | /** 390 | * Function that is called when our favorite URL Incrementer is clicked! 391 | * 392 | * @private 393 | */ 394 | function clickURLI() { 395 | const face = " " + FACES[Math.floor(Math.random() * FACES.length)]; 396 | this.value = +this.value + 1; 397 | URLI.UI.clickHoverCss(this, "hvr-buzz-out-click"); 398 | URLI.UI.generateAlert([+this.value <= 10 ? NUMBERS[+this.value - 1] + " ..." : chrome.i18n.getMessage("urli_click_malfunctioning") + face]); 399 | } 400 | 401 | // Return Public Functions 402 | return { 403 | DOMContentLoaded: DOMContentLoaded 404 | }; 405 | }(); 406 | 407 | document.addEventListener("DOMContentLoaded", URLI.Options.DOMContentLoaded); -------------------------------------------------------------------------------- /src/js/permissions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Permissions = function () { 10 | 11 | // This object contains all of the extension's optional permissions. Each permission contains: 12 | // 1) What storage keys to set, 2) The permission request, 3) The permission conflict to use instead if a conflict exists with another permission (optional), and 4) The script (optional) 13 | const PERMISSIONS = { 14 | "internalShortcuts": { 15 | "storageKey": "permissionsInternalShortcuts", 16 | "request": {permissions: ["declarativeContent"], origins: [""]}, 17 | "requestConflict": {permissions: ["declarativeContent"]}, 18 | "script": {js: ["js/shortcuts.js"]} 19 | }, 20 | "download": { 21 | "storageKey": "permissionsDownload", 22 | "request": {permissions: ["downloads"], origins: [""]}, 23 | "requestConflict": {permissions: ["downloads"]} 24 | }, 25 | "enhancedMode": { 26 | "storageKey": "permissionsEnhancedMode", 27 | "request": {origins: [""]} 28 | } 29 | }; 30 | 31 | /** 32 | * Requests a single permission. 33 | * If granted and a script needs to be added, adds a declarative content rule. 34 | * Then updates the permission key value in storage. 35 | * 36 | * @param permission the permission to request (a string in PERMISSIONS) 37 | * @param callback the callback function to return execution to 38 | * @public 39 | */ 40 | function requestPermissions(permission, callback) { 41 | chrome.permissions.request(PERMISSIONS[permission].request, function(granted) { 42 | if (granted) { 43 | //console.log("URLI.Permissions.requestPermissions() - successfully granted permission request:" + PERMISSIONS[permission].request.permissions + ", origins:" + PERMISSIONS[permission].request.origins); 44 | if (PERMISSIONS[permission].script) { 45 | chrome.declarativeContent.onPageChanged.addRules([{ 46 | conditions: [new chrome.declarativeContent.PageStateMatcher()], 47 | actions: [new chrome.declarativeContent.RequestContentScript(PERMISSIONS[permission].script)] 48 | }], function(rules) { 49 | //console.log("URLI.Permissions.requestPermissions() - successfully added declarativeContent rules:" + rules); 50 | }); 51 | } 52 | chrome.storage.sync.set({[PERMISSIONS[permission].storageKey]: true}, function() { 53 | if (callback) { 54 | callback(true); 55 | } 56 | }); 57 | // Request the permission a second time... 58 | // This is due to a bug that happens when origins had been previously granted and then removed and a 59 | // NEW permission (e.g. Download) is asked to be granted. The bug is that it forgets to also grant origins with the new permission 60 | chrome.permissions.request(PERMISSIONS[permission].request); 61 | } else { 62 | if (callback) { 63 | callback(false); 64 | } 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * Removes a single permission. 71 | * If necessary, removes the script and declarative content rule. Then checks to see if a conflict exists 72 | * with another permission that might share this permission. If a conflict exists, the permission is not removed. 73 | * Then updates the permission key value in storage. 74 | * 75 | * @param permission the permission to remove (a string in PERMISSIONS) 76 | * @param callback the callback function to return execution to 77 | * @public 78 | */ 79 | function removePermissions(permission, callback) { 80 | // Script: 81 | if (chrome.declarativeContent && PERMISSIONS[permission].script) { 82 | chrome.declarativeContent.onPageChanged.getRules(undefined, function(rules) { 83 | for (let rule of rules) { 84 | if (rule.actions[0].js[0] === PERMISSIONS[permission].script.js[0]) { 85 | //console.log("URLI.Permissions.removePermissions() - removing rule " + rule); 86 | chrome.declarativeContent.onPageChanged.removeRules([rule.id], function() {}); 87 | } 88 | } 89 | }); 90 | } 91 | // Remove: 92 | chrome.storage.sync.get(null, function(items) { 93 | // Check for conflicts if another permission is enabled; if conflict, then only remove the request's conflict (not the original request) 94 | if ((permission === "internalShortcuts" && !items.permissionsDownload && !items.permissionsEnhancedMode) || 95 | (permission === "download" && !items.permissionsInternalShortcuts && !items.permissionsEnhancedMode) || 96 | (permission === "enhancedMode" && !items.permissionsInternalShortcuts && !items.permissionsDownload)) { 97 | chrome.permissions.remove(PERMISSIONS[permission].request, function(removed) { 98 | if (removed) { 99 | //console.log("URLI.Permissions.removePermissions() - successfully removed permission request:" + PERMISSIONS[permission].request.permissions + ", origins:" + PERMISSIONS[permission].request.origins); 100 | } 101 | }); 102 | } else if (PERMISSIONS[permission].requestConflict) { 103 | chrome.permissions.remove(PERMISSIONS[permission].requestConflict, function(removed) { 104 | if (removed) { 105 | //console.log("URLI.Permissions.removePermissions() - conflict encountered, successfully removed permission request conflict:" + PERMISSIONS[permission].requestConflict.permissions + ", origins:" + PERMISSIONS[permission].requestConflict.origins); 106 | } 107 | }); 108 | } 109 | }); 110 | chrome.storage.sync.set({[PERMISSIONS[permission].storageKey]: false}, function() { 111 | if (callback) { 112 | callback(true); 113 | } 114 | }); 115 | } 116 | 117 | /** 118 | * Removes all the extension's optional permissions. 119 | * 120 | * @param callback the callback function to return execution to 121 | * @public 122 | */ 123 | function removeAllPermissions(callback) { 124 | if (chrome.declarativeContent) { 125 | chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {}); 126 | } 127 | chrome.permissions.remove({ permissions: ["declarativeContent", "downloads"], origins: [""]}, function(removed) { 128 | if (removed) { 129 | //console.log("URLI.Permissions.removeAllPermissions() - all permissions successfully removed!"); 130 | if (callback) { 131 | callback(true); 132 | } 133 | } 134 | }); 135 | } 136 | 137 | // Return Public Functions 138 | return { 139 | requestPermissions: requestPermissions, 140 | removePermissions: removePermissions, 141 | removeAllPermissions: removeAllPermissions 142 | }; 143 | }(); -------------------------------------------------------------------------------- /src/js/shortcuts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.Shortcuts = function () { 10 | 11 | const FLAG_KEY_ALT = 0x1, // 0001 12 | FLAG_KEY_CTRL = 0x2, // 0010 13 | FLAG_KEY_SHIFT = 0x4, // 0100 14 | FLAG_KEY_META = 0x8; // 1000 15 | 16 | let items_ = {}; // storage items cache 17 | 18 | /** 19 | * Sets the items storage cache. 20 | * 21 | * @param items the storage items 22 | * @public 23 | */ 24 | function setItems(items) { 25 | items_ = items; 26 | } 27 | 28 | /** 29 | * A key up event listener for keyboard shortcuts. 30 | * 31 | * Listens for increment, decrement, next, prev, clear, and auto keyboard shortcuts. 32 | * 33 | * @param event the key event 34 | * @public 35 | */ 36 | function keyListener(event) { 37 | if (keyPressed(event, items_.keyIncrement)) { chrome.runtime.sendMessage({greeting: "performAction", action: "increment"}); } 38 | else if (keyPressed(event, items_.keyDecrement)) { chrome.runtime.sendMessage({greeting: "performAction", action: "decrement"}); } 39 | else if (keyPressed(event, items_.keyNext)) { chrome.runtime.sendMessage({greeting: "performAction", action: "next"}); } 40 | else if (keyPressed(event, items_.keyPrev)) { chrome.runtime.sendMessage({greeting: "performAction", action: "prev"}); } 41 | else if (keyPressed(event, items_.keyClear)) { chrome.runtime.sendMessage({greeting: "performAction", action: "clear"}); } 42 | else if (keyPressed(event, items_.keyAuto)) { chrome.runtime.sendMessage({greeting: "performAction", action: "auto"}); } 43 | } 44 | 45 | /** 46 | * A mouse up event listener for mouse button shortcuts. 47 | * 48 | * Listens for increment, decrement, next, prev, clear, and auto mouse button shortcuts. 49 | * 50 | * @param event the mouse button event 51 | * @public 52 | */ 53 | function mouseListener(event) { 54 | if (mousePressed(event, items_.mouseIncrement)) { chrome.runtime.sendMessage({greeting: "performAction", action: "increment"}); } 55 | else if (mousePressed(event, items_.mouseDecrement)) { chrome.runtime.sendMessage({greeting: "performAction", action: "decrement"}); } 56 | else if (mousePressed(event, items_.mouseNext)) { chrome.runtime.sendMessage({greeting: "performAction", action: "next"}); } 57 | else if (mousePressed(event, items_.mousePrev)) { chrome.runtime.sendMessage({greeting: "performAction", action: "prev"}); } 58 | else if (mousePressed(event, items_.mouseClear)) { chrome.runtime.sendMessage({greeting: "performAction", action: "clear"}); } 59 | else if (mousePressed(event, items_.mouseAuto)) { chrome.runtime.sendMessage({greeting: "performAction", action: "auto"}); } 60 | } 61 | 62 | /** 63 | * Checks if the key was pressed by comparing the event against the flags 64 | * using bitwise operators and checking if the key code matches. 65 | * 66 | * @param event the key event 67 | * @param key the action key to check (e.g. increment shortcut key) 68 | * @return boolean true if the key event matches the action key, false otherwise 69 | * @private 70 | */ 71 | function keyPressed(event, key) { 72 | //console.log("URLI.Shortcuts.keyPressed() - event.code=" + event.code + ", actionKey=" + key); 73 | return key && key.length !== 0 && event.code === key[1] && 74 | (!(event.altKey ^ (key[0] & FLAG_KEY_ALT) ) && 75 | !(event.ctrlKey ^ (key[0] & FLAG_KEY_CTRL) >> 1) && 76 | !(event.shiftKey ^ (key[0] & FLAG_KEY_SHIFT) >> 2) && 77 | !(event.metaKey ^ (key[0] & FLAG_KEY_META) >> 3)); 78 | } 79 | 80 | /** 81 | * Checks if the mouse button was pressed. 82 | * 83 | * @param event the mouse event 84 | * @param mouse the action mouse button to check (e.g. increment shortcut mouse button) 85 | * @return boolean true if the mouse button event matches the action mouse button, false otherwise 86 | * @private 87 | */ 88 | function mousePressed(event, mouse) { 89 | //console.log("URLI.Shortcuts.mousePressed() - event.button=" + event.button + ", actionMouse=" + mouse); 90 | return event.button === mouse; 91 | } 92 | 93 | // Return Public Functions 94 | return { 95 | setItems: setItems, 96 | keyListener: keyListener, 97 | mouseListener: mouseListener 98 | }; 99 | }(); 100 | 101 | // Content Script Start: Cache items from storage and check if quick shortcuts or instance are enabled 102 | chrome.storage.sync.get(null, function(items) { 103 | chrome.runtime.sendMessage({greeting: "getInstance"}, function(response) { 104 | //console.log("URLI.Shortcuts.chrome.runtime.sendMessage() - response.instance=" + response.instance); 105 | URLI.Shortcuts.setItems(items); 106 | // Key 107 | if (items.keyEnabled && (items.keyQuickEnabled || (response.instance && (response.instance.enabled || response.instance.autoEnabled)))) { 108 | document.addEventListener("keyup", URLI.Shortcuts.keyListener); 109 | } 110 | // Mouse 111 | if (items.mouseEnabled && (items.mouseQuickEnabled || (response.instance && (response.instance.enabled || response.instance.autoEnabled)))) { 112 | document.addEventListener("mouseup", URLI.Shortcuts.mouseListener); 113 | } 114 | }); 115 | }); 116 | 117 | // Listen for requests from chrome.tabs.sendMessage (Extension Environment: Background / Popup) 118 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 119 | //console.log("URLI.Shortcuts.chrome.runtime.onMessage() - request.greeting=" + request.greeting); 120 | switch (request.greeting) { 121 | case "addKeyListener": 122 | document.addEventListener("keyup", URLI.Shortcuts.keyListener); 123 | break; 124 | case "removeKeyListener": 125 | document.removeEventListener("keyup", URLI.Shortcuts.keyListener); 126 | break; 127 | case "addMouseListener": 128 | document.addEventListener("mouseup", URLI.Shortcuts.mouseListener); 129 | break; 130 | case "removeMouseListener": 131 | document.removeEventListener("mouseup", URLI.Shortcuts.mouseListener); 132 | break; 133 | default: 134 | break; 135 | } 136 | sendResponse({}); 137 | }); -------------------------------------------------------------------------------- /src/js/ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Incrementer 3 | * @copyright © 2020 Roy Six 4 | * @license https://github.com/sixcious/url-incrementer/blob/main/LICENSE 5 | */ 6 | 7 | var URLI = URLI || {}; 8 | 9 | URLI.UI = function () { 10 | 11 | /** 12 | * Generates an alert to display messages. 13 | * 14 | * This function is derived from the sample Google extension, Proxy Settings, 15 | * by Mike West. 16 | * 17 | * @param messages the messages array to display, line by line 18 | * @param callback (optional) the callback function to return execution to 19 | * @public 20 | */ 21 | function generateAlert(messages, callback) { 22 | let div = document.createElement("div"), 23 | ul = document.createElement("ul"), 24 | li; 25 | div.classList.add("overlay"); 26 | for (let message of messages) { 27 | li = document.createElement("li"); 28 | li.appendChild(document.createTextNode(message)); 29 | ul.appendChild(li); 30 | } 31 | div.appendChild(ul); 32 | document.body.appendChild(div); 33 | setTimeout(function () { div.classList.add("overlay-visible"); }, 10); 34 | setTimeout(function () { div.classList.remove("overlay-visible"); document.body.removeChild(div); if (callback) { callback(); } }, 4000); 35 | } 36 | 37 | /** 38 | * Applies a Hover.css effect to DOM elements on click events. 39 | * 40 | * @param el the DOM element to apply the effect to 41 | * @param effect the Hover.css effect (class name) to use 42 | * @public 43 | */ 44 | function clickHoverCss(el, effect) { 45 | // Carefully toggle the Hover.css class using setTimeout() to force a delay 46 | el.classList.remove(effect); 47 | setTimeout(function () { el.classList.add(effect); }, 50); 48 | } 49 | 50 | // Return Public Functions 51 | return { 52 | generateAlert: generateAlert, 53 | clickHoverCss: clickHoverCss 54 | }; 55 | }(); -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_name__", 4 | "version": "5.8", 5 | "default_locale": "en", 6 | "description": "__MSG_description__", 7 | "icons": { "16": "img/icons/dark/16.png", "48": "img/icons/dark/48.png", "128": "img/icons/dark/128.png" }, 8 | "browser_action": { "default_title": "__MSG_title__", "default_icon": { "16": "img/icons/dark/16.png", "24": "img/icons/dark/24.png", "32": "img/icons/dark/32.png" }, "default_popup": "html/popup.html" }, 9 | "background": { "scripts": ["js/background.js", "js/action.js", "js/increment-decrement.js", "js/auto.js"], "persistent": true }, 10 | "commands": { "increment": { "suggested_key": { "default": "Ctrl+Shift+Up"}, "description": "Increment [+]" }, 11 | "decrement": { "suggested_key": { "default": "Ctrl+Shift+Down"}, "description": "Decrement [-]" }, 12 | "next": { "description": "Next [>]" }, 13 | "prev": { "description": "Prev [<]" }, 14 | "clear": { "suggested_key": { "default": "Ctrl+Shift+X"}, "description": "Clear [x]" }, 15 | "auto": { "suggested_key": { "default": "Ctrl+Shift+A"}, "description": "Auto Pause / Resume" } }, 16 | "minimum_chrome_version": "55", 17 | "optional_permissions": ["declarativeContent", "downloads", ""], 18 | "options_ui": { "page": "html/options.html", "chrome_style": true }, 19 | "permissions": ["activeTab", "storage"], 20 | "short_name": "__MSG_short_name__" 21 | } --------------------------------------------------------------------------------