Initial commit
This commit is contained in:
7
.vscode/launch.json
vendored
Normal file
7
.vscode/launch.json
vendored
Normal 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
11
README.md
Normal 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
126
background.js
Normal 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
12
content.js
Normal 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
BIN
icons/icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
0
icons/icon128.png
Normal file
0
icons/icon128.png
Normal file
0
icons/icon16.png
Normal file
0
icons/icon16.png
Normal file
0
icons/icon48.png
Normal file
0
icons/icon48.png
Normal file
41
manifest.json
Normal file
41
manifest.json
Normal 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
19
options.html
Normal 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
15
options.js
Normal 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
97
popup.css
Normal 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
28
popup.html
Normal 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
38
popup.js
Normal 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 });
|
||||
});
|
||||
Reference in New Issue
Block a user