commit 4f120b68f5ecd9c3a482f164207476168f987f96 Author: Gergo Dulai Date: Tue Sep 23 00:29:33 2025 +0200 Initial commit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4da7a98 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Starter Chrome Extension + +A minimal Chrome extension starter (Manifest V3) with a popup, options page, content script and background service worker. + +## How to run locally + +1. Save the project files to a folder (e.g. `chrome-extension-starter`). +2. Open Chrome and go to `chrome://extensions`. +3. Toggle **Developer mode** on (top-right). +4. Click **Load unpacked** and pick the extension folder. +5. The extension should appear in the toolbar (pin it for convenience). diff --git a/background.js b/background.js new file mode 100644 index 0000000..8b3a970 --- /dev/null +++ b/background.js @@ -0,0 +1,126 @@ +function saveRevision(source) { + console.log('Syncing started from: ' + source); + + chrome.bookmarks.getTree(async (tree) => { + const apiUrl = await chrome.storage.sync.get('apiUrl'); + const syncToken = await chrome.storage.sync.get("syncToken"); + if (!apiUrl || !syncToken) + return; + + + const savedRev = await chrome.storage.sync.get('revId'); + + const url = apiUrl.apiUrl + '/saveRevision'; + const options = { + headers: { + 'content-type': 'application/json', + 'syncToken': syncToken.syncToken + }, + method: 'POST', + body: JSON.stringify({ + 'revId': savedRev.revId, + 'bookmarks': tree + }) + }; + + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); // Convert to JSON + chrome.storage.sync.set({ revId: data.revId }); + + } catch (e) { + // TODO GD: Indicate error + } + }); +} + +async function getRevision() { + const apiUrl = await chrome.storage.sync.get('apiUrl'); + const syncToken = await chrome.storage.sync.get("syncToken"); + + if (!apiUrl || !syncToken) + return; + + const url = apiUrl.apiUrl + '/getRevision'; + const options = { + headers: { + 'content-type': 'application/json', + 'syncToken': syncToken.syncToken + }, + method: 'GET' + }; + + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); // Convert to JSON + const savedRev = await chrome.storage.sync.get('revId'); + if (data.revId === savedRev.revId) + return; + + chrome.storage.sync.set({ revId: data.revId }); + + removeAllChildren("1", () => createBookmarkTree(data.bookmarks, "1")); + removeAllChildren("2", () => createBookmarkTree(data.bookmarks, "2")); + } catch (e) { + // TODO GD: Indicate error + } +} + +// Recursively delete all children +function removeAllChildren(parentId, callback) { + chrome.bookmarks.getChildren(parentId, (children) => { + if (!children || children.length === 0) return callback?.(); + let count = children.length; + children.forEach((child) => { + if (child.url) { + chrome.bookmarks.remove(child.id, () => { + if (--count === 0) callback?.(); + }); + } else { + removeAllChildren(child.id, () => { + chrome.bookmarks.remove(child.id, () => { + if (--count === 0) callback?.(); + }); + }); + } + }); + }); +} + +// Sync events + +chrome.bookmarks.onCreated.addListener((id, bookmark) => { + saveRevision('onCreated'); +}); + +chrome.bookmarks.onRemoved.addListener((id, removeInfo) => { + saveRevision('onRemoved'); +}); + +chrome.bookmarks.onChanged.addListener((id, changeInfo) => { + saveRevision('onChanged'); +}); + +chrome.bookmarks.onMoved.addListener((id, moveInfo) => { + saveRevision('onMoved'); +}); + +chrome.bookmarks.onChildrenReordered.addListener((id, reorderInfo) => { + saveRevision('onChildrenReordered'); +}); + +chrome.bookmarks.onImportEnded.addListener(() => { + saveRevision('onImportEnded'); +}); + + +getRevision(); + +setInterval(getRevision, 5000); \ No newline at end of file diff --git a/content.js b/content.js new file mode 100644 index 0000000..cefe8f5 --- /dev/null +++ b/content.js @@ -0,0 +1,12 @@ +function onLoad() { + console.log('Starter extension content script loaded on', location.href); +} + +window.addEventListener('DOMContentLoaded', onLoad); + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message?.type === 'GET_NUM_LINKS') { + const count = document.querySelectorAll('a').length; + sendResponse({ count }); + } +}); \ No newline at end of file diff --git a/icons/icon.jpg b/icons/icon.jpg new file mode 100644 index 0000000..a4d2600 Binary files /dev/null and b/icons/icon.jpg differ diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..e69de29 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..e69de29 diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..e69de29 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ba3393b --- /dev/null +++ b/manifest.json @@ -0,0 +1,41 @@ +{ + "manifest_version": 3, + "name": "Simple Sync", + "description": "A simple extension", + "version": "1.0.0", + "icons": { + "16": "icons/icon.jpg", + "48": "icons/icon.jpg", + "128": "icons/icon.jpg" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon.jpg", + "48": "icons/icon.jpg" + } + }, + "options_page": "options.html", + "background": { + "service_worker": "background.js" + }, + "permissions": [ + "storage", + "scripting", + "activeTab", + "bookmarks" + ], + "host_permissions": [ + "" + ], + "content_scripts": [ + { + "matches": [ + "" + ], + "js": [ + "content.js" + ] + } + ] +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..be60dd2 --- /dev/null +++ b/options.html @@ -0,0 +1,19 @@ + + + + + + Extension Options + + + +

