├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ └── publish.yml ├── .gitignore ├── .idea ├── $PRODUCT_WORKSPACE_FILE$ ├── InstagramDownloader.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── copyright │ ├── LGPL_3_0_License.xml │ ├── No_licence.xml │ └── profiles_settings.xml ├── dictionaries │ └── niclas.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── saveactions_settings.xml ├── vcs.xml └── watcherTasks.xml ├── CHANGELOG.md ├── README.md ├── assets ├── icon.fig ├── icon_1029x1029.png ├── icon_128x128.png ├── icon_512x512.png ├── large_tile.png └── small_tile.png ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _includes │ ├── changelog.md │ ├── cookies.html │ ├── credits.md │ ├── donation.html │ ├── footer.html │ ├── head.html │ ├── header.html │ └── readme.md ├── _layouts │ ├── ad_layout.html │ ├── base.html │ └── default.html ├── assets │ ├── fixes.scss │ ├── main.scss │ └── paypal.svg ├── changelog │ └── index.md ├── favicon.png ├── index.md └── privacy-policy │ └── index.md ├── package-lock.json ├── package.json ├── src ├── build.js ├── global.js ├── icons │ ├── close_black_24dp.svg │ ├── download_black.png │ ├── download_white.png │ ├── favicon.png │ ├── instagram.png │ ├── instagram.svg │ └── paypal.svg ├── manifest_chrome.json ├── manifest_firefox.json ├── options │ └── options.html ├── scss │ ├── alert.scss │ ├── main.scss │ └── modal.scss ├── ts │ ├── ForegroundMessageHandler.ts │ ├── QuerySelectors.ts │ ├── background │ │ ├── BackgroundMessageHandler.ts │ │ └── download.ts │ ├── components │ │ ├── Alert.ts │ │ └── Modal.ts │ ├── decorators.ts │ ├── downloaders │ │ ├── AccountImageDownloader.ts │ │ ├── Downloader.ts │ │ ├── HotkeyDownloader.ts │ │ ├── PostDownloader.ts │ │ ├── StoryDownloader.ts │ │ └── download-functions.ts │ ├── functions.ts │ ├── globals.d.ts │ ├── helper-classes │ │ ├── DomObserver.ts │ │ ├── EventHandler.ts │ │ └── URLChangeEmitter.ts │ ├── index.ts │ └── modles │ │ ├── extension.ts │ │ ├── post.ts │ │ ├── story.ts │ │ └── typeguards.ts └── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.com/paypalme/NiclasHaderer/3 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Create a report ' 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Important: Issues without the following information will be closed 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behaviour: 14 | 1. Go to URL 'https://www.instagram.com/world_record_egg/' 15 | 2. Click on '....' 16 | 3. See error 17 | 18 | **Expected behaviour** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Device (please complete the following information):** 25 | - Resolution: [e.g 2K] 26 | - Browser [e.g. chrome, firefox] 27 | - Extension Version [e.g. 4.0.1] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | browser: 7 | description: 'Publish , or .' 8 | required: true 9 | default: 'both' 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - run: npm install 19 | - run: npm run build:dev 20 | - name: Publish Firefox 21 | if: github.event.inputs.browser == 'firefox' || github.event.inputs.browser == 'both' 22 | uses: trmcnvn/firefox-addon@v1 23 | 24 | with: 25 | uuid: 'HuiiBuh.InstagramDownloader@github.com' 26 | xpi: zip/firefox.zip 27 | manifest: src/manifest_firefox.json 28 | api-key: ${{ secrets.FIREFOX_API_KEY }} 29 | api-secret: ${{ secrets.FIREFOX_API_SECRET }} 30 | 31 | - name: Publish Chrome 32 | if: github.event.inputs.browser == 'chrome' || github.event.inputs.browser == 'both' 33 | 34 | uses: trmcnvn/chrome-addon@v2 35 | with: 36 | extension: cpgaheeihidjmolbakklolchdplenjai 37 | zip: zip/chrome.zip 38 | client-id: ${{ secrets.CHROME_CLIENT_ID }} 39 | client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} 40 | refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .firefox/*.zip 2 | firefox/*.zip 3 | *.map 4 | _dist/ 5 | node_modules/ 6 | .idea/workspace.xml 7 | /dist/ 8 | /zip/ 9 | ./InstagramDownloader.zip 10 | InstagramDownloader.zip 11 | .idea/*.iws 12 | *.iws 13 | tmp.json 14 | -------------------------------------------------------------------------------- /.idea/$PRODUCT_WORKSPACE_FILE$: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Everything 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/InstagramDownloader.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | 17 | 18 | 20 | 21 | 24 | 25 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | BY_NAME 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 | BY_NAME 52 | 53 |
54 |
55 |
56 |
57 | 58 | 73 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 | BY_NAME 82 | 83 |
84 |
85 | 86 | 87 | 88 | 89 | BY_NAME 90 | 91 |
92 |
93 |
94 |
95 |
96 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/LGPL_3_0_License.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/No_licence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/dictionaries/niclas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hotkey 5 | huii 6 | igtv 7 | instagram 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/saveactions_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | docs/_includes/changelog.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docs/_includes/readme.md -------------------------------------------------------------------------------- /assets/icon.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/icon.fig -------------------------------------------------------------------------------- /assets/icon_1029x1029.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/icon_1029x1029.png -------------------------------------------------------------------------------- /assets/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/icon_128x128.png -------------------------------------------------------------------------------- /assets/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/icon_512x512.png -------------------------------------------------------------------------------- /assets/large_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/large_tile.png -------------------------------------------------------------------------------- /assets/small_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/assets/small_tile.png -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ad_layout 3 | --- 4 | 5 | 12 | 13 | 26 | 27 |
28 |

404

29 | 30 |

