From 5d7f54decdd331b3f7b7db417dbdf48f57832104 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Tue, 20 Jan 2026 12:38:28 -0800 Subject: [PATCH] Feat: Implement real Git push and haptic feedback --- index.html | 283 ++++++++++++++++++++++++++++------------------------- 1 file changed, 148 insertions(+), 135 deletions(-) diff --git a/index.html b/index.html index 848f58d..7c3ed80 100644 --- a/index.html +++ b/index.html @@ -13,8 +13,7 @@ .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; } .truncate-2-lines { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } - /* Prevent text selection during long press */ - .select-none { -webkit-user-select: none; user-select: none; } + .select-none { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } @@ -31,11 +30,11 @@ fileTree: [], currentPath: '', isLoading: false, + isPushing: false, - // Context Menu State contextMenu: { show: false, x: 0, y: 0, target: null, type: 'deadspace' }, pressTimer: null, - clipboard: { item: null, action: null }, // action: 'copy' | 'cut' + clipboard: { item: null, action: null }, pendingChanges: [], commitMessage: '', @@ -50,6 +49,22 @@ this.$nextTick(() => lucide.createIcons()); }, + async apiRequest(endpoint, method = 'GET', body = null) { + const [fullRepo] = this.currentRepo.split('@'); + const url = `https://api.github.com/repos/${fullRepo}${endpoint}`; + const headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${this.githubPat}`, + 'Content-Type': 'application/json' + }; + const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : null }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err.message || 'API Error'); + } + return response.json(); + }, + async selectRepo(repoStr) { this.currentRepo = repoStr; this.currentPath = ''; @@ -59,18 +74,12 @@ const [fullRepo, branchPart] = repoStr.split('@'); const branch = branchPart || 'main'; - const url = `https://api.github.com/repos/${fullRepo}/git/trees/${branch}?recursive=1`; try { - const headers = { 'Accept': 'application/vnd.github.v3+json' }; - if (this.githubPat) headers['Authorization'] = `token ${this.githubPat}`; - - const response = await fetch(url, { headers }); - if (!response.ok) throw new Error('Failed to fetch'); - const data = await response.json(); + const data = await this.apiRequest(`/git/trees/${branch}?recursive=1`); this.fileTree = data.tree || []; } catch (e) { - alert('Error fetching repository, Meowster.'); + alert('Error fetching repository, Meowster: ' + e.message); this.currentRepo = null; } finally { this.isLoading = false; @@ -82,29 +91,22 @@ if (!this.fileTree.length) return []; return this.fileTree.filter(item => { const parts = item.path.split('/'); - if (this.currentPath === '') { - return parts.length === 1; - } - const parentPath = parts.slice(0, -1).join('/'); - return parentPath === this.currentPath; + if (this.currentPath === '') return parts.length === 1; + return parts.slice(0, -1).join('/') === this.currentPath; }).sort((a, b) => (b.type === 'tree') - (a.type === 'tree') || a.path.localeCompare(b.path)); }, get breadcrumbs() { - if (!this.currentPath) return []; - return this.currentPath.split('/'); + return this.currentPath ? this.currentPath.split('/') : []; }, get repoDisplayName() { - if (!this.currentRepo) return ''; - return this.currentRepo.split('@')[0].split('/').pop(); + return this.currentRepo ? this.currentRepo.split('@')[0].split('/').pop() : ''; }, navigateTo(path) { const item = this.fileTree.find(i => i.path === path); - if (item && item.type === 'tree') { - this.currentPath = path; - } + if (item && item.type === 'tree') this.currentPath = path; }, goUp() { @@ -114,11 +116,11 @@ this.currentPath = parts.join('/'); }, - // Long Press Logic handlePressStart(e, item, type) { this.pressTimer = setTimeout(() => { + if ('vibrate' in navigator) navigator.vibrate(50); this.showContextMenu(e, item, type); - }, 600); + }, 500); }, handlePressEnd() { @@ -136,13 +138,15 @@ }; }, - // File Operations renameItem() { const oldPath = this.contextMenu.target.path; const oldName = oldPath.split('/').pop(); const newName = prompt('Rename to:', oldName); if (newName && newName !== oldName) { - const newPath = oldPath.replace(oldName, newName); + const newPath = oldPath.substring(0, oldPath.lastIndexOf(oldName)) + newName; + const originalItem = this.fileTree.find(i => i.path === oldPath); + this.trackChange('rename', newPath, oldPath, originalItem.sha); + this.fileTree = this.fileTree.map(item => { if (item.path === oldPath) return { ...item, path: newPath }; if (item.path.startsWith(oldPath + '/')) { @@ -150,18 +154,17 @@ } return item; }); - this.trackChange('rename', newPath, oldPath); } this.contextMenu.show = false; }, deleteItem() { - if (confirm('Are you sure you want to delete this, Meowster?')) { - const targetPath = this.contextMenu.target.path; + if (confirm('Are you sure, Meowster?')) { + const target = this.contextMenu.target; + this.trackChange('delete', target.path, null, target.sha); this.fileTree = this.fileTree.filter(item => - item.path !== targetPath && !item.path.startsWith(targetPath + '/') + item.path !== target.path && !item.path.startsWith(target.path + '/') ); - this.trackChange('delete', targetPath); } this.contextMenu.show = false; }, @@ -179,14 +182,11 @@ const name = this.clipboard.item.path.split('/').pop(); const newPath = this.currentPath ? `${this.currentPath}/${name}` : name; - // Check for collisions - if (this.fileTree.some(i => i.path === newPath)) { - alert('An item with this name already exists here.'); - return; - } + if (this.fileTree.some(i => i.path === newPath)) return alert('Collision detected.'); if (this.clipboard.action === 'cut') { const oldPath = this.clipboard.item.path; + this.trackChange('move', newPath, oldPath, this.clipboard.item.sha); this.fileTree = this.fileTree.map(item => { if (item.path === oldPath) return { ...item, path: newPath }; if (item.path.startsWith(oldPath + '/')) { @@ -194,12 +194,10 @@ } return item; }); - this.trackChange('move', newPath, oldPath); this.clipboard = { item: null, action: null }; } else { - const newItem = { ...this.clipboard.item, path: newPath }; - this.fileTree.push(newItem); - this.trackChange('add', newPath); + this.fileTree.push({ ...this.clipboard.item, path: newPath }); + this.trackChange('add', newPath, null, this.clipboard.item.sha); } this.contextMenu.show = false; }, @@ -208,23 +206,78 @@ const name = prompt('Folder name:', 'new-folder'); if (name) { const newPath = this.currentPath ? `${this.currentPath}/${name}` : name; - this.fileTree.push({ path: newPath, type: 'tree', sha: 'local' }); + this.fileTree.push({ path: newPath, type: 'tree', sha: null }); this.trackChange('add', newPath); } this.contextMenu.show = false; }, - trackChange(type, path, oldPath = null) { - this.pendingChanges.push({ type, path, oldPath, id: Date.now() }); - this.refreshIcons(); + trackChange(type, path, oldPath = null, sha = null) { + this.pendingChanges.push({ type, path, oldPath, sha, id: Date.now() }); }, - commitChanges() { - if (!this.commitMessage) return alert('Please enter a commit message, Meowster.'); - alert(`Committed ${this.pendingChanges.length} changes: ${this.commitMessage}`); - this.pendingChanges = []; - this.commitMessage = ''; - this.commitModal = false; + async pushChanges() { + if (!this.commitMessage) return alert('Enter a message, Meowster.'); + this.isPushing = true; + try { + const branch = this.currentRepo.split('@')[1] || 'main'; + + // 1. Get latest commit SHA + const refData = await this.apiRequest(`/git/refs/heads/${branch}`); + const lastCommitSha = refData.object.sha; + + // 2. Get the tree SHA of that commit + const lastCommitData = await this.apiRequest(`/git/commits/${lastCommitSha}`); + const baseTreeSha = lastCommitData.tree.sha; + + // 3. Construct the new tree + const treePayload = this.pendingChanges.map(change => { + if (change.type === 'delete') return { path: change.path, mode: '100644', type: 'blob', sha: null }; + if (change.type === 'rename' || change.type === 'move') { + // In Git Data API, move is a delete + an add with existing SHA + return [ + { path: change.oldPath, mode: '100644', type: 'blob', sha: null }, + { path: change.path, mode: '100644', type: 'blob', sha: change.sha } + ]; + } + // Add + return { + path: change.path, + mode: change.sha ? '100644' : '100644', + type: 'blob', + sha: change.sha || null, + content: change.sha ? undefined : '\n' // Empty file if new + }; + }).flat(); + + const newTree = await this.apiRequest('/git/trees', 'POST', { + base_tree: baseTreeSha, + tree: treePayload + }); + + // 4. Create commit + const newCommit = await this.apiRequest('/git/commits', 'POST', { + message: this.commitMessage, + tree: newTree.sha, + parents: [lastCommitSha] + }); + + // 5. Update reference (Push) + await this.apiRequest(`/git/refs/heads/${branch}`, 'PATCH', { + sha: newCommit.sha, + force: false + }); + + alert('Successfully pushed to GitHub, Meowster!'); + this.pendingChanges = []; + this.commitMessage = ''; + this.commitModal = false; + await this.selectRepo(this.currentRepo); + } catch (e) { + alert('Push failed: ' + e.message); + } finally { + this.isPushing = false; + } }, addRepo() { @@ -232,7 +285,6 @@ if (repo && repo.includes('/')) { this.repos.push(repo); this.saveRepos(); - this.refreshIcons(); } }, @@ -275,7 +327,6 @@ @mousedown="handlePressStart($event, null, 'deadspace')" @mouseup="handlePressEnd()" > - -