Options

+ + +
+ + + \ No newline at end of file diff --git a/options.js b/options.js new file mode 100644 index 0000000..e55d102 --- /dev/null +++ b/options.js @@ -0,0 +1,15 @@ +const favInput = document.getElementById('favcolor'); +const saveBtn = document.getElementById('save'); +const msg = document.getElementById('msg'); + +chrome.storage.sync.get(['favcolor']).then(data => { + favInput.value = data.favcolor || ''; +}); + +saveBtn.addEventListener('click', () => { + const val = favInput.value || ''; + chrome.storage.sync.set({ favcolor: val }).then(() => { + msg.textContent = 'Saved.'; + setTimeout(() => (msg.textContent = ''), 1500); + }); +}); \ No newline at end of file diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..4027559 --- /dev/null +++ b/popup.css @@ -0,0 +1,97 @@ +body { + font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial; + margin: 0; + padding: 12px; + width: 240px; +} + +.container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.header { + display: grid; + grid-template-columns: 4fr 1fr; +} + +button { + padding: 8px 10px; + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.08); + background: white; + cursor: pointer; +} + +button:hover { + filter: brightness(0.98); +} + +h1 { + font-size: 18px; + margin: 0 0 6px 0 +} + +#status { + font-size: 12px; + color: #444 +} + +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 25px; + margin-top: 10px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .3s; + border-radius: 20px; +} + +.slider:before { + position: absolute; + content: ""; + height: 15px; + width: 15px; + left: 3px; + margin-top: 5px; + background-color: white; + transition: .3s; + border-radius: 50%; +} + +input:checked+.slider { + background-color: #4caf50; +} + +input:checked+.slider:before { + transform: translateX(18px); +} + +input[readonly], +textarea[readonly] { + background-color: #fafafa; + /* subtle gray background */ + color: #555; + /* dim text color */ + border: 1px solid #ddd; + /* lighter border */ + cursor: default; + /* no text cursor change */ +} \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..1935733 --- /dev/null +++ b/popup.html @@ -0,0 +1,28 @@ + + + + + + + Starter Popup + + + + +
+
+

Simple Sync

+ +
+ + + + +
+ + + + \ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..a87127c --- /dev/null +++ b/popup.js @@ -0,0 +1,38 @@ +const editToggle = document.getElementById('editToggle'); +const revIdLabel = document.getElementById('revId'); +const apiUrl = document.getElementById('apiUrl'); +const syncToken = document.getElementById('syncToken'); + +document.addEventListener('DOMContentLoaded', () => { + chrome.storage.sync.get('revId', (savedRev) => { + revIdLabel.textContent = savedRev.revId || "No value stored"; + }); + + chrome.storage.sync.get('apiUrl', (savedUrl) => { + apiUrl.value = savedUrl.apiUrl || "No value stored"; + }); + + chrome.storage.sync.get('syncToken', (savedToken) => { + syncToken.value = savedToken.syncToken || "No value stored"; + }); +}); + +editToggle.addEventListener('change', () => { + if (editToggle.checked) { + apiUrl.readOnly = false; + syncToken.readOnly = false; + } + else { + apiUrl.readOnly = true; + syncToken.readOnly = true; + } + +}); + +apiUrl.addEventListener('input', () => { + chrome.storage.sync.set({ apiUrl: apiUrl.value }); +}); + +syncToken.addEventListener('input', () => { + chrome.storage.sync.set({ syncToken: syncToken.value }); +}); \ No newline at end of file