mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
Delete sync/index.html
This commit is contained in:
309
sync/index.html
309
sync/index.html
@@ -1,309 +0,0 @@
|
||||
<div id="githubSyncSune" class="p-4 bg-gray-50 border rounded-lg m-2">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="ghPathInput" class="block text-sm font-medium text-gray-700 mb-1">GitHub Sync Path</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="ghPathInput" placeholder="gh://owner/repo/path/to/your.sune" class="flex-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 bg-white">
|
||||
<button id="checkStatusBtn" class="inline-flex items-center justify-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700" title="Check Status">
|
||||
<i data-lucide="refresh-cw" class="h-5 w-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">Path to a `.sune` or `.json` file on GitHub for the active sune.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="syncBtn" class="w-full inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed" disabled>
|
||||
<i data-lucide="upload-cloud" class="h-5 w-5 mr-2"></i>
|
||||
<span>Sync to GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="logArea" class="block text-sm font-medium text-gray-700 mb-1">Logs</label>
|
||||
<pre id="logArea" class="w-full h-48 p-3 rounded-md border border-gray-300 bg-gray-100 overflow-auto font-mono text-xs text-gray-600 whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
// Ensure this script runs only once and is self-contained.
|
||||
if (window.sune_github_sync_initialized) return;
|
||||
window.sune_github_sync_initialized = true;
|
||||
|
||||
const suneContainer = document.getElementById('githubSyncSune');
|
||||
if (!suneContainer) return;
|
||||
|
||||
const ghPathInput = suneContainer.querySelector('#ghPathInput');
|
||||
const checkStatusBtn = suneContainer.querySelector('#checkStatusBtn');
|
||||
const syncBtn = suneContainer.querySelector('#syncBtn');
|
||||
const logArea = suneContainer.querySelector('#logArea');
|
||||
|
||||
// SUNE.id is the ID of this sune, not the active one.
|
||||
// So we'll use a dynamic key based on the *active* sune's ID.
|
||||
const getCacheKey = (suneId) => `sune_github_sync_path_${suneId}`;
|
||||
|
||||
let worker;
|
||||
const state = {
|
||||
isBusy: false,
|
||||
currentSha: null,
|
||||
pathInfo: null,
|
||||
lastCheckedSuneId: null,
|
||||
};
|
||||
|
||||
const log = (message) => {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logArea.textContent += `[${timestamp}] ${message}\n`;
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
};
|
||||
|
||||
const setBusy = (busy) => {
|
||||
state.isBusy = busy;
|
||||
checkStatusBtn.disabled = busy;
|
||||
ghPathInput.disabled = busy;
|
||||
if (busy) {
|
||||
syncBtn.disabled = true;
|
||||
checkStatusBtn.innerHTML = `<i data-lucide="loader-2" class="h-5 w-5 animate-spin"></i>`;
|
||||
} else {
|
||||
checkStatusBtn.innerHTML = `<i data-lucide="refresh-cw" class="h-5 w-5"></i>`;
|
||||
}
|
||||
lucide.createIcons();
|
||||
};
|
||||
|
||||
const createWorker = () => {
|
||||
const workerCode = `
|
||||
const GITHUB_API = 'https://api.github.com';
|
||||
|
||||
async function apiCall(method, path, token, body = null) {
|
||||
const headers = {
|
||||
'Authorization': \`token \${token}\`,
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
};
|
||||
if (body) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const options = { method, headers };
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(GITHUB_API + path, options);
|
||||
if (method === 'HEAD') {
|
||||
return { ok: response.ok, status: response.status, headers: response.headers };
|
||||
}
|
||||
const data = response.status === 204 || response.status === 201 ? {} : await response.json();
|
||||
return { ok: response.ok, status: response.status, data };
|
||||
} catch (error) {
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const { type, pat, pathInfo, suneContentB64, sha } = e.data;
|
||||
|
||||
if (!pat) {
|
||||
self.postMessage({ type: 'log', message: 'ERROR: GitHub PAT not found. Set it in Account Settings.' });
|
||||
self.postMessage({ type: 'error', reason: 'No PAT' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo, path } = pathInfo;
|
||||
const contentsPath = \`/repos/\${owner}/\${repo}/contents/\${path}\`;
|
||||
|
||||
if (type === 'check') {
|
||||
self.postMessage({ type: 'log', message: \`Checking for file: \${path} in \${owner}/\${repo}\` });
|
||||
const res = await apiCall('GET', contentsPath, pat);
|
||||
|
||||
if (res.status === 404) {
|
||||
self.postMessage({ type: 'log', message: 'File not found. It will be created on first sync.' });
|
||||
// Check if repo exists
|
||||
const repoRes = await apiCall('GET', \`/repos/\${owner}/\${repo}\`, pat);
|
||||
if(repoRes.status === 404) {
|
||||
self.postMessage({ type: 'log', message: 'ERROR: Repository not found. Please create it on GitHub first.' });
|
||||
self.postMessage({ type: 'error', reason: 'Repo not found' });
|
||||
} else {
|
||||
self.postMessage({ type: 'status', exists: false, sha: null });
|
||||
}
|
||||
} else if (res.ok) {
|
||||
self.postMessage({ type: 'log', message: \`File found. SHA: \${res.data.sha}\` });
|
||||
self.postMessage({ type: 'status', exists: true, sha: res.data.sha });
|
||||
} else {
|
||||
self.postMessage({ type: 'log', message: \`ERROR: \${res.data?.message || 'Failed to check file status.'}\` });
|
||||
self.postMessage({ type: 'error', reason: res.data?.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'sync') {
|
||||
const commitMessage = sha ? \`Sync: Update sune configuration\` : \`Sync: Create sune configuration\`;
|
||||
const body = {
|
||||
message: commitMessage,
|
||||
content: suneContentB64,
|
||||
};
|
||||
if (sha) {
|
||||
body.sha = sha;
|
||||
}
|
||||
|
||||
self.postMessage({ type: 'log', message: \`Committing to \${owner}/\${repo}/\${path}...\` });
|
||||
const res = await apiCall('PUT', contentsPath, pat, body);
|
||||
|
||||
if (res.ok) {
|
||||
const newSha = res.data.content.sha;
|
||||
self.postMessage({ type: 'log', message: \`✅ Sync successful! New SHA: \${newSha}\` });
|
||||
self.postMessage({ type: 'sync_complete', newSha });
|
||||
} else {
|
||||
self.postMessage({ type: 'log', message: \`ERROR: \${res.data?.message || 'Failed to sync file.'}\` });
|
||||
self.postMessage({ type: 'error', reason: res.data?.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
`;
|
||||
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||
return new Worker(URL.createObjectURL(blob));
|
||||
};
|
||||
|
||||
const handleWorkerMessage = (e) => {
|
||||
const { type, message, exists, sha, newSha, reason } = e.data;
|
||||
|
||||
if (type === 'log') {
|
||||
log(message);
|
||||
} else if (type === 'status') {
|
||||
state.currentSha = sha;
|
||||
syncBtn.disabled = false;
|
||||
syncBtn.querySelector('span').textContent = exists ? 'Update on GitHub' : 'Create on GitHub';
|
||||
setBusy(false);
|
||||
} else if (type === 'sync_complete') {
|
||||
state.currentSha = newSha;
|
||||
syncBtn.querySelector('span').textContent = 'Update on GitHub';
|
||||
log('Ready for next sync.');
|
||||
setBusy(false);
|
||||
} else if (type === 'error') {
|
||||
setBusy(false);
|
||||
syncBtn.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const parseGhPath = (path) => {
|
||||
const regex = /^gh:\/\/([a-zA-Z0-9\-\._]+)\/([a-zA-Z0-9\-\._]+)\/(.+\.(?:sune|json))$/;
|
||||
const match = path.trim().match(regex);
|
||||
if (!match) return null;
|
||||
return {
|
||||
owner: match[1],
|
||||
repo: match[2],
|
||||
path: match[3],
|
||||
};
|
||||
};
|
||||
|
||||
checkStatusBtn.addEventListener('click', () => {
|
||||
const pathInfo = parseGhPath(ghPathInput.value);
|
||||
if (!pathInfo) {
|
||||
log('ERROR: Invalid GitHub path format. Use gh://owner/repo/path/to/file.sune');
|
||||
return;
|
||||
}
|
||||
|
||||
const pat = window.USER && window.USER.PAT;
|
||||
if (!pat) {
|
||||
log('ERROR: GitHub PAT not found. Please set it via the Account Settings menu.');
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
logArea.textContent = '';
|
||||
log('Starting check...');
|
||||
state.pathInfo = pathInfo;
|
||||
worker.postMessage({ type: 'check', pat, pathInfo });
|
||||
});
|
||||
|
||||
syncBtn.addEventListener('click', () => {
|
||||
if (!state.pathInfo) {
|
||||
log('ERROR: Please check status before syncing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pat = window.USER && window.USER.PAT;
|
||||
if (!pat) {
|
||||
log('ERROR: GitHub PAT not found. Please set it via the Account Settings menu.');
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
log('Preparing sune data for sync...');
|
||||
|
||||
// Create a clean, serializable object from the active sune proxy
|
||||
const activeSune = window.SUNE.active;
|
||||
if (!activeSune) {
|
||||
log('ERROR: No active sune found.');
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match the export format which is an array containing the sune object
|
||||
const suneData = [JSON.parse(JSON.stringify(activeSune))];
|
||||
const suneString = JSON.stringify(suneData, null, 2);
|
||||
const suneContentB64 = btoa(unescape(encodeURIComponent(suneString)));
|
||||
|
||||
log(`Sune "${activeSune.name}" data prepared.`);
|
||||
worker.postMessage({
|
||||
type: 'sync',
|
||||
pat,
|
||||
pathInfo: state.pathInfo,
|
||||
suneContentB64,
|
||||
sha: state.currentSha,
|
||||
});
|
||||
});
|
||||
|
||||
const loadStateForActiveSune = () => {
|
||||
const activeSuneId = window.SUNE.id;
|
||||
if (state.lastCheckedSuneId === activeSuneId) {
|
||||
return; // No change
|
||||
}
|
||||
|
||||
log(`Active sune changed to "${window.SUNE.name}". Loading its sync path.`);
|
||||
state.lastCheckedSuneId = activeSuneId;
|
||||
const cacheKey = getCacheKey(activeSuneId);
|
||||
const savedPath = localStorage.getItem(cacheKey);
|
||||
|
||||
ghPathInput.value = savedPath || '';
|
||||
state.pathInfo = null;
|
||||
state.currentSha = null;
|
||||
syncBtn.disabled = true;
|
||||
syncBtn.querySelector('span').textContent = 'Sync to GitHub';
|
||||
logArea.textContent = 'Enter a sync path and click the refresh button to check its status.';
|
||||
};
|
||||
|
||||
ghPathInput.addEventListener('input', () => {
|
||||
const activeSuneId = window.SUNE.id;
|
||||
if (activeSuneId) {
|
||||
const cacheKey = getCacheKey(activeSuneId);
|
||||
localStorage.setItem(cacheKey, ghPathInput.value);
|
||||
}
|
||||
// As user types, the checked state becomes invalid
|
||||
syncBtn.disabled = true;
|
||||
state.currentSha = null;
|
||||
});
|
||||
|
||||
const init = () => {
|
||||
if (!window.SUNE || !window.USER) {
|
||||
log('ERROR: Core Sune environment not found.');
|
||||
suneContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
worker = createWorker();
|
||||
worker.onmessage = handleWorkerMessage;
|
||||
|
||||
// Check for active sune changes and update UI accordingly
|
||||
setInterval(loadStateForActiveSune, 1000);
|
||||
|
||||
// Initial load
|
||||
loadStateForActiveSune();
|
||||
|
||||
// Initial icon render
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user