From 7b0eed7b6d3b78cbb60278e6f299e79dc98d7511 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Thu, 2 May 2024 22:21:34 -0600 Subject: [PATCH] new option to save downloads on path --- package.json | 4 +- public/manifest.json | 3 +- src/App.tsx | 60 ++++++++++++------- src/background.ts | 37 +++++++++++- src/content_script.ts | 46 +++++++++++++- src/customHooks/useAddOn/index.ts | 27 +++++++-- src/customHooks/useAddOn/types.ts | 8 +++ src/customHooks/useGlobal/index.ts | 41 +++++++++---- src/customHooks/useGlobal/types.ts | 14 ++++- src/helpers/content_script/types.ts | 16 ++++- src/helpers/dom/index.ts | 41 ++++++++++++- src/helpers/files/index.ts | 46 +++++++++++--- src/molescules/Clickme/index.tsx | 12 ++++ src/molescules/Header/index.tsx | 2 +- src/molescules/ImageDetected/index.tsx | 12 +++- src/molescules/ImageDetected/types.ts | 1 + src/structure/Gallery/index.tsx | 1 + .../configuration/PathDownload/index.tsx | 60 +++++++++++++++++++ src/structure/configuration/index.tsx | 24 +------- src/styles.module.scss | 13 ++++ src/typesContentScript.ts | 1 + 21 files changed, 387 insertions(+), 82 deletions(-) create mode 100644 src/customHooks/useAddOn/types.ts create mode 100644 src/molescules/Clickme/index.tsx create mode 100644 src/structure/configuration/PathDownload/index.tsx diff --git a/package.json b/package.json index e9fae8a..532e1a0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "telegram_addon_downloader", "private": true, - "version": "0.0.0", + "version": "1.4.0", "author": { "name": "José Luis Pérez Olguín" }, - "description": "Firefox extension to download content from private/non downloable groups or channels", + "description": "Firefox extension to download content from private/non downloable groups or channels. https://allmylinks.com/programador51", "keywords": [ "react", "download", diff --git a/public/manifest.json b/public/manifest.json index 69f6413..f389d08 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -9,7 +9,8 @@ "tabs", "webRequest", "webRequestBlocking", - "https://web.telegram.org/k" + "https://web.telegram.org/k", + "downloads" ], "icons": { "48": "48x48.png", diff --git a/src/App.tsx b/src/App.tsx index 7874c78..211a5ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,13 @@ import ui from "./styles.module.scss"; import useAddOn from "./customHooks/useAddOn"; import Header from "./molescules/Header"; import Gallery from "./structure/Gallery"; +import { createContext } from "react"; +import { ReturnUseAddOn } from "./customHooks/useAddOn/types"; +import ClickMe from "./molescules/Clickme"; + +export const AddOnContext = createContext( + undefined +); function App() { const hook = useAddOn(); @@ -20,6 +27,11 @@ function App() {

Once you enter the link, close and open this popup

+ +
+

Btw, if you like this extension check out my other works

+ +
); @@ -32,27 +44,33 @@ function App() { ); return ( -
-
- - - - {hook.state.length >= 1 ? ( -
-

- Pictures found - {hook.state.length}{" "} -

- - -
- ) : null} -
+ +
+
+ + + + {hook.state.length >= 1 ? ( +
+

+ Pictures found + {hook.state.length} +

+ +
+ + + +
+
+ ) : null} +
+
); } diff --git a/src/background.ts b/src/background.ts index 01274b4..acfdcc3 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,8 +1,9 @@ import browser from "webextension-polyfill"; import { AddOnMessage} from "./typesBackgroundScript"; -import { MessageBrowserActions } from "./helpers/content_script/types"; -import { decodeStreamChunks, fetchAndCombineStreams } from "./helpers/files"; +import { DownloadImageMessage, MessageBrowserActions } from "./helpers/content_script/types"; +import { base64toBlob, decodeStreamChunks, fetchAndCombineStreams, retrieveExtension } from "./helpers/files"; import { saveAs } from "file-saver"; +import { retrieveAddOnConfiguration } from "./helpers/dom"; // Listen for messages from the content script browser.runtime.onMessage.addListener(async (message: string) => { @@ -28,6 +29,12 @@ browser.runtime.onMessage.addListener(async (message: string) => { break; } + case "downloadImage":{ + const dto:MessageBrowserActions<"downloadImage"> = JSON.parse(message) + downloadImage(dto.message); + break; + } + default: return; } @@ -36,6 +43,32 @@ browser.runtime.onMessage.addListener(async (message: string) => { storeVideoStreamRequests(); //////////////////////////////////////////////////////////////////////////// +function downloadImage(dto:DownloadImageMessage){ + try { + const url = URL.createObjectURL(base64toBlob(dto.base64)); + + const { downloadPath } = retrieveAddOnConfiguration(); + + const extension = retrieveExtension(dto.base64); + + let filename = ""; + + if(downloadPath!=="/") filename = `${downloadPath.substring(1,downloadPath.length)}/${window.crypto.randomUUID()}.${extension}` + + if(downloadPath==="/"||downloadPath==="") filename = `${window.crypto.randomUUID()}.${extension}`; + + console.log({filename , downloadPath}) + + browser.downloads.download({ + url, + filename, + }); + } catch (error) { + console.log({error}); + alert(`There's has been an error to download the image, please report it on telegram group. Error code: 'f4853378-ff59-4fc6-ad26-5e2dc423eec6'`); + } +} + function downloadMediaVideo(urls:string[]){ fetchAndCombineStreams(urls) diff --git a/src/content_script.ts b/src/content_script.ts index b5ef971..0f1c581 100644 --- a/src/content_script.ts +++ b/src/content_script.ts @@ -5,6 +5,7 @@ import { } from "./helpers/content_script"; import { MessageBrowserActions } from "./helpers/content_script/types"; import { CrawledImageDom } from "./typesContentScript"; +import { checkIsVideDomTag, retrieveAddOnConfiguration } from "./helpers/dom"; showHiddenMediaBtns(); listenAddOn(); @@ -40,14 +41,27 @@ function observeMediaElementChanges() { observer.observe(document.body, config); } -async function downloadImage(dom: HTMLElement) { +/** + * + * @param targetElement - This can be an `img` tag or `video` tag + */ +async function downloadImage(dom: HTMLImageElement|HTMLElement) { + + const isVideo = checkIsVideDomTag(dom); + + // TODO: Implement the download for video as possible without dom interaction + if(dom instanceof HTMLImageElement && !isVideo){ + await downloadImageDom(dom.src); + return; + } dom.click(); const media = await getMediaControls(); const downloadBtn = media.childNodes[2] as HTMLElement; - downloadBtn.click(); + downloadBtn.click(); + const escapeKeyEvent = new KeyboardEvent("keydown", { key: "Escape", code: "Escape", @@ -66,7 +80,6 @@ async function getMediaControls(): Promise { const containerMedia = document.getElementsByClassName( "media-viewer-buttons" ); - console.log({ containerMedia }); if (containerMedia.length > 0) { resolve(containerMedia[0] as HTMLElement); } else { @@ -176,6 +189,7 @@ async function getAllImages(): Promise { blob: base64Image, height: imageElement.naturalWidth, width: imageElement.naturalHeight, + urlSrc:imageElement.src }; return crawledContent; @@ -193,6 +207,32 @@ async function getAllImages(): Promise { return []; } +// blob:https://web.telegram.org/81018939-9915-4693-ae35-3e8828db5080 + +/** + * + * @param srcBlobUrl - Url of the blob saved on memory + * @returns {Promise} + * @example + * + * downloadImageItemDowm('blob:https://web.telegram.org/81018939-9915-4693-ae35-3e8828db5080') + */ +async function downloadImageDom(srcBlobUrl:string):Promise{ + const base64Media = await retrieveImageAsBase64(srcBlobUrl); + + const config = retrieveAddOnConfiguration(); + + const message = { + base64:base64Media, + path:config.downloadPath + }; + + browser.runtime.sendMessage(JSON.stringify({ + action:"downloadImage", + message + })); +} + function listenAddOn() { browser.runtime.onMessage.addListener(async () => { const [images] = await Promise.all([getAllImages()]); diff --git a/src/customHooks/useAddOn/index.ts b/src/customHooks/useAddOn/index.ts index caa7fe6..ddecbde 100644 --- a/src/customHooks/useAddOn/index.ts +++ b/src/customHooks/useAddOn/index.ts @@ -2,11 +2,11 @@ import browser from "webextension-polyfill"; import { CrawledImageDom } from "../../typesContentScript"; import { useContext, useEffect, useState } from "react"; import { MessageBrowserActions } from "../../helpers/content_script/types"; -import { downloadBase64, zipGallery } from "../../helpers/files"; +import { blobToBase64, zipGallery } from "../../helpers/files"; import { ContextGlobal } from "../../structure/configuration/Global"; -import { saveAs } from "file-saver"; +import { ReturnUseAddOn } from "./types"; -export default function useAddOn() { +export default function useAddOn():ReturnUseAddOn { const [state, setState] = useState([]); const [isTelegramK, setIsTelegramK] = useState(null); @@ -85,15 +85,32 @@ export default function useAddOn() { const zippedFiles = await zipGallery(toZip); - saveAs(zippedFiles, `${window.crypto.randomUUID()}.zip`); + const b64ZipFile = await blobToBase64(zippedFiles); + attemptDownloadFile(`data:application/zip;base64,${b64ZipFile}`) } else { - state.forEach((data) => downloadBase64(data.blob)); + state.forEach((data) => attemptDownloadFile(data.blob)); } } + /** + * Download the file with `background_scripts` file + * @param blob - Url of the blob + */ + async function attemptDownloadFile(blob:string){ + const dto:MessageBrowserActions<"downloadImage"> = { + action:"downloadImage", + message:{ + base64:blob + } + } + + browser.runtime.sendMessage(JSON.stringify(dto)); + } + return { handleDownloadAll, state, isTelegramK, + attemptDownloadFile }; } diff --git a/src/customHooks/useAddOn/types.ts b/src/customHooks/useAddOn/types.ts new file mode 100644 index 0000000..dab4d4c --- /dev/null +++ b/src/customHooks/useAddOn/types.ts @@ -0,0 +1,8 @@ +import { CrawledImageDom } from "../../typesContentScript"; + +export interface ReturnUseAddOn { + state: CrawledImageDom[]; + isTelegramK: boolean | null; + handleDownloadAll: () => Promise; + attemptDownloadFile: (blob:string) => Promise; +} diff --git a/src/customHooks/useGlobal/index.ts b/src/customHooks/useGlobal/index.ts index 533b8cd..6d6588e 100644 --- a/src/customHooks/useGlobal/index.ts +++ b/src/customHooks/useGlobal/index.ts @@ -1,31 +1,33 @@ -import { useEffect, useState } from "react"; -import { GlobalStateI, ReturnUseGlobal } from "./types"; +import { MutableRefObject, useEffect, useRef, useState } from "react"; +import { GlobalStateI, ReturnUseGlobal, SetPathDownload, TypePath } from "./types"; +import { retrieveAddOnConfiguration } from "../../helpers/dom"; const INITIAL_STATE: GlobalStateI = { showThumbnails: false, zipBulkDownloads: false, displayDownloadOnChat: true, initialLoad: true, + downloadPath: "/", + typePath:"root" }; export default function useGlobal(): ReturnUseGlobal { const [state, setState] = useState(INITIAL_STATE); - useEffect(() => { - const data = window.localStorage.getItem("configuration"); - - const parsedData: GlobalStateI = - data === null ? INITIAL_STATE : JSON.parse(data); + const pathTypes:MutableRefObject<{[key in TypePath]:TypePath}> = useRef({ + custom:"custom", + default:"default", + root:"root" + }); - window.localStorage.setItem("configuration", JSON.stringify(parsedData)); + useEffect(() => { + const parsedData = retrieveAddOnConfiguration(); setState({ ...parsedData, initialLoad: false }); }, []); useEffect(() => { if (state.initialLoad) return; - const info = JSON.stringify(state); - window.localStorage.setItem("configuration", info); }, [state]); @@ -47,10 +49,29 @@ export default function useGlobal(): ReturnUseGlobal { displayDownloadOnChat, })); + const setPathDownload: SetPathDownload = (type, path = "/") => { + if (type === "default" || type==="custom") { + setState((current) => ({ + ...current, + typePath:type, + downloadPath: path, + })); + return; + } + + setState(current=>({ + ...current, + typePath:type, + downloadPath:"" + })) + }; + return { ...state, setShowThumbnails, setBulkDownload, setDisplayDownloadOnChat, + setPathDownload, + pathTypes:pathTypes.current }; } diff --git a/src/customHooks/useGlobal/types.ts b/src/customHooks/useGlobal/types.ts index dd351b4..fb068c4 100644 --- a/src/customHooks/useGlobal/types.ts +++ b/src/customHooks/useGlobal/types.ts @@ -2,11 +2,23 @@ export interface GlobalStateI { showThumbnails: boolean; zipBulkDownloads: boolean; displayDownloadOnChat: boolean; - initialLoad:boolean; + initialLoad: boolean; + downloadPath: string; + typePath:TypePath; } +export type TypePath = "default" | "root" | "custom"; + +export type IndexedTypePath = { + [key in TypePath]:TypePath|string; +} + +export type SetPathDownload = (type: TypePath, path?: string) => void; + export interface ReturnUseGlobal extends GlobalStateI { setShowThumbnails: (showThumbnails: boolean) => void; setBulkDownload: (zipBulkDownloads: boolean) => void; setDisplayDownloadOnChat: (displayDownloadOnChat: boolean) => void; + setPathDownload: SetPathDownload; + pathTypes:IndexedTypePath; } diff --git a/src/helpers/content_script/types.ts b/src/helpers/content_script/types.ts index 90db369..7104e47 100644 --- a/src/helpers/content_script/types.ts +++ b/src/helpers/content_script/types.ts @@ -5,9 +5,21 @@ export interface CrawlingInformation { urlTab: string; } -export type BrowserMessage = "crawledContent" | "popUpOpened" | "chunks"; +export type BrowserMessage = "crawledContent" | "popUpOpened" | "chunks" | "downloadImage"; export interface MessageBrowserActions { action: T; - message: T extends "crawledContent" ? CrawlingInformation | string : string; + message: T extends "crawledContent" + ? CrawlingInformation|string + : T extends "popUpOpened" + ? string + : T extends "chunks" + ? string + : T extends "downloadImage" + ? DownloadImageMessage + : string +} + +export interface DownloadImageMessage{ + base64:string; } \ No newline at end of file diff --git a/src/helpers/dom/index.ts b/src/helpers/dom/index.ts index 577fb34..f55ce68 100644 --- a/src/helpers/dom/index.ts +++ b/src/helpers/dom/index.ts @@ -1,4 +1,8 @@ -export function getVideoSibling(targetElement: HTMLElement): HTMLVideoElement | null { +import { GlobalStateI } from "../../customHooks/useGlobal/types"; + +export function getVideoSibling( + targetElement: HTMLElement +): HTMLVideoElement | null { // Get the parent node of the target element const parent = targetElement.parentNode; @@ -18,7 +22,7 @@ export function getVideoSibling(targetElement: HTMLElement): HTMLVideoElement | // Iterate over siblings to find the