191 lines
5.1 KiB
TypeScript
191 lines
5.1 KiB
TypeScript
class ThiefService {
|
|
base64ToFile(base64: string, filename: string, mimeType: string): File {
|
|
// Nếu có tiền tố "data:image/xxx;base64," thì cắt bỏ
|
|
const pureBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
|
|
const byteString = atob(pureBase64);
|
|
const ab = new ArrayBuffer(byteString.length);
|
|
const ia = new Uint8Array(ab);
|
|
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
ia[i] = byteString.charCodeAt(i);
|
|
}
|
|
|
|
const blob = new Blob([ab], { type: mimeType });
|
|
return new File([blob], filename, { type: mimeType });
|
|
}
|
|
|
|
clickByPoint(el: Element) {
|
|
const rect: DOMRect = el.getBoundingClientRect();
|
|
const x: number = rect.left + rect.width / 2;
|
|
const y: number = rect.top + rect.height / 2;
|
|
|
|
const target: Element | null = document.elementFromPoint(x, y);
|
|
if (target) {
|
|
target.dispatchEvent(
|
|
new MouseEvent("click", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
async getElementByXPath(
|
|
xpath: string,
|
|
retryCount: number = 3,
|
|
delay: number = 400
|
|
): Promise<HTMLElement | null> {
|
|
return new Promise((resolve) => {
|
|
let attempts = 0;
|
|
|
|
const tryFind = () => {
|
|
const el: Node | null = document.evaluate(
|
|
xpath,
|
|
document,
|
|
null,
|
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
null
|
|
).singleNodeValue;
|
|
|
|
if (el instanceof HTMLElement) {
|
|
resolve(el);
|
|
return;
|
|
}
|
|
|
|
attempts++;
|
|
if (attempts < retryCount) {
|
|
setTimeout(tryFind, delay);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
};
|
|
|
|
tryFind();
|
|
});
|
|
}
|
|
|
|
async imageUrlToBase64(url: string): Promise<string> {
|
|
const response = await fetch(url);
|
|
const blob = await response.blob();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
if (typeof reader.result === "string") {
|
|
// Kết quả là data URL (base64)
|
|
resolve(reader.result.split(",")[1]);
|
|
} else {
|
|
reject("Không thể đọc dữ liệu ảnh");
|
|
}
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
getImageExtension(url: string): string | null {
|
|
try {
|
|
const pathname = new URL(url).pathname; // Lấy phần path từ URL
|
|
const match = pathname.match(/\.([a-zA-Z0-9]+)$/); // Tìm đuôi file
|
|
return match ? match[1].toLowerCase() : null;
|
|
} catch {
|
|
// Nếu url không hợp lệ thì fallback sang split
|
|
const parts = url.split("?");
|
|
const path = parts[0];
|
|
const match = path.match(/\.([a-zA-Z0-9]+)$/);
|
|
return match ? match[1].toLowerCase() : null;
|
|
}
|
|
}
|
|
|
|
imageLocalToBase64(relativePath: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// Lấy URL đầy đủ trong extension
|
|
const url = chrome.runtime.getURL(`${relativePath}`);
|
|
|
|
// Fetch file
|
|
fetch(url)
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => resolve(reader.result as string); // base64
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
})
|
|
.catch(reject);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
scrollToElement(el: HTMLElement, behavior: ScrollBehavior = "smooth") {
|
|
if (!el) return;
|
|
el.scrollIntoView({
|
|
behavior, // "smooth" hoặc "auto"
|
|
block: "center", // Đưa phần tử ra giữa màn hình
|
|
inline: "nearest",
|
|
});
|
|
}
|
|
|
|
getElementPointCoores(el: HTMLElement): { x: number; y: number } | null {
|
|
if (!el) return null;
|
|
const rect = el.getBoundingClientRect();
|
|
const x = rect.left + rect.width / 2;
|
|
const y = rect.top + rect.height / 2;
|
|
return { x, y };
|
|
}
|
|
|
|
setInputValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
|
if (!el) return;
|
|
|
|
// Gán giá trị trực tiếp
|
|
el.value = value;
|
|
|
|
// Tạo và dispatch sự kiện input
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
|
|
// Tạo và dispatch sự kiện change (nếu cần)
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
|
|
writeToInput = async (value: string, xpath: string) => {
|
|
const el = (await this.getElementByXPath(xpath)) as HTMLInputElement;
|
|
|
|
if (!el) throw new Error("Xpath is not found");
|
|
|
|
// Scroll to EL
|
|
this.scrollToElement(el);
|
|
|
|
this.clickByPoint(el);
|
|
|
|
this.setInputValue(el, value);
|
|
};
|
|
|
|
pressEnter(el: HTMLElement) {
|
|
if (!el) {
|
|
throw new Error("Textarea not found:", el);
|
|
}
|
|
|
|
el.focus();
|
|
|
|
// Chuỗi sự kiện bàn phím
|
|
["keydown", "keypress", "keyup"].forEach((type) => {
|
|
el.dispatchEvent(
|
|
new KeyboardEvent(type, {
|
|
key: "Enter",
|
|
code: "Enter",
|
|
keyCode: 13,
|
|
which: 13,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
})
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
export const thiefService = new ThiefService();
|