diff --git a/FirefoxExtension/background.js b/FirefoxExtension/background.js
new file mode 100644
index 0000000..8485f9a
--- /dev/null
+++ b/FirefoxExtension/background.js
@@ -0,0 +1,86 @@
+if (typeof browser === "undefined") {
+ var browser = chrome;
+}
+
+let autoRecordInterval = null;
+let autoRecordAllowedPrefixes = []; // Global storage for allowed URL prefixes
+
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message.action === "login") {
+ fetch("http://192.168.0.226:25570/api/login", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: message.username, password: message.password })
+ })
+ .then(response => response.json())
+ .then(data => {
+ console.log("Login response:", data);
+ sendResponse(data);
+ })
+ .catch(err => sendResponse({ error: err.toString() }));
+ return true;
+ } else if (message.action === "register") {
+ fetch("http://192.168.0.226:25570/api/register", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: message.username, password: message.password })
+ })
+ .then(response => response.json())
+ .then(data => {
+ console.log("Registration response:", data);
+ sendResponse(data);
+ })
+ .catch(err => sendResponse({ error: err.toString() }));
+ return true;
+ } else if (message.action === "startAutoRecord") {
+ if (message.allowedPrefixes) {
+ autoRecordAllowedPrefixes = message.allowedPrefixes
+ .split(",")
+ .map(s => s.trim())
+ .filter(s => s.length > 0);
+ } else {
+ autoRecordAllowedPrefixes = [];
+ }
+ if (autoRecordInterval) {
+ clearInterval(autoRecordInterval);
+ }
+ autoRecordInterval = setInterval(() => {
+ browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
+ if (tabs && tabs.length > 0) {
+ const currentUrl = tabs[0].url;
+ let recordUrl = false;
+ for (let prefix of autoRecordAllowedPrefixes) {
+ if (currentUrl.startsWith(prefix)) {
+ recordUrl = true;
+ break;
+ }
+ }
+ if (recordUrl) {
+ fetch("http://192.168.0.226:25570/record", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ url: currentUrl })
+ })
+ .then(response => response.json())
+ .then(data => console.log("Recorded URL:", data))
+ .catch(err => console.error("Recording error:", err));
+ } else {
+ console.log("URL does not match allowed prefixes, skipping: " + currentUrl);
+ }
+ }
+ });
+ }, message.interval || 10000);
+ sendResponse({ success: true, message: "Auto recording started" });
+ return true;
+ } else if (message.action === "stopAutoRecord") {
+ if (autoRecordInterval) {
+ clearInterval(autoRecordInterval);
+ autoRecordInterval = null;
+ }
+ sendResponse({ success: true, message: "Auto recording stopped" });
+ return true;
+ }
+});
diff --git a/FirefoxExtension/icon.png b/FirefoxExtension/icon.png
new file mode 100644
index 0000000..f59e63c
Binary files /dev/null and b/FirefoxExtension/icon.png differ
diff --git a/FirefoxExtension/manifest.json b/FirefoxExtension/manifest.json
new file mode 100644
index 0000000..3784303
--- /dev/null
+++ b/FirefoxExtension/manifest.json
@@ -0,0 +1,19 @@
+{
+ "manifest_version": 2,
+ "name": "NovelBin Auto Recorder",
+ "version": "1.0",
+ "description": "Records the current chapter URL from NovelBin via the server, with account registration.",
+ "permissions": [
+ "tabs",
+ "storage",
+ "http://192.168.0.226:25570/*"
+ ],
+ "background": {
+ "scripts": ["background.js"],
+ "persistent": true
+ },
+ "browser_action": {
+ "default_popup": "popup.html",
+ "default_icon": "icon.png"
+ }
+}
diff --git a/FirefoxExtension/popup.html b/FirefoxExtension/popup.html
new file mode 100644
index 0000000..f703dac
--- /dev/null
+++ b/FirefoxExtension/popup.html
@@ -0,0 +1,79 @@
+
+
+
+ NovelBin Auto Recorder
+
+
+
+
+
+
+
+
+
Auto Record Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FirefoxExtension/popup.js b/FirefoxExtension/popup.js
new file mode 100644
index 0000000..9063f3f
--- /dev/null
+++ b/FirefoxExtension/popup.js
@@ -0,0 +1,200 @@
+if (typeof browser === "undefined") {
+ var browser = chrome;
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ // Authentication elements
+ const authSection = document.getElementById('authSection');
+ const loginSection = document.getElementById('loginSection');
+ const registerSection = document.getElementById('registerSection');
+ const loginBtn = document.getElementById('loginBtn');
+ const loginUsernameInput = document.getElementById('loginUsername');
+ const loginPasswordInput = document.getElementById('loginPassword');
+ const showRegisterLink = document.getElementById('showRegisterLink');
+ const registerBtn = document.getElementById('registerBtn');
+ const registerUsernameInput = document.getElementById('registerUsername');
+ const registerPasswordInput = document.getElementById('registerPassword');
+ const registerConfirmPasswordInput = document.getElementById('registerConfirmPassword');
+ const showLoginLink = document.getElementById('showLoginLink');
+
+ // Recorder and list elements
+ const recorderSection = document.getElementById('recorderSection');
+ const autoRecordToggle = document.getElementById('autoRecordToggle');
+ const allowedPrefixesInput = document.getElementById('allowedPrefixes');
+ const logoutBtn = document.getElementById('logoutBtn');
+ const openResizableBtn = document.getElementById('openResizableBtn');
+ const chapterListSection = document.getElementById('chapterListSection');
+ const searchQueryInput = document.getElementById('searchQuery');
+ const excludeWordsInput = document.getElementById('excludeWords');
+ const chapterListElement = document.getElementById('chapterList');
+ const statusDiv = document.getElementById('status');
+
+ // Load stored allowedPrefixes from browser.storage
+ browser.storage.local.get('allowedPrefixes', function(result) {
+ if (result.allowedPrefixes) {
+ allowedPrefixesInput.value = result.allowedPrefixes;
+ }
+ });
+ allowedPrefixesInput.addEventListener('change', function() {
+ const allowedPrefixes = allowedPrefixesInput.value.trim();
+ browser.storage.local.set({ allowedPrefixes: allowedPrefixes });
+ });
+
+ // Toggle between login and registration views
+ showRegisterLink.addEventListener('click', function(e) {
+ e.preventDefault();
+ loginSection.style.display = "none";
+ registerSection.style.display = "block";
+ });
+ showLoginLink.addEventListener('click', function(e) {
+ e.preventDefault();
+ registerSection.style.display = "none";
+ loginSection.style.display = "block";
+ });
+
+ // Check login status on load
+ fetch("http://192.168.0.226:25570/api/status", {
+ method: "GET",
+ credentials: "include"
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.loggedIn) {
+ authSection.style.display = "none";
+ recorderSection.style.display = "block";
+ chapterListSection.style.display = "block";
+ statusDiv.textContent = "Logged in as " + data.username;
+ } else {
+ authSection.style.display = "block";
+ recorderSection.style.display = "none";
+ chapterListSection.style.display = "none";
+ }
+ })
+ .catch(err => {
+ console.error("Error checking login status:", err);
+ });
+
+ // Login handler
+ loginBtn.addEventListener('click', function() {
+ const username = loginUsernameInput.value.trim();
+ const password = loginPasswordInput.value.trim();
+ if (!username || !password) {
+ statusDiv.textContent = "Please enter username and password.";
+ return;
+ }
+ browser.runtime.sendMessage({ action: "login", username, password }, (response) => {
+ console.log("Login response:", response);
+ if (response.success) {
+ statusDiv.textContent = response.message;
+ authSection.style.display = "none";
+ recorderSection.style.display = "block";
+ chapterListSection.style.display = "block";
+ } else {
+ statusDiv.textContent = response.message || "Login failed.";
+ }
+ });
+ });
+
+ // Registration handler
+ registerBtn.addEventListener('click', function() {
+ const username = registerUsernameInput.value.trim();
+ const password = registerPasswordInput.value.trim();
+ const confirmPassword = registerConfirmPasswordInput.value.trim();
+ if (!username || !password || !confirmPassword) {
+ statusDiv.textContent = "Please fill in all registration fields.";
+ return;
+ }
+ if (password !== confirmPassword) {
+ statusDiv.textContent = "Passwords do not match.";
+ return;
+ }
+ browser.runtime.sendMessage({ action: "register", username, password }, (response) => {
+ console.log("Registration response:", response);
+ if (response.success) {
+ statusDiv.textContent = response.message;
+ authSection.style.display = "none";
+ recorderSection.style.display = "block";
+ chapterListSection.style.display = "block";
+ } else {
+ statusDiv.textContent = response.message || "Registration failed.";
+ }
+ });
+ });
+
+ // Auto record toggle handler
+ autoRecordToggle.addEventListener('change', function(e) {
+ if (e.target.checked) {
+ const allowedPrefixes = allowedPrefixesInput.value.trim();
+ browser.runtime.sendMessage({ action: "startAutoRecord", interval: 10000, allowedPrefixes }, (response) => {
+ statusDiv.textContent = response.message || response.error;
+ });
+ } else {
+ browser.runtime.sendMessage({ action: "stopAutoRecord" }, (response) => {
+ statusDiv.textContent = response.message || response.error;
+ });
+ }
+ });
+
+ // Open resizable window button
+ openResizableBtn.addEventListener('click', function() {
+ browser.windows.create({
+ url: browser.runtime.getURL("popup.html"),
+ type: "popup",
+ width: 600,
+ height: 600
+ });
+ });
+
+ // Logout handler
+ logoutBtn.addEventListener('click', function() {
+ window.location.href = "http://192.168.0.226:25570/logout";
+ });
+
+ // Function to update the chapter list
+ function updateChapterList() {
+ fetch("http://192.168.0.226:25570/logs", {
+ method: "GET",
+ credentials: "include"
+ })
+ .then(response => response.json())
+ .then(data => {
+ let chapters = data.updates || [];
+ // Sort alphabetically by URL
+ chapters.sort((a, b) => a.url.localeCompare(b.url));
+ // Filter by search query (case-insensitive)
+ const searchQuery = searchQueryInput.value.trim().toLowerCase();
+ if (searchQuery) {
+ chapters = chapters.filter(item => item.url.toLowerCase().includes(searchQuery));
+ }
+ // Filter out entries with excluded words
+ const excludeWordsStr = excludeWordsInput.value.trim().toLowerCase();
+ if (excludeWordsStr) {
+ const excludeWords = excludeWordsStr.split(",").map(s => s.trim()).filter(s => s.length > 0);
+ if (excludeWords.length > 0) {
+ chapters = chapters.filter(item => {
+ for (const word of excludeWords) {
+ if (item.url.toLowerCase().includes(word)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+ }
+ // Update the chapter list UI
+ chapterListElement.innerHTML = "";
+ chapters.forEach(item => {
+ const li = document.createElement("li");
+ li.textContent = item.url + " (" + new Date(item.timestamp).toLocaleTimeString() + ")";
+ chapterListElement.appendChild(li);
+ });
+ })
+ .catch(err => {
+ console.error("Error fetching logs:", err);
+ });
+ }
+
+ setInterval(updateChapterList, 5000);
+ searchQueryInput.addEventListener('input', updateChapterList);
+ excludeWordsInput.addEventListener('input', updateChapterList);
+});