Initial commit

This commit is contained in:
2025-09-23 00:29:33 +02:00
commit 4f120b68f5
14 changed files with 394 additions and 0 deletions

7
.vscode/launch.json vendored Normal file
View File

@@ -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": []
}

11
README.md Normal file
View File

@@ -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).

126
background.js Normal file
View File

@@ -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);

12
content.js Normal file
View File

@@ -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 });
}
});

BIN
icons/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

0
icons/icon128.png Normal file
View File

0
icons/icon16.png Normal file
View File

0
icons/icon48.png Normal file
View File

41
manifest.json Normal file
View File

@@ -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": [
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
]
}
]
}

19
options.html Normal file
View File

@@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Extension Options</title>
<style>
body { font-family: system-ui, sans-serif; padding: 12px; }
label { display:block; margin-bottom:8px }
</style>
</head>
<body>
<h1>Options</h1>
<label>Favorite color: <input id="favcolor" type="text" placeholder="e.g. teal" /></label>
<button id="save">Save</button>
<div id="msg"></div>
<script src="options.js"></script>
</body>
</html>

15
options.js Normal file
View File

@@ -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);
});
});

97
popup.css Normal file
View File

@@ -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 */
}

28
popup.html Normal file
View File

@@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Starter Popup</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="container">
<div class='header'>
<h3 for='revId'>Simple Sync</h3>
<label class="switch">
<input type="checkbox" id="editToggle" />
<span class="slider"></span>
</label>
</div>
<label>API apiUrl</label>
<input id='apiUrl' type='text' readonly />
<label>Sync Token</label>
<input id='syncToken' type='textarea' readonly />
</div>
<script src="popup.js"></script>
</body>
</html>

38
popup.js Normal file
View File

@@ -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 });
});