Page not found :(

31 |

The requested page could not be found.

32 |
33 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | # gem "jekyll", "~> 4.2.0" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | gem "github-pages", "~> 214",group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (6.0.3.6) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 0.7, < 2) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | zeitwerk (~> 2.2, >= 2.2.2) 10 | addressable (2.8.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.11.1) 16 | colorator (1.1.0) 17 | commonmarker (0.17.13) 18 | ruby-enum (~> 0.5) 19 | concurrent-ruby (1.1.8) 20 | dnsruby (1.61.5) 21 | simpleidn (~> 0.1) 22 | em-websocket (0.5.2) 23 | eventmachine (>= 0.12.9) 24 | http_parser.rb (~> 0.6.0) 25 | ethon (0.12.0) 26 | ffi (>= 1.3.0) 27 | eventmachine (1.2.7) 28 | execjs (2.7.0) 29 | faraday (1.3.0) 30 | faraday-net_http (~> 1.0) 31 | multipart-post (>= 1.2, < 3) 32 | ruby2_keywords 33 | faraday-net_http (1.0.1) 34 | ffi (1.15.0) 35 | forwardable-extended (2.6.0) 36 | gemoji (3.0.1) 37 | github-pages (214) 38 | github-pages-health-check (= 1.17.0) 39 | jekyll (= 3.9.0) 40 | jekyll-avatar (= 0.7.0) 41 | jekyll-coffeescript (= 1.1.1) 42 | jekyll-commonmark-ghpages (= 0.1.6) 43 | jekyll-default-layout (= 0.1.4) 44 | jekyll-feed (= 0.15.1) 45 | jekyll-gist (= 1.5.0) 46 | jekyll-github-metadata (= 2.13.0) 47 | jekyll-mentions (= 1.6.0) 48 | jekyll-optional-front-matter (= 0.3.2) 49 | jekyll-paginate (= 1.1.0) 50 | jekyll-readme-index (= 0.3.0) 51 | jekyll-redirect-from (= 0.16.0) 52 | jekyll-relative-links (= 0.6.1) 53 | jekyll-remote-theme (= 0.4.3) 54 | jekyll-sass-converter (= 1.5.2) 55 | jekyll-seo-tag (= 2.7.1) 56 | jekyll-sitemap (= 1.4.0) 57 | jekyll-swiss (= 1.0.0) 58 | jekyll-theme-architect (= 0.1.1) 59 | jekyll-theme-cayman (= 0.1.1) 60 | jekyll-theme-dinky (= 0.1.1) 61 | jekyll-theme-hacker (= 0.1.2) 62 | jekyll-theme-leap-day (= 0.1.1) 63 | jekyll-theme-merlot (= 0.1.1) 64 | jekyll-theme-midnight (= 0.1.1) 65 | jekyll-theme-minimal (= 0.1.1) 66 | jekyll-theme-modernist (= 0.1.1) 67 | jekyll-theme-primer (= 0.5.4) 68 | jekyll-theme-slate (= 0.1.1) 69 | jekyll-theme-tactile (= 0.1.1) 70 | jekyll-theme-time-machine (= 0.1.1) 71 | jekyll-titles-from-headings (= 0.5.3) 72 | jemoji (= 0.12.0) 73 | kramdown (= 2.3.1) 74 | kramdown-parser-gfm (= 1.1.0) 75 | liquid (= 4.0.3) 76 | mercenary (~> 0.3) 77 | minima (= 2.5.1) 78 | nokogiri (>= 1.10.4, < 2.0) 79 | rouge (= 3.26.0) 80 | terminal-table (~> 1.4) 81 | github-pages-health-check (1.17.0) 82 | addressable (~> 2.3) 83 | dnsruby (~> 1.60) 84 | octokit (~> 4.0) 85 | public_suffix (>= 2.0.2, < 5.0) 86 | typhoeus (~> 1.3) 87 | html-pipeline (2.14.0) 88 | activesupport (>= 2) 89 | nokogiri (>= 1.4) 90 | http_parser.rb (0.6.0) 91 | i18n (0.9.5) 92 | concurrent-ruby (~> 1.0) 93 | jekyll (3.9.0) 94 | addressable (~> 2.4) 95 | colorator (~> 1.0) 96 | em-websocket (~> 0.5) 97 | i18n (~> 0.7) 98 | jekyll-sass-converter (~> 1.0) 99 | jekyll-watch (~> 2.0) 100 | kramdown (>= 1.17, < 3) 101 | liquid (~> 4.0) 102 | mercenary (~> 0.3.3) 103 | pathutil (~> 0.9) 104 | rouge (>= 1.7, < 4) 105 | safe_yaml (~> 1.0) 106 | jekyll-avatar (0.7.0) 107 | jekyll (>= 3.0, < 5.0) 108 | jekyll-coffeescript (1.1.1) 109 | coffee-script (~> 2.2) 110 | coffee-script-source (~> 1.11.1) 111 | jekyll-commonmark (1.3.1) 112 | commonmarker (~> 0.14) 113 | jekyll (>= 3.7, < 5.0) 114 | jekyll-commonmark-ghpages (0.1.6) 115 | commonmarker (~> 0.17.6) 116 | jekyll-commonmark (~> 1.2) 117 | rouge (>= 2.0, < 4.0) 118 | jekyll-default-layout (0.1.4) 119 | jekyll (~> 3.0) 120 | jekyll-feed (0.15.1) 121 | jekyll (>= 3.7, < 5.0) 122 | jekyll-gist (1.5.0) 123 | octokit (~> 4.2) 124 | jekyll-github-metadata (2.13.0) 125 | jekyll (>= 3.4, < 5.0) 126 | octokit (~> 4.0, != 4.4.0) 127 | jekyll-mentions (1.6.0) 128 | html-pipeline (~> 2.3) 129 | jekyll (>= 3.7, < 5.0) 130 | jekyll-optional-front-matter (0.3.2) 131 | jekyll (>= 3.0, < 5.0) 132 | jekyll-paginate (1.1.0) 133 | jekyll-readme-index (0.3.0) 134 | jekyll (>= 3.0, < 5.0) 135 | jekyll-redirect-from (0.16.0) 136 | jekyll (>= 3.3, < 5.0) 137 | jekyll-relative-links (0.6.1) 138 | jekyll (>= 3.3, < 5.0) 139 | jekyll-remote-theme (0.4.3) 140 | addressable (~> 2.0) 141 | jekyll (>= 3.5, < 5.0) 142 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) 143 | rubyzip (>= 1.3.0, < 3.0) 144 | jekyll-sass-converter (1.5.2) 145 | sass (~> 3.4) 146 | jekyll-seo-tag (2.7.1) 147 | jekyll (>= 3.8, < 5.0) 148 | jekyll-sitemap (1.4.0) 149 | jekyll (>= 3.7, < 5.0) 150 | jekyll-swiss (1.0.0) 151 | jekyll-theme-architect (0.1.1) 152 | jekyll (~> 3.5) 153 | jekyll-seo-tag (~> 2.0) 154 | jekyll-theme-cayman (0.1.1) 155 | jekyll (~> 3.5) 156 | jekyll-seo-tag (~> 2.0) 157 | jekyll-theme-dinky (0.1.1) 158 | jekyll (~> 3.5) 159 | jekyll-seo-tag (~> 2.0) 160 | jekyll-theme-hacker (0.1.2) 161 | jekyll (> 3.5, < 5.0) 162 | jekyll-seo-tag (~> 2.0) 163 | jekyll-theme-leap-day (0.1.1) 164 | jekyll (~> 3.5) 165 | jekyll-seo-tag (~> 2.0) 166 | jekyll-theme-merlot (0.1.1) 167 | jekyll (~> 3.5) 168 | jekyll-seo-tag (~> 2.0) 169 | jekyll-theme-midnight (0.1.1) 170 | jekyll (~> 3.5) 171 | jekyll-seo-tag (~> 2.0) 172 | jekyll-theme-minimal (0.1.1) 173 | jekyll (~> 3.5) 174 | jekyll-seo-tag (~> 2.0) 175 | jekyll-theme-modernist (0.1.1) 176 | jekyll (~> 3.5) 177 | jekyll-seo-tag (~> 2.0) 178 | jekyll-theme-primer (0.5.4) 179 | jekyll (> 3.5, < 5.0) 180 | jekyll-github-metadata (~> 2.9) 181 | jekyll-seo-tag (~> 2.0) 182 | jekyll-theme-slate (0.1.1) 183 | jekyll (~> 3.5) 184 | jekyll-seo-tag (~> 2.0) 185 | jekyll-theme-tactile (0.1.1) 186 | jekyll (~> 3.5) 187 | jekyll-seo-tag (~> 2.0) 188 | jekyll-theme-time-machine (0.1.1) 189 | jekyll (~> 3.5) 190 | jekyll-seo-tag (~> 2.0) 191 | jekyll-titles-from-headings (0.5.3) 192 | jekyll (>= 3.3, < 5.0) 193 | jekyll-watch (2.2.1) 194 | listen (~> 3.0) 195 | jemoji (0.12.0) 196 | gemoji (~> 3.0) 197 | html-pipeline (~> 2.2) 198 | jekyll (>= 3.0, < 5.0) 199 | kramdown (2.3.1) 200 | rexml 201 | kramdown-parser-gfm (1.1.0) 202 | kramdown (~> 2.0) 203 | liquid (4.0.3) 204 | listen (3.5.1) 205 | rb-fsevent (~> 0.10, >= 0.10.3) 206 | rb-inotify (~> 0.9, >= 0.9.10) 207 | mercenary (0.3.6) 208 | minima (2.5.1) 209 | jekyll (>= 3.5, < 5.0) 210 | jekyll-feed (~> 0.9) 211 | jekyll-seo-tag (~> 2.1) 212 | minitest (5.14.4) 213 | multipart-post (2.1.1) 214 | nokogiri (1.12.5-x86_64-linux) 215 | racc (~> 1.4) 216 | octokit (4.20.0) 217 | faraday (>= 0.9) 218 | sawyer (~> 0.8.0, >= 0.5.3) 219 | pathutil (0.16.2) 220 | forwardable-extended (~> 2.6) 221 | public_suffix (4.0.6) 222 | racc (1.5.2) 223 | rb-fsevent (0.10.4) 224 | rb-inotify (0.10.1) 225 | ffi (~> 1.0) 226 | rexml (3.2.5) 227 | rouge (3.26.0) 228 | ruby-enum (0.9.0) 229 | i18n 230 | ruby2_keywords (0.0.4) 231 | rubyzip (2.3.0) 232 | safe_yaml (1.0.5) 233 | sass (3.7.4) 234 | sass-listen (~> 4.0.0) 235 | sass-listen (4.0.0) 236 | rb-fsevent (~> 0.9, >= 0.9.4) 237 | rb-inotify (~> 0.9, >= 0.9.7) 238 | sawyer (0.8.2) 239 | addressable (>= 2.3.5) 240 | faraday (> 0.8, < 2.0) 241 | simpleidn (0.2.1) 242 | unf (~> 0.1.4) 243 | terminal-table (1.8.0) 244 | unicode-display_width (~> 1.1, >= 1.1.1) 245 | thread_safe (0.3.6) 246 | typhoeus (1.4.0) 247 | ethon (>= 0.9.0) 248 | tzinfo (1.2.9) 249 | thread_safe (~> 0.1) 250 | unf (0.1.4) 251 | unf_ext 252 | unf_ext (0.0.7.7) 253 | unicode-display_width (1.7.0) 254 | zeitwerk (2.4.2) 255 | 256 | PLATFORMS 257 | x86_64-linux 258 | 259 | DEPENDENCIES 260 | github-pages (~> 214) 261 | jekyll-feed (~> 0.12) 262 | minima (~> 2.5) 263 | tzinfo (~> 1.2) 264 | tzinfo-data 265 | wdm (~> 0.1.1) 266 | 267 | BUNDLED WITH 268 | 2.2.15 269 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Instagram Downloader 2 | description: >- 3 | Open source Instagram Downloader which helps you download all the images/videos you like. 4 | This project is only intended to be used on your own instagram page. The usage of this tool on other pages is discouraged. 5 | baseurl: '/InstagramDownloader' 6 | github_username: HuiiBuh 7 | github_project: InstagramDownloader 8 | safe: false 9 | sass: 10 | sass_dir: ./assets 11 | 12 | # Build settings 13 | theme: minima 14 | exclude: 15 | - .sass-cache/ 16 | - .jekyll-cache/ 17 | - gemfiles/ 18 | - Gemfile 19 | - Gemfile.lock 20 | - node_modules/ 21 | - vendor/bundle/ 22 | - vendor/cache/ 23 | - vendor/gems/ 24 | - vendor/ruby/ 25 | -------------------------------------------------------------------------------- /docs/_includes/changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # Looking for a new maintainer or buyer 3 | Hi, I am looking for a new maintainer, because I do not use IG and I do not find any pleasure in developing this extension any more. If you want to maintain this extension please contact me. 4 | 5 | ### Version 4.6.3 6 | 7 | `11.06.2022` 8 | 9 | #### Fixes 10 | 11 | + Fixed some issues with image download not showing up 12 | 13 | #### Known Issues 14 | 15 | + Unable to download some videos due to change in IG format 16 | 17 | --- 18 | 19 | ### Version 4.6.2 20 | 21 | `02.06.2022` 22 | 23 | #### Fixes 24 | 25 | + Fixed all kinds of issues regarding IG changes 26 | + Had to remove the bulk and hover downloader because IG removed a public API 27 | 28 | --- 29 | 30 | ### Version 4.6.1 31 | 32 | `01.03.2022` 33 | 34 | #### Fixes 35 | 36 | + Hotkey for downloading posts and stories is not *ctrs + shift + d* so you can type a response to a post 37 | 38 | --- 39 | 40 | ### Version 4.6.0 41 | 42 | `28.02.2022` 43 | 44 | #### Improvements: 45 | 46 | + Added option to download complete post with the hover downloader 47 | + Added new hotkey `shift + d` 48 | 49 | #### Fixes 50 | 51 | + Fix InstaID precision without using bigint by @Azureit in https://github.com/HuiiBuh/InstagramDownloader/pull/260 52 | 53 | --- 54 | 55 | ### Version 4.5.6 56 | 57 | `08.01.2021` 58 | 59 | #### Improvements: 60 | 61 | + Added new extension icon. 62 | 63 | --- 64 | 65 | ### Version 4.5.5 66 | 67 | `21.11.2021` 68 | 69 | #### Fixes: 70 | 71 | + Fixed bulk download button not visible [#240](https://github.com/HuiiBuh/InstagramDownloader/issues/240) 72 | 73 | --- 74 | 75 | ### Version 4.5.3 76 | 77 | `04.11.2021` 78 | 79 | #### Fixes: 80 | 81 | + Fixed post download button [#233](https://github.com/HuiiBuh/InstagramDownloader/issues/233). Huge thanks 82 | to [Akhil](https://github.com/officialakhil) 83 | 84 | --- 85 | 86 | ### Version 4.5.2 87 | 88 | `16.09.2021` 89 | 90 | #### Fixes: 91 | 92 | + Fixed story download button 93 | 94 | --- 95 | 96 | ### Version 4.5.1 97 | 98 | `03.09.2021` 99 | 100 | #### Fixes: 101 | 102 | + Updated copyright 103 | + Small code improvements 104 | 105 | --- 106 | 107 | ### Version 4.5.0 108 | 109 | `28.07.2021` 110 | 111 | #### New: 112 | 113 | + Added support for dark reader [#216](https://github.com/HuiiBuh/InstagramDownloader/issues/216) 114 | + New alert design 115 | + New bulk download design 116 | + Better obfuscation, so Instagram does not detect that you are using a downloader 117 | 118 | #### Fixes: 119 | 120 | + Fixed Low-Resolution Story Image download [#218](https://github.com/HuiiBuh/InstagramDownloader/issues/218) 121 | + Improved mutation observer, so events don't get fired so fast 122 | 123 | --- 124 | 125 | ### Version 4.4.7 126 | 127 | `30.06.2021` 128 | 129 | #### Fixes: 130 | 131 | + Improved video download speed by [Rayron Victor](https://github.com/rayronvictor) 132 | + Improved story download for mobile by [PublicConstructor](https://github.com/PublicConstructor) 133 | 134 | --- 135 | 136 | ### Version 4.4.6 137 | 138 | `29.05.2021` 139 | 140 | #### Fixes: 141 | 142 | + Changed the icon and the name, to prevent instagram copyright strike 143 | 144 | --- 145 | 146 | ### Version 4.4.5 147 | 148 | #### Fixes: 149 | 150 | + Made the download more robust 151 | 152 | --- 153 | 154 | ### Version 4.4.4 155 | 156 | #### Fixes: 157 | 158 | + **NEW PERMISSIONS:** These are necessary to download the images from the facebook (IG parent company) and instagram 159 | CDNs. 160 | + Multiple download issues in chrome and firefox 161 | 162 | #### New: 163 | 164 | + Show you if IG detected that you use a download tool 165 | 166 | --- 167 | 168 | ### Version 4.4.2 169 | 170 | #### Fixes: 171 | 172 | + Some chrome version require the webRequest permission to be able to download images, so here it is. 173 | 174 | --- 175 | 176 | ### Version 4.4.1 177 | 178 | #### Fixes: 179 | 180 | + Fixed query selector for the download all button 181 | 182 | --- 183 | 184 | ### Version 4.4.0 185 | 186 | #### New: 187 | 188 | + Removed the ugly download all button and replaced it with a new button right next th your home icon. 189 | + I requested **new permissions** in the firefox version, so the bulk download can work again. As always this will not 190 | put your data at risk and if anyone has any doubts you can view the code 191 | on [GitHub](https://github.com/HuiiBuh/InstagramDownloader) . 192 | 193 | #### Fixes: 194 | 195 | + Fixed bulk download not working [#170](https://github.com/HuiiBuh/InstagramDownloader/issues/170) 196 | + Fixed the story download which was sometimes not 197 | working [#166](https://github.com/HuiiBuh/InstagramDownloader/issues/166) 198 | + Made the bulk download slower to avoid a ban from 199 | instagram [#158](https://github.com/HuiiBuh/InstagramDownloader/issues/158) 200 | 201 | --- 202 | 203 | ### Version 4.3.1 204 | 205 | #### Fixes: 206 | 207 | + Fixed download not working for 208 | Firefox [#165](https://github.com/HuiiBuh/InstagramDownloader/issues/165) [@Kwizatz](https://github.com/Kwizatz) 209 | 210 | --- 211 | 212 | ### Version 4.3.0 213 | 214 | #### New: 215 | 216 | + Added Reel download [#140](https://github.com/HuiiBuh/InstagramDownloader/issues/140) 217 | + Support new Post 218 | View [#157](https://github.com/HuiiBuh/InstagramDownloader/issues/157) [@andrewsuzuki](https://github.com/andrewsuzuki) 219 | + Added new Instagram Downloader Website 220 | 221 | #### Fixes: 222 | 223 | + Fixed naming to improve file system compatibility [#161](https://github.com/HuiiBuh/InstagramDownloader/issues/161) 224 | 225 | --- 226 | 227 | ### Version 4.2.2 228 | 229 | #### New: 230 | 231 | + Improved error handling. New errors will be displayed to the user 232 | 233 | #### Fixes: 234 | 235 | + Fixed error logging decorator [@UaQuetzalcoatl](https://github.com/UaQuetzalcoatl) 236 | + Fix story download not working with localization 237 | parameter [#146](https://github.com/HuiiBuh/InstagramDownloader/issues/146) 238 | 239 | --- 240 | 241 | ### Version 4.2.1 242 | 243 | #### New: 244 | 245 | + Automated the build even more. Now building with --watch is working 246 | 247 | #### Fixes: 248 | 249 | + Download button not appearing on multi image posts [#134](https://github.com/HuiiBuh/InstagramDownloader/issues/134) 250 | + Continually tries to download "Download All" bulk zip 251 | file [#130](https://github.com/HuiiBuh/InstagramDownloader/issues/130) 252 | + Use the linux zipping util for building in prod, so the zip can be used for uploading to the chrome store 253 | 254 | --- 255 | 256 | ### Version 4.2.0 257 | 258 | #### New: 259 | 260 | + See the progress of the download, and the compression progress 261 | + Added Easter egg on the changelog page 262 | 263 | #### Fixes: 264 | 265 | + Preserve original file name [#125](https://github.com/HuiiBuh/InstagramDownloader/issues/125) 266 | + Fixed story download resolution [#123](https://github.com/HuiiBuh/InstagramDownloader/issues/123) 267 | + Increased account image resolution [#123](https://github.com/HuiiBuh/InstagramDownloader/issues/123) 268 | 269 | --- 270 | 271 | ### Version 4.1.1 272 | 273 | #### Fixes: 274 | 275 | + Changed PayPal link so PayPal does not keep 40% 276 | + Download button moves down when you scroll, so you can start the download from everywhere 277 | + Customize the download speed 278 | + Source Code bundling with Webpack 279 | 280 | --- 281 | 282 | ### Version 4.0.0 283 | 284 | #### New: 285 | 286 | + See the progress of you background downloads 287 | 288 | #### Fixes: 289 | 290 | + Download all button stays on top of the page #105 291 | + Customize scroll interval [#106](https://github.com/HuiiBuh/InstagramDownloader/issues/106) 292 | + Download correct story image [#108](https://github.com/HuiiBuh/InstagramDownloader/issues/108) 293 | + Fix video download [#109](https://github.com/HuiiBuh/InstagramDownloader/issues/109) 294 | + Removed lots of unnecessary code 295 | 296 | --- 297 | 298 | ### Version 3.4.1 299 | 300 | #### New: 301 | 302 | + Download all button on saved page 303 | 304 | #### Fixes: 305 | 306 | + Download all button not appearing 307 | -------------------------------------------------------------------------------- /docs/_includes/cookies.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | 40 | 41 | 51 | 52 | 64 | -------------------------------------------------------------------------------- /docs/_includes/credits.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | - The files get ziped with [JSZip](https://github.com/Stuk/jszip) 4 | - Error logging inspired by [redifined-github](https://github.com/sindresorhus/refined-github) 5 | - The extension icon was creted by [lnz_sarah](https://www.instagram.com/lnz_sarah/) 6 | - The Download Icon is from [ShareIcon](https://www.shareicon.net/instagram-social-media-icons-880117) and was created by [Aarthi Padmanabhan](https://www.shareicon.net/author/aarthi-padmanabhan) 7 | - The PayPal Icon is from [Wikipedia](https://wikipedia.org) 8 | -------------------------------------------------------------------------------- /docs/_includes/donation.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | 38 | 39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | {%- seo -%} 14 | 15 | 16 | {%- feed_meta -%} 17 | {%- if jekyll.environment == 'production' and site.google_analytics -%} 18 | {%- include google-analytics.html -%} 19 | {%- endif -%} 20 | 21 |
22 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 53 | -------------------------------------------------------------------------------- /docs/_includes/readme.md: -------------------------------------------------------------------------------- 1 | ## Looking for a new maintainer or buyer 2 | 3 | Hi, I am looking for a new maintainer, because I do not use IG and I do not find any pleasure in developing this extension any more. 4 | If you want to maintain this extension please contact me. 5 | 6 | 7 | # InstagramDownloader 8 | 9 | Firefox and Chrome Extension which creates an download button for instagram images and videos on the right of the 10 | bookmark icon. 11 | 12 | Install on [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/instagram_download/) 13 | and on [Chrome](https://chrome.google.com/webstore/detail/instagram-downloader/cpgaheeihidjmolbakklolchdplenjai). 14 | 15 | ## General Download 16 | 17 | ![DownloadButton](https://i.imgur.com/IG7Im8F.jpg) 18 | 19 | A Download Button appearers while you hover over the image you want to download. 20 | 21 | ![Hover and Download](https://i.imgur.com/ZFA6ct0.jpg) 22 | 23 | ## Profile Picture 24 | 25 | The same happens if you hover over the profile picture. 26 | 27 | ![Hover and Download](https://i.imgur.com/axnMJgD.png) 28 | 29 | ## Bulk Download 30 | 31 | Now it is possible to download all images and videos of one profile at once. The Button apperes right next to 32 | the `follow` button. The feature includes scrolling down until all images are loaded, so it may take a while. It is also 33 | possible that instagram will ban you temporarily (just a few minutes) if you try to download like 1000 pictures. 34 | If you click on the `download all` button the page will begin scrolling down to load the new pictures until every 35 | picture of the profile got loaded once. After that a ZIP file with all the images in it will be created. 36 | This may take a while depending on your internet connection and the amount of pictures you plan to download. 37 | 38 | ![Download all](https://i.imgur.com/8DFcGVp.png) 39 | 40 | ## Story Download 41 | 42 | From version 1.5+ it is possible to download Instagram Stories. The extension supports both image and video downloads. 43 | 44 | ![Download Story](https://i.imgur.com/Hy3qJod.png) 45 | 46 | ## Development 47 | 48 | ### Getting started 49 | 50 | The main class is (obviously) the `index.ts`. Here the different downloaders subscribe to the `URLChangeEmitter` which 51 | in turn notifies the different downloaders when they should be added to the page. 52 | The collection of the image links is handled in the downloaders. The retrieved image links get send to the background 53 | script where the download happens. 54 | 55 | ### Building 56 | 57 | The build script depends on linux, especially on the zip util which should be included in most linux distros. Building 58 | in Windows is only partially supported. 59 | To Execute the build script run `npm install` and after the installation is complete execute `webpack`. There are 60 | different flags which change the build. 61 | 62 | - _--watch_ starts the build in watch mode and rebuilds the project if files get changed 63 | - _--mode=production_ generates a production build without source maps and logging. In addition to these changes a zip 64 | files for the different browsers will get generated and linted. 65 | - _--mode=development_ generates a development build with source maps and logging. No zip files get generated and no 66 | linting script gets executed. 67 | 68 | The two flags can be combined if needed `webpack --mode=production --watch`. 69 | 70 | ## Credits 71 | 72 | - The files get zipped with [JSZip](https://github.com/Stuk/jszip) 73 | - Error logging inspired by [refined-github](https://github.com/sindresorhus/refined-github) 74 | - The Download Icon is from [ShareIcon](https://www.shareicon.net/instagram-social-media-icons-880117) and was created 75 | by [Aarthi Padmanabhan](https://www.shareicon.net/author/aarthi-padmanabhan) 76 | - The PayPal Icon is from [Wikipedia](https://wikipedia.org) 77 | - The close icon is from [Google material design icons](https://github.com/google/material-design-icons) 78 | -------------------------------------------------------------------------------- /docs/_layouts/ad_layout.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 | {% include donation.html %} 6 | 7 | 14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | {% if page.title %} 23 |
24 |

{{ page.title | escape }}

25 |
26 | {% endif page.title %} {{ content }} 27 |
28 |
29 |
30 | 31 | {% if page.with_credits %} 32 |
33 |
{% capture my_include %}{% include credits.md %}{% endcapture %} {{ my_include | markdownify }}
34 |
35 | {% endif %} 36 | -------------------------------------------------------------------------------- /docs/_layouts/base.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 | 6 | 13 | 14 | 15 | 16 | {%- include head.html -%} 17 | 18 | 19 | 20 | {%- include header.html -%} 21 | 22 |
23 | {{content}} 24 |
25 | 26 | {%- include footer.html -%} 27 | {%- include cookies.html -%} 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 | {% include donation.html %} 6 | 7 | 14 | 15 |
16 |
{{ content }}
17 |
18 | 19 | {% if page.with_credits %} 20 |
21 |
{% capture my_include %}{% include credits.md %}{% endcapture %} {{ my_include | markdownify }}
22 |
23 | {% endif %} 24 | -------------------------------------------------------------------------------- /docs/assets/fixes.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | @media screen and (min-width: $on-large-desktop) { 3 | max-width: calc(#{$large-content-width} - (#{$spacing-unit} * 2)); 4 | } 5 | } 6 | 7 | // Remove the top border and set the header to the fixed top 8 | .header-mock { 9 | height: $spacing-unit * 1.865; 10 | } 11 | 12 | .site-header { 13 | border-top: none; 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | width: 100vw; 18 | height: $spacing-unit * 1.865; 19 | background-color: white; 20 | z-index: 1; 21 | 22 | .site-title, 23 | .trigger, 24 | .trigger a { 25 | display: flex; 26 | align-items: center; 27 | height: $spacing-unit * 1.865; 28 | } 29 | } 30 | 31 | // Site footer should be over the side cards 32 | .site-footer { 33 | position: relative; 34 | z-index: 1; 35 | background-color: white; 36 | padding-bottom: 0; 37 | } 38 | 39 | // Let the footer be aways the same width 40 | .footer-col-1, 41 | .footer-col-2 { 42 | width: -webkit-calc(50% - (#{$spacing-unit} / 2)); 43 | width: calc(50% - (#{$spacing-unit} / 2)); 44 | } 45 | 46 | .footer-col-3 { 47 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 48 | width: calc(100% - (#{$spacing-unit} / 2)); 49 | } 50 | 51 | // Prevent float none 52 | .footer-col { 53 | float: inline-start; 54 | } 55 | 56 | .page-link { 57 | margin: 0 !important; 58 | padding: 0 0.7rem !important; 59 | } 60 | 61 | h1 { 62 | font-size: x-large!important; 63 | } 64 | -------------------------------------------------------------------------------- /docs/assets/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $content-width: 600px; 5 | $on-laptop: 900px; 6 | $base-font-family: ProximaNova, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 7 | $base-font-size: 15px; 8 | $base-font-weight: normal; 9 | $background-color: #fafafa; 10 | 11 | $large-content-width: 900px; 12 | $on-large-desktop: 1200px; 13 | 14 | @import "{{ site.theme }}"; 15 | @import "fixes.scss"; 16 | 17 | %sidecard { 18 | height: calc(100vh - #{$spacing-unit} * 1.865); 19 | position: fixed; 20 | top: $spacing-unit * 1.865; 21 | 22 | width: calc((100vw - #{$content-width}) / 2); 23 | 24 | @include media-query($on-laptop) { 25 | display: none; 26 | } 27 | 28 | @media screen and (min-width: $on-large-desktop) { 29 | width: calc((100vw - #{$large-content-width}) / 2); 30 | } 31 | } 32 | 33 | .left-sidecard { 34 | @extend %sidecard; 35 | left: 0; 36 | } 37 | 38 | .right-sidecard { 39 | @extend %sidecard; 40 | right: 0; 41 | } 42 | 43 | .top-card { 44 | display: none; 45 | height: 10vh; 46 | margin-bottom: $spacing-unit; 47 | @extend .wrapper; 48 | 49 | @include media-query($on-laptop) { 50 | display: block; 51 | } 52 | } 53 | 54 | .large-svg-icon { 55 | width: 32px; 56 | height: 32px; 57 | padding-right: 0; 58 | 59 | &:hover { 60 | fill: black; 61 | transition: all 0.3s; 62 | } 63 | } 64 | 65 | a { 66 | text-decoration: none!important; 67 | } 68 | 69 | header a[href].page-link { 70 | padding-left: 0.2rem; 71 | padding-right: 0.2rem; 72 | 73 | transition: all 0.3s; 74 | 75 | &.active { 76 | background-color: rgba(0, 0, 0, 0.05); 77 | } 78 | 79 | &:hover { 80 | background-color: rgba(0, 0, 0, 0.05); 81 | } 82 | } 83 | 84 | .card { 85 | background-color: white; 86 | padding: 1rem; 87 | border-radius: 3px; 88 | margin-bottom: 1rem; 89 | } 90 | 91 | .view-on-github { 92 | position: absolute; 93 | right: 1em; 94 | } 95 | 96 | @include media-query($on-palm) { 97 | .view-on-github { 98 | position: inherit; 99 | float: left !important; 100 | width: calc(100% - 22px); 101 | } 102 | } 103 | 104 | .share-width { 105 | display: flex; 106 | align-items: center; 107 | justify-content: flex-start; 108 | } 109 | 110 | .btn { 111 | cursor: pointer; 112 | background-color: #0095f6; 113 | color: #fff; 114 | min-height: 36px; 115 | text-align: center; 116 | padding: 9px 2rem; 117 | border-radius: 1.5rem; 118 | margin: 10px 10px 10px 0; 119 | font-weight: bold; 120 | overflow: hidden; 121 | border: none; 122 | } 123 | 124 | * { 125 | transition: all 0.3s; 126 | } 127 | 128 | hr { 129 | border-color: lightgray; 130 | margin: 2rem -1rem; 131 | } 132 | -------------------------------------------------------------------------------- /docs/assets/paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | image/svg+xml -------------------------------------------------------------------------------- /docs/changelog/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | layout: ad_layout 4 | with_credits: false 5 | --- 6 | 7 | # I have sold this extension to https://github.com/igdownloader 8 | # Maintainance form now on will be done by the new maintainer! 9 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- 1 | ../src/icons/favicon.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ad_layout 3 | with_credits: false 4 | --- 5 | 6 | {% include readme.md %} 7 | -------------------------------------------------------------------------------- /docs/privacy-policy/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy Policy 3 | layout: default 4 | --- 5 | 6 | # Privacy Policy for InstagramDownloader 7 | 8 | The privacy of our visitors to InstagramDownloader is important to us. 9 | 10 | At InstagramDownloader, we recognize that privacy of your personal information is important. Here is information on what types of personal information we receive and collect when you use and visit InstagramDownloader, and how we safeguard your information. We never sell your personal information to third parties. 11 | 12 | Log Files 13 | 14 | As with most other websites, we collect and use the data contained in log files. The information in the log files include your IP (internet protocol) address, your ISP (internet service provider, such as AOL or Shaw Cable), the browser you used to visit our site (such as Internet Explorer or Firefox), the time you visited our site and which pages you visited throughout our site. 15 | 16 | Cookies and Web Beacons 17 | 18 | We do use cookies to store information, such as your personal preferences when you visit our site. This could include only showing you a popup once in your visit, or the ability to login to some of our features, such as forums. 19 | 20 | We also use third party advertisements on InstagramDownloader to support our site. Some of these advertisers may use technology such as cookies and web beacons when they advertise on our site, which will also send these advertisers (such as Google through the Google AdSense program) information including your IP address, your ISP , the browser you used to visit our site, and in some cases, whether you have Flash installed. This is generally used for geotargeting purposes (showing New York real estate ads to someone in New York, for example) or showing certain ads based on specific sites visited (such as showing cooking ads to someone who frequents cooking sites). 21 | 22 | DoubleClick DART cookies 23 | 24 | We also may use DART cookies for ad serving through Google’s DoubleClick, which places a cookie on your computer when you are browsing the web and visit a site using DoubleClick advertising (including some Google AdSense advertisements). This cookie is used to serve ads specific to you and your interests (”interest based targeting”). 25 | 26 | The ads served will be targeted based on your previous browsing history (For example, if you have been viewing sites about visiting Las Vegas, you may see Las Vegas hotel advertisements when viewing a non-related site, such as on a site about hockey). DART uses “non personally identifiable information”. It does NOT track personal information about you, such as your name, email address, physical address, telephone number, social security numbers, bank account numbers or credit card numbers. 27 | 28 | You can opt-out of this ad serving on all sites using this advertising by visiting http://www.doubleclick.com/privacy/dart_adserving.aspx 29 | 30 | You can choose to disable or selectively turn off our cookies or third-party cookies in your browser settings, or by managing preferences in programs such as Norton Internet Security. However, this can affect how you are able to interact with our site as well as other websites. This could include the inability to login to services or programs, such as logging into forums or accounts. 31 | 32 | Deleting cookies does not mean you are permanently opted out of any advertising program. Unless you have settings that disallow cookies, the next time you visit a site running the advertisements, a new cookie will be added. 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagram_downloader", 3 | "version": "4.6.3", 4 | "description": "Browser extension which downloads images and videos from instagram", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:dev": "webpack --mode=development", 8 | "build:dev:w": "webpack --mode=development --watch", 9 | "build:prod": "webpack --mode=production", 10 | "build:prod:w": "webpack --mode=production --watch" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/HuiiBuh/InstagramDownloader.git" 15 | }, 16 | "author": "HuiiBuh", 17 | "license": "LGPL-3.0", 18 | "bugs": { 19 | "url": "https://github.com/HuiiBuh/InstagramDownloader/issues" 20 | }, 21 | "homepage": "https://github.com/HuiiBuh/InstagramDownloader#readme", 22 | "dependencies": { 23 | "jszip": "^3.6.0", 24 | "webextension-polyfill-ts": "^0.22.0" 25 | }, 26 | "devDependencies": { 27 | "addons-linter": "^3.10.0", 28 | "css-loader": "^6.2.0", 29 | "fs-extra": "^10.0.0", 30 | "html-webpack-plugin": "^5.3.2", 31 | "mini-css-extract-plugin": "^2.1.0", 32 | "sass": "^1.36.0", 33 | "sass-loader": "^12.1.0", 34 | "stream": "^0.0.2", 35 | "ts-config": "^20.10.0", 36 | "ts-loader": "^9.2.4", 37 | "ts-node": "^10.1.0", 38 | "tslib": "^2.3.0", 39 | "tslint": "^6.1.3", 40 | "typescript": "^4.3.4", 41 | "webpack": "^5.47.0", 42 | "webpack-cli": "^4.7.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. HuiiBuh 3 | * This file (build.js) is part of InstagramDownloader which is released under 4 | * GNU LESSER GENERAL PUBLIC LICENSE. 5 | * You are not allowed to use this code or this file for another project without 6 | * linking to the original source AND open sourcing your code. 7 | */ 8 | 9 | const fs = require('fs'); 10 | const fse = require('fs-extra'); 11 | const exec = require('child_process').execSync; 12 | 13 | class BuildExtensionPlugin { 14 | 15 | constructor() { 16 | this.production = false; 17 | } 18 | 19 | 20 | apply(compiler) { 21 | compiler.hooks.done.tap('BuildExtensionPlugin', async (params) => { 22 | this.production = params.compilation.options.mode === "production"; 23 | await this.build(); 24 | }); 25 | } 26 | 27 | async build() { 28 | 29 | // Check if dir exists and create it otherwise 30 | if (!(await fs.existsSync('zip'))) { 31 | await fs.promises.mkdir('zip'); 32 | } 33 | 34 | // Generate the extension for every browser 35 | for (const browser of ["chrome", "firefox"]) { 36 | await this.assembleExtensionFiles(browser); 37 | try { 38 | await this.execute(`cd zip/${browser} && zip ../${browser}.zip * -r && cd ../..`); 39 | } catch { 40 | console.warn("Please install the zip util to get automatic zipping"); 41 | // No zip installed 42 | } 43 | 44 | if (this.production) { 45 | console.log(`Linting ${browser}`); 46 | console.log( 47 | await this.execute(`addons-linter zip/${browser}.zip`), 48 | ); 49 | } 50 | } 51 | 52 | if (this.production) { 53 | await this.execute("git archive --format zip --output zip/InstagramDownloader.zip HEAD"); 54 | } 55 | }; 56 | 57 | /** 58 | * Collect all files necessary to build the extension 59 | * @param browser The browser 60 | */ 61 | async assembleExtensionFiles(browser) { 62 | const path = `zip/${browser}`; 63 | if (await fs.existsSync(path)) { 64 | await fs.promises.rmdir(path, {recursive: true}); 65 | } 66 | 67 | await fs.promises.mkdir(path); 68 | 69 | await fs.promises.copyFile(`src/manifest_${browser}.json`, `${path}/manifest.json`); 70 | await fse.copy("src/icons/", `${path}/icons`); 71 | await fse.copy("dist", path); 72 | } 73 | 74 | 75 | /** 76 | * Execute a command in the linux command line 77 | * @param command The command which should be executed 78 | */ 79 | execute(command) { 80 | let response = ""; 81 | try { 82 | response = exec(command, {encoding: "utf-8"}); 83 | } catch (e) { 84 | console.error(e); 85 | } 86 | return response; 87 | } 88 | } 89 | 90 | 91 | module.exports = BuildExtensionPlugin; 92 | -------------------------------------------------------------------------------- /src/global.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. HuiiBuh 3 | * This file (global.js) is part of InstagramDownloader which is released under 4 | * GNU LESSER GENERAL PUBLIC LICENSE. 5 | * You are not allowed to use this code or this file for another project without 6 | * linking to the original source AND open sourcing your code. 7 | */ 8 | 9 | // Provide a global for webpack 10 | module.exports = window 11 | -------------------------------------------------------------------------------- /src/icons/close_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/download_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/src/icons/download_black.png -------------------------------------------------------------------------------- /src/icons/download_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/src/icons/download_white.png -------------------------------------------------------------------------------- /src/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/src/icons/favicon.png -------------------------------------------------------------------------------- /src/icons/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igdownloader/InstagramDownloader/1a484c38a547560b064271785beba714e4c03203/src/icons/instagram.png -------------------------------------------------------------------------------- /src/icons/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 66 | -------------------------------------------------------------------------------- /src/manifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "IG Downloader", 4 | "description": "Open Source und privacy conscious Instagram Downloader, which downloads images, videos, Instagram stories and IGTV.", 5 | "version": "4.7.0", 6 | "icons": { 7 | "512": "icons/instagram.png" 8 | }, 9 | "web_accessible_resources": [ 10 | "icons/download_black.png", 11 | "icons/download_white.png", 12 | "icons/close_black_24dp.svg", 13 | "icons/instagram.png" 14 | ], 15 | "options_ui": { 16 | "page": "options.html" 17 | }, 18 | "permissions": [ 19 | "downloads", 20 | "tabs", 21 | "*://*.instagram.com/*", 22 | "*://*.cdninstagram.com/*", 23 | "*://*.cdninstagram.net/*", 24 | "*://*.fbcdn.net/*" 25 | ], 26 | "background": { 27 | "scripts": [ 28 | "js/background.js" 29 | ] 30 | }, 31 | "content_scripts": [ 32 | { 33 | "matches": [ 34 | "*://*.instagram.com/*" 35 | ], 36 | "js": [ 37 | "js/extension.js" 38 | ], 39 | "css": [ 40 | "css/extension.css" 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/manifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Instagram Downloader", 4 | "version": "4.7.0", 5 | "icons": { 6 | "512": "icons/instagram.png" 7 | }, 8 | "web_accessible_resources": [ 9 | "icons/download_black.png", 10 | "icons/download_white.png", 11 | "icons/close_black_24_dp.svg", 12 | "icons/instagram.png" 13 | ], 14 | "options_ui": { 15 | "page": "options.html", 16 | "browser_style": false 17 | }, 18 | "permissions": [ 19 | "downloads", 20 | "tabs", 21 | "*://*.instagram.com/*", 22 | "*://*.cdninstagram.com/*", 23 | "*://*.fbcdn.net/*" 24 | ], 25 | "background": { 26 | "scripts": [ 27 | "js/background.js" 28 | ] 29 | }, 30 | "content_scripts": [ 31 | { 32 | "matches": [ 33 | "*://*.instagram.com/*" 34 | ], 35 | "js": [ 36 | "js/extension.js" 37 | ], 38 | "css": [ 39 | "css/extension.css" 40 | ] 41 | } 42 | ], 43 | "applications": { 44 | "gecko": { 45 | "id": "HuiiBuh.InstagramDownloader@github.com" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 26 | Instagram Downloader 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/scss/alert.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2022. HuiiBuh 3 | * This file (alert.scss) is part of InstagramDownloader which is not released 4 | * under any licence. 5 | * Any usage of this code outside this project is not allowed. 6 | */ 7 | 8 | .alert-wrapper { 9 | position: fixed; 10 | bottom: 4rem; 11 | right: 1rem; 12 | width: calc(80vw - 1rem); 13 | z-index: 2000; 14 | max-width: 550px; 15 | } 16 | 17 | 18 | .alert { 19 | opacity: 0; 20 | transition: opacity .2s; 21 | position: relative; 22 | 23 | margin-top: 20px; 24 | padding: 15px 29px 15px 15px; 25 | 26 | animation: fade-in .3s; 27 | border-radius: 5px; 28 | 29 | border: solid 3px; 30 | background-color: white; 31 | color: rgb(38, 38, 38); 32 | 33 | &.fade-in { 34 | opacity: 1; 35 | } 36 | 37 | &.fade-out { 38 | opacity: 0; 39 | } 40 | } 41 | 42 | .close { 43 | user-select: none; 44 | position: absolute; 45 | content: var(--extension-close-icon); 46 | cursor: pointer; 47 | right: 5px; 48 | top: 5px; 49 | } 50 | 51 | .default { 52 | border-color: rgba(105, 187, 255, 0.5); 53 | } 54 | 55 | .warn { 56 | border-color: rgba(255, 191, 100, 0.5); 57 | } 58 | 59 | .error { 60 | border-color: rgba(255, 105, 79, 0.5); 61 | } 62 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2022. HuiiBuh 3 | * This file (main.scss) is part of InstagramDownloader which is not released 4 | * under any licence. 5 | * Any usage of this code outside this project is not allowed. 6 | */ 7 | 8 | .post-download-button { 9 | background-image: var(--extension-download-black); 10 | background-size: 59%; 11 | background-repeat: no-repeat; 12 | background-position: center; 13 | 14 | cursor: pointer; 15 | width: 40px; 16 | height: 40px; 17 | } 18 | 19 | .story-download-button { 20 | background-image: var(--extension-download-white); 21 | background-size: 59%; 22 | background-repeat: no-repeat; 23 | background-position: center; 24 | 25 | cursor: pointer; 26 | width: 40px; 27 | height: 40px 28 | } 29 | 30 | 31 | .h-v-center { 32 | position: absolute; 33 | transform: translate(-50%, +60%); 34 | top: 50%; 35 | left: 50%; 36 | } 37 | 38 | .account-download-button { 39 | height: 30px; 40 | width: 30px; 41 | background: transparent; 42 | cursor: pointer; 43 | opacity: 0; 44 | visibility: hidden; 45 | 46 | transform: translate(-50%, -50%); 47 | 48 | img { 49 | width: 100%; 50 | height: 100%; 51 | } 52 | } 53 | 54 | .hover-download-button { 55 | height: 30px; 56 | width: 30px; 57 | background: transparent; 58 | cursor: pointer; 59 | opacity: 0; 60 | visibility: hidden; 61 | 62 | z-index: 5000000000000000000000000; 63 | 64 | img { 65 | width: 100%; 66 | height: 100%; 67 | } 68 | } 69 | 70 | .bulk-download-button { 71 | background-image: var(--extension-download-black); 72 | background-size: 59%; 73 | background-repeat: no-repeat; 74 | background-position: center; 75 | cursor: pointer; 76 | width: 30px; 77 | height: 30px; 78 | margin-right: -5px; 79 | } 80 | 81 | 82 | // Hover over a square image to display the download button 83 | //account explore reels 84 | ._bz0w:hover, .pKKVh:hover, .Tjpra > a:hover { 85 | .hover-download-button { 86 | opacity: 1; 87 | visibility: visible; 88 | } 89 | } 90 | 91 | 92 | // Hover over account to display the download button 93 | .RR-M-:hover, .M-jxE:hover, ._aarf._aarg:hover, ._aa_j ._aarf:hover { 94 | .account-download-button { 95 | opacity: 1; 96 | visibility: visible; 97 | } 98 | } 99 | 100 | // For the igtv tab 101 | ._bz0w { 102 | position: relative; 103 | 104 | // Hide the overlay if you download an image (igtv) 105 | &:active { 106 | opacity: 1 !important; 107 | } 108 | } 109 | 110 | // For the post download 111 | .wmtNn, ._aamz > div { 112 | display: flex !important; 113 | flex-direction: row !important; 114 | } 115 | 116 | // Vertically the buttons around the bulk download button 117 | ._47KiJ, .XCodT, .r9-Os { 118 | align-items: center; 119 | } 120 | 121 | 122 | // V-center the icons in the top bar 123 | .J5g42 { 124 | align-items: center; 125 | } 126 | -------------------------------------------------------------------------------- /src/scss/modal.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2022. HuiiBuh 3 | * This file (modal.scss) is part of InstagramDownloader which is not released 4 | * under any licence. 5 | * Any usage of this code outside this project is not allowed. 6 | */ 7 | 8 | .modal-overlay { 9 | display: none; 10 | opacity: 0; 11 | transition: all ease 100ms; 12 | 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | z-index: 1000; 19 | 20 | background: rgba(0, 0, 0, .65); 21 | 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | .show { 27 | opacity: 1; 28 | } 29 | 30 | .visible { 31 | display: flex; 32 | } 33 | 34 | .modal { 35 | transition: width ease-in-out 100ms; 36 | display: inline-block; 37 | width: 400px; 38 | padding: 1rem; 39 | z-index: 1001; 40 | 41 | select { 42 | margin-left: .5rem; 43 | border: solid 1px #dbdbdb; 44 | border-radius: 3px; 45 | color: #262626; 46 | outline: 0; 47 | padding: 3px; 48 | text-align: center; 49 | } 50 | } 51 | 52 | @media (min-width: 736px) { 53 | .modal { 54 | width: 500px; 55 | } 56 | } 57 | 58 | .modal-header { 59 | color: rgb(38, 38, 38); 60 | font-size: 22px; 61 | line-height: 26px; 62 | 63 | padding: 20px 0 10px 0; 64 | } 65 | 66 | .modal-content { 67 | margin: 16px 32px; 68 | } 69 | 70 | .modal-text { 71 | padding: 5px; 72 | color: rgb(142, 142, 142); 73 | font-size: 14px; 74 | line-height: 18px; 75 | } 76 | 77 | .modal-body { 78 | background: white; 79 | border-radius: 12px; 80 | text-align: center; 81 | } 82 | 83 | .modal-button { 84 | background-color: transparent; 85 | 86 | border-top: 1px solid rgb(219, 219, 219); 87 | border-bottom: 0; 88 | border-left: 0; 89 | border-right: 0; 90 | 91 | line-height: 1.5; 92 | min-height: 48px; 93 | padding: 4px 8px; 94 | 95 | cursor: pointer; 96 | 97 | user-select: none; 98 | width: 100%; 99 | 100 | &.active { 101 | color: rgb(0, 149, 226); 102 | } 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/ts/ForegroundMessageHandler.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (ForegroundMessageHandler.ts) is part of InstagramDownloader which is not released 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | import { browser } from 'webextension-polyfill-ts'; 8 | import { Alert } from './components/Alert'; 9 | import { AlertMessage, DownloadProgress } from './modles/extension'; 10 | import { isDownloadProgress } from './modles/typeguards'; 11 | 12 | export class ForegroundMessageHandler { 13 | private progressElement!: HTMLElement; 14 | private inProgress = false; 15 | 16 | private static displayAlert({text, type = 'default', timeout = 5000, dismissible = true}: AlertMessage): void { 17 | Alert.createAndAdd(text, type, dismissible, timeout); 18 | } 19 | 20 | public init(): void { 21 | browser.runtime.onMessage.addListener((message: DownloadProgress | AlertMessage) => { 22 | if (isDownloadProgress(message)) { 23 | this.updateProgress(message); 24 | } else { 25 | ForegroundMessageHandler.displayAlert(message); 26 | } 27 | }); 28 | this.progressElement = Alert.create('', 'default', false); 29 | } 30 | 31 | /** 32 | * Update the progress of the download display element 33 | */ 34 | private async updateProgress(download: DownloadProgress): Promise { 35 | 36 | // Add the message button 37 | if (download.isFirst) { 38 | this.inProgress = true; 39 | await Alert.add(this.progressElement, null); 40 | } 41 | 42 | const text = `${download.type === 'download' ? 'Downloading' : 'Compression'} progress at ${download.percent}%`; 43 | 44 | // Remove the message button and set the progress to false 45 | if (download.isLast && this.inProgress) { 46 | this.inProgress = false; 47 | (this.progressElement.querySelector('.alert-message') as HTMLElement).innerText = text; 48 | await Alert.remove(this.progressElement); 49 | if (download.type === 'compression') { 50 | await Alert.createAndAdd('Bulk download finished'); 51 | } 52 | } 53 | 54 | // Prevent async messages which arrive after the last message to change the number 55 | if (this.inProgress) { 56 | (this.progressElement.querySelector('.alert-message') as HTMLElement).innerText = text; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ts/QuerySelectors.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (QuerySelectors.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | /** 9 | * A bunch of css selectors which get used by instagram 10 | */ 11 | 12 | export enum QuerySelectors { 13 | // Post 14 | postWrapper = '.M9sTE, .NI8nC, article._aalr, article._aa6a, article._aatb, article._ab0-, article._ab6k', 15 | postBookmark = '.wmtNn, ._aamz > div', 16 | postSliderBubble = '.JSZAJ, .ijCUd, ._acnb', 17 | postAccountName = "._aacl > ._aap6 > .oajrlxb2, ._acan._acao._acat._acaw", 18 | postSliderBubbleActive = '.XCodT, ._acnb._acnf', 19 | sliderItem = "._acaz", 20 | postContentWrapper = "._aatk._aatl, ._aatk._aatn, ._aato._ab1k._ab1l, ._aagu._ab64 ._aagv", 21 | 22 | // Story 23 | storyImage = '.y-yJ5, img._aa63', 24 | storyCloseButton = '.K_10X, ._g3zU, .aUIsh, ._ac0g>._abl-', 25 | storyAccountName = '._a3gq ._ac0q a, ._ac0q a', 26 | 27 | // Account, Hover, Explore, Reels 28 | accountImage = '._aarf._aarg ._aa8j, ._aa_j ._aarf ._aa8h ._aa8j', 29 | accountName = '._7UhW9.fKFbl.yUEEX.KV-D4.fDxYl, ._aacl._aacs._aact._aacx._aada, ._acan._acao._acat._acaw', 30 | 31 | // Account Image 32 | accountImageWrapper = '.RR-M-, .M-jxE, ._aarf._aarg, ._aarf ._aa8h', 33 | } 34 | -------------------------------------------------------------------------------- /src/ts/background/BackgroundMessageHandler.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (BackgroundMessageHandler.ts) is part of InstagramDownloader which is not released 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { browser, Runtime } from 'webextension-polyfill-ts'; 9 | import { singleton } from '../decorators'; 10 | import { AlertMessage, DownloadMessage, DownloadProgress, DownloadType } from '../modles/extension'; 11 | import { isDownloadProgress } from '../modles/typeguards'; 12 | import { downloadBulk, downloadSingleImage } from './download'; 13 | import OnInstalledDetailsType = Runtime.OnInstalledDetailsType; 14 | 15 | @singleton 16 | export class BackgroundMessageHandler { 17 | 18 | private lastMessageSent = new Date().getTime(); 19 | 20 | public constructor() { 21 | browser.runtime.onInstalled.addListener(BackgroundMessageHandler.onUpdate); 22 | browser.runtime.onMessage.addListener(BackgroundMessageHandler.onMessage); 23 | } 24 | 25 | private static async onUpdate(reason: OnInstalledDetailsType): Promise { 26 | if (reason.reason !== 'update') return; 27 | 28 | const options = browser.runtime.getURL('options.html'); 29 | await browser.tabs.create({ 30 | url: options, 31 | }); 32 | } 33 | 34 | private static async onMessage(message: DownloadMessage): Promise { 35 | if (message.type === DownloadType.single) { 36 | await downloadSingleImage(message); 37 | } else if (message.type === DownloadType.bulk) { 38 | await downloadBulk(message.imageURL, message.accountName); 39 | } 40 | } 41 | 42 | /** 43 | * Send a message every second. If more messages are passed to this method the message will be discarded 44 | * @param message The message which should be sent 45 | * @param url The urls of the browser tabs the message should be sent to 46 | */ 47 | public async sendMessage(message: DownloadProgress | AlertMessage, url: string = '*://*.instagram.com/*'): Promise { 48 | if (isDownloadProgress(message)) { 49 | const timestamp = new Date().getTime(); 50 | if (timestamp - this.lastMessageSent < 1000 && !message.isLast && !message.isFirst) return; 51 | this.lastMessageSent = timestamp; 52 | } 53 | 54 | const tabList = await browser.tabs.query({url}); 55 | 56 | for (const tab of tabList) { 57 | if (tab.id) browser.tabs.sendMessage(tab.id, message); 58 | } 59 | } 60 | 61 | } 62 | 63 | // @ts-ignore 64 | const messageHandler = new BackgroundMessageHandler(); 65 | -------------------------------------------------------------------------------- /src/ts/background/download.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (download.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import * as JSZip from 'jszip'; 9 | import { browser } from 'webextension-polyfill-ts'; 10 | import { downloadFile } from '../downloaders/download-functions'; 11 | import { sleep } from '../functions'; 12 | import { DownloadMessage, Metadata } from '../modles/extension'; 13 | import { BackgroundMessageHandler } from './BackgroundMessageHandler'; 14 | 15 | const IS_FIREFOX = 'browser' in window; 16 | 17 | const downloadFailed = async (downloadId: number): Promise => { 18 | const downloadItem = (await browser.downloads.search({id: downloadId})).pop(); 19 | 20 | return downloadItem ? !!downloadItem.error : false; 21 | }; 22 | 23 | const fetchDownload = async (url: string, fileName: string): Promise => { 24 | const downloadBlob = await downloadFile(url); 25 | 26 | return browser.downloads.download({url: window.URL.createObjectURL(downloadBlob), filename: fileName}); 27 | }; 28 | 29 | const nativeDownload = async (url: string, fileName: string): Promise => { 30 | const headers: { name: string; value: string }[] = []; 31 | if (IS_FIREFOX) headers.push({name: 'Referer', value: 'instagram.com'}); 32 | 33 | return browser.downloads.download({url, filename: fileName, headers}); 34 | }; 35 | 36 | export async function downloadSingleImage(message: DownloadMessage): Promise { 37 | // Get the image id 38 | let imageName = getImageId(message.imageURL[0]); 39 | imageName = `${message.accountName}_${imageName}`; 40 | const downloadURL: string = message.imageURL[0]; 41 | 42 | if (IS_FIREFOX) { 43 | const downloadId = await fetchDownload(downloadURL, imageName); 44 | await sleep(2000); 45 | 46 | if (await downloadFailed(downloadId)) { 47 | new BackgroundMessageHandler().sendMessage({text: 'Download did not succeed, trying different method', type: 'error'}); 48 | setTimeout(() => nativeDownload(downloadURL, imageName), 100); 49 | } 50 | } else { 51 | const downloadId = await nativeDownload(downloadURL, imageName); 52 | await sleep(2000); 53 | if (await downloadFailed(downloadId)) { 54 | new BackgroundMessageHandler().sendMessage({text: 'Download did not succeed, trying different method', type: 'error'}); 55 | setTimeout(() => fetchDownload(downloadURL, imageName), 100); 56 | } 57 | } 58 | } 59 | 60 | export async function downloadBulk(urls: string[], accountName: string): Promise { 61 | const zip: JSZip = new JSZip(); 62 | for (const [imageIndex, url] of urls.entries()) { 63 | try { 64 | const response = await downloadFile(url); 65 | zip.file(getImageId(url), response, {binary: true}); 66 | } catch (e) { 67 | const blob = new Blob([ 68 | `Request did not succeed. If you are using Firefox go into you privacy settings ans select the 69 | standard setting (https://support.mozilla.org/en-US/kb/content-blocking). If that is not the problem you tried to download to many images 70 | and instagram has blocked you temporarily.\n\n`, 71 | `If you are using chrome there is currently a bug in chrome which seems to block my requests. So stay strong and hope that this error gets fixed soon.`, 72 | e.toString()]); 73 | zip.file('error_read_me.txt', blob, {binary: true}); 74 | } 75 | 76 | await new BackgroundMessageHandler().sendMessage({ 77 | percent: Number((((imageIndex + 1) / urls.length) * 100).toFixed(2)), 78 | isFirst: imageIndex === 0, 79 | isLast: imageIndex + 1 === urls.length, 80 | type: 'download', 81 | }); 82 | } 83 | await downloadZIP(zip, accountName); 84 | } 85 | 86 | /** 87 | * Download the zip file 88 | * @param zip The JSZip file which should be downloaded 89 | * @param accountName The account name 90 | */ 91 | export async function downloadZIP(zip: JSZip, accountName: string): Promise { 92 | let isFirst = true; 93 | const dZIP = await zip.generateAsync({type: 'blob'}, (u: Metadata) => { 94 | new BackgroundMessageHandler().sendMessage({ 95 | percent: Number(u.percent.toFixed(2)), 96 | isFirst, 97 | isLast: u.percent === 100, 98 | type: 'compression', 99 | }); 100 | isFirst = false; 101 | }); 102 | 103 | const kindaUrl = window.URL.createObjectURL(dZIP); 104 | 105 | if (accountName) { 106 | await browser.downloads.download({url: kindaUrl, filename: `${accountName}.zip`}); 107 | } else { 108 | await browser.downloads.download({url: kindaUrl, filename: 'bulk_download.zip'}); 109 | } 110 | 111 | } 112 | 113 | /** 114 | * Gets the image name based on the url of the image 115 | * @param url the url of the image or video 116 | * @returns the image/video name 117 | */ 118 | function getImageId(url: string): string { 119 | // tslint:disable-next-line:no-non-null-assertion 120 | return url.split('?')[0]!.split('/').pop()!; 121 | } 122 | -------------------------------------------------------------------------------- /src/ts/components/Alert.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (Alert.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | import '../../scss/alert.scss'; 8 | 9 | export type AlertType = 'default' | 'warn' | 'error'; 10 | 11 | const HTML = ` 12 |
13 | 14 |
15 |
16 | `; 17 | 18 | const WRAPPER = (() => { 19 | const alertWrapper = document.createElement('div'); 20 | alertWrapper.classList.add('alert-wrapper'); 21 | document.body.appendChild(alertWrapper); 22 | 23 | return alertWrapper; 24 | })(); 25 | 26 | // tslint:disable-next-line:no-namespace 27 | export namespace Alert { 28 | 29 | export const create = (text: string, type: AlertType, dismissible: boolean): HTMLElement => { 30 | const div = document.createElement('div'); 31 | div.innerHTML = HTML; 32 | const alert = div.children[0] as HTMLElement; 33 | 34 | const close = (alert.querySelector('.close') as HTMLElement); 35 | if (dismissible) { 36 | close.onclick = () => remove(alert); 37 | } else { 38 | close.remove(); 39 | } 40 | alert.classList.add(type); 41 | (alert.querySelector('.alert-message') as HTMLElement).innerText = text; 42 | 43 | return alert; 44 | }; 45 | 46 | export const createAndAdd = async (text: string, type: AlertType = 'default', dismissible: boolean = true, timeout: number | null = 5000): Promise => { 47 | const alert = create(text, type, dismissible); 48 | await add(alert, timeout); 49 | 50 | return alert; 51 | }; 52 | 53 | export const add = async (alert: HTMLElement, timeout: number | null): Promise => { 54 | WRAPPER.appendChild(alert); 55 | await animateIn(alert); 56 | 57 | timeout && setTimeout(() => remove(alert), timeout); 58 | }; 59 | 60 | export const remove = async (element: HTMLElement): Promise => { 61 | const animation = element.animate( 62 | [{opacity: '1'}, {opacity: '0'}], 63 | {duration: 300, fill: 'forwards'}, 64 | ); 65 | await animation.finished; 66 | element.remove(); 67 | }; 68 | 69 | const animateIn = async (element: HTMLElement): Promise => { 70 | const animation = element.animate( 71 | [{opacity: '0'}, {opacity: '1'}], 72 | {duration: 300, fill: 'forwards'}, 73 | ); 74 | await animation.finished; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/ts/components/Modal.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (Modal.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import '../../scss/modal.scss'; 9 | import { sleep } from '../functions'; 10 | 11 | export interface ModalOptions { 12 | heading?: string; 13 | buttonList?: ModalButton[]; 14 | imageURL?: string, 15 | content?: (HTMLElement | string)[]; 16 | } 17 | 18 | export class Modal { 19 | public imageURL: string; 20 | public heading: string; 21 | public content: (HTMLElement | string)[]; 22 | public buttonList: ModalButton[]; 23 | 24 | private modal: HTMLDivElement | null = null; 25 | 26 | public constructor(modalOptions?: ModalOptions) { 27 | this.imageURL = modalOptions?.imageURL || ''; 28 | this.heading = modalOptions?.heading || ''; 29 | this.content = modalOptions?.content || ['']; 30 | this.buttonList = modalOptions?.buttonList || []; 31 | } 32 | 33 | public get element(): HTMLDivElement | null { 34 | return this.modal; 35 | } 36 | 37 | public async open(): Promise { 38 | if (this.modal) await this.close(); 39 | 40 | this.modal = this.createModal(); 41 | document.body.appendChild(this.modal); 42 | this.modal.classList.add('visible'); 43 | setTimeout(() => { 44 | this.modal!.classList.add('show'); 45 | }); 46 | } 47 | 48 | public async close(): Promise { 49 | if (!this.modal) return; 50 | 51 | this.modal.classList.remove('show'); 52 | await sleep(100); 53 | this.modal.classList.remove('visible'); 54 | this.modal.remove(); 55 | this.modal = null; 56 | } 57 | 58 | private createModal(): HTMLDivElement { 59 | const modalElement = document.createElement('div'); 60 | modalElement.classList.add('modal-overlay'); 61 | 62 | const modal = document.createElement('div'); 63 | modal.classList.add('modal'); 64 | modalElement.appendChild(modal); 65 | 66 | const modalBody = document.createElement('div'); 67 | modalBody.classList.add('modal-body'); 68 | modal.appendChild(modalBody); 69 | 70 | const modalContent = document.createElement('div'); 71 | modalContent.classList.add('modal-content'); 72 | modalBody.appendChild(modalContent); 73 | 74 | const imageWrapper = document.createElement('div'); 75 | modalContent.appendChild(imageWrapper); 76 | 77 | const image = document.createElement('img'); 78 | image.setAttribute('height', '76px'); 79 | image.setAttribute('width', '76px'); 80 | image.style.margin = 'auto'; 81 | image.setAttribute('src', this.imageURL); 82 | imageWrapper.appendChild(image); 83 | 84 | const modalHeader = document.createElement('h2'); 85 | modalHeader.classList.add('modal-header'); 86 | modalHeader.innerText = this.heading; 87 | modalContent.appendChild(modalHeader); 88 | 89 | this.content.forEach(content => { 90 | if (typeof content === 'string') { 91 | const modalText = document.createElement('p'); 92 | modalText.classList.add('modal-text'); 93 | modalText.innerText = content; 94 | modalContent.appendChild(modalText); 95 | } else { 96 | modalContent.appendChild(content); 97 | } 98 | }); 99 | 100 | this.buttonList.forEach((button: ModalButton) => { 101 | const modalButton = document.createElement('button'); 102 | modalButton.classList.add('modal-button'); 103 | modalButton.innerText = button.text; 104 | 105 | if (button.active) modalButton.classList.add('active'); 106 | 107 | modalButton.onclick = button?.callback ? button.callback : this.close.bind(this); 108 | modalBody.appendChild(modalButton); 109 | }); 110 | 111 | return modalElement; 112 | } 113 | } 114 | 115 | /** 116 | * An interface for the modal button 117 | */ 118 | export interface ModalButton { 119 | text: string; 120 | active?: boolean; 121 | 122 | callback?(): void; 123 | } 124 | -------------------------------------------------------------------------------- /src/ts/decorators.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (decorators.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | /**************************************************************************************** 9 | * Inspiration for the error logging is from * 10 | * https://github.com/sindresorhus/refined-github which is licensed under MIT * 11 | *****************************************************************************************/ 12 | 13 | // tslint:disable:no-any 14 | import { Alert } from './components/Alert'; 15 | import { Downloader } from './downloaders/Downloader'; 16 | import { sleep } from './functions'; 17 | 18 | export function singleton(constructor: any): any { 19 | return new Proxy(constructor, { 20 | construct(target: any, argArray: any, newTarget?: any): object { 21 | if (target.prototype !== newTarget.prototype) { 22 | return Reflect.construct(target, argArray, newTarget); 23 | } 24 | if (!target.SINGLETON_INSTANCE) { 25 | target.SINGLETON_INSTANCE = Reflect.construct(target, argArray, newTarget); 26 | } 27 | 28 | return target.SINGLETON_INSTANCE; 29 | }, 30 | }); 31 | } 32 | 33 | export function stopObservation(_: object, 34 | __: string, 35 | descriptor: PropertyDescriptor): void { 36 | 37 | const value = descriptor.value; 38 | descriptor.value = function(): void { 39 | Downloader.observer.disconnect(); 40 | Downloader.observer.takeRecords(); 41 | sleep(100).then(() => value.apply(this, arguments)); 42 | sleep(150).then(() => Downloader.observer.observe()); 43 | }; 44 | 45 | } 46 | 47 | // tslint:disable-next-line:ban-types 48 | export function LogClassErrors(constructor: Function): void { 49 | 50 | const reported: Record = {}; 51 | 52 | for (const key of Object.getOwnPropertyNames(constructor.prototype)) { 53 | const descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, key) as PropertyDescriptor; 54 | const method = descriptor.value; 55 | if (!(method instanceof Function) || key === 'constructor') continue; 56 | descriptor.value = function(...args: any[]): void { 57 | try { 58 | return method.apply(this, args); 59 | } catch (e) { 60 | 61 | // Extension update 62 | if (e.toString() === 'Error: Extension context invalidated.') return; 63 | 64 | const issue = encodeURIComponent(`${constructor.name} ${e.toString()}`); 65 | if (reported[issue]) return; 66 | 67 | reported[issue] = true; 68 | Alert.createAndAdd(`Instagram Downloader:\n ${constructor.name}-${e.toString()} \nLook in your browse console to see more details`, 'error'); 69 | 70 | console.error( 71 | `❌ Instagram Downloader → ${constructor.name} → \n`, 72 | e.stack, 73 | `\nSearch issue: https://github.com/huiibuh/InstagramDownloader/issues?q=is%3Aissue+${issue}`, 74 | `\nOpen an issue: https://github.com/huiibuh/InstagramDownloader/issues/new?labels=bug&template=bug_report.md&title=${issue}`, 75 | ); 76 | } 77 | }; 78 | Object.defineProperty(constructor.prototype, key, descriptor); 79 | } 80 | } 81 | 82 | // tslint:disable-next-line:ban-types 83 | export function LogIGRequest(method: T): T { 84 | return ((...args: any[]) => { 85 | try { 86 | return method(...args); 87 | } catch (e) { 88 | Alert.createAndAdd('Looks like Instagram has figured out you are using a downloader. The download may not work for the next time'); 89 | throw e; 90 | } 91 | }) as unknown as T; 92 | } 93 | -------------------------------------------------------------------------------- /src/ts/downloaders/AccountImageDownloader.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (AccountImageDownloader.ts) is part of InstagramDownloader which is not released 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { browser } from 'webextension-polyfill-ts'; 9 | import { LogClassErrors } from '../decorators'; 10 | import { log } from '../functions'; 11 | import { DownloadMessage, DownloadType, LoggingLevel } from '../modles/extension'; 12 | import { QuerySelectors } from '../QuerySelectors'; 13 | import { Downloader } from './Downloader'; 14 | 15 | /** 16 | * Downloader which can be used to download account images 17 | */ 18 | @LogClassErrors 19 | export class AccountImageDownloader extends Downloader { 20 | 21 | /** 22 | * Download the account image 23 | */ 24 | private static async downloadContent(): Promise { 25 | const image = document.querySelector(QuerySelectors.accountImage) as HTMLImageElement | null; 26 | if (!image) { 27 | log('Could not find account image', LoggingLevel.error); 28 | return Promise.resolve(); 29 | } 30 | 31 | const accountName: HTMLHeadingElement | null = document.querySelector(QuerySelectors.accountName); 32 | 33 | const downloadMessage: DownloadMessage = { 34 | imageURL: [image.src], 35 | accountName: accountName?.innerText || 'unknown', 36 | type: DownloadType.single, 37 | }; 38 | await browser.runtime.sendMessage(downloadMessage); 39 | } 40 | 41 | /** 42 | * Create a new download button 43 | */ 44 | public createDownloadButton(): void { 45 | const accountImageWrapper: HTMLElement = document.querySelector(QuerySelectors.accountImageWrapper) as HTMLElement; 46 | if (!accountImageWrapper) return; 47 | 48 | const downloadButton: HTMLAnchorElement = document.createElement('a'); 49 | downloadButton.classList.add('h-v-center', 'account-download-button'); 50 | downloadButton.onclick = AccountImageDownloader.downloadContent; 51 | accountImageWrapper.appendChild(downloadButton); 52 | 53 | const downloadImage: HTMLImageElement = document.createElement('img'); 54 | downloadImage.src = browser.runtime.getURL('icons/download_white.png'); 55 | downloadButton.appendChild(downloadImage); 56 | } 57 | 58 | /** 59 | * Reinitialize the downloader 60 | */ 61 | public reinitialize(): void { 62 | this.remove(); 63 | this.init(); 64 | } 65 | 66 | /** 67 | * Remove the downloader 68 | */ 69 | public remove(): void { 70 | super.remove('.account-download-button'); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/ts/downloaders/Downloader.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (Downloader.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { LogClassErrors, stopObservation } from '../decorators'; 9 | import { DomObserver } from '../helper-classes/DomObserver'; 10 | import { SubscriptionInterface } from '../helper-classes/EventHandler'; 11 | 12 | /** 13 | * The base class of every downloader. 14 | */ 15 | @LogClassErrors 16 | export abstract class Downloader { 17 | public static observer: DomObserver = new DomObserver(); 18 | private subscription: SubscriptionInterface = {unsubscribe: () => null}; 19 | 20 | /** 21 | * Create a new downloader 22 | */ 23 | @stopObservation 24 | public init(): void { 25 | this.subscription = Downloader.observer.subscribe(this.reinitialize.bind(this)); 26 | this.createDownloadButton(); 27 | } 28 | 29 | /** 30 | * This method has to create a new download button 31 | */ 32 | protected abstract createDownloadButton(): void; 33 | 34 | /** 35 | * This method has to remove and initialize the downloader 36 | */ 37 | protected abstract reinitialize(): void; 38 | 39 | /** 40 | * Remove the downloader 41 | */ 42 | @stopObservation 43 | protected remove(className: string): void { 44 | this.subscription.unsubscribe(); 45 | // Remove all added elements if they have not already been removed 46 | const elements: HTMLElement[] = Array.from(document.querySelectorAll(className)) as HTMLElement[]; 47 | elements.forEach((element: HTMLElement) => { 48 | try { 49 | element.remove(); 50 | } catch { 51 | // Do nothing 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * Get the account name of a post 58 | * @param element The post element 59 | * @param accountClass The class the account has 60 | */ 61 | protected getAccountName(element: HTMLElement, accountClass: string): string { 62 | let accountName: string; 63 | try { 64 | accountName = (element.querySelector(accountClass) as HTMLElement).innerText; 65 | } catch { 66 | accountName = 'no_account_found'; 67 | } 68 | 69 | return accountName; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/ts/downloaders/HotkeyDownloader.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (HotkeyDownloader.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | import { Alert } from '../components/Alert'; 8 | import { LogClassErrors } from '../decorators'; 9 | import { URLChangeEmitter } from '../helper-classes/URLChangeEmitter'; 10 | import { QuerySelectors } from '../QuerySelectors'; 11 | import { PostDownloader } from './PostDownloader'; 12 | import { StoryDownloader } from './StoryDownloader'; 13 | 14 | @LogClassErrors 15 | export class HotkeyDownloader { 16 | 17 | private readonly hotKeyListener: (e: KeyboardEvent) => void; 18 | 19 | public constructor() { 20 | this.hotKeyListener = this.keyPressed.bind(this); 21 | } 22 | 23 | public async keyPressed(event: KeyboardEvent): Promise { 24 | const key: string = event.key.toLowerCase(); 25 | 26 | if (key === 'd' && event.shiftKey && event.ctrlKey) { 27 | event.preventDefault(); 28 | event.stopPropagation(); 29 | 30 | if (URLChangeEmitter.isPost(location.href)) { 31 | await this.savePost(); 32 | } else if (URLChangeEmitter.isStory(location.href)) { 33 | await StoryDownloader.downloadContent(event); 34 | } 35 | } else if (key === 'd' && event.shiftKey) { 36 | // tslint:disable-next-line:radix 37 | let shortcutReminder = localStorage.getItem('new_shortcut') ? parseInt(localStorage.getItem('new_shortcut')!) : 0; 38 | if (shortcutReminder < 5) { 39 | shortcutReminder += 1; 40 | localStorage.setItem('new_shortcut', shortcutReminder.toString()); 41 | Alert.createAndAdd('The new hotkey for saving images and videos is `ctrl + shift + d`'); 42 | } 43 | } else if (key === 's' && event.ctrlKey) { 44 | Alert.createAndAdd('The new hotkey for saving images and videos is `ctrl + shift + d`'); 45 | } 46 | } 47 | 48 | public init(): void { 49 | document.addEventListener('keydown', this.hotKeyListener); 50 | } 51 | 52 | public remove(): void { 53 | document.removeEventListener('keydown', this.hotKeyListener); 54 | } 55 | 56 | private savePost(): void { 57 | const post = document.querySelector(QuerySelectors.postWrapper) as HTMLElement | null; 58 | post && PostDownloader.downloadContent(post); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ts/downloaders/PostDownloader.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (PostDownloader.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { browser } from 'webextension-polyfill-ts'; 9 | import { Alert } from '../components/Alert'; 10 | import { LogClassErrors } from '../decorators'; 11 | import { log } from '../functions'; 12 | import { DownloadMessage, DownloadType, LoggingLevel } from '../modles/extension'; 13 | import { QuerySelectors } from '../QuerySelectors'; 14 | import { extractSrcSet, getSliderIndex } from './download-functions'; 15 | import { Downloader } from './Downloader'; 16 | 17 | function getSliderElementFromPosition({index, isLast}: { index: number; isLast: boolean }, sliderItems: HTMLElement[]): HTMLElement { 18 | // First or second. In this case only 3 slider for the second and 2 for the first items are visible 19 | if (index === 0 || index === 1) { 20 | return sliderItems[index]; 21 | } 22 | 23 | // Return last for last element 24 | if (isLast) { 25 | return sliderItems[sliderItems.length - 1]; 26 | } 27 | 28 | // Last. In this case only 3 slider items are visible 29 | if (sliderItems.length === 3) { 30 | return sliderItems[1]; 31 | } 32 | 33 | // For everything else it is the 2 element 34 | return sliderItems[1]; 35 | 36 | // 2 5 37 | } 38 | 39 | /** 40 | * A downloader which can be used for instagram posts 41 | */ 42 | @LogClassErrors 43 | export class PostDownloader extends Downloader { 44 | 45 | private creationTimeoutList: number[] = []; 46 | private removed = true; 47 | 48 | /** 49 | * Issue a download 50 | * @param element The element of the main post 51 | */ 52 | public static async downloadContent(element: HTMLElement): Promise { 53 | const isSlider = element.querySelector(QuerySelectors.sliderItem); 54 | if (isSlider) { 55 | await PostDownloader.downloadWithSlider(element); 56 | } else { 57 | await PostDownloader.downloadWithOutSlider(element); 58 | } 59 | } 60 | 61 | private static async downloadWithSlider(element: HTMLElement): Promise { 62 | const index = getSliderIndex(element); 63 | if (index.index === -1) { 64 | Alert.createAndAdd('Could not find slider index', 'warn'); 65 | return; 66 | } 67 | const sliderItems = [...element.querySelectorAll(QuerySelectors.sliderItem)] as HTMLElement[]; 68 | const sliderElement = getSliderElementFromPosition(index, sliderItems); 69 | log(['Image index: ', index, sliderElement]); 70 | if (!sliderElement) { 71 | log('Could not find slider element', LoggingLevel.warn); 72 | return; 73 | } 74 | 75 | const img = sliderElement.querySelector('img'); 76 | const video = sliderElement.querySelector('video'); 77 | 78 | await PostDownloader.download(img, video, element); 79 | } 80 | 81 | private static async download(img: HTMLImageElement | null | undefined, video: HTMLVideoElement | undefined | null, element: HTMLElement): Promise { 82 | let dlLink: string; 83 | if (img) { 84 | dlLink = extractSrcSet(img); 85 | } else { 86 | const currentSrc = video?.currentSrc; 87 | if (!currentSrc) { 88 | Alert.createAndAdd('Could not find post', 'warn'); 89 | return; 90 | } 91 | if (currentSrc?.startsWith?.('blob:')) { 92 | Alert.createAndAdd('Videos cannot be downloaded, because IG started blocking it.', 'warn'); 93 | return; 94 | } 95 | dlLink = currentSrc; 96 | } 97 | 98 | const postAccountName = (element.querySelector(QuerySelectors.postAccountName) as HTMLElement | null)?.innerText || 'unknown'; 99 | 100 | const downloadMessage: DownloadMessage = { 101 | imageURL: [dlLink]!, 102 | accountName: postAccountName, 103 | type: DownloadType.single, 104 | }; 105 | await browser.runtime.sendMessage(downloadMessage); 106 | } 107 | 108 | private static async downloadWithOutSlider(element: HTMLElement): Promise { 109 | const postContentWrapper = element.querySelector(QuerySelectors.postContentWrapper) 110 | || document.querySelector(QuerySelectors.postContentWrapper); 111 | const img = postContentWrapper?.querySelector?.('img'); 112 | const video = postContentWrapper?.querySelector?.('video'); 113 | 114 | await PostDownloader.download(img, video, element); 115 | } 116 | 117 | /** 118 | * Create a new download button 119 | */ 120 | public async createDownloadButton(): Promise { 121 | let postList: HTMLElement[] = [...document.querySelectorAll(QuerySelectors.postWrapper)] as HTMLElement[]; 122 | 123 | // Sometimes the button gets added at the moment the image gets updated 124 | // If this is the case the image download button cannot be added, so here is a timeout to try it again 125 | if (postList.length === 0) { 126 | postList = await this.retryCreateButton(); 127 | } 128 | this.creationTimeoutList.forEach(t => clearTimeout(t)); 129 | this.creationTimeoutList = []; 130 | 131 | postList.forEach((element: HTMLElement) => { 132 | this.addDownloadButton(element); 133 | }); 134 | } 135 | 136 | /** 137 | * Reinitialize the downloader 138 | */ 139 | public reinitialize(): void { 140 | this.remove(); 141 | this.init(); 142 | } 143 | 144 | public init(): void { 145 | this.removed = false; 146 | super.init(); 147 | } 148 | 149 | /** 150 | * Remove the downloader 151 | */ 152 | public remove(): void { 153 | this.removed = true; 154 | super.remove('.post-download-button'); 155 | } 156 | 157 | private async retryCreateButton(maxRetries: number = 20, retries: number = 0): Promise { 158 | await new Promise(resolve => { 159 | this.creationTimeoutList.push(setTimeout(resolve, 100) as unknown as number); 160 | }); 161 | let postList = [...document.querySelectorAll(QuerySelectors.postWrapper)] as HTMLElement[]; 162 | log(['with timeout', postList]); 163 | 164 | if (postList.length === 0 || maxRetries <= retries) { 165 | if (!this.removed) { 166 | postList = await this.retryCreateButton(maxRetries, retries + 1); 167 | } 168 | } 169 | 170 | return postList; 171 | } 172 | 173 | /** 174 | * Add the download button to the posts on the page 175 | * @param element The Post the download button should be added to 176 | */ 177 | private addDownloadButton(element: HTMLElement): void { 178 | 179 | // Only first post 180 | const bookmarkElement: HTMLElement = element.querySelector(QuerySelectors.postBookmark) as HTMLElement; 181 | const downloadButton: HTMLElement = document.createElement('span'); 182 | downloadButton.classList.add('post-download-button'); 183 | downloadButton.onclick = () => PostDownloader.downloadContent(element); 184 | bookmarkElement?.appendChild(downloadButton); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/ts/downloaders/StoryDownloader.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (StoryDownloader.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { browser } from 'webextension-polyfill-ts'; 9 | import { LogClassErrors } from '../decorators'; 10 | import { log } from '../functions'; 11 | import { DownloadMessage, DownloadType } from '../modles/extension'; 12 | import { QuerySelectors } from '../QuerySelectors'; 13 | import { extractSrcSet } from './download-functions'; 14 | import { Downloader } from './Downloader'; 15 | 16 | /** 17 | * Download class which can be used to download stories 18 | */ 19 | @LogClassErrors 20 | export class StoryDownloader extends Downloader { 21 | 22 | /** 23 | * Download the correct content 24 | */ 25 | public static async downloadContent(event: MouseEvent | KeyboardEvent): Promise { 26 | event.stopPropagation(); 27 | event.preventDefault(); 28 | 29 | const video = document.querySelector('video'); 30 | const img = document.querySelector(QuerySelectors.storyImage) as HTMLImageElement | null; 31 | 32 | log(video); 33 | log(img); 34 | 35 | let url: string = ''; 36 | if (video) { 37 | url = video.currentSrc; 38 | } else if (img) { 39 | url = extractSrcSet(img); 40 | } 41 | 42 | const storyAccountName = (document.querySelector(QuerySelectors.storyAccountName) as HTMLElement | null)?.innerText || 'unknown'; 43 | 44 | const downloadMessage: DownloadMessage = { 45 | imageURL: [url], 46 | accountName: storyAccountName, 47 | type: DownloadType.single, 48 | }; 49 | await browser.runtime.sendMessage(downloadMessage); 50 | } 51 | 52 | /** 53 | * Create a new download button 54 | */ 55 | public createDownloadButton(): void { 56 | const closeButton = document.querySelector(QuerySelectors.storyCloseButton)?.parentElement as HTMLElement | null; 57 | 58 | // Check if the story has already loaded 59 | if (!closeButton) { 60 | log('Could not find story close button'); 61 | return; 62 | } 63 | 64 | const downloadButton = document.createElement('span') as HTMLElement; 65 | downloadButton.classList.add('story-download-button'); 66 | 67 | downloadButton.onclick = StoryDownloader.downloadContent; 68 | 69 | closeButton.appendChild(downloadButton); 70 | } 71 | 72 | /** 73 | * Reinitialize the downloader 74 | */ 75 | public reinitialize(): void { 76 | this.remove(); 77 | this.init(); 78 | } 79 | 80 | /** 81 | * Remove the downloader 82 | */ 83 | public remove(): void { 84 | super.remove('.story-download-button'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ts/downloaders/download-functions.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (download-functions.ts) is part of InstagramDownloader which is not released 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { QuerySelectors } from '../QuerySelectors'; 9 | 10 | const IS_FIREFOX = 'browser' in window; 11 | 12 | export const downloadFile = (downloadUrl: string, progress: ((this: XMLHttpRequest, ev: ProgressEvent) => void) | null = null) => 13 | new Promise((resolve, reject) => { 14 | const xhr = new XMLHttpRequest(); 15 | 16 | xhr.open('GET', downloadUrl); 17 | if (IS_FIREFOX) { 18 | xhr.setRequestHeader('User-Agent', 'curl/7.64.1'); 19 | } 20 | 21 | xhr.onprogress = progress; 22 | 23 | xhr.onload = function(): void { 24 | if (xhr.status !== 200) return; 25 | const blob: Blob = this.response; 26 | resolve(blob); 27 | }; 28 | 29 | xhr.onerror = reject; 30 | xhr.responseType = 'blob'; 31 | xhr.send(); 32 | }); 33 | 34 | /** 35 | * Get the current index of a slider 36 | * @param element The element the slider is in 37 | */ 38 | export function getSliderIndex(element: HTMLElement): { index: number; isLast: boolean } { 39 | const sliderIndicator = [...element.querySelectorAll(QuerySelectors.postSliderBubble)]; 40 | const activeElement = element.querySelector(QuerySelectors.postSliderBubbleActive)!; 41 | 42 | const index = sliderIndicator.findIndex(e => e === activeElement); 43 | return { 44 | index, 45 | isLast: index === sliderIndicator.length - 1, 46 | }; 47 | } 48 | 49 | /** 50 | * Get the highest resolution src from a srcSet String 51 | */ 52 | export function extractSrcSet(img: HTMLImageElement): string { 53 | 54 | const getSrcSet = (srcSet: string): { res: number; url: string } | undefined => { 55 | const srcSetList: { res: number; url: string }[] = []; 56 | srcSet.split(',').forEach(set => { 57 | const [url, resolution] = (set.split(' ') as [string, string]); 58 | srcSetList.push({ 59 | res: parseInt(resolution.replace('w', ''), 0), 60 | url, 61 | }); 62 | }); 63 | srcSetList.sort((a, b) => b.res - a.res); 64 | 65 | return srcSetList[0]; 66 | }; 67 | 68 | try { 69 | const url = getSrcSet(img.srcset + `,${img.src} ${img.width}w`)?.url; 70 | if (typeof url !== 'string') { 71 | return img.src; 72 | } 73 | // tslint:disable-next-line:no-unused-expression Check if url 74 | new URL(url); 75 | return url; 76 | } catch { 77 | return img.src; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ts/functions.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (functions.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { LoggingLevel } from './modles/extension'; 9 | 10 | /** 11 | * Sleep 12 | * @param ms How long the program should pause 13 | */ 14 | export function sleep(ms: number): Promise { 15 | return new Promise((resolve) => setTimeout(resolve, ms)); 16 | } 17 | 18 | /** 19 | * Check if the string is a valid url 20 | * @param urlString The string that should be checked 21 | */ 22 | export function validURL(urlString: string): boolean { 23 | const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol 24 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name 25 | '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address 26 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path 27 | '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string 28 | '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator 29 | 30 | return pattern.test(urlString); 31 | } 32 | 33 | // tslint:disable-next-line:no-any 34 | export function log(message: any[] | any, level: LoggingLevel = LoggingLevel.default): void { 35 | if (PRODUCTION) return; 36 | 37 | let logMessage = message; 38 | if (!Array.isArray(message)) { 39 | logMessage = [message]; 40 | } 41 | 42 | if (level === LoggingLevel.default) { 43 | console.log(...logMessage); 44 | } else if (level === LoggingLevel.warn) { 45 | console.warn(...logMessage); 46 | } else { 47 | console.error(...logMessage); 48 | } 49 | 50 | } 51 | 52 | export const shortcodeToDateString = (shortcode: string): string => 53 | instaIDToTimestamp( 54 | shortcodeToInstaID(shortcode), 55 | ); 56 | 57 | export const shortcodeToInstaID = (shortcode: string): string => { 58 | const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; 59 | 60 | let mediaID = BigInt(0); 61 | for (const letter of shortcode) { 62 | mediaID = (mediaID * BigInt(64)) + BigInt(alphabet.charCodeAt(parseInt(letter, 0))); 63 | } 64 | 65 | return mediaID.toString(); 66 | }; 67 | 68 | export const instaIDToTimestamp = (id: string) => { 69 | const timestamp = (Number(id) / Math.pow(2, 23)) + 1314220021721; 70 | 71 | return new Date(timestamp).toLocaleString(); 72 | }; 73 | -------------------------------------------------------------------------------- /src/ts/globals.d.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (globals.d.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | export {}; 9 | 10 | declare global { 11 | const PRODUCTION: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/ts/helper-classes/DomObserver.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (DomObserver.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { singleton } from '../decorators'; 9 | import { Emitter } from './EventHandler'; 10 | 11 | /** 12 | * Firefox bug which does not let me inherit from MutationObserver 13 | */ 14 | @singleton 15 | export class DomObserver extends Emitter implements MutationObserver { 16 | // tslint:disable-next-line:no-any 17 | private timeout: any = null; 18 | private mutationObserver: MutationObserver; 19 | 20 | public constructor() { 21 | super(); 22 | this.mutationObserver = new MutationObserver(this.changeCallback.bind(this)); 23 | } 24 | 25 | /** 26 | * Stop observing for changes 27 | */ 28 | public disconnect(): void { 29 | this.mutationObserver.disconnect(); 30 | } 31 | 32 | /** 33 | * Observe the body for changes 34 | */ 35 | public observe(): void { 36 | const options: MutationObserverInit = { 37 | childList: true, 38 | subtree: true, 39 | }; 40 | this.mutationObserver.observe(document.body, options); 41 | } 42 | 43 | /** 44 | * Empties the record queue and returns what was in there. 45 | */ 46 | public takeRecords(): MutationRecord[] { 47 | return this.mutationObserver.takeRecords(); 48 | } 49 | 50 | private changeCallback(): void { 51 | if (this.timeout) { 52 | clearTimeout(this.timeout); 53 | } 54 | 55 | this.timeout = setTimeout(() => { 56 | this.emit(null); 57 | }, 100); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ts/helper-classes/EventHandler.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (EventHandler.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | // tslint:disable:no-any 8 | 9 | type Callback = (data: T | null) => any; 10 | 11 | export class Emitter { 12 | 13 | private subscribers: Callback[] = []; 14 | 15 | public subscribe(callback: Callback): Subscription { 16 | if (!this.subscribers.includes(callback)) { 17 | this.subscribers.push(callback); 18 | } 19 | 20 | return new Subscription(callback, this.subscribers); 21 | } 22 | 23 | protected emit(data: T | null = null): void { 24 | for (const subscriber of this.subscribers) { 25 | subscriber(data); 26 | } 27 | } 28 | } 29 | 30 | export class TopicEmitter { 31 | private subscribers: Record[]> = {}; 32 | 33 | public on(topic: string, callback: Callback): Subscription { 34 | 35 | if (!(topic in this.subscribers)) { 36 | this.subscribers[topic] = []; 37 | } 38 | 39 | const topicListeners = this.subscribers[topic]; 40 | if (!topicListeners.includes(callback)) { 41 | topicListeners.push(callback); 42 | } 43 | 44 | return new Subscription(callback, this.subscribers[topic]); 45 | } 46 | 47 | public emit(topic: string, data: any = null): void { 48 | if (!(topic in this.subscribers)) return; 49 | 50 | for (const subscriber of this.subscribers[topic]) { 51 | subscriber(data); 52 | } 53 | } 54 | 55 | } 56 | 57 | export interface SubscriptionInterface { 58 | unsubscribe(): void; 59 | } 60 | 61 | export class Subscription implements SubscriptionInterface { 62 | 63 | public constructor(private callback: Callback, private subscribers: Callback[]) { 64 | } 65 | 66 | public unsubscribe(): void { 67 | if (this.subscribers.includes(this.callback)) { 68 | this.subscribers.splice(this.subscribers.findIndex(c => c === this.callback), 1); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ts/helper-classes/URLChangeEmitter.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (URLChangeEmitter.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { TopicEmitter } from './EventHandler'; 9 | 10 | /** 11 | * Subscribe to the emitter of this class to get the current instagram page 12 | */ 13 | export class URLChangeEmitter extends TopicEmitter { 14 | 15 | private url: string = location.href; 16 | 17 | /** 18 | * Add a location change event dispatcher 19 | */ 20 | public constructor() { 21 | super(); 22 | URLChangeEmitter.addLocationChangeListener(); 23 | this.subscribeToLocationChangeListener(); 24 | } 25 | 26 | public static isHome(url: string): boolean { 27 | return /^https:\/\/www\.instagram\.com\/(\?.*)*$/.test(url); 28 | } 29 | 30 | public static isPost(url: string): boolean { 31 | // The normal posts 32 | return /https:\/\/www\.instagram\.com\/p\/[^/]*\/(\?.*)*$/.test(url) || 33 | // The reel page 34 | /https:\/\/www\.instagram\.com\/reel\/[^/]*\/(\?.*)*$/.test(url); 35 | } 36 | 37 | public static isExplore(url: string): boolean { 38 | return /https:\/\/www\.instagram\.com\/explore\/tags\/[^\/]*\/(\?.*)*$/.test(url) || 39 | /https:\/\/www\.instagram\.com\/explore\/$/.test(url); 40 | } 41 | 42 | public static isStory(url: string): boolean { 43 | return /https:\/\/www\.instagram\.com\/stories\/[^/]*\/[^/]*\/(\?.*)*$/.test(url) || 44 | /https:\/\/www\.instagram\.com\/stories\/highlights\/[^/]*\/(\?.*)*$/.test(url); 45 | } 46 | 47 | public static isChannel(url: string): boolean { 48 | return /https:\/\/www\.instagram\.com\/[^/]*\/channel\/(\?.*)*$/.test(url); 49 | } 50 | 51 | public static isReel(url: string): boolean { 52 | return /https:\/\/www\.instagram\.com\/[^/]*\/reels\/(\?.*)*$/.test(url); 53 | } 54 | 55 | public static isTV(url: string): boolean { 56 | return /https:\/\/www\.instagram\.com\/tv\/[^/]*\/(\?.*)*$/.test(url); 57 | } 58 | 59 | public static isSaved(url: string): boolean { 60 | return /https:\/\/www\.instagram\.com\/[^/]*\/saved\/(\?.*)*$/.test(url); 61 | } 62 | 63 | public static isTagged(url: string): boolean { 64 | return /https:\/\/www\.instagram\.com\/[^/]*\/tagged\/(\?.*)*$/.test(url); 65 | } 66 | 67 | public static isAccount(url: string): boolean { 68 | return /https:\/\/www\.instagram\.com\/[^/]*\/(\?.*)*$/.test(url) && !/.*explore\/$/.test(url); 69 | } 70 | 71 | /** 72 | * Add a replace state event listener 73 | * This fires a location change event from the windows element 74 | */ 75 | private static addLocationChangeListener(): void { 76 | // Nice working with stable software! 77 | // Workaround because it does not work to let the extension execute the code 78 | const script: HTMLScriptElement = document.createElement('script'); 79 | script.id = 'instagram-downloader'; 80 | script.innerText = ` 81 | history.pushState = (f => function pushState() { 82 | var ret = f.apply(this, arguments); 83 | window.dispatchEvent(new Event('pushstate')); 84 | window.dispatchEvent(new Event('locationchange')); 85 | return ret; 86 | })(history.pushState); 87 | 88 | history.replaceState = (f => function replaceState() { 89 | var ret = f.apply(this, arguments); 90 | window.dispatchEvent(new Event('replacestate')); 91 | window.dispatchEvent(new Event('locationchange')); 92 | return ret; 93 | })(history.replaceState); 94 | 95 | window.addEventListener('popstate', () => { 96 | window.dispatchEvent(new Event('locationchange')) 97 | }); 98 | `; 99 | document.head.appendChild(script); 100 | } 101 | 102 | /** 103 | * Check the url and emit the right event 104 | */ 105 | public emitLocationEvent(): void { 106 | 107 | // Home 108 | if (URLChangeEmitter.isHome(this.url)) { 109 | this.emit('home'); 110 | } 111 | 112 | // Post 113 | if (URLChangeEmitter.isPost(this.url)) { 114 | this.emit('post'); 115 | } 116 | 117 | // Explore 118 | if (URLChangeEmitter.isExplore(this.url)) { 119 | this.emit('explore'); 120 | } 121 | 122 | // Story 123 | if (URLChangeEmitter.isStory(this.url)) { 124 | this.emit('story'); 125 | } 126 | 127 | // Channel 128 | if (URLChangeEmitter.isChannel(this.url)) { 129 | this.emit('channel'); 130 | } 131 | 132 | // TV 133 | if (URLChangeEmitter.isTV(this.url)) { 134 | this.emit('tv'); 135 | } 136 | 137 | // Saved 138 | if (URLChangeEmitter.isSaved(this.url)) { 139 | this.emit('saved'); 140 | } 141 | 142 | // Tagged 143 | if (URLChangeEmitter.isTagged(this.url)) { 144 | this.emit('tagged'); 145 | } 146 | 147 | // Account 148 | if (URLChangeEmitter.isAccount(this.url)) { 149 | this.emit('account'); 150 | } 151 | 152 | // Reel 153 | if (URLChangeEmitter.isReel(this.url)) { 154 | this.emit('reels'); 155 | } 156 | 157 | } 158 | 159 | /** 160 | * Subscribe to the location change listener and emit a event if the location has changed 161 | */ 162 | private subscribeToLocationChangeListener(): void { 163 | window.addEventListener('locationchange', () => { 164 | if (this.url !== location.href) { 165 | this.url = location.href; 166 | this.emitLocationEvent(); 167 | } 168 | }); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/ts/index.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (index.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | import { browser } from 'webextension-polyfill-ts'; 9 | import '../scss/main.scss'; 10 | import { singleton } from './decorators'; 11 | import { AccountImageDownloader } from './downloaders/AccountImageDownloader'; 12 | import { HotkeyDownloader } from './downloaders/HotkeyDownloader'; 13 | import { PostDownloader } from './downloaders/PostDownloader'; 14 | import { StoryDownloader } from './downloaders/StoryDownloader'; 15 | import { ForegroundMessageHandler } from './ForegroundMessageHandler'; 16 | import { log } from './functions'; 17 | import { DomObserver } from './helper-classes/DomObserver'; 18 | import { URLChangeEmitter } from './helper-classes/URLChangeEmitter'; 19 | 20 | /** 21 | * Create a new Addon manager (only once) 22 | */ 23 | @singleton 24 | export class AddonManager { 25 | private urlChangeEmitter: URLChangeEmitter = new URLChangeEmitter(); 26 | 27 | private postDownloader: PostDownloader = new PostDownloader(); 28 | private storyDownloader: StoryDownloader = new StoryDownloader(); 29 | private accountImageDownloader: AccountImageDownloader = new AccountImageDownloader(); 30 | private hotkeyDownloader: HotkeyDownloader = new HotkeyDownloader(); 31 | private downloadProgress: ForegroundMessageHandler = new ForegroundMessageHandler(); 32 | 33 | /** 34 | * Create a new Addon manager. This class has to be constructed only once 35 | */ 36 | public constructor() { 37 | new DomObserver().subscribe(() => AddonManager.addBackgroundVariable()); 38 | AddonManager.addBackgroundVariable(); 39 | AddonManager.adjustForAndroid(); 40 | 41 | this.addListeners(); 42 | this.urlChangeEmitter.emitLocationEvent(); 43 | } 44 | 45 | /** 46 | * Check if the browser is mobile 47 | * @returns Is Mobile 48 | */ 49 | private static isMobile(): boolean { 50 | return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); 51 | } 52 | 53 | /** 54 | * Hide the hover icons if the mobile firefox is detected 55 | */ 56 | private static adjustForAndroid(): void { 57 | if (AddonManager.isMobile()) { 58 | const style: HTMLStyleElement = document.createElement('style'); 59 | style.innerText = '' + 60 | '.hover-download-button, .account-download-button {' + 61 | ' display: none!important;' + 62 | '}'; 63 | document.head.appendChild(style); 64 | } 65 | } 66 | 67 | /** 68 | * Add the download image as css variable 69 | */ 70 | private static addBackgroundVariable(): void { 71 | const darkReader = document.querySelector('[data-darkreader-scheme="dark"]'); 72 | const downloadImageBlack = darkReader ? browser.runtime.getURL('icons/download_white.png') : browser.runtime.getURL('icons/download_black.png'); 73 | document.documentElement.style.setProperty('--extension-download-black', `url(${downloadImageBlack}`); 74 | 75 | const downloadImageWhite = browser.runtime.getURL('icons/download_white.png'); 76 | document.documentElement.style.setProperty('--extension-download-white', `url(${downloadImageWhite}`); 77 | 78 | const instagramAddonImage = browser.runtime.getURL('icons/instagram.png'); 79 | document.documentElement.style.setProperty('--extension-ig-icon', `url(${instagramAddonImage}`); 80 | 81 | const extensionCloseIcon = browser.runtime.getURL('icons/close_black_24dp.svg'); 82 | document.documentElement.style.setProperty('--extension-close-icon', `url(${extensionCloseIcon}`); 83 | 84 | } 85 | 86 | /** 87 | * Add listeners for an url change 88 | */ 89 | private addListeners(): void { 90 | this.downloadProgress.init(); 91 | 92 | this.urlChangeEmitter.on('home', () => { 93 | log('home'); 94 | this.removeEveryDownloader(); 95 | this.postDownloader.init(); 96 | }); 97 | 98 | this.urlChangeEmitter.on('post', () => { 99 | log('post'); 100 | this.removeEveryDownloader(); 101 | this.postDownloader.init(); 102 | this.hotkeyDownloader.init(); 103 | }); 104 | 105 | this.urlChangeEmitter.on('explore', () => { 106 | log('explore'); 107 | }); 108 | 109 | this.urlChangeEmitter.on('story', () => { 110 | log('story'); 111 | this.removeEveryDownloader(); 112 | this.storyDownloader.init(); 113 | this.hotkeyDownloader.init(); 114 | }); 115 | 116 | this.urlChangeEmitter.on('channel', () => { 117 | log('channel'); 118 | this.removeEveryDownloader(); 119 | 120 | this.accountImageDownloader.init(); 121 | }); 122 | 123 | this.urlChangeEmitter.on('tv', () => { 124 | log('tv'); 125 | this.removeEveryDownloader(); 126 | 127 | this.postDownloader.init(); 128 | this.accountImageDownloader.init(); 129 | }); 130 | 131 | this.urlChangeEmitter.on('saved', () => { 132 | log('saved'); 133 | this.removeEveryDownloader(); 134 | 135 | this.accountImageDownloader.init(); 136 | }); 137 | 138 | this.urlChangeEmitter.on('tagged', () => { 139 | log('tagged'); 140 | this.removeEveryDownloader(); 141 | 142 | this.accountImageDownloader.init(); 143 | }); 144 | 145 | this.urlChangeEmitter.on('account', () => { 146 | log('account'); 147 | this.removeEveryDownloader(); 148 | 149 | this.accountImageDownloader.init(); 150 | }); 151 | this.urlChangeEmitter.on('reels', () => { 152 | log('reels'); 153 | this.removeEveryDownloader(); 154 | 155 | this.accountImageDownloader.init(); 156 | }); 157 | 158 | } 159 | 160 | /** 161 | * Remove every downloader which might be active 162 | */ 163 | private removeEveryDownloader(): void { 164 | this.storyDownloader.remove(); 165 | this.postDownloader.remove(); 166 | this.accountImageDownloader.remove(); 167 | this.hotkeyDownloader.remove(); 168 | } 169 | } 170 | 171 | // tslint:disable-next-line:no-unused-expression 172 | new AddonManager(); 173 | -------------------------------------------------------------------------------- /src/ts/modles/extension.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (extension.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | import { AlertType } from '../components/Alert'; 8 | import { PostItem, ShortcodeMedia } from './post'; 9 | 10 | export interface DownloadMessage { 11 | imageURL: string[]; 12 | accountName: string; 13 | type: DownloadType; 14 | } 15 | 16 | export interface AlertMessage { 17 | text: string; 18 | type?: AlertType; 19 | dismissible?: boolean; 20 | timeout?: number; 21 | } 22 | 23 | export enum DownloadType { 24 | single, 25 | bulk 26 | } 27 | 28 | export interface ContentResponse { 29 | accountName: string; 30 | mediaURL: string[]; 31 | originalResponse: PostItem | ShortcodeMedia; 32 | } 33 | 34 | export type DownloadProgressType = 'download' | 'compression' 35 | 36 | export interface DownloadProgress { 37 | isLast: boolean; 38 | isFirst: boolean; 39 | percent: number; 40 | type: DownloadProgressType; 41 | } 42 | 43 | export enum LoggingLevel { 44 | default = 'log', 45 | warn = 'warn', 46 | error = 'error', 47 | } 48 | 49 | export interface Metadata { 50 | percent: number; 51 | currentFile: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/ts/modles/post.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (post.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | // tslint:disable:no-any 8 | 9 | export interface Dimensions { 10 | height: number; 11 | width: number; 12 | } 13 | 14 | export interface DisplayResource { 15 | src: string; 16 | config_width: number; 17 | config_height: number; 18 | } 19 | 20 | export interface DashInfo { 21 | is_dash_eligible: boolean; 22 | video_dash_manifest: string; 23 | number_of_qualities: number; 24 | } 25 | 26 | export interface EdgeMediaToTaggedUser { 27 | edges: any[]; 28 | } 29 | 30 | export interface Edge { 31 | node: Node; 32 | } 33 | 34 | export interface EdgeMediaToCaption { 35 | edges: Edge[]; 36 | } 37 | 38 | export interface PageInfo { 39 | has_next_page: boolean; 40 | end_cursor: string; 41 | } 42 | 43 | export interface Owner { 44 | id: string; 45 | is_verified: boolean; 46 | profile_pic_url: string; 47 | username: string; 48 | } 49 | 50 | export interface EdgeLikedBy { 51 | count: number; 52 | } 53 | 54 | export interface PageInfo2 { 55 | has_next_page: boolean; 56 | end_cursor: string; 57 | } 58 | 59 | export interface Owner2 { 60 | id: string; 61 | is_verified: boolean; 62 | profile_pic_url: string; 63 | username: string; 64 | } 65 | 66 | export interface EdgeLikedBy2 { 67 | count: number; 68 | } 69 | 70 | export interface Node3 { 71 | id: string; 72 | text: string; 73 | created_at: number; 74 | did_report_as_spam: boolean; 75 | owner: Owner2; 76 | viewer_has_liked: boolean; 77 | edge_liked_by: EdgeLikedBy2; 78 | is_restricted_pending: boolean; 79 | } 80 | 81 | export interface Edge3 { 82 | node: Node3; 83 | } 84 | 85 | export interface EdgeThreadedComments { 86 | count: number; 87 | page_info: PageInfo2; 88 | edges: Edge3[]; 89 | } 90 | 91 | export interface Node2 { 92 | id: string; 93 | text: string; 94 | created_at: number; 95 | did_report_as_spam: boolean; 96 | owner: Owner; 97 | viewer_has_liked: boolean; 98 | edge_liked_by: EdgeLikedBy; 99 | is_restricted_pending: boolean; 100 | edge_threaded_comments: EdgeThreadedComments; 101 | } 102 | 103 | export interface Edge2 { 104 | node: Node2; 105 | } 106 | 107 | export interface EdgeMediaToParentComment { 108 | count: number; 109 | page_info: PageInfo; 110 | edges: Edge2[]; 111 | } 112 | 113 | export interface EdgeMediaToHoistedComment { 114 | edges: any[]; 115 | } 116 | 117 | export interface Owner3 { 118 | id: string; 119 | is_verified: boolean; 120 | profile_pic_url: string; 121 | username: string; 122 | } 123 | 124 | export interface EdgeLikedBy3 { 125 | count: number; 126 | } 127 | 128 | export interface Node4 { 129 | id: string; 130 | text: string; 131 | created_at: number; 132 | did_report_as_spam: boolean; 133 | owner: Owner3; 134 | viewer_has_liked: boolean; 135 | edge_liked_by: EdgeLikedBy3; 136 | is_restricted_pending: boolean; 137 | } 138 | 139 | export interface Edge4 { 140 | node: Node4; 141 | } 142 | 143 | export interface EdgeMediaPreviewComment { 144 | count: number; 145 | edges: Edge4[]; 146 | } 147 | 148 | export interface EdgeMediaPreviewLike { 149 | count: number; 150 | edges: any[]; 151 | } 152 | 153 | export interface EdgeMediaToSponsorUser { 154 | edges: any[]; 155 | } 156 | 157 | export interface EdgeOwnerToTimelineMedia { 158 | count: number; 159 | } 160 | 161 | export interface Owner4 { 162 | id: string; 163 | is_verified: boolean; 164 | profile_pic_url: string; 165 | profile_pic_url_hd: string; 166 | username: string; 167 | blocked_by_viewer: boolean; 168 | restricted_by_viewer: boolean; 169 | followed_by_viewer: boolean; 170 | full_name: string; 171 | has_blocked_viewer: boolean; 172 | is_private: boolean; 173 | is_unpublished: boolean; 174 | requested_by_viewer: boolean; 175 | edge_owner_to_timeline_media: EdgeOwnerToTimelineMedia; 176 | } 177 | 178 | export interface EdgeWebMediaToRelatedMedia { 179 | edges: any[]; 180 | } 181 | 182 | export interface GraphqlQuery { 183 | graphql: { 184 | shortcode_media: ShortcodeMedia; 185 | }; 186 | } 187 | 188 | export interface Candidate { 189 | width: number; 190 | height: number; 191 | url: string; 192 | } 193 | 194 | export interface ImageVersions { 195 | candidates: Candidate[]; 196 | } 197 | 198 | export interface CarouselMedia { 199 | image_versions2: ImageVersions; 200 | video_versions?: Candidate[]; 201 | } 202 | 203 | export interface PostItem { 204 | carousel_media_count?: number; 205 | carousel_media?: CarouselMedia[]; 206 | image_versions2?: ImageVersions; 207 | user: Pick; 208 | video_versions?: Candidate[]; 209 | } 210 | 211 | export interface PostQuery { 212 | items: PostItem[]; 213 | } 214 | 215 | export interface ShortcodeMedia { 216 | __typename: 'GraphSidecar' | 'GraphImage' | 'GraphVideo'; 217 | id: string; 218 | shortcode: string; 219 | dimensions: Dimensions; 220 | gating_info?: any; 221 | fact_check_overall_rating?: any; 222 | fact_check_information?: any; 223 | sensitivity_friction_info?: any; 224 | media_preview: string; 225 | display_url: string; 226 | display_resources: DisplayResource[]; 227 | accessibility_caption?: any; 228 | dash_info: DashInfo; 229 | video_url: string; 230 | video_view_count: number; 231 | is_video: boolean; 232 | tracking_token: string; 233 | edge_media_to_tagged_user: EdgeMediaToTaggedUser; 234 | edge_media_to_caption: EdgeMediaToCaption; 235 | caption_is_edited: boolean; 236 | has_ranked_comments: boolean; 237 | edge_media_to_parent_comment: EdgeMediaToParentComment; 238 | edge_media_to_hoisted_comment: EdgeMediaToHoistedComment; 239 | edge_media_preview_comment: EdgeMediaPreviewComment; 240 | comments_disabled: boolean; 241 | commenting_disabled_for_viewer: boolean; 242 | taken_at_timestamp: number; 243 | edge_media_preview_like: EdgeMediaPreviewLike; 244 | edge_media_to_sponsor_user: EdgeMediaToSponsorUser; 245 | location?: any; 246 | viewer_has_liked: boolean; 247 | viewer_has_saved: boolean; 248 | viewer_has_saved_to_collection: boolean; 249 | viewer_in_photo_of_you: boolean; 250 | viewer_can_reshare: boolean; 251 | owner: Owner4; 252 | is_ad: boolean; 253 | edge_web_media_to_related_media: EdgeWebMediaToRelatedMedia; 254 | encoding_status?: any; 255 | is_published: boolean; 256 | product_type: string; 257 | title: string; 258 | video_duration: number; 259 | thumbnail_src: string; 260 | edge_sidecar_to_children: EdgeSidecarToChildren; 261 | 262 | } 263 | 264 | export interface EdgeSidecarToChildren { 265 | edges: Edge[]; 266 | } 267 | 268 | export interface Node { 269 | __typename: string; 270 | id: string; 271 | shortcode: string; 272 | dimensions: Dimensions; 273 | gating_info?: any; 274 | fact_check_overall_rating?: any; 275 | fact_check_information?: any; 276 | sensitivity_friction_info?: any; 277 | media_preview: string; 278 | display_url: string; 279 | display_resources: DisplayResource[]; 280 | accessibility_caption: string; 281 | is_video: boolean; 282 | video_url: string; 283 | tracking_token: string; 284 | edge_media_to_tagged_user: EdgeMediaToTaggedUser; 285 | } 286 | -------------------------------------------------------------------------------- /src/ts/modles/story.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (story.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | 8 | export interface User { 9 | id: string; 10 | profile_pic_url: string; 11 | username: string; 12 | } 13 | 14 | export interface Highlight { 15 | id: number; 16 | title: string; 17 | } 18 | 19 | export interface StoryResponse { 20 | user: User; 21 | highlight: Highlight; 22 | } 23 | -------------------------------------------------------------------------------- /src/ts/modles/typeguards.ts: -------------------------------------------------------------------------------- 1 | /**************************************************************************************** 2 | * Copyright (c) 2022. HuiiBuh * 3 | * This file (typeguards.ts) is part of InstagramDownloader which is not released * 4 | * under any licence. * 5 | * Any usage of this code outside this project is not allowed. * 6 | ****************************************************************************************/ 7 | import { AlertMessage, DownloadProgress } from './extension'; 8 | 9 | // tslint:disable-next-line:no-any 10 | export const isDownloadProgress = (message: DownloadProgress | AlertMessage): message is DownloadProgress => (message as any).percent !== undefined; 11 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "moduleResolution": "Node", 6 | "strict": false, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "experimentalDecorators": true, 15 | "module": "esnext", 16 | "importHelpers": true, 17 | "target": "ES6", 18 | "lib": [ 19 | "ESNext", 20 | "DOM", 21 | "DOM.Iterable", 22 | "es2015.proxy" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "ts-config", 3 | "rules": { 4 | "ordered-imports": true, 5 | "no-non-null-assertion": false, 6 | "no-inferrable-types": false, 7 | "file-name-casing": [ 8 | false, 9 | "kebab-case" 10 | ], 11 | "curly": { 12 | "options": "ignore-same-line" 13 | }, 14 | "variable-name": { 15 | "options": "allow-leading-underscore" 16 | }, 17 | "no-unused-expression": { 18 | "options": "allow-fast-null-checks" 19 | }, 20 | "newline-before-return": false, 21 | "member-ordering": { 22 | "options": { 23 | "order": [ 24 | "public-static-field", 25 | "protected-static-field", 26 | "private-static-field", 27 | "public-instance-field", 28 | "protected-instance-field", 29 | "private-instance-field", 30 | "constructor", 31 | "public-static-method", 32 | "protected-static-method", 33 | "private-static-method", 34 | "public-instance-method", 35 | "protected-instance-method", 36 | "private-instance-method" 37 | ] 38 | } 39 | } 40 | }, 41 | "linterOptions": { 42 | "exclude": [ 43 | "**/lib/**", 44 | "**/node_modules/**" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. HuiiBuh 3 | * This file (webpack.config.js) is part of InstagramDownloader which is released under 4 | * GNU LESSER GENERAL PUBLIC LICENSE. 5 | * You are not allowed to use this code or this file for another project without 6 | * linking to the original source AND open sourcing your code. 7 | */ 8 | 9 | const path = require('path'); 10 | const webpack = require('webpack'); 11 | 12 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 13 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 14 | const BuildExtensionPlugin = require('./src/build'); 15 | 16 | const webpackConfig = { 17 | node: { 18 | global: false, 19 | }, 20 | entry: { 21 | extension: "/src/ts/index.ts", 22 | background: "/src/ts/background/BackgroundMessageHandler.ts", 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.ts$/, 28 | use: 'ts-loader', 29 | exclude: /node_modules/, 30 | }, 31 | { 32 | test: /\.scss$/, 33 | use: [ 34 | // Extract and save the final CSS. 35 | MiniCssExtractPlugin.loader, 36 | // Load the CSS, set url = false to prevent following urls to fonts and images. 37 | {loader: "css-loader", options: {url: false, importLoaders: 1}}, 38 | // Load the SCSS/SASS 39 | {loader: 'sass-loader'}, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.ts', '.js'], 46 | }, 47 | output: { 48 | filename: 'js/[name].js', 49 | path: path.resolve(__dirname, 'dist'), 50 | }, 51 | plugins: [ 52 | new MiniCssExtractPlugin({ 53 | filename: 'css/[name].css', 54 | chunkFilename: '[id].css', 55 | }), 56 | new HtmlWebpackPlugin({ 57 | template: 'src/options/options.html', 58 | filename: "options.html", 59 | excludeChunks: ["extension", "background"], 60 | }), 61 | new webpack.ProvidePlugin({ 62 | global: require.resolve('./src/global.js'), 63 | }), 64 | new BuildExtensionPlugin(), 65 | ], 66 | }; 67 | 68 | 69 | module.exports = (env, argv) => { 70 | webpackConfig.devtool = 'inline-source-map'; 71 | if (argv.mode === "production") { 72 | webpackConfig.plugins.push( 73 | new webpack.DefinePlugin({ 74 | PRODUCTION: JSON.stringify(true), 75 | }), 76 | ); 77 | } else { 78 | webpackConfig.plugins.push( 79 | new webpack.DefinePlugin({ 80 | PRODUCTION: JSON.stringify(false), 81 | }), 82 | ); 83 | } 84 | return webpackConfig; 85 | }; 86 | --------------------------------------------------------------------------------