├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── build.js ├── eslint.config.mjs ├── jsconfig.json ├── locale ├── manifest.json └── translations │ ├── es.json │ ├── fr.json │ ├── it.json │ ├── pt.json │ ├── ro.json │ ├── zh-CN.json │ └── zh-TW.json ├── main.js ├── package.json ├── src ├── entry.js ├── events.js ├── functions │ ├── highlight.js │ ├── post.js │ ├── profile.js │ ├── reel.js │ └── story.js ├── initial.js ├── metadata.js ├── settings.js ├── timer.js └── utils │ ├── api.js │ ├── general.js │ └── i18n.js └── style.css /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: snkoarashi 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Userscript File 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | if: contains(github.event.head_commit.message, 'Automated Build PR') == false 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: "20" 25 | 26 | - name: Setup GitHub CLI 27 | run: | 28 | sudo apt-get install gh 29 | 30 | - name: Configure Git 31 | run: | 32 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 33 | git config --local user.name "github-actions[bot]" 34 | 35 | - name: Check if automated-build-branch exists and checkout 36 | run: | 37 | git fetch origin 38 | if git show-ref --quiet refs/heads/automated-build-branch; then 39 | git checkout automated-build-branch 40 | else 41 | git checkout -b automated-build-branch 42 | fi 43 | 44 | - name: Check if remote automated-build-branch exists and pull 45 | run: | 46 | if git ls-remote --heads origin automated-build-branch | grep -q automated-build-branch; then 47 | git pull --rebase --strategy-option theirs origin automated-build-branch || echo "There were conflicts that were automatically resolved using 'theirs'." 48 | else 49 | echo "Remote branch automated-build-branch does not exist." 50 | fi 51 | 52 | - name: Install dependencies 53 | run: npm install 54 | 55 | - name: Run build script 56 | run: node ./build.js 57 | 58 | - name: Run ESLint 59 | run: npx eslint main.js --quiet 60 | 61 | - name: Check for changes and merge master 62 | run: | 63 | if [ -n "$(git status --porcelain)" ]; then 64 | git add . 65 | git commit -m "Automated build commit" 66 | git fetch origin master 67 | echo "Merging origin/master with automated-build-branch using '-X ours' to resolve conflicts" 68 | git merge -s recursive -X ours origin/master --allow-unrelated-histories || true 69 | git push origin automated-build-branch 70 | gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}" 71 | gh pr create --base master --head automated-build-branch --title "Automated Build PR" --body "This PR is automatically generated by GitHub Actions." 72 | else 73 | echo "No changes to commit" 74 | exit 0 75 | fi 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Build File", 8 | "skipFiles": ["/**"], 9 | "program": "${workspaceFolder}/build.js", 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/node_modules": true, 4 | "/main.js": true 5 | }, 6 | "search.exclude": { 7 | "**/node_modules": true, 8 | "/main.js": true 9 | }, 10 | "cSpell.diagnosticLevel": "Hint" 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram Download Helper (IG Helper) 2 |
3 | 4 | [![](https://img.shields.io/badge/Kofi-F16061.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/F1F1J6VZH) 5 | [![](https://img.shields.io/discord/505936774531514388?color=5865F3&logo=discord&logoColor=white)](https://discord.gg/q3KT4hdq8x) 6 | 7 | Buy me a coffee, Donate to ig-helper if it makes your help. Your donation makes ig-helper better: [https://ko-fi.com/snkoarashi](https://ko-fi.com/snkoarashi) 8 |
9 | 10 | **Simple and fast download of resources from Instagram (posts, reels, stories, and more).** 11 | 12 | ## 🎉 Features 13 | - Force the fetching of all resources in the post. 14 | - Download the resources in the post with one click. 15 | - Download the resources currently displayed in the post with one click. 16 | - Open the resource in a new window. 17 | - Download the video thumbnail. 18 | - Fetch high-quality photos or videos through the Media API. 19 | - Disable video looping. 20 | - Enable the native HTML5 video controller for video resources. 21 | - Download the user's profile picture. 22 | - Provide scroll buttons for Reels pages. 23 | - Automatically modify and control the playback volume of all video elements. 24 | - Redirect to a user's profile page when clicking on their avatar in the story area on the homepage. 25 | - Customize the naming format when downloading according to your preferences. 26 | 27 | ## ⚙ How to Change Settings 28 | 1. Go to Instagram.com. 29 | 2. Check your Tampermonkey extension. 30 | 3. See the following: 31 | 32 | 33 | ## 📌 Hot keys 34 | - `ALT+Q` - Close pop-up window 35 | - `ALT+W` - Open the settings menu 36 | - `ALT+Z` - Open the debug menu 37 | - `ALT+S` - Download resource in story page 38 | 39 | ## 📢 Developer Statement 40 | 1. All code development and testing are based on the Chrome browser and the extension Tampermonkey. 41 | 2. Due to the framework and personal differences used in Instagram development, the page layout and node names presented by each person may be different. Therefore, all development and testing are based on the pages I have seen, which may cause a few people to The script does not work, please forgive me. 42 | 3. Extensive use of this script, especially when enabling the "Media API" or "Force Fetch API," may trigger Instagram's automation bot checks, potentially leading to account warnings, being logged out, or even being banned. Please use it with caution. 43 | 44 | > [!IMPORTANT] 45 | > On GreasyFork: https://greasyfork.org/scripts/404535-ig-helper 46 | > 47 | > The extensions we support and test is Tampermonkey and make sure that you are downloaded the script from GreasyFork. 48 | 49 | ## ✨ Development Guide 50 | This guide provides comprehensive documentation for developers who want to understand, modify, or contribute to the IG Helper codebase. It covers the build system, development environment setup, and contribution workflow. 51 | 52 | ### Development Environment Setup 53 | #### Prerequisites 54 | - Node.js (v20 or newer) 55 | - A userscript manager (Tampermonkey, Greasemonkey, or Violentmonkey) 56 | 57 | #### Setup Steps 58 | 1. Clone the repository: 59 | ``` 60 | git clone https://github.com/SN-Koarashi/ig-helper.git 61 | cd ig-helper 62 | ``` 63 | 2. Install dependencies: 64 | ``` 65 | npm install 66 | ``` 67 | 3. Build the script: 68 | ``` 69 | node build.js 70 | ``` 71 | Install the generated main.js file in your userscript manager. 72 | 73 | ### Contribution Workflow 74 | 75 | #### Development Process 76 | 1. **Fork & Clone**: Fork the repository and clone it locally 77 | 2. **Branch**: Create a feature branch 78 | 3. **Develop**: Make changes to the source files 79 | 4. **Build**: Run node build.js to generate the `main.js` file 80 | 5. **Test**: Install the script in your userscript manager and test your changes 81 | 6. **Submit**: Create a pull request 82 | 83 | #### Code Standards 84 | - Follow the existing code style 85 | - Use ESLint to ensure code quality: npx eslint `main.js` 86 | - Document new functions and features 87 | 88 | ## Contributiner 89 | ### [@Yomisana](https://github.com/yomisana) 90 | - Menu design suggestions and batch-download ideas 91 | - Enhancement suggestions 92 | 93 | ### [@sn-o-w](https://github.com/sn-o-w) 94 | - Text translation (Romanian) 95 | - Miscellaneous optimizations and enhancements, plus code debugging 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const readline = require("readline"); 4 | 5 | const inputRootPath = path.join(__dirname, 'src'); 6 | const inputFilePath = "./src/entry.js"; 7 | const outputFilePath = "./main.js"; 8 | 9 | const inputStream = fs.createReadStream(inputFilePath); 10 | const outputStream = fs.createWriteStream(outputFilePath); 11 | 12 | const rl = readline.createInterface({ 13 | input: inputStream, 14 | output: null, 15 | terminal: false, 16 | }); 17 | 18 | const processImportedFile = (full_path) => { 19 | return new Promise((resolve) => { 20 | const importedStream = fs.createReadStream(full_path); 21 | const importedRl = readline.createInterface({ 22 | input: importedStream, 23 | output: null, 24 | terminal: false, 25 | }); 26 | var startingCatch = false; 27 | 28 | importedRl.on("line", (importedLine) => { 29 | if (startingCatch || path.basename(full_path) === "metadata.js") { 30 | if (importedLine.trim() === "") { 31 | outputStream.write("\n"); 32 | } 33 | else { 34 | var prefix = ""; 35 | if (path.basename(full_path) !== "metadata.js") { 36 | prefix = " "; 37 | } 38 | outputStream.write(prefix + importedLine.replace(/^(export )/i, "") + "\n"); 39 | } 40 | } 41 | 42 | /*! ESLINT IMPORT END !*/ 43 | const cuttingPattern = importedLine.trim().match(/^(\/\*\!)(.*?)(\!\*\/)$/i); 44 | if (!startingCatch && cuttingPattern != null && cuttingPattern.at(2).trim().includes("ESLINT IMPORT END")) { 45 | startingCatch = true; 46 | } 47 | }); 48 | 49 | importedRl.on("close", () => { 50 | resolve(); 51 | }); 52 | }); 53 | }; 54 | 55 | const processLines = async () => { 56 | for await (const line of rl) { 57 | if (line.trim().startsWith("FS_IMPORT")) { 58 | const filter_path = line.trim().match(/^FS_IMPORT\(['"]?(.*?)['"]?\);?$/i); 59 | if (filter_path !== null && filter_path.length > 0) { 60 | const full_path = path.join(inputRootPath, filter_path[1]); 61 | 62 | await processImportedFile(full_path); 63 | } 64 | } 65 | else { 66 | outputStream.write(line + "\n"); 67 | } 68 | } 69 | }; 70 | 71 | processLines().then(() => { 72 | outputStream.end(); 73 | console.log("File concat done."); 74 | }).catch(err => { 75 | console.error("Error processing lines:", err); 76 | }); -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | /** @type {import('eslint').Linter.Config[]} */ 5 | export default [ 6 | { 7 | files: ["**/*.js"], 8 | languageOptions: { 9 | globals: { 10 | // ** 11 | ...globals.browser, 12 | $: 'readonly', 13 | jQuery: 'readonly', 14 | piexif: 'readonly', 15 | GM_info: 'readable', 16 | GM_addStyle: 'readable', 17 | GM_setValue: 'readable', 18 | GM_getValue: 'readable', 19 | GM_xmlhttpRequest: 'readable', 20 | GM_registerMenuCommand: 'readable', 21 | GM_unregisterMenuCommand: 'readable', 22 | GM_getResourceText: 'readable', 23 | GM_notification: 'readable', 24 | GM_openInTab: 'readable', 25 | FS_IMPORT: 'readable' 26 | }, 27 | }, 28 | }, 29 | { 30 | files: ["build.js"], 31 | sourceType: "commonjs", 32 | languageOptions: { 33 | globals: { 34 | ...globals.browser, 35 | ...globals.node 36 | } 37 | } 38 | }, 39 | pluginJs.configs.recommended, 40 | ]; 41 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ] 9 | } -------------------------------------------------------------------------------- /locale/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "en-US": "English", 3 | "es": "Español (Spanish)", 4 | "fr": "Français (French)", 5 | "it": "Italiano (Italian)", 6 | "pt": "Português (Portuguese)", 7 | "ro": "Română (Romanian)", 8 | "zh-CN": "简体中文 (Simplified Chinese)", 9 | "zh-TW": "繁體中文 (Traditional Chinese)" 10 | } 11 | -------------------------------------------------------------------------------- /locale/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "¡Wololo! Nueva versión disponible.", 3 | "NOTICE_UPDATE_CONTENT": "IG-Helper ha lanzado una nueva versión, haz clic aquí para actualizar.", 4 | "CHECK_UPDATE": "Comprobar actualizaciones del script", 5 | "CHECK_UPDATE_MENU": "Comprobar actualizaciones", 6 | "CHECK_UPDATE_INTRO": "Comprobar actualizaciones cuando se ejecuta el script (comprueba cada 300 segundos).\nLas notificaciones de actualización se enviarán como notificaciones de escritorio a través del navegador.", 7 | "RELOAD_SCRIPT": "Recargar script", 8 | "DONATE": "Donar", 9 | "FEEDBACK": "Comentarios", 10 | "IMAGE_VIEWER": "Abrir imagen en el visor", 11 | "NEW_TAB": "Abrir en una pestaña nueva", 12 | "SHOW_DOM_TREE": "Mostrar árbol DOM", 13 | "SELECT_AND_COPY": "Seleccionar todo y copiar desde el cuadro de entrada", 14 | "DOWNLOAD_DOM_TREE": "Descargar árbol DOM como un archivo de texto", 15 | "REPORT_GITHUB": "Reportar un problema en GitHub", 16 | "REPORT_DISCORD": "Reportar un problema en el servidor de soporte de Discord", 17 | "REPORT_FORK": "Reportar un problema en Greasy Fork", 18 | "DEBUG": "Ventana de depuración", 19 | "CLOSE": "Cerrar", 20 | "ALL_CHECK": "Seleccionar todo", 21 | "BATCH_DOWNLOAD_SELECTED": "Descargar recursos seleccionados", 22 | "BATCH_DOWNLOAD_DIRECT": "Descargar todos los recursos", 23 | "IMG": "Imagen", 24 | "VID": "Vídeo", 25 | "DW": "Descargar", 26 | "DW_ALL": "Descargar todos los recursos", 27 | "THUMBNAIL_INTRO": "Descargar miniatura del vídeo", 28 | "LOAD_BLOB_ONE": "Cargando el medio Blob...", 29 | "LOAD_BLOB_MULTIPLE": "Cargando el medio Blob y otros...", 30 | "LOAD_BLOB_RELOAD": "Detectando el medio Blob, recargando...", 31 | "NO_CHECK_RESOURCE": "Necesitas seleccionar un recurso para descargar.", 32 | "NO_VID_URL": "No se puede encontrar la URL del vídeo.", 33 | "SETTING": "Ajustes", 34 | "AUTO_RENAME": "Renombrar archivos automáticamente (clic derecho para configurar)", 35 | "RENAME_SHORTCODE": "Renombrar el archivo e incluir el código corto", 36 | "RENAME_PUBLISH_DATE": "Establecer la marca de tiempo de los archivos renombrados a la fecha de publicación del recurso", 37 | "RENAME_LOCATE_DATE": "Modificar el formato de fecha de la marca de tiempo de los archivos renombrados (clic derecho para configurar)", 38 | "DISABLE_VIDEO_LOOPING": "Desactivar la reproducción automática en bucle de los vídeos", 39 | "HTML5_VIDEO_CONTROL": "Mostrar el controlador de vídeo HTML5", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirigir al hacer clic en la foto de perfil en la historia del usuario", 41 | "FORCE_FETCH_ALL_RESOURCES": "Forzar la obtención de todos los recursos en la publicación", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Descargar directamente los recursos visibles en la publicación", 43 | "DIRECT_DOWNLOAD_ALL": "Descargar directamente todos los recursos en la publicación", 44 | "MODIFY_VIDEO_VOLUME": "Modificar el volumen del vídeo (clic derecho para configurar)", 45 | "SCROLL_BUTTON": "Habilitar botones de desplazamiento para la página de Reels", 46 | "FORCE_RESOURCE_VIA_MEDIA": "Forzar la obtención de recursos a través de la API de Media", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Usar métodos alternativos para descargar cuando la API de Media no es accesible", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "\"Abrir en una pestaña nueva\" en las publicaciones siempre usa la API de Media", 49 | "AUTO_RENAME_INTRO": "Renombrar archivos automáticamente a un formato personalizado\nLista de formatos personalizados: \n%USERNAME% - Nombre de usuario\n%SOURCE_TYPE% - Tipo de fuente\n%SHORTCODE% - Código corto de la publicación\n%YEAR% - Año de descarga/publicación\n%2-YEAR% - Año (últimos dos dígitos) de descarga/publicación\n%MONTH% - Mes de descarga/publicación\n%DAY% - Día de descarga/publicación\n%HOUR% - Hora de descarga/publicación\n%MINUTE% - Minuto de descarga/publicación\n%SECOND% - Segundo de descarga/publicación\n%ORIGINAL_NAME% - Nombre original del archivo descargado\n%ORIGINAL_NAME_FIRST% - Nombre original del archivo descargado (primera parte del nombre)\n\nSi se establece en falso, el nombre del archivo permanecerá sin cambios.\nEjemplo: instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "Renombrar archivos automáticamente con el siguiente formato:\nNOMBRE_DE_USUARIO-TIPO-CODIGO_CORTO-MARCA_DE_TIEMPO.TIPO_ARCHIVO\nEjemplo: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nEsto SOLO funcionará si [Renombrar archivos automáticamente] está establecido en VERDADERO.", 51 | "RENAME_PUBLISH_DATE_INTRO": "Establece la marca de tiempo en el formato de renombramiento de archivos a la fecha de publicación del recurso (zona horaria del navegador).\n\nEsta función solo funciona cuando [Renombrar archivos automáticamente] está establecido en VERDADERO.", 52 | "RENAME_LOCATE_DATE_INTRO": "Modificar el formato de fecha de la marca de tiempo del archivo renombrado a la hora local del navegador y formatearlo a su formato de fecha regional preferido.\n\nEsta función solo funciona cuando [Renombrar archivos automáticamente] está establecido en VERDADERO.", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "Desactivar la reproducción automática en bucle de los vídeos en Reels y publicaciones.", 54 | "HTML5_VIDEO_CONTROL_INTRO": "Mostrar el controlador de vídeo HTML5 en el recurso de vídeo.\n\nEsto ocultará el control deslizante de volumen de vídeo personalizado y lo reemplazará con el controlador HTML5. El controlador HTML5 se puede ocultar haciendo clic derecho en el vídeo para revelar los detalles originales.", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Redirigir a la página de perfil de un usuario al hacer clic derecho en su avatar en el área de historias en la página de inicio.\nSi usa el botón central del ratón para hacer clic, se abrirá en una nueva pestaña.", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Forzar la obtención de todos los recursos (fotos y vídeos) en una publicación a través de la API de Instagram para eliminar el límite de tres recursos por publicación.", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Descargar directamente los recursos actuales disponibles en la publicación.", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "Al hacer clic en el botón de descarga, todos los recursos de la publicación se obtendrán y descargarán directamente a la fuerza.", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "Modificar el volumen de reproducción de vídeo en Reels y publicaciones (haga clic derecho para abrir el control deslizante de configuración de volumen).", 60 | "SCROLL_BUTTON_INTRO": "Habilitar botones de desplazamiento para la esquina inferior derecha de la página de Reels.", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "La API de Media intentará obtener la foto o el vídeo de mayor calidad posible, pero puede tardar más en cargarse.", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "Cuando la API de Media alcanza su límite de tasa o no se puede usar por otras razones, se utilizará la API de obtención forzada para descargar recursos (la calidad del recurso puede ser ligeramente inferior).", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "El botón [Abrir en una pestaña nueva] en las publicaciones siempre usará la API de Media para obtener recursos de alta resolución.", 64 | "SKIP_VIEW_STORY_CONFIRM": "Omitir la página de confirmación al ver una historia o un contenido destacado", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Omitir automáticamente cuando se muestra la página de confirmación en la historia o el contenido destacado.", 66 | "MODIFY_RESOURCE_EXIF": "Modificar los atributos EXIF del recurso", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "Modificar los atributos EXIF del recurso de imagen para colocar el enlace de la publicación en él." 68 | } -------------------------------------------------------------------------------- /locale/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "Wololo ! Nouvelle version disponible.", 3 | "NOTICE_UPDATE_CONTENT": "IG-Helper a publié une nouvelle version, cliquez ici pour mettre à jour.", 4 | "CHECK_UPDATE": "Vérifier les mises à jour du script", 5 | "CHECK_UPDATE_MENU": "Vérifier les mises à jour", 6 | "CHECK_UPDATE_INTRO": "Vérifier les mises à jour lorsque le script est déclenché (vérification toutes les 300 secondes).\nLes notifications de mise à jour seront envoyées en tant que notifications de bureau via le navigateur.", 7 | "RELOAD_SCRIPT": "Recharger le script", 8 | "DONATE": "Faire un don", 9 | "FEEDBACK": "Commentaires", 10 | "IMAGE_VIEWER": "Ouvrir l'image dans le visionneur", 11 | "NEW_TAB": "Ouvrir dans un nouvel onglet", 12 | "SHOW_DOM_TREE": "Afficher l'arborescence DOM", 13 | "SELECT_AND_COPY": "Tout sélectionner et copier depuis la zone de saisie", 14 | "DOWNLOAD_DOM_TREE": "Télécharger l'arborescence DOM en tant que fichier texte", 15 | "REPORT_GITHUB": "Signaler un problème sur GitHub", 16 | "REPORT_DISCORD": "Signaler un problème sur le serveur d'assistance Discord", 17 | "REPORT_FORK": "Signaler un problème sur Greasy Fork", 18 | "DEBUG": "Fenêtre de débogage", 19 | "CLOSE": "Fermer", 20 | "ALL_CHECK": "Tout sélectionner", 21 | "BATCH_DOWNLOAD_SELECTED": "Télécharger les ressources sélectionnées", 22 | "BATCH_DOWNLOAD_DIRECT": "Télécharger toutes les ressources", 23 | "IMG": "Image", 24 | "VID": "Vidéo", 25 | "DW": "Télécharger", 26 | "DW_ALL": "Télécharger toutes les ressources", 27 | "THUMBNAIL_INTRO": "Télécharger la miniature de la vidéo", 28 | "LOAD_BLOB_ONE": "Chargement du média Blob...", 29 | "LOAD_BLOB_MULTIPLE": "Chargement du média Blob et autres...", 30 | "LOAD_BLOB_RELOAD": "Détection du média Blob, rechargement...", 31 | "NO_CHECK_RESOURCE": "Vous devez sélectionner une ressource à télécharger.", 32 | "NO_VID_URL": "Impossible de trouver l'URL de la vidéo.", 33 | "SETTING": "Paramètres", 34 | "AUTO_RENAME": "Renommer automatiquement les fichiers (clic droit pour configurer)", 35 | "RENAME_SHORTCODE": "Renommer le fichier et inclure le code court", 36 | "RENAME_PUBLISH_DATE": "Définir l'horodatage des fichiers renommés à la date de publication de la ressource", 37 | "RENAME_LOCATE_DATE": "Modifier le format de date de l'horodatage des fichiers renommés (clic droit pour configurer)", 38 | "DISABLE_VIDEO_LOOPING": "Désactiver la lecture automatique en boucle des vidéos", 39 | "HTML5_VIDEO_CONTROL": "Afficher le contrôleur vidéo HTML5", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Rediriger lors d'un clic sur la photo de profil dans la story de l'utilisateur", 41 | "FORCE_FETCH_ALL_RESOURCES": "Forcer la récupération de toutes les ressources du post", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Télécharger directement les ressources visibles dans le post", 43 | "DIRECT_DOWNLOAD_ALL": "Télécharger directement toutes les ressources du post", 44 | "MODIFY_VIDEO_VOLUME": "Modifier le volume de la vidéo (clic droit pour configurer)", 45 | "SCROLL_BUTTON": "Activer les boutons de défilement pour la page des Reels", 46 | "FORCE_RESOURCE_VIA_MEDIA": "Forcer la récupération des ressources via l'API Media", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Utiliser des méthodes alternatives de téléchargement lorsque l'API Media n'est pas accessible", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "« Ouvrir dans un nouvel onglet » dans les posts utilise toujours l'API Media", 49 | "AUTO_RENAME_INTRO": "Renommer automatiquement les fichiers dans un format personnalisé\nListe des formats personnalisés : \n%USERNAME% - Nom d'utilisateur\n%SOURCE_TYPE% - Type de source\n%SHORTCODE% - Code court du post\n%YEAR% - Année de téléchargement/publication\n%2-YEAR% - Année (deux derniers chiffres) de téléchargement/publication\n%MONTH% - Mois de téléchargement/publication\n%DAY% - Jour de téléchargement/publication\n%HOUR% - Heure de téléchargement/publication\n%MINUTE% - Minute de téléchargement/publication\n%SECOND% - Seconde de téléchargement/publication\n%ORIGINAL_NAME% - Nom original du fichier téléchargé\n%ORIGINAL_NAME_FIRST% - Nom original du fichier téléchargé (première partie du nom)\n\nSi défini sur faux, le nom du fichier restera inchangé.\nExemple : instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "Renommer automatiquement les fichiers avec le format suivant :\nNOM_UTILISATEUR-TYPE-CODE_COURT-HORODATAGE.TYPE_FICHIER\nExemple : instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nFonctionne UNIQUEMENT si [Renommer automatiquement les fichiers] est défini sur VRAI.", 51 | "RENAME_PUBLISH_DATE_INTRO": "Définit l'horodatage dans le format de renommage de fichier à la date de publication de la ressource (fuseau horaire du navigateur).\n\nFonctionne UNIQUEMENT si [Renommer automatiquement les fichiers] est défini sur VRAI.", 52 | "RENAME_LOCATE_DATE_INTRO": "Modifier le format de date de l'horodatage de renommage de fichier à l'heure locale du navigateur et le formater selon le format de date régional souhaité.\n\nFonctionne UNIQUEMENT si [Renommer automatiquement les fichiers] est défini sur VRAI.", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "Désactiver la lecture automatique en boucle des vidéos dans les Reels et les posts.", 54 | "HTML5_VIDEO_CONTROL_INTRO": "Afficher le contrôleur vidéo HTML5 dans la ressource vidéo.\n\nCela masquera le curseur de volume vidéo personnalisé et le remplacera par le contrôleur HTML5. Le contrôleur HTML5 peut être masqué en faisant un clic droit sur la vidéo pour révéler les détails d'origine.", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Rediriger vers la page de profil d'un utilisateur en faisant un clic droit sur son avatar dans la zone des stories sur la page d'accueil.\nSi vous utilisez le bouton central de la souris pour cliquer, cela s'ouvrira dans un nouvel onglet.", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Forcer la récupération de toutes les ressources (photos et vidéos) d'un post via l'API Instagram pour supprimer la limite de trois ressources par post.", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Télécharger directement les ressources actuelles disponibles dans le post.", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "Lorsque vous cliquez sur le bouton de téléchargement, toutes les ressources du post seront directement forcées d'être récupérées et téléchargées.", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "Modifier le volume de lecture vidéo dans les Reels et les posts (clic droit pour ouvrir le curseur de réglage du volume).", 60 | "SCROLL_BUTTON_INTRO": "Activer les boutons de défilement pour le coin inférieur droit de la page des Reels.", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "L'API Media essaiera d'obtenir la meilleure qualité possible pour les photos ou les vidéos, mais le chargement prendra plus de temps.", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "Lorsque l'API Media atteint sa limite de taux ou ne peut pas être utilisée pour d'autres raisons, l'API de récupération forcée sera utilisée pour télécharger les ressources (la qualité des ressources peut être légèrement inférieure).", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "Le bouton [Ouvrir dans un nouvel onglet] dans les posts utilisera toujours l'API Media pour obtenir des ressources en haute résolution.", 64 | "SKIP_VIEW_STORY_CONFIRM": "Ignorer la page de confirmation lors de la visualisation d'une story ou d'un contenu à la une", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Ignorer automatiquement lorsque la page de confirmation est affichée dans la story ou le contenu à la une.", 66 | "MODIFY_RESOURCE_EXIF": "Modifier les attributs EXIF de la ressource", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "Modifier les attributs EXIF de la ressource image afin de placer le lien du post à l'intérieur." 68 | } -------------------------------------------------------------------------------- /locale/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "Wololo! Nuova versione rilasciata.", 3 | "NOTICE_UPDATE_CONTENT": "IG-Helper ha rilasciato una nuova versione, clicca qui per aggiornare.", 4 | "CHECK_UPDATE": "Controllo aggiornamenti per lo script", 5 | "CHECK_UPDATE_MENU": "Controllo aggiornamenti", 6 | "CHECK_UPDATE_INTRO": "Controlla gli aggiornamenti quando lo script viene attivato (controlla ogni 300 secondi).\nLe notifiche di aggiornamento saranno inviate come notifiche desktop tramite il browser.", 7 | "RELOAD_SCRIPT": "Ricarica lo script", 8 | "DONATE": "Dona", 9 | "FEEDBACK": "Feedback", 10 | "IMAGE_VIEWER": "Apri immagine nel visualizzatore", 11 | "NEW_TAB": "Apri in una nuova scheda", 12 | "SHOW_DOM_TREE": "Mostra albero DOM", 13 | "SELECT_AND_COPY": "Seleziona tutto e copia dalla casella di input", 14 | "DOWNLOAD_DOM_TREE": "Scarica albero DOM come file di testo", 15 | "REPORT_GITHUB": "Segnala un problema su GitHub", 16 | "REPORT_DISCORD": "Segnala un problema sul server di supporto Discord", 17 | "REPORT_FORK": "Segnala un problema su Greasy Fork", 18 | "DEBUG": "Finestra di debug", 19 | "CLOSE": "Chiudi", 20 | "ALL_CHECK": "Seleziona tutti", 21 | "BATCH_DOWNLOAD_SELECTED": "Scarica le risorse selezionate", 22 | "BATCH_DOWNLOAD_DIRECT": "Scarica tutte le risorse", 23 | "IMG": "Immagine", 24 | "VID": "Video", 25 | "DW": "Scarica", 26 | "DW_ALL": "Scarica tutte le risorse", 27 | "THUMBNAIL_INTRO": "Scarica la miniatura del video", 28 | "LOAD_BLOB_ONE": "Caricamento del contenuto multimediale in formato Blob...", 29 | "LOAD_BLOB_MULTIPLE": "Caricamento dei contenuti multimediali in formato Blob e altri...", 30 | "LOAD_BLOB_RELOAD": "Rilevamento del contenuto multimediale in formato Blob, ricaricamento...", 31 | "NO_CHECK_RESOURCE": "È necessario selezionare una risorsa da scaricare.", 32 | "NO_VID_URL": "Impossibile trovare l'URL del video.", 33 | "SETTING": "Impostazioni", 34 | "AUTO_RENAME": "Rinomina automaticamente i file (clic destro per impostare)", 35 | "RENAME_SHORTCODE": "Rinomina il file e includi il codice breve", 36 | "RENAME_PUBLISH_DATE": "Imposta il timestamp dei file rinominati alla data di pubblicazione della risorsa", 37 | "RENAME_LOCATE_DATE": "Modifica il formato della data del timestamp per i file rinominati (clic destro per impostare)", 38 | "DISABLE_VIDEO_LOOPING": "Disabilita la riproduzione automatica in loop dei video", 39 | "HTML5_VIDEO_CONTROL": "Mostra il controller video HTML5", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Reindirizza quando si fa clic sulla foto del profilo nella storia dell'utente", 41 | "FORCE_FETCH_ALL_RESOURCES": "Forza il recupero di tutte le risorse nel post", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Scarica direttamente le risorse visibili nel post", 43 | "DIRECT_DOWNLOAD_ALL": "Scarica direttamente tutte le risorse nel post", 44 | "MODIFY_VIDEO_VOLUME": "Modifica il volume del video (clic destro per impostare)", 45 | "SCROLL_BUTTON": "Abilita i pulsanti di scorrimento per la pagina dei Reels", 46 | "FORCE_RESOURCE_VIA_MEDIA": "Forza il recupero delle risorse tramite l'API Media", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Utilizza metodi alternativi per scaricare quando l'API Media non è accessibile", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "\"Apri in una nuova scheda\" nei post utilizza sempre l'API Media", 49 | "AUTO_RENAME_INTRO": "Rinomina automaticamente i file in un formato personalizzato\nElenco dei formati personalizzati: \n%USERNAME% - Nome utente\n%SOURCE_TYPE% - Tipo di sorgente\n%SHORTCODE% - Codice breve del post\n%YEAR% - Anno di download/pubblicazione\n%2-YEAR% - Anno (ultime due cifre) di download/pubblicazione\n%MONTH% - Mese di download/pubblicazione\n%DAY% - Giorno di download/pubblicazione\n%HOUR% - Ora di download/pubblicazione\n%MINUTE% - Minuto di download/pubblicazione\n%SECOND% - Secondo di download/pubblicazione\n%ORIGINAL_NAME% - Nome originale del file scaricato\n%ORIGINAL_NAME_FIRST% - Nome originale del file scaricato (prima parte del nome)\n\nSe impostato su falso, il nome del file rimarrà invariato.\nEsempio: instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "Rinomina automaticamente i file con il seguente formato:\nNOME_UTENTE-TIPO-CODICE_BREVE-TIMESTAMP.TIPO_FILE\nEsempio: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nFunziona SOLO se [Rinomina automaticamente i file] è impostato su VERO.", 51 | "RENAME_PUBLISH_DATE_INTRO": "Imposta il timestamp nel formato di ridenominazione del file alla data di pubblicazione della risorsa (fuso orario del browser).\n\nFunziona SOLO se [Rinomina automaticamente i file] è impostato su VERO.", 52 | "RENAME_LOCATE_DATE_INTRO": "Modifica il formato della data del timestamp di ridenominazione del file all'ora locale del browser e formattalo nel formato data regionale desiderato.\n\nFunziona SOLO se [Rinomina automaticamente i file] è impostato su VERO.", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "Disattiva la riproduzione automatica in loop dei video nei Reels e nei post.", 54 | "HTML5_VIDEO_CONTROL_INTRO": "Mostra il controller video HTML5 nella risorsa video.\n\nQuesto nasconderà il cursore del volume video personalizzato e lo sostituirà con il controller HTML5. Il controller HTML5 può essere nascosto facendo clic con il pulsante destro del mouse sul video per rivelare i dettagli originali.", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Reindirizza alla pagina del profilo di un utente quando si fa clic con il pulsante destro del mouse sul suo avatar nell'area delle storie sulla homepage.\nSe si utilizza il pulsante centrale del mouse per fare clic, si aprirà in una nuova scheda.", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Forza il recupero di tutte le risorse (foto e video) in un post tramite l'API di Instagram per rimuovere il limite di tre risorse per post.", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Scarica direttamente le risorse correnti disponibili nel post.", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "Quando si preme il pulsante di download, tutte le risorse nel post verranno forzate direttamente per essere recuperate e scaricate.", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "Modifica il volume di riproduzione del video nei Reels e nei post (fare clic con il pulsante destro del mouse per aprire il cursore di impostazione del volume).", 60 | "SCROLL_BUTTON_INTRO": "Abilita i pulsanti di scorrimento per l'angolo in basso a destra della pagina dei Reels.", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "L'API Media proverà a ottenere la massima qualità possibile per foto o video, ma il caricamento richiederà più tempo.", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "Quando l'API Media raggiunge il limite di richieste o non può essere utilizzata per altri motivi, l'API di recupero forzato verrà utilizzata per scaricare le risorse (la qualità delle risorse potrebbe essere leggermente inferiore).", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "Il pulsante [Apri in una nuova scheda] nei post utilizzerà sempre l'API Media per ottenere risorse ad alta risoluzione.", 64 | "SKIP_VIEW_STORY_CONFIRM": "Salta la pagina di conferma per la visualizzazione di una storia o di un contenuto in evidenza", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Salta automaticamente quando la pagina di conferma viene visualizzata nella storia o nel contenuto in evidenza.", 66 | "MODIFY_RESOURCE_EXIF": "Modifica le proprietà EXIF della risorsa", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "Modifica le proprietà EXIF della risorsa immagine per posizionare il link del post al suo interno." 68 | } -------------------------------------------------------------------------------- /locale/translations/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "Wololo! Nova versão lançada.", 3 | "NOTICE_UPDATE_CONTENT": "IG-Helper lançou uma nova versão, clique aqui para atualizar.", 4 | "CHECK_UPDATE": "Verificar atualizações do script", 5 | "CHECK_UPDATE_MENU": "Verificar atualizações", 6 | "CHECK_UPDATE_INTRO": "Verificar atualizações quando o script é acionado (verificar a cada 300 segundos).\nAs notificações de atualização serão enviadas como notificações de desktop através do navegador.", 7 | "RELOAD_SCRIPT": "Recarregar script", 8 | "DONATE": "Doar", 9 | "FEEDBACK": "Feedback", 10 | "IMAGE_VIEWER": "Abrir imagem no visualizador", 11 | "NEW_TAB": "Abrir em nova aba", 12 | "SHOW_DOM_TREE": "Mostrar árvore DOM", 13 | "SELECT_AND_COPY": "Selecionar tudo e copiar da caixa de entrada", 14 | "DOWNLOAD_DOM_TREE": "Baixar árvore DOM como um arquivo de texto", 15 | "REPORT_GITHUB": "Reportar um problema no GitHub", 16 | "REPORT_DISCORD": "Reportar um problema no servidor de suporte do Discord", 17 | "REPORT_FORK": "Reportar um problema no Greasy Fork", 18 | "DEBUG": "Janela de depuração", 19 | "CLOSE": "Fechar", 20 | "ALL_CHECK": "Selecionar tudo", 21 | "BATCH_DOWNLOAD_SELECTED": "Baixar recursos selecionados", 22 | "BATCH_DOWNLOAD_DIRECT": "Baixar todos os recursos", 23 | "IMG": "Imagem", 24 | "VID": "Vídeo", 25 | "DW": "Baixar", 26 | "DW_ALL": "Baixar todos os recursos", 27 | "THUMBNAIL_INTRO": "Baixar miniatura do vídeo", 28 | "LOAD_BLOB_ONE": "Carregando a mídia Blob...", 29 | "LOAD_BLOB_MULTIPLE": "Carregando a mídia Blob e outros...", 30 | "LOAD_BLOB_RELOAD": "Detectando a mídia Blob, recarregando...", 31 | "NO_CHECK_RESOURCE": "Você precisa selecionar um recurso para baixar.", 32 | "NO_VID_URL": "Não é possível encontrar o URL do vídeo.", 33 | "SETTING": "Configurações", 34 | "AUTO_RENAME": "Renomear arquivos automaticamente (clique com o botão direito para definir)", 35 | "RENAME_SHORTCODE": "Renomear o arquivo e incluir o código curto", 36 | "RENAME_PUBLISH_DATE": "Definir o carimbo de data/hora dos arquivos renomeados para a data de publicação do recurso", 37 | "RENAME_LOCATE_DATE": "Modificar o formato da data do carimbo de data/hora dos arquivos renomeados (clique com o botão direito para definir)", 38 | "DISABLE_VIDEO_LOOPING": "Desativar a reprodução automática em loop de vídeos", 39 | "HTML5_VIDEO_CONTROL": "Exibir o controlador de vídeo HTML5", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirecionar ao clicar na foto do perfil na story do usuário", 41 | "FORCE_FETCH_ALL_RESOURCES": "Forçar a busca de todos os recursos na publicação", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Baixar diretamente os recursos visíveis na publicação", 43 | "DIRECT_DOWNLOAD_ALL": "Baixar diretamente todos os recursos na publicação", 44 | "MODIFY_VIDEO_VOLUME": "Modificar o volume do vídeo (clique com o botão direito para definir)", 45 | "SCROLL_BUTTON": "Habilitar botões de rolagem para a página de Reels", 46 | "FORCE_RESOURCE_VIA_MEDIA": "Forçar a busca de recursos via API de Mídia", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Usar métodos alternativos para baixar quando a API de Mídia não estiver acessível", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "\"Abrir em nova aba\" nas publicações sempre usa a API de Mídia", 49 | "AUTO_RENAME_INTRO": "Renomear automaticamente os arquivos para um formato personalizado\nLista de formatos personalizados: \n%USERNAME% - Nome de usuário\n%SOURCE_TYPE% - Tipo de fonte\n%SHORTCODE% - Código curto da publicação\n%YEAR% - Ano de download/publicação\n%2-YEAR% - Ano (dois últimos dígitos) de download/publicação\n%MONTH% - Mês de download/publicação\n%DAY% - Dia de download/publicação\n%HOUR% - Hora de download/publicação\n%MINUTE% - Minuto de download/publicação\n%SECOND% - Segundo de download/publicação\n%ORIGINAL_NAME% - Nome original do arquivo baixado\n%ORIGINAL_NAME_FIRST% - Nome original do arquivo baixado (primeira parte do nome)\n\nSe definido como falso, o nome do arquivo permanecerá inalterado.\nExemplo: instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "Renomear automaticamente os arquivos com o seguinte formato:\nNOME_DE_USUÁRIO-TIPO-CÓDIGO_CURTO-CARIMBO_DE_DATA/HORA.TIPO_DE_ARQUIVO\nExemplo: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nIsso funcionará APENAS se [Renomear arquivos automaticamente] estiver definido como VERDADEIRO.", 51 | "RENAME_PUBLISH_DATE_INTRO": "Define o carimbo de data/hora no formato de renomeação de arquivo para a data de publicação do recurso (fuso horário do navegador).\n\nEsse recurso só funciona quando [Renomear arquivos automaticamente] está definido como VERDADEIRO.", 52 | "RENAME_LOCATE_DATE_INTRO": "Modificar o formato da data do carimbo de data/hora do arquivo renomeado para a hora local do navegador e formatá-lo para o formato de data regional de sua preferência.\n\nEsse recurso só funciona quando [Renomear arquivos automaticamente] está definido como VERDADEIRO.", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "Desativar a reprodução automática em loop de vídeos em Reels e publicações.", 54 | "HTML5_VIDEO_CONTROL_INTRO": "Exibir o controlador de vídeo HTML5 no recurso de vídeo.\n\nIsso ocultará o controle deslizante de volume de vídeo personalizado e o substituirá pelo controlador HTML5. O controlador HTML5 pode ser ocultado clicando com o botão direito do mouse no vídeo para revelar os detalhes originais.", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Redirecionar para a página de perfil de um usuário ao clicar com o botão direito do mouse em seu avatar na área de stories na página inicial.\nSe você usar o botão do meio do mouse para clicar, ele será aberto em uma nova aba.", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Forçar a busca de todos os recursos (fotos e vídeos) em uma publicação por meio da API do Instagram para remover o limite de três recursos por publicação.", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Baixar diretamente os recursos atuais disponíveis na publicação.", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "Ao clicar no botão de download, todos os recursos da publicação serão forçados a serem buscados e baixados diretamente.", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "Modificar o volume de reprodução de vídeo em Reels e publicações (clique com o botão direito do mouse para abrir o controle deslizante de configuração de volume).", 60 | "SCROLL_BUTTON_INTRO": "Habilitar botões de rolagem para o canto inferior direito da página de Reels.", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "A API de Mídia tentará obter a foto ou o vídeo da mais alta qualidade possível, mas pode demorar mais para carregar.", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "Quando a API de Mídia atingir seu limite de taxa ou não puder ser usada por outros motivos, a API de Busca Forçada será usada para baixar recursos (a qualidade do recurso pode ser um pouco menor).", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "O botão [Abrir em nova aba] nas publicações sempre usará a API de Mídia para obter recursos de alta resolução.", 64 | "SKIP_VIEW_STORY_CONFIRM": "Pular a página de confirmação ao visualizar uma story ou um destaque", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Pular automaticamente quando a página de confirmação for exibida na story ou no destaque.", 66 | "MODIFY_RESOURCE_EXIF": "Modificar atributos EXIF do recurso", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "Modificar os atributos EXIF do recurso de imagem para colocar o link do post nele." 68 | } -------------------------------------------------------------------------------- /locale/translations/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "Wololo! O nouă versiune a fost lansată.", 3 | "NOTICE_UPDATE_CONTENT": "IG-Helper a lansat o nouă versiune, dă click aici pentru a actualiza.", 4 | "CHECK_UPDATE": "Caută actualizări pentru script", 5 | "CHECK_UPDATE_MENU": "Caută actualizări", 6 | "CHECK_UPDATE_INTRO": "Caută actualizări atunci când scriptul este activat (verifică la fiecare 300 de secunde).\nNotificările pentru actualizare vor fi trimise ca notificări desktop prin intermediul browserului.", 7 | "RELOAD_SCRIPT": "Reîncarcă scriptul", 8 | "DONATE": "Donează", 9 | "FEEDBACK": "Feedback", 10 | "IMAGE_VIEWER": "Deschide imaginea în vizualizator", 11 | "NEW_TAB": "Deschide într-o filă nouă", 12 | "SHOW_DOM_TREE": "Afișează arborele DOM", 13 | "SELECT_AND_COPY": "Selectează tot și copiază din caseta de introducere", 14 | "DOWNLOAD_DOM_TREE": "Descarcă arborele DOM în format text", 15 | "REPORT_GITHUB": "Raportează o problemă pe GitHub", 16 | "REPORT_DISCORD": "Raportează o problemă pe serverul de suport Discord", 17 | "REPORT_FORK": "Raportează o problemă pe Greasy Fork", 18 | "DEBUG": "Fereastră de depanare", 19 | "CLOSE": "Închide", 20 | "ALL_CHECK": "Selectează toate", 21 | "BATCH_DOWNLOAD_SELECTED": "Descarcă resursele selectate", 22 | "BATCH_DOWNLOAD_DIRECT": "Descarcă toate resursele", 23 | "IMG": "Imagine", 24 | "VID": "Videoclip", 25 | "DW": "Descarcă", 26 | "DW_ALL": "Descărcați toate resursele", 27 | "THUMBNAIL_INTRO": "Descarcă miniatura videoclipului", 28 | "LOAD_BLOB_ONE": "Se încarcă conținutul media în format Blob...", 29 | "LOAD_BLOB_MULTIPLE": "Se încarcă conținutul media în format Blob și celelalte...", 30 | "LOAD_BLOB_RELOAD": "Se detectează conținutul media în format Blob, se reîncarcă acum...", 31 | "NO_CHECK_RESOURCE": "Trebuie să bifezi resursa pentru a o descărca.", 32 | "NO_VID_URL": "Nu se poate găsi URL-ul videoclipului.", 33 | "SETTING": "Setări", 34 | "AUTO_RENAME": "Redenumește automat fișierele (Click dreapta pentru a seta)", 35 | "RENAME_SHORTCODE": "Redenumește fișierul și include cod scurt", 36 | "RENAME_PUBLISH_DATE": "Setează marcajul de timp al fișierelor redenumite la data publicării resurselor", 37 | "RENAME_LOCATE_DATE": "Modifică formatul de dată al marcajului de timp pentru fișierele redenumite (Click dreapta pentru a seta)", 38 | "DISABLE_VIDEO_LOOPING": "Dezactivează redarea automată în buclă a videoclipurilor", 39 | "HTML5_VIDEO_CONTROL": "Afișează controllerul video HTML5", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirecționează când dai click pe fotografia profilului în storyul utilizatorului", 41 | "FORCE_FETCH_ALL_RESOURCES": "Forțează preluarea tuturor resurselor din postare", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Descarcă direct resursele vizibile din postare", 43 | "DIRECT_DOWNLOAD_ALL": "Descarcă direct toate resursele din postare", 44 | "MODIFY_VIDEO_VOLUME": "Modifică volumul videoclipurilor (Click dreapta pentru a seta)", 45 | "SCROLL_BUTTON": "Activează butoanele de derulare pentru pagina Reels", 46 | "FORCE_RESOURCE_VIA_MEDIA": "Forțează preluarea resurselor prin intermediul Media API", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Folosește metode alternative pentru a descărca atunci când Media API nu este accesibil", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "„Deschide într-o filă nouă” în postări folosește întotdeauna Media API", 49 | "AUTO_RENAME_INTRO": "Redenumește automat fișierele într-un format personalizat\nListă de formaturi personalizate: \n%USERNAME% - Numele de utilizator\n%SOURCE_TYPE% - Tipul sursei\n%SHORTCODE% - Codul scurt al postării\n%YEAR% - Anul descărcării/publicării\n%2-YEAR% - Anul (ultimele două cifre) descărcării/publicării\n%MONTH% - Luna descărcării/publicării\n%DAY% - Ziua descărcării/publicării\n%HOUR% - Ora descărcării/publicării\n%MINUTE% - Minutul descărcării/publicării\n%SECOND% - Secunda descărcării/publicării\n%ORIGINAL_NAME% - Numele original al fișierului descărcat\n%ORIGINAL_NAME_FIRST% - Numele original al fișierului descărcat (prima parte a numelui)\n\nDacă este setat pe fals, numele fișierului va rămâne neschimbat.\nExemplu: instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "Redenumește automat fișierele cu formatul următor:\nNUME_DE_UTILIZATOR-TIP-COD_SCURT-MARCAJ_DE_TIMP.TIPUL_FIȘIERULUI\nExemplu: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nFuncționează DOAR dacă setarea [Redenumește automat fișierele] este pe ADEVĂRAT.", 51 | "RENAME_PUBLISH_DATE_INTRO": "Setează marcajul de timp în formatul de redenumire a fișierelor la data publicării resurselor (fusul orar al browserului)\n\nFuncționează DOAR dacă setarea [Redenumește automat fișierele] este pe ADEVĂRAT.", 52 | "RENAME_LOCATE_DATE_INTRO": "Modifică formatul de dată al marcajului de timp pentru redenumirea fișierelor la ora locală a browserului și formatează-l la formatul regional de dată dorit.\n\nFuncționează DOAR dacă setarea [Redenumește automat fișierele] este pe ADEVĂRAT.", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "Dezactivează redarea automată în buclă a videoclipurilor din Reels și Postări.", 54 | "HTML5_VIDEO_CONTROL_INTRO": "Afișează controlerul video HTML5 în resursa video.\n\nAcest lucru va ascunde glisorul personalizat de volum video și îl va înlocui cu controlerul HTML5. Controlerul HTML5 poate fi ascuns făcând clic dreapta pe video pentru a dezvălui detaliile originale.", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Redirecționează către pagina de profil a unui utilizator când dai click dreapta pe avatarul acestuia în zona dedicată storyurilor de pe pagina principală.\nDacă folosești butonul din mijloc pentru a da click, se va deschide într-o filă nouă.", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Forțează preluarea tuturor resurselor (fotografii și videoclipuri) dintr-o postare prin intermediul API-ului Instagram pentru a elimina limita de trei resurse per postare.", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Descarcă direct resursele actuale din postare.", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "Atunci când apeși butonul de descărcare, toate resursele din postare vor fi direct forțate să fie preluate și descărcate.", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "Modifică volumul redării videoclipurilor în Reels și Postări (click dreapta pentru a deschide cursorul de setare a volumului).", 60 | "SCROLL_BUTTON_INTRO": "Activează butoanele de derulare pentru colțul din dreapta jos al paginii Reels.", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "Media API va încerca să obțină cea mai înaltă calitate posibilă pentru fotografii sau videoclipuri, dar încărcarea va dura mai mult.", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "Când Media API atinge limita de rată sau nu poate fi folosit din alte motive, Forced Fetch API este folosit pentru a descărca resursele (calitatea resurselor este puțin mai scăzută).", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "Butonul [Deschide într-o filă nouă] în postări va folosi întotdeauna Media API pentru a obține resursele la rezoluție înaltă.", 64 | "SKIP_VIEW_STORY_CONFIRM": "Omite pagina de confirmare la vizualizarea unui story/highlight", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Omite automat când pagina de confirmare este afișată în story sau highlight.", 66 | "MODIFY_RESOURCE_EXIF": "Modifică atributele EXIF ale resursei", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "Modifică atributele EXIF ale resursei imaginii pentru a plasa linkul postării în acesta." 68 | } -------------------------------------------------------------------------------- /locale/translations/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "呜噜噜!新版本已发布。", 3 | "NOTICE_UPDATE_CONTENT": "IG小助手已发布版本更新,点击这里以更新。", 4 | "CHECK_UPDATE": "检查脚本更新", 5 | "CHECK_UPDATE_MENU": "检查更新", 6 | "CHECK_UPDATE_INTRO": "脚本触发时检查更新 (每300秒执行一次检查)。\n更新通知将透过浏览器传送桌面通知。", 7 | "RELOAD_SCRIPT": "重新载入脚本", 8 | "DONATE": "捐助", 9 | "FEEDBACK": "反馈问题", 10 | "IMAGE_VIEWER": "在检视器中打开图片", 11 | "NEW_TAB": "在新选项卡中打开", 12 | "SHOW_DOM_TREE": "显示 DOM Tree", 13 | "SELECT_AND_COPY": "全选并复制输入框的内容", 14 | "DOWNLOAD_DOM_TREE": "将 DOM Tree 下载为文本文件", 15 | "REPORT_GITHUB": "在 GitHub 上报告问题", 16 | "REPORT_DISCORD": "在 Discord 支援服务器上报告问题", 17 | "REPORT_FORK": "在 Greasy Fork 上报告问题", 18 | "DEBUG": "调试窗口", 19 | "CLOSE": "关闭", 20 | "ALL_CHECK": "全选", 21 | "BATCH_DOWNLOAD_SELECTED": "批量下载已勾选资源", 22 | "BATCH_DOWNLOAD_DIRECT": "批量下载全部资源", 23 | "IMG": "图像", 24 | "VID": "视频", 25 | "DW": "下载", 26 | "DW_ALL": "下载全部资源", 27 | "THUMBNAIL_INTRO": "下载视频缩略图", 28 | "LOAD_BLOB_ONE": "正在载入大型媒体对象...", 29 | "LOAD_BLOB_MULTIPLE": "正在载入多个大型媒体对象...", 30 | "LOAD_BLOB_RELOAD": "正在重新载入大型媒体对象...", 31 | "NO_CHECK_RESOURCE": "您需要勾選资源才能下載。", 32 | "NO_VID_URL": "找不到视频网址", 33 | "SETTING": "设置", 34 | "AUTO_RENAME": "自动重命名文件(右击设置)", 35 | "RENAME_SHORTCODE": "重命名文件并包含物件短码", 36 | "RENAME_PUBLISH_DATE": "设置重命名文件时间戳为资源发布日期", 37 | "RENAME_LOCATE_DATE": "修改重命名档案时间戳日期格式(右击设置)", 38 | "DISABLE_VIDEO_LOOPING": "禁用视频自动循环", 39 | "HTML5_VIDEO_CONTROL": "显示 HTML5 视频控制器", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "单击用户故事区域头像时重定向", 41 | "FORCE_FETCH_ALL_RESOURCES": "强制抓取帖子中所有资源", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "直接下载帖子中的可见资源", 43 | "DIRECT_DOWNLOAD_ALL": "直接下载帖子中的所有资源", 44 | "MODIFY_VIDEO_VOLUME": "修改视频音量(右击设置)", 45 | "SCROLL_BUTTON": "为 Reels 页面启用卷动按钮", 46 | "FORCE_RESOURCE_VIA_MEDIA": "通过 Media API 强制获取资源", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "当 Media API 无法存取时使用其他方式下载", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "在帖子中的「在新选项卡中打开」永远使用 Media API", 49 | "AUTO_RENAME_INTRO": "自动将文件重命名为自定义格式\n自定义格式列表:\n%USERNAME% - 用户名\n%SOURCE_TYPE% - 下载源\n%SHORTCODE% - 帖子短码\n%YEAR% - 下载/发布的年份\n%2-YEAR% - 下载/发布的年份(后两位数)\n%MONTH% - 下载/发布的月份\n%DAY% - 下载/发布的日期\n%HOUR% - 下载/发布的小时\n%MINUTE% - 下载/发布的分钟\n%SECOND% - 下载/发布的秒\n%ORIGINAL_NAME% - 下载文件的原始名称\n%ORIGINAL_NAME_FIRST% - 下载文件的原始名称(第一部分)\n\n若设为false,则文件名将保持原样。 \n例如:instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "自动重命名文件为以下格式类型:\n用户名-类型-短码-时间戳.文件类型\n示例:instagram-photo-CwkxyiVynpW-1670350000.jpg\n\n它仅在[自动重命名文件]设置为 TRUE 时有效。", 51 | "RENAME_PUBLISH_DATE_INTRO": "将文件重命名格式中的时间戳设置为资源发布日期 (浏览器时区)\n\n此功能仅在[自动重命名文件]设置为 TRUE 时有效。", 52 | "RENAME_LOCATE_DATE_INTRO": "修改重命名档案时间戳日期格式为浏览器当地时间,并且格式化为您所选择的地区日期格式。\n\n此功能仅在[自动重命名文件]设置为 TRUE 时有效。", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "禁用 Reels 和帖子中的视频自动播放。", 54 | "HTML5_VIDEO_CONTROL_INTRO": "在视频资源中显示 HTML5 视频控制器。\n\n这将隐藏自定义视频音量滑块并将其替换为 HTML5 控制器。\n可以通过右键单击视频来隐藏 HTML5 控制器以显示原始详细信息。", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "右键单击主页故事区域中的用户头像,重定向到用户的个人资料页面;若使用中键单击,则以新选项卡开启。", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "通过 Instagram API 强制获取帖子中的所有资源(照片和视频),以取消每个帖子单次抓取三个资源的限制。", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "直接下载帖子中的当前资源。", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "当您点击下载按钮时,帖子中的所有资源将被直接强制抓取并下载。", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "修改 Reels 和帖子中的视频播放音量(右击可开启音量设置滑条)。", 60 | "SCROLL_BUTTON_INTRO": "为 Reels 页面的右下角启用上下卷动按钮。", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "Media API 将尝试获取尽可能最高质量的照片或视频,但加载时间会更长。", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "当 Media API 到达速率限制或因为其他原因而无法下载时,则使用强制获取API下载资源 (资源质量略低)。", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "在帖子中的「在新选项卡中打开」功能将一律使用 Media API 取得高分辨率资源", 64 | "SKIP_VIEW_STORY_CONFIRM": "自动跳过在快拍/快拍精选中显示的确认页面", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "在查看快拍/快拍精选时,如果有确认页面时会自动跳过。", 66 | "MODIFY_RESOURCE_EXIF": "修改资源EXIF属性", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "修改图片资源的EXIF属性,以便将帖子链结放置于其中。" 68 | } -------------------------------------------------------------------------------- /locale/translations/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOTICE_UPDATE_TITLE": "嗚嚕嚕!新版本已發布。", 3 | "NOTICE_UPDATE_CONTENT": "IG小精靈已發布版本更新,點擊這裡以更新。", 4 | "CHECK_UPDATE": "檢查腳本更新", 5 | "CHECK_UPDATE_MENU": "檢查更新", 6 | "CHECK_UPDATE_INTRO": "腳本觸發時檢查更新 (每300秒執行一次檢查)。\n更新通知將透過瀏覽器傳送桌面通知。", 7 | "RELOAD_SCRIPT": "重新載入腳本", 8 | "DONATE": "贊助", 9 | "FEEDBACK": "回報問題", 10 | "IMAGE_VIEWER": "在檢視器中開啟圖片", 11 | "NEW_TAB": "在新分頁中開啟", 12 | "SHOW_DOM_TREE": "顯示 DOM Tree", 13 | "SELECT_AND_COPY": "全選並複製輸入框的內容", 14 | "DOWNLOAD_DOM_TREE": "將 DOM Tree 下載為文字文件", 15 | "REPORT_GITHUB": "在 GitHub 上回報問題", 16 | "REPORT_DISCORD": "在 Discord 支援伺服器上回報問題", 17 | "REPORT_FORK": "在 Greasy Fork 上回報問題", 18 | "DEBUG": "偵錯視窗", 19 | "CLOSE": "關閉", 20 | "ALL_CHECK": "全選", 21 | "BATCH_DOWNLOAD_SELECTED": "批次下載已勾選資源", 22 | "BATCH_DOWNLOAD_DIRECT": "批次下載全部資源", 23 | "IMG": "相片", 24 | "VID": "影片", 25 | "DW": "下載", 26 | "DW_ALL": "下載所有資源", 27 | "THUMBNAIL_INTRO": "下載影片縮圖", 28 | "LOAD_BLOB_ONE": "正在載入二進位大型物件...", 29 | "LOAD_BLOB_MULTIPLE": "正在載入多個二進位大型物件...", 30 | "LOAD_BLOB_RELOAD": "正在重新載入二進位大型物件...", 31 | "NO_CHECK_RESOURCE": "您需要勾選資源才能下載。", 32 | "NO_VID_URL": "找不到影片網址", 33 | "SETTING": "設定", 34 | "AUTO_RENAME": "自動重新命名檔案(右鍵設定)", 35 | "RENAME_SHORTCODE": "重新命名檔案並包含 Shortcode", 36 | "RENAME_PUBLISH_DATE": "設定重新命名檔案時間戳為資源發佈日期", 37 | "RENAME_LOCATE_DATE": "修改重新命名檔案時間戳日期格式(右鍵設定)", 38 | "DISABLE_VIDEO_LOOPING": "關閉影片自動循環播放", 39 | "HTML5_VIDEO_CONTROL": "顯示 HTML5 影片控制器", 40 | "REDIRECT_CLICK_USER_STORY_PICTURE": "點擊使用者限時動態區域頭貼時重定向", 41 | "FORCE_FETCH_ALL_RESOURCES": "強制提取貼文中所有資源", 42 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "直接下載貼文中的可見資源", 43 | "DIRECT_DOWNLOAD_ALL": "直接下載貼文中的所有資源", 44 | "MODIFY_VIDEO_VOLUME": "修改影片音量(右鍵設定)", 45 | "SCROLL_BUTTON": "為連續短片頁面啟用捲動按鈕", 46 | "FORCE_RESOURCE_VIA_MEDIA": "透過 Media API 強制提取資源", 47 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "當 Media API 無法存取時使用其他方式下載", 48 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "在貼文中的「在新分頁中開啟」永遠使用 Media API", 49 | "AUTO_RENAME_INTRO": "自動將檔案重新命名為自訂格式\n自訂格式清單:\n%USERNAME% - 使用者名稱\n%SOURCE_TYPE% - 下載來源\n%SHORTCODE% - 貼文 Shortcode\n%YEAR% - 下載/發布年份\n%2-YEAR% - 下載/發布年份(後兩位數)\n%MONTH% - 下載/發佈時的月份\n%DAY% - 下載/發佈時的日期\n%HOUR% - 下載/發佈時的小時\n%MINUTE% - 下載/發佈時的分鐘\n%SECOND% - 下載/發佈時的秒\n%ORIGINAL_NAME% - 下載檔案的原始名稱\n%ORIGINAL_NAME_FIRST% - 下載檔案的原始名稱(第一部分)\n\n若設為 false,則檔案名稱將保持原始樣貌。 \n例如:instagram_321565527_679025940443063_4318007696887450953_n.jpg", 50 | "RENAME_SHORTCODE_INTRO": "將檔案自動重新命名為以下格式:\n使用者名稱-類型-Shortcode-時間戳.檔案類型\n例如:instagram-photo-CwkxyiVynpW-1670350000.jpg\n\n此功能僅在[自動重新命名檔案]設定為 TRUE 時有效。", 51 | "RENAME_PUBLISH_DATE_INTRO": "將檔案重新命名格式中的時間戳設定為資源發佈日期 (瀏覽器時區)\n\n此功能僅在[自動重新命名檔案]設定為 TRUE 時有效。", 52 | "RENAME_LOCATE_DATE_INTRO": "修改重新命名檔案時間戳日期格式為瀏覽器當地時間,並且格式化為您所選擇的地區日期格式。\n\n此功能僅在[自動重新命名檔案]設定為 TRUE 時有效。", 53 | "DISABLE_VIDEO_LOOPING_INTRO": "關閉連續短片和貼文中影片自動循環播放。", 54 | "HTML5_VIDEO_CONTROL_INTRO": "在影片資源中顯示 HTML5 影片控制器。\n\n這將會隱藏自訂影片音量滑條,並由 HTML5 控制器取代。\nHTML5 控制器可以透過右鍵點擊影片來隱藏,以顯示原始詳細資訊。", 55 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "右鍵點選首頁限時動態區域中的使用者頭貼時,重新導向到使用者的個人資料頁面;若使用中鍵點擊,則以新分頁開啟。", 56 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "透過 Instagram API 強制取得貼文中的所有資源(照片和影片),以取消每個貼文單次提取三個資源的限制。", 57 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "直接下載貼文中的目前資源。", 58 | "DIRECT_DOWNLOAD_ALL_INTRO": "按下下載按鈕時將直接強制提取貼文中的所有資源並下載。", 59 | "MODIFY_VIDEO_VOLUME_INTRO": "修改連續短片和貼文的影片播放音量(右鍵可開啟音量設定條)。", 60 | "SCROLL_BUTTON_INTRO": "為連續短片頁面的右下角啟用上下捲動按鈕。", 61 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "Media API 將嘗試獲取盡可能最高品質的照片或影片,但加載時間會更長。", 62 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "當 Media API 到達速率限制或因為其他原因而無法下載時,則使用強制提取API下載資源 (資源品質略低)。", 63 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "在貼文中的「在新分頁中開啟」功能將一律使用 Media API 取得高解析度資源", 64 | "SKIP_VIEW_STORY_CONFIRM": "自動跳過在限時動態/精選動態中顯示的確認頁面", 65 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "在檢視限時動態/精選動態時,若有確認頁面時將自動跳過", 66 | "MODIFY_RESOURCE_EXIF": "修改資源EXIF屬性", 67 | "MODIFY_RESOURCE_EXIF_INTRO": "修改圖片資源的EXIF屬性,以便將貼文連結放置於其中。" 68 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ig-helper", 3 | "version": "1.0.0", 4 | "description": "Simple and fast download of resources from Instagram (posts, reels, stories, and more).", 5 | "private": true, 6 | "author": "SN-Koarashi", 7 | "license": "GPLv3", 8 | "devDependencies": { 9 | "@eslint/js": "^9.17.0", 10 | "esbuild": "^0.25.0", 11 | "eslint": "^9.17.0", 12 | "globals": "^15.14.0" 13 | }, 14 | "dependencies": { 15 | "jquery": "^3.7.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | FS_IMPORT('./metadata.js'); 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | (function ($) { 5 | 'use strict'; 6 | 7 | /* initial */ 8 | FS_IMPORT('./settings.js'); 9 | FS_IMPORT('./initial.js'); 10 | FS_IMPORT('./timer.js'); 11 | 12 | /* Main functions */ 13 | FS_IMPORT('./functions/highlight.js'); 14 | FS_IMPORT('./functions/post.js'); 15 | FS_IMPORT('./functions/profile.js'); 16 | FS_IMPORT('./functions/reel.js'); 17 | FS_IMPORT('./functions/story.js'); 18 | 19 | /* untils */ 20 | FS_IMPORT('./utils/api.js'); 21 | FS_IMPORT('./utils/general.js'); 22 | FS_IMPORT('./utils/i18n.js'); 23 | 24 | /* register all events */ 25 | FS_IMPORT('./events.js'); 26 | })(jQuery); 27 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | import { state, USER_SETTING } from "./settings"; 2 | import { 3 | showSetting, showDebugDOM, reloadScript, 4 | triggerLinkElement, openNewTab, saveFiles, logger, toggleVolumeSilder 5 | } from "./utils/general"; 6 | import { onStory, onStoryAll, onStoryThumbnail } from "./functions/story"; 7 | import { onProfileAvatar } from "./functions/profile"; 8 | import { onHighlightsStory, onHighlightsStoryAll, onHighlightsStoryThumbnail } from "./functions/highlight"; 9 | import { onReels } from "./functions/reel"; 10 | import { _i18n, getTranslationText, repaintingTranslations, registerMenuCommand } from "./utils/i18n"; 11 | /*! ESLINT IMPORT END !*/ 12 | 13 | // Running if document is ready 14 | $(function () { 15 | function ConvertDOM(domEl) { 16 | var obj = []; 17 | for (var ele of domEl) { 18 | obj.push({ 19 | tagName: ele.tagName, 20 | id: ele.id, 21 | className: ele.className 22 | }); 23 | } 24 | 25 | return obj; 26 | } 27 | 28 | function setDOMTreeContent() { 29 | let text = $('div[id^="mount"]')[0]; 30 | var logger = ""; 31 | state.GL_logger.forEach(log => { 32 | var jsonData = JSON.stringify(log.content, function (key, value) { 33 | if (Array.isArray(this)) { 34 | if (typeof value === "object" && value instanceof jQuery) { 35 | return ConvertDOM(value); 36 | } 37 | return value; 38 | } 39 | else { 40 | return value; 41 | } 42 | }, "\t"); 43 | logger += `${new Date(log.time).toISOString()}: ${jsonData}\n` 44 | }); 45 | $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text("Logger:\n" + logger + "\n-----\n\nLocation: " + location.pathname + "\nDOM Tree with div#mount:\n" + text.innerHTML); 46 | } 47 | 48 | $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_DISPLAY_DOM_TREE', function () { 49 | setDOMTreeContent(); 50 | }); 51 | 52 | $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_SELECT_DOM_TREE', function () { 53 | $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').select(); 54 | document.execCommand('copy'); 55 | }); 56 | 57 | $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_DOWNLOAD_DOM_TREE', function () { 58 | if ($('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text().length === 0) { 59 | setDOMTreeContent(); 60 | } 61 | 62 | var text = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text(); 63 | var a = document.createElement("a"); 64 | var file = new Blob([text], { type: "text/plain" }); 65 | a.href = URL.createObjectURL(file); 66 | a.download = "DOMTree-" + new Date().getTime() + ".txt"; 67 | 68 | document.body.appendChild(a); 69 | a.click(); 70 | a.remove(); 71 | }); 72 | 73 | // Close the download dialog if user click the close icon 74 | $('body').on('click', '.IG_POPUP_DIG_BTN, .IG_POPUP_DIG_BG', function () { 75 | if ($(this).parent('#tempWrapper').length > 0) { 76 | $(this).parent('#tempWrapper').fadeOut(250, function () { 77 | $(this).remove(); 78 | }); 79 | } 80 | else { 81 | $('.IG_POPUP_DIG').remove(); 82 | } 83 | }); 84 | 85 | $(window).on('keydown', function (e) { 86 | // Hot key [Alt+Q] to close the download dialog 87 | if (e.which == '81' && e.altKey) { 88 | $('.IG_POPUP_DIG').remove(); 89 | e.preventDefault(); 90 | } 91 | // Hot key [Alt+W] to open the settings dialog 92 | if (e.which == '87' && e.altKey) { 93 | showSetting(); 94 | e.preventDefault(); 95 | } 96 | 97 | // Hot key [Alt+Z] to open the settings dialog 98 | if (e.which == '90' && e.altKey) { 99 | showDebugDOM(); 100 | e.preventDefault(); 101 | } 102 | 103 | // Hot key [Alt+R] to open the settings dialog 104 | if (e.which == '82' && e.altKey) { 105 | reloadScript(); 106 | e.preventDefault(); 107 | } 108 | 109 | // Hot key [Alt+S] to download story/highlights resource 110 | if (e.which == '83' && e.altKey) { 111 | if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig) && $('.IG_DWSTORY').length > 0) { 112 | $('.IG_DWSTORY')?.trigger("click"); 113 | } 114 | if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig) && $('.IG_DWHISTORY').length > 0) { 115 | $('.IG_DWHISTORY')?.trigger("click"); 116 | } 117 | e.preventDefault(); 118 | } 119 | }); 120 | 121 | $('body').on('change', '.IG_POPUP_DIG input', function () { 122 | var name = $(this).attr('id'); 123 | 124 | if (name && USER_SETTING[name] !== undefined) { 125 | let isChecked = $(this).prop('checked'); 126 | GM_setValue(name, isChecked); 127 | USER_SETTING[name] = isChecked; 128 | 129 | console.log('user settings', name, isChecked); 130 | } 131 | }); 132 | 133 | $('body').on('click', '.IG_POPUP_DIG .globalSettings', function (e) { 134 | if ($(this).find('#tempWrapper').length > 0) { 135 | e.preventDefault(); 136 | } 137 | }); 138 | 139 | $('body').on('change', '.IG_POPUP_DIG #tempWrapper input:not(#date_format)', function () { 140 | let value = $(this).val(); 141 | 142 | if ($(this).attr('type') == 'range') { 143 | $(this).next().val(value); 144 | } 145 | else { 146 | $(this).prev().val(value); 147 | } 148 | 149 | if (value >= 0 && value <= 1) { 150 | state.videoVolume = value; 151 | GM_setValue('G_VIDEO_VOLUME', value); 152 | } 153 | }); 154 | 155 | $('body').on('input', '.IG_POPUP_DIG #tempWrapper input:not(#date_format)', function () { 156 | if ($(this).attr('type') == 'range') { 157 | let value = $(this).val(); 158 | $(this).next().val(value); 159 | } 160 | else { 161 | let value = $(this).val(); 162 | if (value >= 0 && value <= 1) { 163 | $(this).prev().val(value); 164 | } 165 | else { 166 | if (value < 0) { 167 | $(this).val(0); 168 | } 169 | else { 170 | $(this).val(1); 171 | } 172 | } 173 | } 174 | }); 175 | 176 | $('body').on('input', '.IG_POPUP_DIG #tempWrapper input#date_format', function () { 177 | GM_setValue('G_RENAME_FORMAT', $(this).val()); 178 | state.fileRenameFormat = $(this).val(); 179 | }); 180 | 181 | $('body').on('click', 'a[data-needed="direct"]', function (e) { 182 | e.preventDefault(); 183 | triggerLinkElement(this); 184 | }); 185 | 186 | $('body').on('click', '.IG_POPUP_DIG_BODY .newTab', function () { 187 | // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video) 188 | 189 | if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST) { 190 | triggerLinkElement($(this).parent().children('a').first()[0], true); 191 | } 192 | else { 193 | var urlObj = new URL($(this).parent().children('a').attr('data-href')); 194 | urlObj.host = 'scontent.cdninstagram.com'; 195 | 196 | openNewTab(urlObj.href); 197 | } 198 | }); 199 | 200 | $('body').on('click', '.IG_POPUP_DIG_BODY .videoThumbnail', function () { 201 | let timestamp = new Date().getTime(); 202 | 203 | if (USER_SETTING.RENAME_PUBLISH_DATE && $(this).parent().children('a').attr('datetime')) { 204 | timestamp = $(this).parent().children('a').attr('datetime'); 205 | } 206 | 207 | let postPath = $(this).parent().children('a').attr('data-path') ?? $('#article-id').text(); 208 | 209 | saveFiles($(this).parent().children('a').find('img').first().attr('src'), $(this).parent().children('a').attr('data-username'), 'thumbnail', timestamp, 'jpg', postPath); 210 | }); 211 | 212 | // Running if user left-click download icon in stories 213 | $('body').on('click', '.IG_DWSTORY', function () { 214 | onStory(true); 215 | }); 216 | 217 | // Running if user left-click all download icon in stories 218 | $('body').on('click', '.IG_DWSTORY_ALL', function () { 219 | onStoryAll(); 220 | }); 221 | 222 | // Running if user left-click 'open in new tab' icon in stories 223 | $('body').on('click', '.IG_DWNEWTAB', function (e) { 224 | e.preventDefault(); 225 | onStory(true, true, true); 226 | }); 227 | 228 | // Running if user left-click download thumbnail icon in stories 229 | $('body').on('click', '.IG_DWSTORY_THUMBNAIL', function () { 230 | onStoryThumbnail(true); 231 | }); 232 | 233 | // Running if user left-click download icon in profile 234 | $('body').on('click', '.IG_DWPROFILE', function (e) { 235 | e.stopPropagation(); 236 | onProfileAvatar(true); 237 | }); 238 | 239 | // Running if user left-click download icon in highlight stories 240 | $('body').on('click', '.IG_DWHISTORY', function () { 241 | onHighlightsStory(true); 242 | }); 243 | 244 | // Running if user left-click all download icon in highlight stories 245 | $('body').on('click', '.IG_DWHISTORY_ALL', function () { 246 | onHighlightsStoryAll(); 247 | }); 248 | 249 | // Running if user left-click 'open in new tab' icon in highlight stories 250 | $('body').on('click', '.IG_DWHINEWTAB', function (e) { 251 | e.preventDefault(); 252 | onHighlightsStory(true, true); 253 | }); 254 | 255 | // Running if user left-click thumbnail download icon in highlight stories 256 | $('body').on('click', '.IG_DWHISTORY_THUMBNAIL', function () { 257 | onHighlightsStoryThumbnail(true); 258 | }); 259 | 260 | // Running if user left-click download icon in reels 261 | $('body').on('click', '.IG_REELS', function () { 262 | onReels(true, true); 263 | }); 264 | 265 | // Running if user left-click newtab icon in reels 266 | $('body').on('click', '.IG_REELS_NEWTAB', function () { 267 | onReels(true, true, true); 268 | }); 269 | 270 | // Running if user left-click download icon in reels 271 | $('body').on('click', '.IG_REELS_THUMBNAIL', function () { 272 | onReels(true, false); 273 | }); 274 | 275 | // Running if user right-click profile picture in stories area 276 | $('body').on('mousedown', 'button[role="menuitem"], div[role="menuitem"], ul > li[tabindex="-1"] > div[role="button"]', function (e) { 277 | // Right-Click || Middle-Click 278 | if (e.which === 3 || e.which === 2) { 279 | if (location.href === 'https://www.instagram.com/' && USER_SETTING.REDIRECT_CLICK_USER_STORY_PICTURE) { 280 | e.preventDefault(); 281 | if ($(this).find('canvas._aarh, canvas + span > img').length > 0) { 282 | const targetUrl = 'https://www.instagram.com/' + $(this).children('div').last().text(); 283 | if (e.which === 2) { 284 | GM_openInTab(targetUrl); 285 | } 286 | else { 287 | location.href = targetUrl; 288 | } 289 | } 290 | } 291 | } 292 | }); 293 | 294 | $('body').on('change', '.IG_POPUP_DIG_TITLE .checkbox', function () { 295 | var isChecked = $(this).find('input').prop('checked'); 296 | $('.IG_POPUP_DIG_BODY .inner_box').each(function () { 297 | $(this).prop('checked', isChecked); 298 | }); 299 | }); 300 | 301 | $('body').on('change', '.IG_POPUP_DIG_BODY .inner_box', function () { 302 | var checked = $('.IG_POPUP_DIG_BODY .inner_box:checked').length; 303 | var total = $('.IG_POPUP_DIG_BODY .inner_box').length; 304 | 305 | 306 | $('.IG_POPUP_DIG_TITLE .checkbox').find('input').prop('checked', checked == total); 307 | }); 308 | 309 | $('body').on('click', '.IG_POPUP_DIG_TITLE #batch_download_selected', function () { 310 | let index = 0; 311 | $('.IG_POPUP_DIG_BODY a[data-needed="direct"]').each(function () { 312 | if ($(this).prev().children('input').prop('checked')) { 313 | $(this).trigger("click"); 314 | index++; 315 | } 316 | }); 317 | 318 | if (index == 0) { 319 | alert(_i18n('NO_CHECK_RESOURCE')); 320 | } 321 | }); 322 | 323 | $('body').on('change', '.IG_POPUP_DIG_TITLE #langSelect', function () { 324 | GM_setValue('lang', $(this).val()); 325 | state.lang = $(this).val(); 326 | 327 | if (state.lang?.startsWith('en') || state.locale[state.lang] != null) { 328 | repaintingTranslations(); 329 | registerMenuCommand(); 330 | } 331 | else { 332 | getTranslationText(state.lang).then((res) => { 333 | state.locale[state.lang] = res; 334 | repaintingTranslations(); 335 | registerMenuCommand(); 336 | }).catch((err) => { 337 | console.error('getTranslationText catch error:', err); 338 | }); 339 | } 340 | }); 341 | 342 | $('body').on('click', '.IG_POPUP_DIG_TITLE #batch_download_direct', function () { 343 | $('.IG_POPUP_DIG_BODY a[data-needed="direct"]').each(function () { 344 | $(this).trigger("click"); 345 | }); 346 | }); 347 | 348 | const element_observer = new MutationObserver((mutationsList) => { 349 | for (const mutation of mutationsList) { 350 | if (mutation.type === 'childList') { 351 | mutation.addedNodes.forEach((node) => { 352 | const $videos = $(node).find('video'); 353 | 354 | if (location.pathname.startsWith("/stories/highlights/")) { 355 | if ( 356 | $(node).attr("data-ih-locale-title") == null && 357 | $(node).attr("data-visualcompletion") == null && 358 | node.tagName === "DIV" 359 | ) { 360 | // replace something times ago format to publish time when switch highlight 361 | var $time = $(node).find("time[datetime]"); 362 | let publishTitle = $time?.attr('title'); 363 | if (publishTitle != null) { 364 | $time.text(publishTitle); 365 | } 366 | } 367 | } 368 | 369 | if ($videos.length > 0) { 370 | // Modify video volume 371 | if (USER_SETTING.MODIFY_VIDEO_VOLUME) { 372 | $videos.each(function () { 373 | $(this).on('play playing', function () { 374 | if (!$(this).data('modify')) { 375 | $(this).attr('data-modify', true); 376 | this.volume = state.videoVolume; 377 | logger('(audio_observer) Added video event listener #modify'); 378 | } 379 | }); 380 | }); 381 | } 382 | 383 | if (location.pathname.match(/^(\/stories\/)/ig)) { 384 | const isHighlight = location.pathname.match(/^(\/stories\/highlights\/)/ig) != null; 385 | const storyType = isHighlight ? 'highlight' : 'story'; 386 | 387 | $videos.each(function () { 388 | $(this).on('timeupdate', function () { 389 | if (!$(this).data('modify-thumbnail')) { 390 | let $video = $(this); 391 | if ($video.parents('div[style][class]').filter(function () { 392 | return $(this).width() == $video.width(); 393 | }).find('.IG_DWSTORY_THUMBNAIL, .IG_DWHISTORY_THUMBNAIL').length === 0) { 394 | $(this).attr('data-modify-thumbnail', true); 395 | 396 | if (isHighlight) { 397 | onHighlightsStoryThumbnail(false); 398 | } 399 | else { 400 | onStoryThumbnail(false); 401 | } 402 | 403 | logger(`(${storyType})`, 'Manually inserting thumbnail button'); 404 | } 405 | else { 406 | $(this).attr('data-modify-thumbnail', true); 407 | logger(`(${storyType})`, 'Thumbnail button already inserted'); 408 | } 409 | } 410 | }); 411 | 412 | var $video = $(this); 413 | 414 | if (USER_SETTING.HTML5_VIDEO_CONTROL) { 415 | if (!$video.data('controls')) { 416 | logger(`(${storyType})`, 'Added video html5 contorller #modify'); 417 | 418 | if (USER_SETTING.MODIFY_VIDEO_VOLUME) { 419 | this.volume = state.videoVolume; 420 | 421 | $video.on('loadstart', function () { 422 | this.volume = state.videoVolume; 423 | }); 424 | } 425 | 426 | let $videoParent = $video.parents('div').filter(function () { 427 | return $(this).attr('class') == null && $(this).attr('style') == null; 428 | }).first(); 429 | 430 | // story bottom bar 431 | let $bottomBar = $videoParent.next(); 432 | $bottomBar.hide(); 433 | 434 | // read more button in center 435 | let $readMoreButton = $videoParent.find('div[class][role="button"]'); 436 | $readMoreButton.hide(); 437 | 438 | const hideContextmenu = function (e) { 439 | e.preventDefault(); 440 | $video.css('z-index', '2'); 441 | $video.attr('controls', true); 442 | 443 | $readMoreButton.hide(); 444 | $bottomBar.hide(); 445 | 446 | toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () { 447 | return $(this).width() == $video.width(); 448 | }).first(), storyType, 'vertical'); 449 | }; 450 | 451 | // Hide layout to show controller 452 | $video.parent().find('video + div').on('contextmenu', hideContextmenu); 453 | $readMoreButton.on('contextmenu', hideContextmenu); 454 | $bottomBar.on('contextmenu', hideContextmenu); 455 | 456 | // Restore layout to show details interface 457 | $video.on('contextmenu', function (e) { 458 | e.preventDefault(); 459 | $video.css('z-index', '-1'); 460 | $video.removeAttr('controls'); 461 | 462 | $bottomBar.show(); 463 | $readMoreButton.show(); 464 | 465 | toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () { 466 | return $(this).width() == $video.width(); 467 | }).first(), storyType, 'vertical'); 468 | }); 469 | 470 | $video.on('volumechange', function () { 471 | // This is mute/unmute's icon 472 | let $element_mute_button = $videoParent.parent().find('svg > path[d^="M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0"], svg > path[d^="M16.636 7.028a1.5 1.5"]').parents('[role="button"]').first(); 473 | 474 | var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0; 475 | 476 | if (this.muted != is_elelment_muted) { 477 | this.volume = state.videoVolume; 478 | $element_mute_button?.trigger("click"); 479 | } 480 | 481 | if ($(this).attr('data-completed')) { 482 | state.videoVolume = this.volume; 483 | GM_setValue('G_VIDEO_VOLUME', this.volume); 484 | } 485 | 486 | if (this.volume == state.videoVolume) { 487 | $(this).attr('data-completed', true); 488 | } 489 | }); 490 | 491 | $video.css('position', 'absolute'); 492 | $video.css('z-index', '2'); 493 | $video.attr('data-controls', true); 494 | $video.attr('controls', true); 495 | } 496 | } 497 | else { 498 | toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () { 499 | return $(this).width() == $video.width(); 500 | }).first(), storyType, 'vertical'); 501 | } 502 | }); 503 | } 504 | } 505 | }); 506 | } 507 | } 508 | }); 509 | 510 | element_observer.observe($('div[id^="mount"]')[0], { 511 | childList: true, 512 | subtree: true, 513 | }); 514 | }); -------------------------------------------------------------------------------- /src/functions/highlight.js: -------------------------------------------------------------------------------- 1 | import { USER_SETTING, SVG, state } from "../settings"; 2 | import { 3 | updateLoadingBar, openNewTab, logger, 4 | setDownloadProgress, saveFiles, getStoryProgress 5 | } from "../utils/general"; 6 | import { _i18n } from "../utils/i18n"; 7 | import { getHighlightStories, getMediaInfo } from "../utils/api"; 8 | /*! ESLINT IMPORT END !*/ 9 | 10 | /** 11 | * onHighlightsStoryAll 12 | * @description Trigger user's highlight all download event. 13 | * 14 | * @return {void} 15 | */ 16 | export async function onHighlightsStoryAll() { 17 | updateLoadingBar(true); 18 | 19 | let date = new Date().getTime(); 20 | let timestamp = Math.floor(date / 1000); 21 | let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1); 22 | let highStories = await getHighlightStories(highlightId); 23 | let username = highStories.data.reels_media[0].owner.username; 24 | 25 | let complete = 0; 26 | setDownloadProgress(complete, highStories.data.reels_media[0].items.length); 27 | 28 | highStories.data.reels_media[0].items.forEach((item, idx) => { 29 | setTimeout(() => { 30 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 31 | timestamp = item.taken_at_timestamp; 32 | } 33 | 34 | item.display_resources.sort(function (a, b) { 35 | if (a.config_width < b.config_width) return 1; 36 | if (a.config_width > b.config_width) return -1; 37 | return 0; 38 | }); 39 | 40 | if (item.is_video) { 41 | saveFiles(item.video_resources[0].src, username, "stories", timestamp, 'mp4', item.id).then(() => { 42 | setDownloadProgress(++complete, highStories.data.reels_media[0].items.length); 43 | }); 44 | } 45 | else { 46 | saveFiles(item.display_resources[0].src, username, "stories", timestamp, 'jpg', item.id).then(() => { 47 | setDownloadProgress(++complete, highStories.data.reels_media[0].items.length); 48 | }); 49 | } 50 | }, 100 * idx); 51 | }); 52 | } 53 | 54 | /** 55 | * onHighlightsStory 56 | * @description Trigger user's highlight download event or button display event. 57 | * 58 | * @param {Boolean} isDownload - Check if it is a download operation 59 | * @param {Boolean} isPreview - Check if it is need to open new tab 60 | * @return {void} 61 | */ 62 | export async function onHighlightsStory(isDownload, isPreview) { 63 | var username = $('body > div section:visible a[href^="/"]').filter(function () { 64 | return $(this).attr('href').split('/').filter(e => e.length > 0).length === 1 65 | }).first().attr('href').split('/').filter(e => e.length > 0).at(0); 66 | 67 | if (isDownload) { 68 | let date = new Date().getTime(); 69 | let timestamp = Math.floor(date / 1000); 70 | let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1); 71 | let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length || 72 | $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length || 73 | $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length; 74 | let target = 0; 75 | 76 | updateLoadingBar(true); 77 | 78 | if (state.GL_dataCache.highlights[highlightId]) { 79 | logger('Fetch from memory cache:', highlightId); 80 | 81 | let totIndex = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items.length; 82 | username = state.GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username; 83 | target = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex]; 84 | } 85 | else { 86 | let highStories = await getHighlightStories(highlightId); 87 | let totIndex = highStories.data.reels_media[0].items.length; 88 | username = highStories.data.reels_media[0].owner.username; 89 | target = highStories.data.reels_media[0].items[totIndex - nowIndex]; 90 | 91 | state.GL_dataCache.highlights[highlightId] = highStories; 92 | } 93 | 94 | logger('onHighlightsStory', highlightId, state.GL_dataCache.highlights[highlightId]); 95 | 96 | 97 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 98 | timestamp = target.taken_at_timestamp; 99 | } 100 | 101 | if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) { 102 | let result = await getMediaInfo(target.id); 103 | 104 | if (result.status === 'ok') { 105 | if (result.items[0].video_versions) { 106 | if (isPreview) { 107 | openNewTab(result.items[0].video_versions[0].url); 108 | } 109 | else { 110 | saveFiles(result.items[0].video_versions[0].url, username, "highlights", timestamp, 'mp4', result.items[0].id); 111 | } 112 | } 113 | else { 114 | if (isPreview) { 115 | openNewTab(result.items[0].image_versions2.candidates[0].url); 116 | } 117 | else { 118 | saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', result.items[0].id); 119 | } 120 | } 121 | } 122 | else { 123 | if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) { 124 | delete state.GL_dataCache.highlights[highlightId]; 125 | state.tempFetchRateLimit = true; 126 | 127 | onHighlightsStory(true, isPreview); 128 | } 129 | else { 130 | alert('Fetch failed from Media API. API response message: ' + result.message); 131 | } 132 | 133 | logger(result); 134 | } 135 | } 136 | else { 137 | if (target.is_video) { 138 | if (isPreview) { 139 | openNewTab(target.video_resources.at(-1).src, username); 140 | } 141 | else { 142 | saveFiles(target.video_resources.at(-1).src, username, "highlights", timestamp, 'mp4', target.id); 143 | } 144 | } 145 | else { 146 | if (isPreview) { 147 | openNewTab(target.display_resources.at(-1).src, username); 148 | } 149 | else { 150 | saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', target.id); 151 | } 152 | } 153 | 154 | state.tempFetchRateLimit = false; 155 | } 156 | 157 | updateLoadingBar(false); 158 | } 159 | else { 160 | // Add the stories download button 161 | if (!$('.IG_DWHISTORY').length) { 162 | let $element = null; 163 | 164 | // Default detecter (section layout mode) 165 | if ($('body > div section._ac0a').length > 0) { 166 | $element = $('body > div section:visible._ac0a'); 167 | } 168 | else { 169 | $element = $('body > div section:visible > div > div[style]:not([class])'); 170 | $element.css('position', 'relative'); 171 | } 172 | 173 | // Detecter for div layout mode 174 | if ($element.length === 0) { 175 | let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])'); 176 | let nowSize = 0; 177 | 178 | $$element.each(function () { 179 | if ($(this).width() > nowSize) { 180 | nowSize = $(this).width(); 181 | $element = $(this).children('div').first(); 182 | } 183 | }); 184 | } 185 | 186 | 187 | if ($element != null) { 188 | //$element.css('position','relative'); 189 | $element.append(`
${SVG.DOWNLOAD}
`); 190 | $element.append(`
${SVG.NEW_TAB}
`); 191 | 192 | let $header = getStoryProgress(username); 193 | if ($header.length > 1) { 194 | $element.append(`
${SVG.DOWNLOAD_ALL}
`); 195 | } 196 | 197 | // replace something times ago format to publish time in first init 198 | let publishTitle = $header.parents("div[class]").find("time[datetime]")?.attr('title'); 199 | if (publishTitle != null) { 200 | $header.parents("div[class]").find("time[datetime]").text(publishTitle); 201 | } 202 | 203 | //// Modify video volume 204 | //if(USER_SETTING.MODIFY_VIDEO_VOLUME){ 205 | // $element.find('video').each(function(){ 206 | // $(this).on('play playing', function(){ 207 | // if(!$(this).data('modify')){ 208 | // $(this).attr('data-modify', true); 209 | // this.volume = VIDEO_VOLUME; 210 | // logger('(highlight) Added video event listener #modify'); 211 | // } 212 | // }); 213 | // }); 214 | //} 215 | 216 | // Make sure to first remove thumbnail button if still exists and highlight is a picture 217 | $element.find('img[referrerpolicy]').each(function () { 218 | $(this).on('load', function () { 219 | if (!$(this).data('remove-thumbnail')) { 220 | if ($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0) { 221 | $(this).attr('data-remove-thumbnail', true); 222 | $('.IG_DWHISTORY_THUMBNAIL').remove(); 223 | logger('(highlight) Manually removing thumbnail button'); 224 | } 225 | else { 226 | $(this).attr('data-remove-thumbnail', true); 227 | logger('(highlight) Thumbnail button is not present for this picture'); 228 | } 229 | } 230 | }); 231 | }); 232 | 233 | // Try to use event listener 'timeupdate' in order to detect if highlight is a video 234 | //$element.find('video').each(function(){ 235 | // $(this).on('timeupdate',function(){ 236 | // if(!$(this).data('modify-thumbnail')){ 237 | // if($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0){ 238 | // $(this).attr('data-modify-thumbnail', true); 239 | // onHighlightsStoryThumbnail(false); 240 | // logger('(highlight) Manually inserting thumbnail button'); 241 | // } 242 | // else{ 243 | // $(this).attr('data-modify-thumbnail', true); 244 | // logger('(highlight) Thumbnail button already inserted'); 245 | // } 246 | // } 247 | // }); 248 | //}); 249 | } 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * onHighlightsStoryThumbnail 256 | * @description Trigger user's highlight video thumbnail download event or button display event. 257 | * 258 | * @param {Boolean} isDownload - Check if it is a download operation 259 | * @return {void} 260 | */ 261 | export async function onHighlightsStoryThumbnail(isDownload) { 262 | if (isDownload) { 263 | let date = new Date().getTime(); 264 | let timestamp = Math.floor(date / 1000); 265 | let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1); 266 | let username = ""; 267 | let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length || 268 | $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length || 269 | $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length; 270 | let target = ""; 271 | 272 | updateLoadingBar(true); 273 | 274 | if (state.GL_dataCache.highlights[highlightId]) { 275 | logger('Fetch from memory cache:', highlightId); 276 | 277 | let totIndex = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items.length; 278 | username = state.GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username; 279 | target = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex]; 280 | } 281 | else { 282 | let highStories = await getHighlightStories(highlightId); 283 | let totIndex = highStories.data.reels_media[0].items.length; 284 | username = highStories.data.reels_media[0].owner.username; 285 | target = highStories.data.reels_media[0].items[totIndex - nowIndex]; 286 | 287 | state.GL_dataCache.highlights[highlightId] = highStories; 288 | } 289 | 290 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 291 | timestamp = target.taken_at_timestamp; 292 | } 293 | 294 | if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) { 295 | let result = await getMediaInfo(target.id); 296 | 297 | if (result.status === 'ok') { 298 | saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', highlightId); 299 | } 300 | else { 301 | if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) { 302 | delete state.GL_dataCache.highlights[highlightId]; 303 | state.tempFetchRateLimit = true; 304 | 305 | onHighlightsStoryThumbnail(true); 306 | } 307 | else { 308 | alert('Fetch failed from Media API. API response message: ' + result.message); 309 | } 310 | 311 | logger(result); 312 | } 313 | } 314 | else { 315 | saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', highlightId); 316 | state.tempFetchRateLimit = false; 317 | } 318 | 319 | updateLoadingBar(false); 320 | } 321 | else { 322 | if ($('body > div section video.xh8yej3').length) { 323 | // Add the stories thumbnail download button 324 | if (!$('.IG_DWHISTORY_THUMBNAIL').length) { 325 | let $element = null; 326 | 327 | // Default detecter (section layout mode) 328 | if ($('body > div section._ac0a').length > 0) { 329 | $element = $('body > div section:visible._ac0a'); 330 | } 331 | else { 332 | $element = $('body > div section:visible > div > div[style]:not([class])'); 333 | $element.css('position', 'relative'); 334 | } 335 | 336 | // Detecter for div layout mode 337 | if ($element.length === 0) { 338 | let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])'); 339 | let nowSize = 0; 340 | 341 | $$element.each(function () { 342 | if ($(this).width() > nowSize) { 343 | nowSize = $(this).width(); 344 | $element = $(this).children('div').first(); 345 | } 346 | }); 347 | } 348 | 349 | if ($element != null) { 350 | $element.append(`
${SVG.THUMBNAIL}
`); 351 | } 352 | } 353 | } 354 | else { 355 | $('.IG_DWHISTORY_THUMBNAIL').remove(); 356 | } 357 | } 358 | } -------------------------------------------------------------------------------- /src/functions/profile.js: -------------------------------------------------------------------------------- 1 | import { SVG } from "../settings"; 2 | import { updateLoadingBar, saveFiles } from "../utils/general"; 3 | import { _i18n } from "../utils/i18n"; 4 | import { getUserId, getUserHighSizeProfile } from "../utils/api"; 5 | /*! ESLINT IMPORT END !*/ 6 | 7 | /** 8 | * onProfileAvatar 9 | * @description Trigger user avatar download event or button display event. 10 | * 11 | * @param {Boolean} isDownload - Check if it is a download operation 12 | * @return {void} 13 | */ 14 | export async function onProfileAvatar(isDownload) { 15 | if (isDownload) { 16 | updateLoadingBar(true); 17 | 18 | let date = new Date().getTime(); 19 | let timestamp = Math.floor(date / 1000); 20 | let username = location.pathname.replaceAll(/(reels|tagged)\/$/ig, '').split('/').filter(s => s.length > 0).at(-1); 21 | let userInfo = await getUserId(username); 22 | 23 | try { 24 | let dataURL = await getUserHighSizeProfile(userInfo.user.pk); 25 | saveFiles(dataURL, username, "avatar", timestamp, 'jpg'); 26 | } 27 | // eslint-disable-next-line no-unused-vars 28 | catch (err) { 29 | saveFiles(userInfo.user.profile_pic_url, username, "avatar", timestamp, 'jpg'); 30 | } 31 | 32 | updateLoadingBar(false); 33 | } 34 | else { 35 | // Add the profile download button 36 | if (!$('.IG_DWPROFILE').length) { 37 | let profileTimer = setInterval(() => { 38 | if ($('.IG_DWPROFILE').length) { 39 | clearInterval(profileTimer); 40 | return; 41 | } 42 | 43 | $('header > *[class]:first-child img[alt][draggable]').parent().parent().append(`
${SVG.DOWNLOAD}
`); 44 | $('header > *[class]:first-child img[alt][draggable]').parent().parent().css('position', 'relative'); 45 | $('header > *[class]:first-child img[alt]:not([draggable])').parent().parent().parent().append(`
${SVG.DOWNLOAD}
`); 46 | $('header > *[class]:first-child img[alt]:not([draggable])').parent().parent().parent().css('position', 'relative'); 47 | }, 150); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/functions/reel.js: -------------------------------------------------------------------------------- 1 | import { USER_SETTING, SVG, state } from "../settings"; 2 | import { updateLoadingBar, saveFiles, openNewTab, logger, toggleVolumeSilder } from "../utils/general"; 3 | import { getBlobMedia } from "../utils/api"; 4 | import { filterResourceData } from "./post"; 5 | import { _i18n } from "../utils/i18n"; 6 | /*! ESLINT IMPORT END !*/ 7 | 8 | /** 9 | * onReels 10 | * @description Trigger user's reels download event or button display event. 11 | * 12 | * @param {Boolean} isDownload - Check if it is a download operation 13 | * @param {Boolean} isVideo - Check if reel is a video element 14 | * @param {Boolean} isPreview - Check if it is need to open new tab 15 | * @return {void} 16 | */ 17 | export async function onReels(isDownload, isVideo, isPreview) { 18 | if (isDownload) { 19 | updateLoadingBar(true); 20 | 21 | let reelsPath = location.href.split('?').at(0).split('instagram.com/reels/').at(-1).replaceAll('/', ''); 22 | let result = await getBlobMedia(reelsPath); 23 | let media = filterResourceData(result.data); 24 | 25 | let timestamp = new Date().getTime(); 26 | 27 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 28 | if (result.type === 'query_hash') { 29 | timestamp = media.shortcode_media.taken_at_timestamp; 30 | } 31 | else { 32 | timestamp = media.taken_at; 33 | } 34 | } 35 | 36 | if (result.type === 'query_hash') { 37 | if (isVideo && media.shortcode_media.is_video) { 38 | if (isPreview) { 39 | openNewTab(media.shortcode_media.video_url); 40 | } 41 | else { 42 | let type = 'mp4'; 43 | saveFiles(media.shortcode_media.video_url, media.shortcode_media.owner.username, "reels", timestamp, type, reelsPath); 44 | } 45 | } 46 | else { 47 | if (isPreview) { 48 | openNewTab(media.shortcode_media.display_resources.at(-1).src); 49 | } 50 | else { 51 | let type = 'jpg'; 52 | saveFiles(media.shortcode_media.display_resources.at(-1).src, media.shortcode_media.owner.username, "reels", timestamp, type, reelsPath); 53 | } 54 | } 55 | } 56 | else { 57 | if (isVideo && media.video_versions != null) { 58 | if (isPreview) { 59 | openNewTab(media.video_versions[0].url); 60 | } 61 | else { 62 | let type = 'mp4'; 63 | saveFiles(media.video_versions[0].url, media.owner.username, "reels", timestamp, type, reelsPath); 64 | } 65 | } 66 | else { 67 | if (isPreview) { 68 | openNewTab(media.image_versions2.candidates[0].url); 69 | } 70 | else { 71 | let type = 'jpg'; 72 | saveFiles(media.image_versions2.candidates[0].url, media.owner.username, "reels", timestamp, type, reelsPath); 73 | } 74 | } 75 | } 76 | 77 | updateLoadingBar(false); 78 | } 79 | else { 80 | //$('.IG_REELS_THUMBNAIL, .IG_REELS').remove(); 81 | var timer = setInterval(() => { 82 | if ($('section > main[role="main"] > div div.x1qjc9v5 video').length > 0) { 83 | clearInterval(timer); 84 | 85 | if (USER_SETTING.SCROLL_BUTTON) { 86 | $('#scrollWrapper').remove(); 87 | $('section > main[role="main"]').append('
'); 88 | $('section > main[role="main"] > #scrollWrapper').append('
'); 89 | $('section > main[role="main"] > #scrollWrapper').append('
'); 90 | 91 | $('section > main[role="main"] > #scrollWrapper > .button-up').on('click', function () { 92 | $('section > main[role="main"] > div')[0].scrollBy({ top: -30, behavior: "smooth" }); 93 | }); 94 | $('section > main[role="main"] > #scrollWrapper > .button-down').on('click', function () { 95 | $('section > main[role="main"] > div')[0].scrollBy({ top: 30, behavior: "smooth" }); 96 | }); 97 | } 98 | 99 | // reels scroll has [tabindex] but header not. 100 | $('section > main[role="main"] > div[tabindex], section > main[role="main"] > div[class]').children('div').each(function () { 101 | if ($(this).children().length > 0) { 102 | if (!$(this).children().find('.IG_REELS').length) { 103 | var $main = $(this); 104 | 105 | $(this).children().css('position', 'relative'); 106 | 107 | $(this).children().append(`
${SVG.DOWNLOAD}
`); 108 | $(this).children().append(`
${SVG.NEW_TAB}
`); 109 | $(this).children().append(`
${SVG.THUMBNAIL}
`); 110 | 111 | // Disable video autoplay 112 | if (USER_SETTING.DISABLE_VIDEO_LOOPING) { 113 | $(this).find('video').each(function () { 114 | $(this).on('ended', function () { 115 | if (!$(this).data('loop')) { 116 | let $element_play_button = $(this).next().find('div[role="presentation"] > div svg > path[d^="M5.888"]').parents('button[role="button"], div[role="button"]'); 117 | if ($element_play_button.length > 0) { 118 | $(this).attr('data-loop', true); 119 | $element_play_button.trigger("click"); 120 | logger('Adding video event listener #loop, then paused click()'); 121 | } 122 | else { 123 | $(this).attr('data-loop', true); 124 | $(this).parent().find('.xpgaw4o').removeAttr('style'); 125 | this.pause(); 126 | logger('Adding video event listener #loop, then paused pause()'); 127 | } 128 | } 129 | }); 130 | }); 131 | } 132 | 133 | // Modify video volume 134 | //if(USER_SETTING.MODIFY_VIDEO_VOLUME){ 135 | // $(this).find('video').each(function(){ 136 | // $(this).on('play playing', function(){ 137 | // if(!$(this).data('modify')){ 138 | // $(this).attr('data-modify', true); 139 | // this.volume = VIDEO_VOLUME; 140 | // logger('(reel) Added video event listener #modify'); 141 | // } 142 | // }); 143 | // }); 144 | //} 145 | 146 | if (USER_SETTING.HTML5_VIDEO_CONTROL) { 147 | $(this).find('video').each(function () { 148 | if (!$(this).data('controls')) { 149 | let $video = $(this); 150 | 151 | logger('(reel) Added video html5 contorller #modify'); 152 | 153 | if (USER_SETTING.MODIFY_VIDEO_VOLUME) { 154 | this.volume = state.videoVolume; 155 | 156 | $(this).on('loadstart', function () { 157 | this.volume = state.videoVolume; 158 | }); 159 | } 160 | 161 | // Restore layout to show details interface 162 | $(this).on('contextmenu', function (e) { 163 | e.preventDefault(); 164 | $video.css('z-index', '-1'); 165 | $video.removeAttr('controls'); 166 | }); 167 | 168 | // Hide layout to show controller 169 | $(this).parent().find('video + div div[role="button"]').filter(function () { 170 | return $(this).parent('div[role="presentation"]').length > 0 && $(this).css('cursor') === 'pointer' && $(this).attr('style') != null; 171 | }).first().on('contextmenu', function (e) { 172 | e.preventDefault(); 173 | $video.css('z-index', '2'); 174 | $video.attr('controls', true); 175 | }); 176 | 177 | 178 | $(this).on('volumechange', function () { 179 | // eslint-disable-next-line no-unused-vars 180 | let $element_mute_button = $(this).parent().find('video + div > div').find('button[type="button"], div[role="button"]').filter(function (idx) { 181 | // This is mute/unmute's icon 182 | return $(this).width() <= 64 && $(this).height() <= 64 && $(this).find('svg > path[d^="M16.636 7.028a1.5"], svg > path[d^="M1.5 13.3c-.8"]').length > 0; 183 | }); 184 | 185 | var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0; 186 | 187 | if (this.muted != is_elelment_muted) { 188 | this.volume = state.videoVolume; 189 | $element_mute_button?.trigger("click"); 190 | } 191 | 192 | if ($(this).attr('data-completed')) { 193 | state.videoVolume = this.volume; 194 | GM_setValue('G_VIDEO_VOLUME', this.volume); 195 | } 196 | 197 | if (this.volume == state.videoVolume) { 198 | $(this).attr('data-completed', true); 199 | } 200 | }); 201 | 202 | $(this).css('position', 'absolute'); 203 | $(this).css('z-index', '2'); 204 | $(this).attr('data-controls', true); 205 | $(this).attr('controls', true); 206 | } 207 | }); 208 | } 209 | 210 | var $videos = $main.find('video'); 211 | var $buttonParent = $(this).find('div[role="presentation"] > div[role="button"] > div').first(); 212 | toggleVolumeSilder($videos, $buttonParent, 'reel'); 213 | } 214 | } 215 | }); 216 | } 217 | }, 250); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/functions/story.js: -------------------------------------------------------------------------------- 1 | import { USER_SETTING, SVG, state } from "../settings"; 2 | import { 3 | updateLoadingBar, setDownloadProgress, 4 | saveFiles, getStoryProgress, openNewTab, logger, 5 | getStoryId 6 | } from "../utils/general"; 7 | import { getUserId, getStories, getMediaInfo } from "../utils/api"; 8 | import { _i18n } from "../utils/i18n"; 9 | /*! ESLINT IMPORT END !*/ 10 | 11 | /** 12 | * onStoryAll 13 | * @description Trigger user's story all download event. 14 | * 15 | * @return {void} 16 | */ 17 | export async function onStoryAll() { 18 | updateLoadingBar(true); 19 | 20 | let date = new Date().getTime(); 21 | let timestamp = Math.floor(date / 1000); 22 | let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1); 23 | 24 | let userInfo = await getUserId(username); 25 | let userId = userInfo.user.pk; 26 | let stories = await getStories(userId); 27 | 28 | let complete = 0; 29 | setDownloadProgress(complete, stories.data.reels_media[0].items.length); 30 | 31 | stories.data.reels_media[0].items.forEach((item, idx) => { 32 | setTimeout(() => { 33 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 34 | timestamp = item.taken_at_timestamp; 35 | } 36 | 37 | item.display_resources.sort(function (a, b) { 38 | if (a.config_width < b.config_width) return 1; 39 | if (a.config_width > b.config_width) return -1; 40 | return 0; 41 | }); 42 | 43 | if (item.is_video) { 44 | saveFiles(item.video_resources[0].src, username, "stories", timestamp, 'mp4', item.id).then(() => { 45 | setDownloadProgress(++complete, stories.data.reels_media[0].items.length); 46 | }); 47 | } 48 | else { 49 | saveFiles(item.display_resources[0].src, username, "stories", timestamp, 'jpg', item.id).then(() => { 50 | setDownloadProgress(++complete, stories.data.reels_media[0].items.length); 51 | }); 52 | } 53 | }, 100 * idx); 54 | }); 55 | } 56 | 57 | /** 58 | * onStory 59 | * @description Trigger user's story download event or button display event. 60 | * 61 | * @param {Boolean} isDownload - Check if it is a download operation 62 | * @param {Boolean} isForce - Check if downloading directly from API instead of cache 63 | * @param {Boolean} isPreview - Check if it is need to open new tab 64 | * @return {void} 65 | */ 66 | export async function onStory(isDownload, isForce, isPreview) { 67 | var username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1); 68 | if (isDownload) { 69 | let date = new Date().getTime(); 70 | let timestamp = Math.floor(date / 1000); 71 | 72 | updateLoadingBar(true); 73 | if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) { 74 | let mediaId = null; 75 | 76 | let userInfo = await getUserId(username); 77 | let userId = userInfo.user.pk; 78 | let stories = await getStories(userId); 79 | let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1); 80 | 81 | /* 82 | let latest_reel_media = stories.data.reels_media[0].latest_reel_media; 83 | let last_seen = stories.data.reels_media[0].seen; 84 | logger(stories); 85 | 86 | if(urlID == null){ 87 | mediaId = stories.data.reels_media[0].items.filter(function(item, index){ 88 | return item.taken_at_timestamp === last_seen && item.taken_at_timestamp !== latest_reel_media || last_seen === latest_reel_media && index === 0; 89 | })?.at(0)?.id; 90 | logger('nula', mediaId); 91 | } 92 | else{ 93 | stories.data.reels_media[0].items.forEach(item => { 94 | if(item.id == urlID){ 95 | mediaId = item.id; 96 | } 97 | }); 98 | } 99 | */ 100 | 101 | stories.data.reels_media[0].items.forEach(item => { 102 | if (item.id == urlID) { 103 | mediaId = item.id; 104 | } 105 | }); 106 | 107 | if (mediaId == null) { 108 | let $header = getStoryProgress(username); 109 | 110 | $header.each(function (index) { 111 | if ($(this).children().length > 0) { 112 | mediaId = stories.data.reels_media[0].items[index].id; 113 | } 114 | }); 115 | } 116 | 117 | if (mediaId == null) { 118 | // appear in from profile page to story page 119 | $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) { 120 | if ($(this).hasClass('x1lix1fw')) { 121 | if ($(this).children().length > 0) { 122 | mediaId = stories.data.reels_media[0].items[index].id; 123 | } 124 | } 125 | }); 126 | 127 | // appear in from home page to story page 128 | $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) { 129 | if ($(this).children().hasClass('_ac3q')) { 130 | mediaId = stories.data.reels_media[0].items[index].id; 131 | } 132 | }); 133 | } 134 | 135 | if (mediaId == null) { 136 | mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1); 137 | } 138 | 139 | let result = await getMediaInfo(mediaId); 140 | 141 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 142 | timestamp = result.items[0].taken_at; 143 | } 144 | 145 | if (result.status === 'ok') { 146 | if (result.items[0].video_versions) { 147 | if (isPreview) { 148 | openNewTab(result.items[0].video_versions[0].url); 149 | } 150 | else { 151 | saveFiles(result.items[0].video_versions[0].url, username, "stories", timestamp, 'mp4', mediaId); 152 | } 153 | } 154 | else { 155 | if (isPreview) { 156 | openNewTab(result.items[0].image_versions2.candidates[0].url); 157 | } 158 | else { 159 | saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId); 160 | } 161 | } 162 | } 163 | else { 164 | if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) { 165 | state.tempFetchRateLimit = true; 166 | onStory(isDownload, isForce, isPreview); 167 | } 168 | else { 169 | alert('Fetch failed from Media API. API response message: ' + result.message); 170 | } 171 | logger(result); 172 | } 173 | 174 | updateLoadingBar(false); 175 | return; 176 | } 177 | 178 | if ($('body > div section:visible video[playsinline]').length > 0) { 179 | // Download stories if it is video 180 | let type = "mp4"; 181 | let videoURL = ""; 182 | let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1); 183 | let mediaId = null; 184 | 185 | if (state.GL_dataCache.stories[username] && !isForce) { 186 | logger('Fetch from memory cache:', username); 187 | state.GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => { 188 | if (item.id == targetURL) { 189 | videoURL = item.video_resources[0].src; 190 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 191 | timestamp = item.taken_at_timestamp; 192 | mediaId = item.id; 193 | } 194 | } 195 | }); 196 | 197 | if (videoURL.length == 0) { 198 | logger('Memory cache not found, try fetch from API:', username); 199 | onStory(true, true); 200 | return; 201 | } 202 | } 203 | else { 204 | let userInfo = await getUserId(username); 205 | let userId = userInfo.user.pk; 206 | let stories = await getStories(userId); 207 | 208 | stories.data.reels_media[0].items.forEach(item => { 209 | if (item.id == targetURL) { 210 | videoURL = item.video_resources[0].src; 211 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 212 | timestamp = item.taken_at_timestamp; 213 | mediaId = item.id; 214 | } 215 | } 216 | }); 217 | 218 | // GitHub issue #4: thinkpad4 219 | if (videoURL.length == 0) { 220 | 221 | let $header = getStoryProgress(username); 222 | 223 | $header.each(function (index) { 224 | if ($(this).children().length > 0) { 225 | videoURL = stories.data.reels_media[0].items[index].video_resources[0].src; 226 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 227 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 228 | mediaId = stories.data.reels_media[0].items[index].id; 229 | } 230 | } 231 | }); 232 | 233 | 234 | if (videoURL.length == 0) { 235 | // appear in from profile page to story page 236 | $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) { 237 | if ($(this).hasClass('x1lix1fw')) { 238 | if ($(this).children().length > 0) { 239 | videoURL = stories.data.reels_media[0].items[index].video_resources[0].src; 240 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 241 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 242 | mediaId = stories.data.reels_media[0].items[index].id; 243 | } 244 | } 245 | } 246 | }); 247 | 248 | // appear in from home page to story page 249 | $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) { 250 | if ($(this).children().hasClass('_ac3q')) { 251 | videoURL = stories.data.reels_media[0].items[index].video_resources[0].src; 252 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 253 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 254 | mediaId = stories.data.reels_media[0].items[index].id; 255 | } 256 | } 257 | }); 258 | } 259 | } 260 | 261 | state.GL_dataCache.stories[username] = stories; 262 | } 263 | 264 | if (videoURL.length == 0) { 265 | alert(_i18n("NO_VID_URL")); 266 | } 267 | else { 268 | if (isPreview) { 269 | openNewTab(videoURL); 270 | } 271 | else { 272 | saveFiles(videoURL, username, "stories", timestamp, type, mediaId); 273 | } 274 | } 275 | } 276 | else { 277 | // Download stories if it is image 278 | let srcset = $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').attr('srcset')?.split(',')[0]?.split(' ')[0]; 279 | let link = (srcset) ? srcset : $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').filter(function () { 280 | return $(this).parents('a').length === 0 && $(this).width() === $(this).parent().width(); 281 | }).attr('src'); 282 | 283 | if (!link) { 284 | // _aa63 mean stories picture in stories page (not avatar) 285 | let $element = $('body > div section:visible img._aa63'); 286 | link = ($element.attr('srcset')) ? $element.attr('srcset')?.split(',')[0]?.split(' ')[0] : $element.attr('src'); 287 | } 288 | 289 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 290 | timestamp = new Date($('body > div section:visible time[datetime][class]').first().attr('datetime')).getTime(); 291 | } 292 | 293 | let downloadLink = link; 294 | let type = 'jpg'; 295 | 296 | if (isPreview) { 297 | openNewTab(downloadLink); 298 | } 299 | else { 300 | saveFiles(downloadLink, username, "stories", timestamp, type, getStoryId(downloadLink) ?? ""); 301 | } 302 | } 303 | 304 | state.tempFetchRateLimit = false; 305 | updateLoadingBar(false); 306 | } 307 | else { 308 | // Add the stories download button 309 | if (!$('.IG_DWSTORY').length) { 310 | state.GL_dataCache.stories = {}; 311 | let $element = null; 312 | // Default detecter (section layout mode) 313 | if ($('body > div section._ac0a').length > 0) { 314 | $element = $('body > div section:visible._ac0a'); 315 | } 316 | // detecter (single story layout mode) 317 | else { 318 | $element = $('body > div section:visible > div > div[style]:not([class])'); 319 | $element.css('position', 'relative'); 320 | } 321 | 322 | 323 | if ($element.length === 0) { 324 | $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])'); 325 | $element.css('position', 'relative'); 326 | } 327 | 328 | if ($element.length === 0) { 329 | $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div div[style]:not([class]) > div:not([data-visualcompletion="loading-state"])'); 330 | $element.css('position', 'relative'); 331 | } 332 | 333 | 334 | // Detecter for div layout mode 335 | if ($element.length === 0) { 336 | let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])'); 337 | let nowSize = 0; 338 | 339 | $$element.each(function () { 340 | if ($(this).width() > nowSize) { 341 | nowSize = $(this).width(); 342 | $element = $(this).children('div').first(); 343 | } 344 | }); 345 | } 346 | 347 | 348 | if ($element != null) { 349 | $element.first().css('position', 'relative'); 350 | $element.first().append(`
${SVG.DOWNLOAD}
`); 351 | $element.first().append(`
${SVG.NEW_TAB}
`); 352 | 353 | let $header = getStoryProgress(username); 354 | if ($header.length > 1) { 355 | $element.first().append(`
${SVG.DOWNLOAD_ALL}
`); 356 | } 357 | 358 | // Modify video volume 359 | //if(USER_SETTING.MODIFY_VIDEO_VOLUME){ 360 | // $element.find('video').each(function(){ 361 | // $(this).on('play playing', function(){ 362 | // if(!$(this).data('modify')){ 363 | // $(this).attr('data-modify', true); 364 | // this.volume = VIDEO_VOLUME; 365 | // logger('(story) Added video event listener #modify'); 366 | // } 367 | // }); 368 | // }); 369 | //} 370 | 371 | // Make sure to first remove thumbnail button if still exists and story is a picture 372 | $element.find('img[referrerpolicy]').each(function () { 373 | $(this).on('load', function () { 374 | if (!$(this).data('remove-thumbnail')) { 375 | if ($element.find('.IG_DWSTORY_THUMBNAIL').length === 0) { 376 | $(this).attr('data-remove-thumbnail', true); 377 | $('.IG_DWSTORY_THUMBNAIL').remove(); 378 | logger('(story) Manually removing thumbnail button'); 379 | } 380 | else { 381 | $(this).attr('data-remove-thumbnail', true); 382 | logger('(story) Thumbnail button is not present for this picture'); 383 | } 384 | } 385 | }); 386 | }); 387 | 388 | // Try to use event listener 'timeupdate' in order to detect if story is a video 389 | //$element.find('video').each(function(){ 390 | // $(this).on('timeupdate',function(){ 391 | // if(!$(this).data('modify-thumbnail')){ 392 | // if($element.find('.IG_DWSTORY_THUMBNAIL').length === 0){ 393 | // $(this).attr('data-modify-thumbnail', true); 394 | // onStoryThumbnail(false); 395 | // logger('(story) Manually inserting thumbnail button'); 396 | // } 397 | // else{ 398 | // $(this).attr('data-modify-thumbnail', true); 399 | // logger('(story) Thumbnail button already inserted'); 400 | // } 401 | // } 402 | // }); 403 | //}); 404 | } 405 | } 406 | } 407 | } 408 | 409 | /** 410 | * onStoryThumbnail 411 | * @description Trigger user's story video thumbnail download event or button display event. 412 | * 413 | * @param {Boolean} isDownload - Check if it is a download operation 414 | * @param {Boolean} isForce - Check if downloading directly from API instead of cache 415 | * @return {void} 416 | */ 417 | export async function onStoryThumbnail(isDownload, isForce) { 418 | if (isDownload) { 419 | // Download stories if it is video 420 | let date = new Date().getTime(); 421 | let timestamp = Math.floor(date / 1000); 422 | let type = 'jpg'; 423 | let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split('/').at(2); 424 | // Download thumbnail 425 | let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1); 426 | let videoThumbnailURL = ""; 427 | let mediaId = null; 428 | 429 | updateLoadingBar(true); 430 | 431 | if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) { 432 | let userInfo = await getUserId(username); 433 | let userId = userInfo.user.pk; 434 | let stories = await getStories(userId); 435 | let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1); 436 | 437 | stories.data.reels_media[0].items.forEach(item => { 438 | if (item.id == urlID) { 439 | mediaId = item.id; 440 | } 441 | }); 442 | 443 | if (mediaId == null) { 444 | let $header = getStoryProgress(username); 445 | 446 | $header.each(function (index) { 447 | if ($(this).children().length > 0) { 448 | mediaId = stories.data.reels_media[0].items[index].id; 449 | } 450 | }); 451 | } 452 | 453 | if (mediaId == null) { 454 | // appear in from profile page to story page 455 | $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) { 456 | if ($(this).hasClass('x1lix1fw')) { 457 | if ($(this).children().length > 0) { 458 | mediaId = stories.data.reels_media[0].items[index].id; 459 | } 460 | } 461 | }); 462 | 463 | // appear in from home page to story page 464 | $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) { 465 | if ($(this).children().hasClass('_ac3q')) { 466 | mediaId = stories.data.reels_media[0].items[index].id; 467 | } 468 | }); 469 | } 470 | 471 | if (mediaId == null) { 472 | mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1); 473 | } 474 | 475 | let result = await getMediaInfo(mediaId); 476 | 477 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 478 | timestamp = result.items[0].taken_at; 479 | } 480 | 481 | if (result.status === 'ok') { 482 | saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId); 483 | 484 | } 485 | else { 486 | if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) { 487 | state.tempFetchRateLimit = true; 488 | onStoryThumbnail(true, isForce); 489 | } 490 | else { 491 | alert('Fetch failed from Media API. API response message: ' + result.message); 492 | } 493 | 494 | logger(result); 495 | } 496 | 497 | updateLoadingBar(false); 498 | return; 499 | } 500 | 501 | if (state.GL_dataCache.stories[username] && !isForce) { 502 | logger('Fetch from memory cache:', username); 503 | state.GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => { 504 | if (item.id == targetURL) { 505 | videoThumbnailURL = item.display_url; 506 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 507 | timestamp = item.taken_at_timestamp; 508 | mediaId = item.id; 509 | } 510 | } 511 | }); 512 | 513 | if (videoThumbnailURL.length == 0) { 514 | logger('Memory cache not found, try fetch from API:', username); 515 | onStoryThumbnail(true, true); 516 | return; 517 | } 518 | } 519 | else { 520 | let userInfo = await getUserId(username); 521 | let userId = userInfo.user.pk; 522 | let stories = await getStories(userId); 523 | 524 | stories.data.reels_media[0].items.forEach(item => { 525 | if (item.id == targetURL) { 526 | videoThumbnailURL = item.display_url; 527 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 528 | timestamp = item.taken_at_timestamp; 529 | mediaId = item.id; 530 | } 531 | } 532 | }); 533 | 534 | // GitHub issue #4: thinkpad4 535 | if (videoThumbnailURL.length == 0) { 536 | let $header = getStoryProgress(username); 537 | 538 | $header.each(function (index) { 539 | if ($(this).children().length > 0) { 540 | videoThumbnailURL = stories.data.reels_media[0].items[index].display_url; 541 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 542 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 543 | mediaId = stories.data.reels_media[0].items[index].id; 544 | } 545 | } 546 | }); 547 | 548 | if (videoThumbnailURL.length == 0) { 549 | // appear in from profile page to story page 550 | $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) { 551 | if ($(this).hasClass('x1lix1fw')) { 552 | if ($(this).children().length > 0) { 553 | videoThumbnailURL = stories.data.reels_media[0].items[index].display_url; 554 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 555 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 556 | mediaId = stories.data.reels_media[0].items[index].id; 557 | } 558 | } 559 | } 560 | }); 561 | 562 | // appear in from home page to story page 563 | $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) { 564 | if ($(this).children().hasClass('_ac3q')) { 565 | videoThumbnailURL = stories.data.reels_media[0].items[index].display_url; 566 | if (USER_SETTING.RENAME_PUBLISH_DATE) { 567 | timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp; 568 | mediaId = stories.data.reels_media[0].items[index].id; 569 | } 570 | } 571 | }); 572 | } 573 | } 574 | } 575 | 576 | saveFiles(videoThumbnailURL, username, "thumbnail", timestamp, type, mediaId); 577 | state.tempFetchRateLimit = false; 578 | updateLoadingBar(false); 579 | } 580 | else { 581 | if ($('body > div div.IG_DWSTORY').parent().find('video[class]').length) { 582 | // Add the stories download button 583 | let $element = null; 584 | // Default detecter (section layout mode) 585 | if ($('body > div section._ac0a').length > 0) { 586 | $element = $('body > div section:visible._ac0a'); 587 | } 588 | // detecter (single story layout mode) 589 | else { 590 | $element = $('body > div section:visible > div > div[style]:not([class])'); 591 | $element.css('position', 'relative'); 592 | } 593 | 594 | if ($element.length === 0) { 595 | $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])'); 596 | $element.css('position', 'relative'); 597 | } 598 | 599 | if ($element.length === 0) { 600 | $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div div[style]:not([class]) > div:not([data-visualcompletion="loading-state"])'); 601 | $element.css('position', 'relative'); 602 | } 603 | 604 | // Detecter for div layout mode 605 | if ($element.length === 0) { 606 | let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])'); 607 | let nowSize = 0; 608 | 609 | $$element.each(function () { 610 | if ($(this).width() > nowSize) { 611 | nowSize = $(this).width(); 612 | $element = $(this).children('div').first(); 613 | } 614 | }); 615 | } 616 | 617 | 618 | if ($element != null) { 619 | $element.first().css('position', 'relative'); 620 | $element.first().append(`
${SVG.THUMBNAIL}
`); 621 | } 622 | 623 | } 624 | } 625 | } -------------------------------------------------------------------------------- /src/initial.js: -------------------------------------------------------------------------------- 1 | import { initSettings, registerMenuCommand, checkingScriptUpdate, logger } from "./utils/general"; 2 | import { getTranslationText, repaintingTranslations } from "./utils/i18n"; 3 | import { style, state } from "./settings"; 4 | /*! ESLINT IMPORT END !*/ 5 | 6 | // initialization script 7 | initSettings(); 8 | GM_addStyle(style); 9 | registerMenuCommand(); 10 | 11 | getTranslationText(state.lang).then((res) => { 12 | state.locale[state.lang] = res; 13 | repaintingTranslations(); 14 | registerMenuCommand(); 15 | checkingScriptUpdate(300); 16 | }).catch((err) => { 17 | registerMenuCommand(); 18 | checkingScriptUpdate(300); 19 | 20 | if (!state.lang.startsWith('en')) { 21 | console.error('getTranslationText catch error:', err); 22 | } 23 | }); 24 | 25 | logger('Script Loaded', GM_info.script.name, 'version:', GM_info.script.version); 26 | /*******************************/ -------------------------------------------------------------------------------- /src/metadata.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name IG Helper 3 | // @name:zh-TW IG小精靈 4 | // @name:zh-CN IG小助手 5 | // @name:ja IG助手 6 | // @name:ko IG조수 7 | // @namespace https://github.snkms.com/ 8 | // @version 3.5.5 9 | // @description Downloading is possible for both photos and videos from posts, as well as for stories, reels or profile picture. 10 | // @description:zh-TW 一鍵下載對方 Instagram 貼文中的相片、影片甚至是他們的限時動態、連續短片及大頭貼圖片! 11 | // @description:zh-CN 一键下载对方 Instagram 帖子中的相片、视频甚至是他们的快拍、Reels及头像图片! 12 | // @description:ja 投稿の写真と動画だけでなく、ストーリー、リール、プロフィール写真もダウンロードできます。 13 | // @description:ko 게시물의 사진과 동영상뿐만 아니라 스토리, 릴 또는 프로필 사진도 다운로드할 수 있습니다. 14 | // @description:ro Descărcarea este posibilă atât pentru fotografiile și videoclipurile din postări, cât și pentru storyuri, reels sau poze de profil. 15 | // @author SN-Koarashi (5026) 16 | // @match https://*.instagram.com/* 17 | // @grant GM_info 18 | // @grant GM_addStyle 19 | // @grant GM_setValue 20 | // @grant GM_getValue 21 | // @grant GM_xmlhttpRequest 22 | // @grant GM_registerMenuCommand 23 | // @grant GM_unregisterMenuCommand 24 | // @grant GM_getResourceText 25 | // @grant GM_notification 26 | // @grant GM_openInTab 27 | // @connect i.instagram.com 28 | // @connect raw.githubusercontent.com 29 | // @require https://code.jquery.com/jquery-3.7.1.min.js#sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo= 30 | // @resource INTERNAL_CSS https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/style.css 31 | // @resource LOCALE_MANIFEST https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale/manifest.json 32 | // @supportURL https://github.com/SN-Koarashi/ig-helper/ 33 | // @contributionURL https://ko-fi.com/snkoarashi 34 | // @icon https://www.google.com/s2/favicons?domain=www.instagram.com&sz=32 35 | // @compatible firefox >= 100 36 | // @compatible chrome >= 100 37 | // @compatible edge >= 100 38 | // @license GPL-3.0-only 39 | // @run-at document-idle 40 | // ==/UserScript== 41 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | import { onReadyMyDW } from "./functions/post"; 2 | /*! ESLINT IMPORT END !*/ 3 | 4 | /******** USER SETTINGS ********/ 5 | // !!! DO NOT CHANGE THIS AREA !!! 6 | // ??? PLEASE CHANGE SETTING WITH MENU ??? 7 | export const USER_SETTING = { 8 | 'CHECK_UPDATE': true, 9 | 'AUTO_RENAME': true, 10 | 'RENAME_PUBLISH_DATE': true, 11 | 'DISABLE_VIDEO_LOOPING': false, 12 | 'HTML5_VIDEO_CONTROL': false, 13 | 'REDIRECT_CLICK_USER_STORY_PICTURE': false, 14 | 'FORCE_FETCH_ALL_RESOURCES': false, 15 | 'DIRECT_DOWNLOAD_VISIBLE_RESOURCE': false, 16 | 'DIRECT_DOWNLOAD_ALL': false, 17 | 'MODIFY_VIDEO_VOLUME': false, 18 | 'MODIFY_RESOURCE_EXIF': false, 19 | 'SCROLL_BUTTON': true, 20 | 'FORCE_RESOURCE_VIA_MEDIA': false, 21 | 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT': false, 22 | 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST': false, 23 | 'SKIP_VIEW_STORY_CONFIRM': false 24 | }; 25 | export const CHILD_NODES = ['RENAME_PUBLISH_DATE', 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT', 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST']; 26 | /*******************************/ 27 | 28 | // Icon download by Google Fonts Material Icon 29 | export const SVG = { 30 | DOWNLOAD: '', 31 | NEW_TAB: '', 32 | THUMBNAIL: '', 33 | DOWNLOAD_ALL: '', 34 | CLOSE: '', 35 | FULLSCREEN: '', 36 | TURN_DEG: '' 37 | }; 38 | 39 | /*******************************/ 40 | export const checkInterval = 250; 41 | export const style = GM_getResourceText("INTERNAL_CSS"); 42 | export const locale_manifest = JSON.parse(GM_getResourceText("LOCALE_MANIFEST")); 43 | 44 | export var state = { 45 | videoVolume: (GM_getValue('G_VIDEO_VOLUME')) ? GM_getValue('G_VIDEO_VOLUME') : 1, 46 | tempFetchRateLimit: false, 47 | fileRenameFormat: (GM_getValue('G_RENAME_FORMAT')) ? GM_getValue('G_RENAME_FORMAT') : '%USERNAME%-%SOURCE_TYPE%-%SHORTCODE%-%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND%_%ORIGINAL_NAME_FIRST%', 48 | registerMenuIds: [], 49 | locale: {}, 50 | lang: GM_getValue('lang') || navigator.language || navigator.userLanguage, 51 | currentURL: location.href, 52 | firstStarted: false, 53 | pageLoaded: false, 54 | GL_registerEventList: [], 55 | GL_logger: [], 56 | GL_referrer: null, 57 | GL_postPath: null, 58 | GL_username: null, 59 | GL_repeat: null, 60 | GL_dataCache: { 61 | stories: {}, 62 | highlights: {} 63 | }, 64 | GL_observer: new MutationObserver(function () { 65 | onReadyMyDW(); 66 | }) 67 | }; 68 | /*******************************/ -------------------------------------------------------------------------------- /src/timer.js: -------------------------------------------------------------------------------- 1 | import { state, checkInterval, USER_SETTING } from "./settings"; 2 | import { logger, checkingScriptUpdate } from "./utils/general"; 3 | import { onReadyMyDW } from "./functions/post"; 4 | import { onReels } from "./functions/reel"; 5 | import { onProfileAvatar } from "./functions/profile"; 6 | import { onHighlightsStory, onHighlightsStoryThumbnail } from "./functions/highlight"; 7 | import { onStory } from "./functions/story"; 8 | /*! ESLINT IMPORT END !*/ 9 | 10 | // Main Timer 11 | // eslint-disable-next-line no-unused-vars 12 | export var timer = setInterval(function () { 13 | // page loading or unnecessary route 14 | if ($('div#splash-screen').length > 0 && !$('div#splash-screen').is(':hidden') || 15 | location.pathname.match(/^\/(explore(\/.*)?|challenge\/?.*|direct\/?.*|qr\/?|accounts\/.*|emails\/.*|language\/?.*?|your_activity\/?.*|settings\/help(\/.*)?$)$/ig) || 16 | !location.hostname.startsWith('www.') || 17 | ((location.pathname.endsWith('/followers/') || location.pathname.endsWith('/following/')) && ($(`body > div[class]:not([id^="mount"]) div div[role="dialog"]`).length > 0)) 18 | ) { 19 | state.pageLoaded = false; 20 | return; 21 | } 22 | 23 | if (state.currentURL != location.href || !state.firstStarted || !state.pageLoaded) { 24 | console.log('Main Timer', 'trigging'); 25 | 26 | clearInterval(state.GL_repeat); 27 | state.pageLoaded = false; 28 | state.firstStarted = true; 29 | state.currentURL = location.href; 30 | state.GL_observer.disconnect(); 31 | 32 | if (location.href.startsWith("https://www.instagram.com/p/") || location.pathname.match(/^\/(.*?)\/(p|reel)\//ig) || location.href.startsWith("https://www.instagram.com/reel/")) { 33 | state.GL_dataCache.stories = {}; 34 | state.GL_dataCache.highlights = {}; 35 | 36 | logger('isDialog'); 37 | 38 | // This is a delayed function call that prevents the dialog element from appearing before the function is called. 39 | var dialogTimer = setInterval(() => { 40 | // body > div[id^="mount"] section nav + div > article << (mobile page in single post) >> 41 | // section:visible > main > div > div > div > div > div > hr << (single foreground post in page, non-floating //
element here is literally the line beneath poster's username) >> 42 | // section:visible > main > div > div > article > div > div > div > div > div > header (is the same as above, except that this is on the route of the /{username}/p/{shortcode} structure) 43 | // section:visible > main > div > div.xdt5ytf << (former CSS selector for single foreground post in page, non-floating) >> 44 | //
is much more unique element than "div.xdt5ytf" 45 | if ($(`body > div[class]:not([id^="mount"]) div div[role="dialog"] article, 46 | section:visible > main > div > div > div > div > div > hr, 47 | body > div[id^="mount"] section nav + div > article, 48 | section:visible > main > div > div > article > div > div > div > div > div > header 49 | `).length > 0) { 50 | clearInterval(dialogTimer); 51 | 52 | // This is to prevent the detection of the "Modify Video Volume" setting from being too slow. 53 | setTimeout(() => { 54 | onReadyMyDW(false); 55 | }, 15); 56 | } 57 | }, 100); 58 | 59 | state.pageLoaded = true; 60 | } 61 | 62 | if (location.href.startsWith("https://www.instagram.com/reels/")) { 63 | logger('isReels'); 64 | setTimeout(() => { 65 | onReels(false); 66 | }, 150); 67 | state.pageLoaded = true; 68 | } 69 | 70 | if (location.href.split("?")[0] == "https://www.instagram.com/") { 71 | state.GL_dataCache.stories = {}; 72 | state.GL_dataCache.highlights = {}; 73 | 74 | let hasReferrer = state.GL_referrer?.match(/^\/(stories|highlights)\//ig) != null; 75 | 76 | logger('isHomepage', hasReferrer); 77 | setTimeout(() => { 78 | onReadyMyDW(false, hasReferrer); 79 | 80 | const element = $('div[id^="mount"] > div > div div > section > main div:not([class]):not([style]) > div > article')?.parent()[0]; 81 | if (element) { 82 | state.GL_observer.observe(element, { 83 | childList: true 84 | }); 85 | } 86 | }, 150); 87 | 88 | state.pageLoaded = true; 89 | } 90 | // eslint-disable-next-line no-useless-escape 91 | if ($('header > *[class]:first-child img[alt]').length && location.pathname.match(/^(\/)([0-9A-Za-z\.\-_]+)\/?(tagged|reels|saved)?\/?$/ig) && !location.pathname.match(/^(\/explore\/?$|\/stories(\/.*)?$|\/p\/)/ig)) { 92 | logger('isProfile'); 93 | setTimeout(() => { 94 | onProfileAvatar(false); 95 | }, 150); 96 | state.pageLoaded = true; 97 | } 98 | 99 | if (!state.pageLoaded) { 100 | // Call Instagram stories function 101 | if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig)) { 102 | state.GL_dataCache.highlights = {}; 103 | 104 | logger('isHighlightsStory'); 105 | 106 | onHighlightsStory(false); 107 | state.GL_repeat = setInterval(() => { 108 | onHighlightsStoryThumbnail(false); 109 | }, checkInterval); 110 | 111 | if ($(".IG_DWHISTORY").length) { 112 | setTimeout(() => { 113 | if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) { 114 | var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () { 115 | return $(this).children().length === 0 && this.textContent.trim() !== ""; 116 | }); 117 | $viewStoryButton?.trigger("click"); 118 | } 119 | 120 | state.pageLoaded = true; 121 | }, 150); 122 | } 123 | } 124 | else if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig)) { 125 | logger('isStory'); 126 | 127 | /* 128 | * 129 | * $('body div[id^="mount"] > div > div > div[class]').length >= 2 && 130 | * $('body div[id^="mount"] > div > div > div[class]').last().find('svg > path[d^="M16.792"], svg > path[d^="M34.6 3.1c-4.5"]').length > 0 && 131 | * $('body div[id^="mount"] > div > div > div[class]').last().find('svg > polyline + line').length > 0 132 | * 133 | */ 134 | if ($('div[id^="mount"] section > div > a[href="/"]').length > 0) { 135 | $('.IG_DWSTORY').remove(); 136 | $('.IG_DWNEWTAB').remove(); 137 | if ($('.IG_DWSTORY_THUMBNAIL').length) { 138 | $('.IG_DWSTORY_THUMBNAIL').remove(); 139 | } 140 | 141 | onStory(false); 142 | 143 | // Prevent buttons from being eaten by black holes sometimes 144 | setTimeout(() => { 145 | onStory(false); 146 | }, 150); 147 | } 148 | 149 | if ($(".IG_DWSTORY").length) { 150 | setTimeout(() => { 151 | if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) { 152 | var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () { 153 | return $(this).children().length === 0 && this.textContent.trim() !== ""; 154 | }); 155 | $viewStoryButton?.click(); 156 | } 157 | 158 | state.pageLoaded = true; 159 | }, 150); 160 | } 161 | } 162 | else { 163 | state.pageLoaded = false; 164 | // Remove icons 165 | if ($('.IG_DWSTORY').length) { 166 | $('.IG_DWSTORY').remove(); 167 | } 168 | if ($('.IG_DWSTORY_ALL').length) { 169 | $('.IG_DWSTORY_ALL').remove(); 170 | } 171 | if ($('.IG_DWNEWTAB').length) { 172 | $('.IG_DWNEWTAB').remove(); 173 | } 174 | if ($('.IG_DWSTORY_THUMBNAIL').length) { 175 | $('.IG_DWSTORY_THUMBNAIL').remove(); 176 | } 177 | 178 | if ($('.IG_DWHISTORY').length) { 179 | $('.IG_DWHISTORY').remove(); 180 | } 181 | if ($('.IG_DWHISTORY_ALL').length) { 182 | $('.IG_DWHISTORY_ALL').remove(); 183 | } 184 | if ($('.IG_DWHINEWTAB').length) { 185 | $('.IG_DWHINEWTAB').remove(); 186 | } 187 | if ($('.IG_DWHISTORY_THUMBNAIL').length) { 188 | $('.IG_DWHISTORY_THUMBNAIL').remove(); 189 | } 190 | } 191 | } 192 | 193 | checkingScriptUpdate(300); 194 | state.GL_referrer = new URL(location.href).pathname; 195 | } 196 | }, checkInterval); -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import { logger, getAppID, updateLoadingBar } from "./general"; 2 | /*! ESLINT IMPORT END !*/ 3 | 4 | /** 5 | * getHighlightStories 6 | * @description Get a list of all stories in highlight Id. 7 | * 8 | * @param {Integer} highlightId 9 | * @return {Object} 10 | */ 11 | export function getHighlightStories(highlightId) { 12 | return new Promise((resolve, reject) => { 13 | let getURL = `https://www.instagram.com/graphql/query/?query_hash=45246d3fe16ccc6577e0bd297a5db1ab&variables=%7B%22highlight_reel_ids%22:%5B%22${highlightId}%22%5D,%22precomposed_overlay%22:false%7D`; 14 | 15 | GM_xmlhttpRequest({ 16 | method: "GET", 17 | url: getURL, 18 | onload: function (response) { 19 | try { 20 | let obj = JSON.parse(response.response); 21 | resolve(obj); 22 | } 23 | catch (err) { 24 | logger('getHighlightStories()', 'reject', err.message); 25 | reject(err); 26 | } 27 | }, 28 | onerror: function (err) { 29 | logger('getHighlightStories()', 'reject', err); 30 | reject(err); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | /** 37 | * getStories 38 | * @description Get a list of all stories in user Id. 39 | * 40 | * @param {Integer} userId 41 | * @return {Object} 42 | */ 43 | export function getStories(userId) { 44 | return new Promise((resolve, reject) => { 45 | let getURL = `https://www.instagram.com/graphql/query/?query_hash=15463e8449a83d3d60b06be7e90627c7&variables=%7B%22reel_ids%22:%5B%22${userId}%22%5D,%22precomposed_overlay%22:false%7D`; 46 | 47 | GM_xmlhttpRequest({ 48 | method: "GET", 49 | url: getURL, 50 | onload: function (response) { 51 | try { 52 | let obj = JSON.parse(response.response); 53 | logger('getStories()', obj); 54 | resolve(obj); 55 | } 56 | catch (err) { 57 | logger('getStories()', 'reject', err.message); 58 | reject(err); 59 | } 60 | }, 61 | onerror: function (err) { 62 | logger('getStories()', 'reject', err); 63 | reject(err); 64 | } 65 | }); 66 | }); 67 | } 68 | 69 | /** 70 | * getUserId 71 | * @description Get user's id with username 72 | * 73 | * @param {String} username 74 | * @return {Integer} 75 | */ 76 | export function getUserId(username) { 77 | return new Promise((resolve, reject) => { 78 | let getURL = `https://www.instagram.com/web/search/topsearch/?query=${username}`; 79 | 80 | GM_xmlhttpRequest({ 81 | method: "GET", 82 | url: getURL, 83 | onload: function (response) { 84 | // Fix search issue by Discord: sno_w_ 85 | let obj = JSON.parse(response.response); 86 | let result = null; 87 | obj.users.forEach(pos => { 88 | if (pos.user.username?.toLowerCase() === username?.toLowerCase()) { 89 | result = pos; 90 | } 91 | }); 92 | 93 | if (result != null) { 94 | logger('getUserId()', result); 95 | resolve(result); 96 | } 97 | else { 98 | getUserIdWithAgent(username).then((result) => { 99 | resolve(result); 100 | // eslint-disable-next-line no-unused-vars 101 | }).catch((err) => { 102 | alert("Can not find user info from getUserId()"); 103 | }); 104 | } 105 | }, 106 | onerror: function (err) { 107 | logger('getUserId()', 'reject', err); 108 | reject(err); 109 | } 110 | }); 111 | }); 112 | } 113 | 114 | /** 115 | * getUserIdWithAgent 116 | * @description Get user's id with username 117 | * 118 | * @param {String} username 119 | * @return {Integer} 120 | */ 121 | export function getUserIdWithAgent(username) { 122 | return new Promise((resolve, reject) => { 123 | let getURL = `https://i.instagram.com/api/v1/users/web_profile_info/?username=${username}`; 124 | 125 | GM_xmlhttpRequest({ 126 | method: "GET", 127 | url: getURL, 128 | headers: { 129 | 'X-IG-App-ID': getAppID() 130 | }, 131 | onload: function (response) { 132 | try { 133 | let obj = JSON.parse(response.response); 134 | let hasUser = obj?.data?.user; 135 | 136 | if (hasUser != null) { 137 | let userInfo = obj?.data; 138 | userInfo.user.pk = userInfo.user.id; 139 | logger('getUserIdWithAgent()', obj); 140 | resolve(userInfo); 141 | } 142 | else { 143 | logger('getUserIdWithAgent()', 'reject', 'undefined'); 144 | reject('undefined'); 145 | } 146 | } 147 | catch (err) { 148 | logger('getUserIdWithAgent()', 'reject', err.message); 149 | reject(err); 150 | } 151 | }, 152 | onerror: function (err) { 153 | logger('getUserIdWithAgent()', 'reject', err); 154 | reject(err); 155 | } 156 | }); 157 | }); 158 | } 159 | 160 | /** 161 | * getUserHighSizeProfile 162 | * @description Get user's high quality avatar image. 163 | * 164 | * @param {Integer} userId 165 | * @return {String} 166 | */ 167 | export function getUserHighSizeProfile(userId) { 168 | return new Promise((resolve, reject) => { 169 | let getURL = `https://i.instagram.com/api/v1/users/${userId}/info/`; 170 | 171 | GM_xmlhttpRequest({ 172 | method: "GET", 173 | url: getURL, 174 | headers: { 175 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Pixel 7 XL)Build/RP1A.20845.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 Chrome/117.0.5938.60 Mobile Safari/537.36 Instagram 307.0.0.34.111' 176 | }, 177 | onload: function (response) { 178 | try { 179 | let obj = JSON.parse(response.response); 180 | if (obj.status !== 'ok') { 181 | logger('getUserHighSizeProfile()', 'reject', obj); 182 | reject('faild'); 183 | } 184 | else { 185 | logger('getUserHighSizeProfile()', obj); 186 | resolve(obj.user.hd_profile_pic_url_info?.url); 187 | } 188 | } 189 | catch (err) { 190 | logger('getUserHighSizeProfile()', 'reject', err); 191 | reject(err); 192 | } 193 | }, 194 | onerror: function (err) { 195 | logger('getUserHighSizeProfile()', 'reject', err); 196 | reject(err); 197 | } 198 | }); 199 | }); 200 | } 201 | 202 | /** 203 | * getPostOwner 204 | * @description Get post's author with post shortcode 205 | * 206 | * @param {String} postPath 207 | * @return {String} 208 | */ 209 | export function getPostOwner(postPath) { 210 | return new Promise((resolve, reject) => { 211 | if (!postPath) reject("NOPATH"); 212 | let postShortCode = postPath; 213 | let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`; 214 | 215 | GM_xmlhttpRequest({ 216 | method: "GET", 217 | url: getURL, 218 | onload: function (response) { 219 | try { 220 | let obj = JSON.parse(response.response); 221 | logger('getPostOwner()', obj); 222 | resolve(obj.data.shortcode_media.owner.username); 223 | } 224 | catch (err) { 225 | logger('getPostOwner()', 'reject', err.message); 226 | reject(err); 227 | } 228 | }, 229 | onerror: function (err) { 230 | logger('getPostOwner()', 'reject', err); 231 | reject(err); 232 | } 233 | }); 234 | }); 235 | } 236 | 237 | /** 238 | * getBlobMedia 239 | * @description Get list of all media files in post with post shortcode 240 | * 241 | * @param {String} postPath 242 | * @return {Object} 243 | */ 244 | export function getBlobMedia(postPath) { 245 | return new Promise((resolve, reject) => { 246 | if (!postPath) reject("NOPATH"); 247 | let postShortCode = postPath; 248 | let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`; 249 | 250 | GM_xmlhttpRequest({ 251 | method: "GET", 252 | url: getURL, 253 | headers: { 254 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; Pixel 7 XL)Build/RP1A.20845.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 Chrome/117.0.5938.60 Mobile Safari/537.36 Instagram 307.0.0.34.111" 255 | }, 256 | onload: function (response) { 257 | try { 258 | let obj = JSON.parse(response.response); 259 | logger(obj); 260 | 261 | if (obj.status === 'fail') { 262 | // alert(`Request failed with API response:\n${obj.message}: ${obj.feedback_message}`); 263 | logger('Request with:', 'getBlobMediaWithQuery()', postShortCode); 264 | getBlobMediaWithQueryID(postShortCode).then((res) => { 265 | resolve({ type: 'query_id', data: res.xdt_api__v1__media__shortcode__web_info.items[0] }); 266 | }).catch((err) => { 267 | reject(err); 268 | }) 269 | } 270 | else { 271 | resolve({ type: 'query_hash', data: obj.data }); 272 | } 273 | } 274 | catch (err) { 275 | logger('getBlobMedia()', 'reject', err.message); 276 | reject(err); 277 | } 278 | }, 279 | onerror: function (err) { 280 | logger('getBlobMedia()', 'reject', err); 281 | reject(err); 282 | } 283 | }); 284 | }); 285 | } 286 | 287 | /** 288 | * getBlobMediaWithQueryID 289 | * @description Get list of all media files in post with post shortcode 290 | * 291 | * @param {String} postPath 292 | * @return {Object} 293 | */ 294 | export function getBlobMediaWithQueryID(postPath) { 295 | return new Promise((resolve, reject) => { 296 | if (!postPath) reject("NOPATH"); 297 | let postShortCode = postPath; 298 | let getURL = `https://www.instagram.com/graphql/query/?query_id=9496392173716084&variables={%22shortcode%22:%22${postShortCode}%22,%22__relay_internal__pv__PolarisFeedShareMenurelayprovider%22:true,%22__relay_internal__pv__PolarisIsLoggedInrelayprovider%22:true}`; 299 | 300 | GM_xmlhttpRequest({ 301 | method: "GET", 302 | url: getURL, 303 | headers: { 304 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; Pixel 7 XL)Build/RP1A.20845.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 Chrome/117.0.5938.60 Mobile Safari/537.36 Instagram 307.0.0.34.111", 305 | 'X-IG-App-ID': getAppID() 306 | }, 307 | onload: function (response) { 308 | try { 309 | let obj = JSON.parse(response.response); 310 | logger(obj); 311 | 312 | if (obj.status === 'fail') { 313 | alert(`getBlobMediaWithQueryID(): Request failed with API response:\n${obj.message}: ${obj.feedback_message}`); 314 | logger(`Request failed with API response ${obj.message}: ${obj.feedback_message}`); 315 | reject(response); 316 | } 317 | else { 318 | logger('getBlobMediaWithQueryID()', obj.data); 319 | resolve(obj.data); 320 | } 321 | } 322 | catch (err) { 323 | logger('getBlobMediaWithQueryID()', 'reject', err.message); 324 | reject(err); 325 | } 326 | }, 327 | onerror: function (err) { 328 | logger('getBlobMediaWithQueryID()', 'reject', err); 329 | reject(err); 330 | } 331 | }); 332 | }); 333 | } 334 | 335 | /** 336 | * getMediaInfo 337 | * @description Get Instagram Media object 338 | * 339 | * @param {String} mediaId 340 | * @return {Object} 341 | */ 342 | export function getMediaInfo(mediaId) { 343 | return new Promise((resolve, reject) => { 344 | let getURL = `https://i.instagram.com/api/v1/media/${mediaId}/info/`; 345 | 346 | if (mediaId == null) { 347 | alert("Can not call Media API because of the media id is invalid."); 348 | logger('getMediaInfo()', 'reject', 'Can not call Media API because of the media id is invalid.'); 349 | 350 | updateLoadingBar(false); 351 | reject(-1); 352 | return; 353 | } 354 | if (getAppID() == null) { 355 | alert("Can not call Media API because of the app id is invalid."); 356 | logger('getMediaInfo()', 'reject', 'Can not call Media API because of the app id is invalid.'); 357 | updateLoadingBar(false); 358 | reject(-1); 359 | return; 360 | } 361 | 362 | GM_xmlhttpRequest({ 363 | method: "GET", 364 | url: getURL, 365 | headers: { 366 | "User-Agent": window.navigator.userAgent, 367 | "Accept": "*/*", 368 | 'X-IG-App-ID': getAppID() 369 | }, 370 | onload: function (response) { 371 | if (response.finalUrl == getURL) { 372 | let obj = JSON.parse(response.response); 373 | logger('getMediaInfo()', obj); 374 | resolve(obj); 375 | } 376 | else { 377 | let finalURL = new URL(response.finalUrl); 378 | if (finalURL.pathname.startsWith('/accounts/login')) { 379 | logger('getMediaInfo()', 'reject', 'The account must be logged in to access Media API.'); 380 | alert("The account must be logged in to access Media API."); 381 | } 382 | else { 383 | logger('getMediaInfo()', 'reject', 'Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"'); 384 | alert('Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"'); 385 | } 386 | updateLoadingBar(false); 387 | reject(-1); 388 | } 389 | }, 390 | onerror: function (err) { 391 | logger('getMediaInfo()', 'reject', err); 392 | resolve(err); 393 | } 394 | }); 395 | }); 396 | } -------------------------------------------------------------------------------- /src/utils/i18n.js: -------------------------------------------------------------------------------- 1 | import { state } from "../settings"; 2 | import { logger } from "./general"; 3 | /*! ESLINT IMPORT END !*/ 4 | 5 | /** 6 | * translateText 7 | * @description i18n translation text 8 | * 9 | * @return {void} 10 | */ 11 | export function translateText() { 12 | var eLocale = { 13 | "en-US": { 14 | "NOTICE_UPDATE_TITLE": "Wololo! New version released.", 15 | "NOTICE_UPDATE_CONTENT": "IG-Helper has released a new version, click here to update.", 16 | "CHECK_UPDATE": "Checking for Script Updates", 17 | "CHECK_UPDATE_MENU": "Checking for Updates", 18 | "CHECK_UPDATE_INTRO": "Check for updates when the script is triggered (check every 300 seconds).\nUpdate notifications will be sent as desktop notifications through the browser.", 19 | "RELOAD_SCRIPT": "Reload Script", 20 | "DONATE": "Donate", 21 | "FEEDBACK": "Feedback", 22 | "IMAGE_VIEWER": "Open Image In Viewer", 23 | "NEW_TAB": "Open in New Tab", 24 | "SHOW_DOM_TREE": "Show DOM Tree", 25 | "SELECT_AND_COPY": "Select All and Copy from the Input Box", 26 | "DOWNLOAD_DOM_TREE": "Download DOM Tree as a Text File", 27 | "REPORT_GITHUB": "Report an Issue on GitHub", 28 | "REPORT_DISCORD": "Report an Issue on Discord Support Server", 29 | "REPORT_FORK": "Report an Issue on Greasy Fork", 30 | "DEBUG": "Debug Window", 31 | "CLOSE": "Close", 32 | "ALL_CHECK": "Select All", 33 | "BATCH_DOWNLOAD_SELECTED": "Download Selected Resources", 34 | "BATCH_DOWNLOAD_DIRECT": "Download All Resources", 35 | "IMG": "Image", 36 | "VID": "Video", 37 | "DW": "Download", 38 | "DW_ALL": "Download All Resources", 39 | "THUMBNAIL_INTRO": "Download Video Thumbnail", 40 | "LOAD_BLOB_ONE": "Loading Blob Media...", 41 | "LOAD_BLOB_MULTIPLE": "Loading Blob Media and Others...", 42 | "LOAD_BLOB_RELOAD": "Detecting Blob Media, reloading...", 43 | "NO_CHECK_RESOURCE": "You need to select a resource to download.", 44 | "NO_VID_URL": "Cannot find video URL.", 45 | "SETTING": "Settings", 46 | "AUTO_RENAME": "Automatically Rename Files (Right-Click to Set)", 47 | "RENAME_SHORTCODE": "Rename the File and Include Shortcode", 48 | "RENAME_PUBLISH_DATE": "Set Renamed File Timestamp to Resource Publish Date", 49 | "RENAME_LOCATE_DATE": "Modify Renamed File Timestamp Date Format (Right-Click to Set)", 50 | "DISABLE_VIDEO_LOOPING": "Disable Video Auto-looping", 51 | "HTML5_VIDEO_CONTROL": "Display HTML5 Video Controller", 52 | "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirect When Clicking on User's Story Picture", 53 | "FORCE_FETCH_ALL_RESOURCES": "Force Fetch All Resources in the Post", 54 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Directly Download the Visible Resources in the Post", 55 | "DIRECT_DOWNLOAD_ALL": "Directly Download All Resources in the Post", 56 | "MODIFY_VIDEO_VOLUME": "Modify Video Volume (Right-Click to Set)", 57 | "MODIFY_RESOURCE_EXIF": "Modify Resource EXIF ​​Properties", 58 | "SCROLL_BUTTON": "Enable Scroll Buttons for Reels Page", 59 | "FORCE_RESOURCE_VIA_MEDIA": "Force Fetch Resource via Media API", 60 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Use Alternative Methods to Download When the Media API is Not Accessible", 61 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "Always Use Media API for 'Open in New Tab' in Posts", 62 | "AUTO_RENAME_INTRO": "Auto rename file to custom format:\nCustom Format List: \n%USERNAME% - Username\n%SOURCE_TYPE% - Download Source\n%SHORTCODE% - Post Shortcode\n%YEAR% - Year when downloaded/published\n%2-YEAR% - Year (last two digits) when downloaded/published\n%MONTH% - Month when downloaded/published\n%DAY% - Day when downloaded/published\n%HOUR% - Hour when downloaded/published\n%MINUTE% - Minute when downloaded/published\n%SECOND% - Second when downloaded/published\n%ORIGINAL_NAME% - Original name of downloaded file\n%ORIGINAL_NAME_FIRST% - Original name of downloaded file (first part of name)\n\nIf set to false, the file name will remain unchanged.\nExample: instagram_321565527_679025940443063_4318007696887450953_n.jpg", 63 | "RENAME_SHORTCODE_INTRO": "Auto rename file to the following format:\nUSERNAME-TYPE-SHORTCODE-TIMESTAMP.FILETYPE\nExample: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nThis will ONLY work if [Automatically Rename Files] is set to TRUE.", 64 | "RENAME_PUBLISH_DATE_INTRO": "Sets the timestamp in the file rename format to the resource publish date (browser time zone).\n\nThis feature only works when [Automatically Rename Files] is set to TRUE.", 65 | "RENAME_LOCATE_DATE_INTRO": "Modify the renamed file timestamp date format to the browser's local time, and format it to your preferred regional date format.\n\nThis feature only works when [Automatically Rename Files] is set to TRUE.", 66 | "DISABLE_VIDEO_LOOPING_INTRO": "Disable video auto-looping in Reels and posts.", 67 | "HTML5_VIDEO_CONTROL_INTRO": "Display the HTML5 video controller in video resource.\n\nThis will hide the custom video volume slider and replace it with the HTML5 controller. The HTML5 controller can be hidden by right-clicking on the video to reveal the original details.", 68 | "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Redirect to a user's profile page when right-clicking on their avatar in the story area on the homepage.\nIf you use the middle mouse button to click, it will open in a new tab.", 69 | "FORCE_FETCH_ALL_RESOURCES_INTRO": "Force fetching of all resources (photos and videos) in a post via the Instagram API to remove the limit of three resources per post.", 70 | "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Directly download the current resources available in the post.", 71 | "DIRECT_DOWNLOAD_ALL_INTRO": "When you click the download button, all resources in the post will be forcibly fetched and downloaded.", 72 | "MODIFY_VIDEO_VOLUME_INTRO": "Modify the video playback volume in Reels and posts (right-click to open the volume setting slider).", 73 | "SCROLL_BUTTON_INTRO": "Enable scroll buttons for the lower right corner of the Reels page.", 74 | "FORCE_RESOURCE_VIA_MEDIA_INTRO": "The Media API will try to get the highest quality photo or video possible, but it may take longer to load.", 75 | "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_INTRO": "When the Media API reaches its rate limit or cannot be used for other reasons, the Forced Fetch API will be used to download resources (the resource quality may be slightly lower).", 76 | "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "The [Open in New Tab] button in posts will always use the Media API to obtain high-resolution resources.", 77 | "SKIP_VIEW_STORY_CONFIRM": "Skip the Confirmation Page for Viewing a Story/Highlight", 78 | "SKIP_VIEW_STORY_CONFIRM_INTRO": "Automatically skip when confirmation page is shown in story or highlight.", 79 | "MODIFY_RESOURCE_EXIF_INTRO": "Modify the EXIF ​​properties of the image resource to place the post link in it." 80 | } 81 | }; 82 | 83 | var resultUnsorted = Object.assign({}, eLocale, state.locale); 84 | var resultSorted = Object.keys(resultUnsorted).sort().reduce( 85 | (obj, key) => { 86 | obj[key] = resultUnsorted[key]; 87 | return obj; 88 | }, {} 89 | ); 90 | 91 | return resultSorted; 92 | } 93 | 94 | /** 95 | * getTranslationText 96 | * @description i18n translation text 97 | * 98 | * @param {String} lang 99 | * @return {Object} 100 | */ 101 | export async function getTranslationText(lang) { 102 | return new Promise((resolve, reject) => { 103 | GM_xmlhttpRequest({ 104 | method: "GET", 105 | url: `https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale/translations/${lang}.json`, 106 | onload: function (response) { 107 | try { 108 | let obj = JSON.parse(response.response); 109 | resolve(obj); 110 | } 111 | catch (err) { 112 | reject(err); 113 | } 114 | }, 115 | onerror: function (err) { 116 | logger('getTranslationText()', 'reject', err); 117 | reject(err); 118 | } 119 | }); 120 | }); 121 | } 122 | 123 | /** 124 | * _i18n 125 | * @description Perform i18n translation 126 | * 127 | * @param {String} text 128 | * @return {void} 129 | */ 130 | export function _i18n(text) { 131 | const translate = translateText(); 132 | 133 | if (translate[state.lang] != undefined && translate[state.lang][text] != undefined) { 134 | return translate[state.lang][text]; 135 | } 136 | else { 137 | return translate["en-US"][text]; 138 | } 139 | } 140 | 141 | /** 142 | * repaintingTranslations 143 | * @description Perform i18n translation 144 | * 145 | * @return {void} 146 | */ 147 | export function repaintingTranslations() { 148 | $('[data-ih-locale]').each(function () { 149 | $(this).text(_i18n($(this).attr('data-ih-locale'))); 150 | }); 151 | $('[data-ih-locale-title]').each(function () { 152 | $(this).attr('title', _i18n($(this).attr('data-ih-locale-title'))); 153 | }); 154 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | [data-ih-locale-title] svg { 2 | vertical-align: middle; 3 | } 4 | 5 | .IG_POPUP_DIG { 6 | position: fixed; 7 | left: 0px; 8 | right: 0px; 9 | bottom: 0px; 10 | top: 0px; 11 | z-index: 500; 12 | } 13 | 14 | .IG_POPUP_DIG.hidden { 15 | display: none; 16 | } 17 | 18 | .IG_POPUP_DIG_BG { 19 | position: fixed; 20 | left: 0px; 21 | right: 0px; 22 | bottom: 0px; 23 | top: 0px; 24 | z-index: 502; 25 | background: rgba(0, 0, 0, .75); 26 | } 27 | 28 | .IG_POPUP_DIG_MAIN { 29 | z-index: 510; 30 | padding: 10px 15px; 31 | top: 7%; 32 | position: absolute; 33 | left: 50%; 34 | transform: translateX(-50%); 35 | width: 500px; 36 | background: #fff; 37 | background: rgb(var(--ig-secondary-background)); 38 | color: #000; 39 | color: rgb(var(--ig-primary-text)); 40 | border-radius: 7px; 41 | } 42 | 43 | .IG_POPUP_DIG_BODY { 44 | min-height: 100px; 45 | max-height: 70vh; 46 | overflow-y: auto; 47 | } 48 | 49 | .IG_POPUP_DIG_BODY a { 50 | display: block; 51 | padding: 5px 0px; 52 | color: #111; 53 | color: rgb(var(--ig-primary-text)); 54 | font-size: 1rem; 55 | line-height: 1rem; 56 | text-align: center; 57 | border-radius: 5px; 58 | } 59 | 60 | .IG_DW_MAIN, 61 | .IG_NEWTAB_MAIN, 62 | .IG_THUMBNAIL_MAIN, 63 | .IG_DW_ALL_MAIN, 64 | .IG_IMAGE_VIEWER { 65 | position: relative; 66 | top: 0px; 67 | padding: 2px; 68 | line-height: 0; 69 | background: #fff; 70 | border-radius: 50%; 71 | cursor: pointer; 72 | color: #000; 73 | } 74 | 75 | .button_wrapper { 76 | position: absolute; 77 | top: 15px; 78 | right: 15px; 79 | line-height: 0; 80 | color: #000; 81 | display: flex; 82 | flex-flow: row-reverse; 83 | gap: 5px; 84 | } 85 | 86 | #_SNLOAD { 87 | text-align: center; 88 | font-size: 20px; 89 | } 90 | 91 | .IG_REELS, 92 | .IG_DWSTORY, 93 | .IG_DWHISTORY, 94 | .IG_DWSTORY_ALL, 95 | .IG_DWHISTORY_ALL { 96 | position: absolute; 97 | right: 40px; 98 | top: 15px; 99 | padding: 2px; 100 | line-height: 0; 101 | background: #fff; 102 | border-radius: 5px; 103 | cursor: pointer; 104 | z-index: 5; 105 | } 106 | 107 | .IG_REELS_NEWTAB, 108 | .IG_DWNEWTAB, 109 | .IG_DWHINEWTAB { 110 | position: absolute; 111 | right: 40px; 112 | top: 47px; 113 | padding: 2px; 114 | line-height: 0; 115 | background: #fff; 116 | border-radius: 5px; 117 | cursor: pointer; 118 | z-index: 5; 119 | color: #000; 120 | } 121 | 122 | .IG_REELS_THUMBNAIL, 123 | .IG_DWSTORY_THUMBNAIL, 124 | .IG_DWHISTORY_THUMBNAIL { 125 | position: absolute; 126 | right: 40px; 127 | top: 79px; 128 | padding: 2px; 129 | line-height: 1; 130 | background: #fff; 131 | border-radius: 5px; 132 | cursor: pointer; 133 | z-index: 5; 134 | } 135 | 136 | .IG_DWSTORY, 137 | .IG_DWHISTORY, 138 | .IG_DWSTORY_THUMBNAIL, 139 | .IG_DWHISTORY_THUMBNAIL, 140 | .IG_DWNEWTAB, 141 | .IG_DWHINEWTAB { 142 | right: -40px; 143 | color: #000; 144 | } 145 | 146 | .IG_DWSTORY_ALL, 147 | .IG_DWHISTORY_ALL { 148 | right: -70px; 149 | color: #000; 150 | } 151 | 152 | .IG_DWPROFILE { 153 | position: absolute; 154 | right: 0px; 155 | top: 0px; 156 | padding: 2px; 157 | line-height: 1; 158 | background: #fff; 159 | border-radius: 50%; 160 | cursor: pointer; 161 | border: 1px solid #ccc 162 | } 163 | 164 | .globalSettings { 165 | position: relative; 166 | display: inline-block; 167 | color: #000; 168 | color: rgb(var(--ig-primary-text)); 169 | text-decoration: none; 170 | text-align: left; 171 | width: 100%; 172 | min-height: 30px; 173 | padding: 5px; 174 | padding-right: 60px; 175 | line-height: 18px; 176 | font-size: 18px; 177 | box-sizing: border-box; 178 | border-radius: 5px; 179 | vertical-align: middle; 180 | outline: none; 181 | cursor: pointer; 182 | -ms-user-select: none; 183 | -moz-user-select: none; 184 | -webkit-user-select: none; 185 | user-select: none; 186 | margin: 5px 0px; 187 | } 188 | 189 | .globalSettings:hover { 190 | background: #e7e7e7; 191 | background: rgb(var(--ig-secondary-button-hover)); 192 | } 193 | 194 | .globalSettings:hover>span { 195 | cursor: help; 196 | } 197 | 198 | .globalSettings input:not(#date_format) { 199 | display: none; 200 | } 201 | 202 | .globalSettings input#date_format { 203 | width: calc(100% - 50px); 204 | background: rgb(var(--ig-secondary-background)); 205 | border: 1px solid; 206 | position: relative; 207 | top: 50%; 208 | transform: translateY(-50%); 209 | } 210 | 211 | .globalSettings .chbtn { 212 | width: 40px; 213 | height: 15px; 214 | background: #9c9c9c; 215 | display: inline-block; 216 | vertical-align: middle; 217 | border-radius: 7px; 218 | position: absolute; 219 | right: 15px; 220 | top: 50%; 221 | transition: background 0.2s; 222 | transform: translateY(-50%); 223 | } 224 | 225 | .globalSettings .chbtn .rounds { 226 | width: 20px; 227 | height: 20px; 228 | background: #777; 229 | display: inline-block; 230 | vertical-align: middle; 231 | border-radius: 50%; 232 | position: absolute; 233 | left: 0px; 234 | top: -3px; 235 | transition: left 0.15s, background 0.15s; 236 | } 237 | 238 | .globalSettings input:checked~.chbtn { 239 | background: #004c5a; 240 | } 241 | 242 | .globalSettings input:checked~.chbtn .rounds { 243 | left: 20px; 244 | background: #048aa4; 245 | } 246 | 247 | .checkbox { 248 | font-size: 18px; 249 | vertical-align: middle; 250 | margin: 0px 7px; 251 | user-select: none; 252 | margin-left: 0px; 253 | } 254 | 255 | .checkbox input { 256 | transform: scale(1.5); 257 | margin-right: 10px; 258 | } 259 | 260 | .inner_box_wrapper { 261 | display: block; 262 | position: absolute; 263 | left: 0px; 264 | top: 0px; 265 | width: 50px; 266 | height: 100%; 267 | border-right: 1px solid; 268 | background: #e7e7e7; 269 | background: rgb(var(--ig-secondary-button-hover)); 270 | cursor: pointer; 271 | border-radius: 7px 0px 0px 7px; 272 | } 273 | 274 | .inner_box~span { 275 | position: relative; 276 | height: 100%; 277 | width: 100%; 278 | display: block; 279 | border-radius: 7px 0px 0px 7px; 280 | } 281 | 282 | .inner_box:checked~span { 283 | background: rgb(var(--ig-success)); 284 | } 285 | 286 | .inner_box~span:after { 287 | content: ""; 288 | position: absolute; 289 | display: none; 290 | left: 12px; 291 | top: 50%; 292 | width: 10px; 293 | height: 20px; 294 | margin-top: -8px; 295 | border: solid black; 296 | border: solid rgb(var(--ig-primary-text)); 297 | border-width: 0 3px 3px 0; 298 | -webkit-transform: rotate(45deg) translateY(-50%); 299 | -ms-transform: rotate(45deg) translateY(-50%); 300 | transform: rotate(45deg) translateY(-50%); 301 | } 302 | 303 | .inner_box:checked~span:after { 304 | display: block; 305 | } 306 | 307 | .inner_box { 308 | position: absolute; 309 | top: 10px; 310 | left: 50%; 311 | transform: scale(2.5) translateY(-50%); 312 | cursor: pointer; 313 | appearance: none; 314 | opacity: 0; 315 | } 316 | 317 | .IG_POPUP_DIG_BODY>div { 318 | position: relative; 319 | border: 1px solid #000; 320 | border: 1px solid rgb(var(--ig-primary-text)); 321 | margin: 5px 0px; 322 | border-radius: 7px; 323 | } 324 | 325 | .IG_POPUP_DIG_BODY>div:hover { 326 | background: rgba(var(--ig-hover-overlay)); 327 | } 328 | 329 | .IG_POPUP_DIG_TITLE { 330 | padding-bottom: 5px; 331 | } 332 | 333 | .IG_POPUP_DIG_TITLE button { 334 | font-size: 14px; 335 | vertical-align: middle; 336 | margin: 0px 7px; 337 | } 338 | 339 | kbd { 340 | font-weight: bold; 341 | padding: 4px 5px; 342 | background: rgb(var(--ig-primary-background)); 343 | border-radius: 3px; 344 | border: 1px solid rgb(var(--ig-primary-text)); 345 | } 346 | 347 | .globalSettings #tempWrapper { 348 | position: absolute; 349 | top: 0px; 350 | left: 0px; 351 | right: 0px; 352 | height: 100%; 353 | background: #fff; 354 | background: rgb(var(--ig-secondary-background)); 355 | } 356 | 357 | .globalSettings #tempWrapper input[type="range"] { 358 | display: inline-block; 359 | width: 80%; 360 | position: relative; 361 | top: 50%; 362 | transform: translateY(-50%); 363 | } 364 | 365 | .globalSettings #tempWrapper input[type="number"] { 366 | display: inline-block; 367 | width: 50px; 368 | border-radius: 7px; 369 | outline: 0px; 370 | background: transparent; 371 | border: 1px solid; 372 | position: relative; 373 | top: 50%; 374 | transform: translateY(-50%); 375 | } 376 | 377 | .globalSettings.child { 378 | margin-left: 15px; 379 | width: calc(100% - 15px); 380 | } 381 | 382 | #scrollWrapper { 383 | position: fixed; 384 | right: 15px; 385 | bottom: 15px; 386 | } 387 | 388 | #scrollWrapper .button-up, 389 | #scrollWrapper .button-down { 390 | width: 32px; 391 | height: 32px; 392 | border-radius: 10px; 393 | background-color: rgba(0, 0, 0, 0.25); 394 | background-color: rgba(var(--ig-banner-background)); 395 | cursor: pointer; 396 | margin: 5px 0px; 397 | border: 1px solid; 398 | border-color: rgb(var(--ig-separator)); 399 | } 400 | 401 | #scrollWrapper .button-up:hover, 402 | #scrollWrapper .button-down:hover { 403 | width: 32px; 404 | height: 32px; 405 | border-radius: 10px; 406 | background-color: rgba(0, 0, 0, 0.25); 407 | background-color: rgba(var(--ig-hover-overlay)); 408 | cursor: pointer; 409 | margin: 5px 0px; 410 | } 411 | 412 | #scrollWrapper .button-up>div, 413 | #scrollWrapper .button-down>div { 414 | position: relative; 415 | border: solid #000; 416 | border: solid rgb(var(--ig-primary-text)); 417 | border-width: 0 4px 4px 0; 418 | display: inline-block; 419 | padding: 4px; 420 | left: 9.5px; 421 | } 422 | 423 | #scrollWrapper .button-up>div { 424 | transform: rotate(-135deg); 425 | -webkit-transform: rotate(-135deg); 426 | top: 9px; 427 | } 428 | 429 | #scrollWrapper .button-down>div { 430 | transform: rotate(45deg); 431 | -webkit-transform: rotate(45deg); 432 | top: 6px; 433 | } 434 | 435 | .IG_POPUP_DIG_BODY .newTab { 436 | position: absolute; 437 | right: 0px; 438 | top: 0px; 439 | z-index: 3; 440 | padding: 7px; 441 | cursor: pointer; 442 | line-height: 0; 443 | border-radius: 5px; 444 | } 445 | 446 | .IG_POPUP_DIG_BODY .videoThumbnail { 447 | position: absolute; 448 | right: 0px; 449 | top: 40px; 450 | z-index: 3; 451 | padding: 7px; 452 | cursor: pointer; 453 | line-height: 0; 454 | border-radius: 5px; 455 | } 456 | 457 | .IG_POPUP_DIG_BODY .newTab:hover, 458 | .IG_POPUP_DIG_BODY .videoThumbnail:hover { 459 | background: rgb(var(--ig-secondary-button-hover)); 460 | } 461 | 462 | .IG_POPUP_DIG_BODY .newTab svg, 463 | .IG_POPUP_DIG_BODY .videoThumbnail svg { 464 | fill: rgb(var(--ig-primary-text)); 465 | } 466 | 467 | .IG_POPUP_DIG_BTN { 468 | cursor: pointer; 469 | position: absolute; 470 | right: 0px; 471 | top: 0px; 472 | fill: rgb(var(--ig-primary-text)); 473 | line-height: 0; 474 | } 475 | 476 | #tempWrapper .IG_POPUP_DIG_BTN { 477 | right: 5px; 478 | transform: translateY(-50%); 479 | top: 50%; 480 | } 481 | 482 | #tempWrapper #locatePreview { 483 | margin-left: 5px; 484 | display: inline-block; 485 | text-overflow: ellipsis; 486 | overflow: hidden; 487 | width: 60%; 488 | white-space: nowrap; 489 | vertical-align: middle; 490 | position: relative; 491 | transform: translateY(-50%); 492 | top: 50%; 493 | } 494 | 495 | #tempWrapper #locateSelect { 496 | transform: translateY(-50%); 497 | top: 50%; 498 | position: relative; 499 | vertical-align: middle; 500 | } 501 | 502 | .volume_slider>div { 503 | display: none; 504 | } 505 | 506 | div[class]:hover+.volume_slider>div, 507 | .volume_slider:hover>div { 508 | display: block; 509 | } 510 | 511 | .volume_slider { 512 | position: absolute; 513 | height: 30px; 514 | left: 0px; 515 | right: 40px; 516 | } 517 | 518 | .volume_slider.bottom { 519 | left: 50px; 520 | right: 45px; 521 | bottom: 15px; 522 | } 523 | 524 | .volume_slider.vertical { 525 | top: 80px; 526 | right: 25px; 527 | transform: rotate(-90deg); 528 | transform-origin: right center; 529 | } 530 | 531 | .volume_slider>div { 532 | width: calc(100% - 20px); 533 | position: absolute; 534 | right: 10px; 535 | top: 0px; 536 | height: 30px; 537 | background: rgba(0, 0, 0, 0.25); 538 | border-radius: 25px; 539 | text-align: center; 540 | } 541 | 542 | .volume_slider input[type="range"] { 543 | overflow: hidden; 544 | width: 90%; 545 | height: inherit; 546 | margin: 0 auto; 547 | -webkit-appearance: none; 548 | appearance: none; 549 | background: transparent; 550 | cursor: pointer; 551 | } 552 | 553 | .volume_slider input[type="range"]::-webkit-slider-runnable-track { 554 | background: rgba(255, 255, 255, 0.35); 555 | height: 6px; 556 | border-radius: 7px; 557 | background: linear-gradient(to right, #fff 0%, #fff var(--ig-track-progress), rgba(255, 255, 255, 0.35) var(--ig-track-progress), rgba(255, 255, 255, 0.35) 100%); 558 | } 559 | 560 | .volume_slider input[type="range"]::-moz-range-track { 561 | background: rgba(255, 255, 255, 0.35); 562 | height: 6px; 563 | border-radius: 7px; 564 | } 565 | 566 | .volume_slider input[type="range"]::-moz-range-progress { 567 | background-color: #fff; 568 | height: 6px; 569 | border-radius: 7px; 570 | } 571 | 572 | .volume_slider input[type="range"]::-webkit-slider-thumb { 573 | -webkit-appearance: none; 574 | appearance: none; 575 | margin-top: -5px; 576 | background-color: #fff; 577 | height: 16px; 578 | width: 16px; 579 | border-radius: 50%; 580 | } 581 | 582 | .volume_slider input[type="range"]::-moz-range-thumb { 583 | border: none; 584 | border-radius: 0; 585 | background-color: #fff; 586 | height: 16px; 587 | width: 16px; 588 | border-radius: 50%; 589 | } 590 | 591 | .circle_wrapper { 592 | position: fixed; 593 | display: flex; 594 | right: 15px; 595 | bottom: 15px; 596 | z-index: 50000; 597 | width: fit-content; 598 | align-items: center; 599 | flex-direction: row; 600 | padding: 7px; 601 | border-radius: 7px; 602 | box-shadow: 0 0 5px #202020; 603 | background: rgb(var(--ig-secondary-background)); 604 | } 605 | 606 | .circle_wrapper circle { 607 | display: inline-block; 608 | border: 2px solid rgba(255, 152, 0, 0.9); 609 | opacity: .9; 610 | border-left: 2px solid rgba(0, 0, 0, 0); 611 | border-right: 2px solid rgba(0, 0, 0, 0); 612 | border-radius: 50px; 613 | width: 24px; 614 | height: 24px; 615 | margin: 0 5px; 616 | animation: spinoffPulse 0.5s infinite linear; 617 | } 618 | 619 | .circle_wrapper span { 620 | font-size: 16px; 621 | font-family: monospace; 622 | color: rgb(var(--ig-primary-text)); 623 | } 624 | 625 | @keyframes spinoffPulse { 626 | 0% { 627 | transform: rotate(0deg); 628 | } 629 | 630 | 100% { 631 | transform: rotate(360deg); 632 | } 633 | } 634 | 635 | #imageViewer { 636 | position: fixed; 637 | top: 0; 638 | left: 0; 639 | right: 0; 640 | bottom: 0; 641 | background: rgba(0, 0, 0, 0.8); 642 | display: none; 643 | justify-content: center; 644 | align-items: center; 645 | z-index: 600001; 646 | transform: scale(1); 647 | transition: transform 0.15s; 648 | 649 | @starting-style { 650 | transform: scale(0); 651 | } 652 | } 653 | 654 | #imageViewer>div { 655 | position: absolute; 656 | top: 0px; 657 | left: 0px; 658 | right: 0px; 659 | height: 35px; 660 | background: rgba(0, 0, 0, 0.2); 661 | display: flex; 662 | justify-content: flex-end; 663 | align-items: center; 664 | z-index: 2; 665 | color: #fff; 666 | padding: 0px 15px; 667 | } 668 | 669 | #imageViewer>div>div#iv_close { 670 | cursor: pointer; 671 | } 672 | 673 | #imageViewer>section { 674 | width: auto; 675 | height: auto; 676 | } 677 | 678 | #iv_image { 679 | max-width: none; 680 | max-height: 80svh; 681 | width: auto; 682 | height: 100%; 683 | user-select: none; 684 | cursor: grab; 685 | } 686 | 687 | #iv_close { 688 | filter: invert(1); 689 | } --------------------------------------------------------------------------------