mirror of
https://github.com/multipleof4/GitRight.git
synced 2026-02-04 02:47:57 +00:00
Feat: Add context menus and change tracking
This commit is contained in:
239
index.html
239
index.html
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>GitRight Mobile UI</title>
|
<title>GitRight Mobile UI</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
@@ -13,14 +13,17 @@
|
|||||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
|
.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; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white antialiased text-slate-900 overflow-hidden">
|
<body class="bg-white antialiased text-slate-900 overflow-hidden select-none">
|
||||||
|
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
leftSidebar: false,
|
leftSidebar: false,
|
||||||
rightSidebar: false,
|
rightSidebar: false,
|
||||||
accountModal: false,
|
accountModal: false,
|
||||||
|
commitModal: false,
|
||||||
githubPat: localStorage.getItem('github_pat') || '',
|
githubPat: localStorage.getItem('github_pat') || '',
|
||||||
repos: JSON.parse(localStorage.getItem('github_repos') || '[]'),
|
repos: JSON.parse(localStorage.getItem('github_repos') || '[]'),
|
||||||
|
|
||||||
@@ -29,9 +32,18 @@
|
|||||||
currentPath: '',
|
currentPath: '',
|
||||||
isLoading: false,
|
isLoading: 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'
|
||||||
|
pendingChanges: [],
|
||||||
|
commitMessage: '',
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.$watch('currentPath', () => this.refreshIcons());
|
this.$watch('currentPath', () => this.refreshIcons());
|
||||||
this.$watch('fileTree', () => this.refreshIcons());
|
this.$watch('fileTree', () => this.refreshIcons());
|
||||||
|
this.$watch('contextMenu.show', (val) => val && this.refreshIcons());
|
||||||
|
this.$watch('commitModal', (val) => val && this.refreshIcons());
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshIcons() {
|
refreshIcons() {
|
||||||
@@ -43,6 +55,7 @@
|
|||||||
this.currentPath = '';
|
this.currentPath = '';
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.rightSidebar = false;
|
this.rightSidebar = false;
|
||||||
|
this.pendingChanges = [];
|
||||||
|
|
||||||
const [fullRepo, branchPart] = repoStr.split('@');
|
const [fullRepo, branchPart] = repoStr.split('@');
|
||||||
const branch = branchPart || 'main';
|
const branch = branchPart || 'main';
|
||||||
@@ -101,6 +114,119 @@
|
|||||||
this.currentPath = parts.join('/');
|
this.currentPath = parts.join('/');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Long Press Logic
|
||||||
|
handlePressStart(e, item, type) {
|
||||||
|
this.pressTimer = setTimeout(() => {
|
||||||
|
this.showContextMenu(e, item, type);
|
||||||
|
}, 600);
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePressEnd() {
|
||||||
|
clearTimeout(this.pressTimer);
|
||||||
|
},
|
||||||
|
|
||||||
|
showContextMenu(e, item, type) {
|
||||||
|
const touch = e.touches ? e.touches[0] : e;
|
||||||
|
this.contextMenu = {
|
||||||
|
show: true,
|
||||||
|
x: Math.min(touch.clientX, window.innerWidth - 160),
|
||||||
|
y: Math.min(touch.clientY, window.innerHeight - 200),
|
||||||
|
target: item,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
this.fileTree = this.fileTree.map(item => {
|
||||||
|
if (item.path === oldPath) return { ...item, path: newPath };
|
||||||
|
if (item.path.startsWith(oldPath + '/')) {
|
||||||
|
return { ...item, path: item.path.replace(oldPath + '/', newPath + '/') };
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
this.fileTree = this.fileTree.filter(item =>
|
||||||
|
item.path !== targetPath && !item.path.startsWith(targetPath + '/')
|
||||||
|
);
|
||||||
|
this.trackChange('delete', targetPath);
|
||||||
|
}
|
||||||
|
this.contextMenu.show = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
copyItem(isCut = false) {
|
||||||
|
this.clipboard = {
|
||||||
|
item: JSON.parse(JSON.stringify(this.contextMenu.target)),
|
||||||
|
action: isCut ? 'cut' : 'copy'
|
||||||
|
};
|
||||||
|
this.contextMenu.show = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
pasteItem() {
|
||||||
|
if (!this.clipboard.item) return;
|
||||||
|
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.clipboard.action === 'cut') {
|
||||||
|
const oldPath = this.clipboard.item.path;
|
||||||
|
this.fileTree = this.fileTree.map(item => {
|
||||||
|
if (item.path === oldPath) return { ...item, path: newPath };
|
||||||
|
if (item.path.startsWith(oldPath + '/')) {
|
||||||
|
return { ...item, path: item.path.replace(oldPath + '/', newPath + '/') };
|
||||||
|
}
|
||||||
|
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.contextMenu.show = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
createFolder() {
|
||||||
|
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.trackChange('add', newPath);
|
||||||
|
}
|
||||||
|
this.contextMenu.show = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
trackChange(type, path, oldPath = null) {
|
||||||
|
this.pendingChanges.push({ type, path, oldPath, id: Date.now() });
|
||||||
|
this.refreshIcons();
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
addRepo() {
|
addRepo() {
|
||||||
const repo = prompt('Enter repository (owner/repo@branch):', 'multipleof4/GitRight@master');
|
const repo = prompt('Enter repository (owner/repo@branch):', 'multipleof4/GitRight@master');
|
||||||
if (repo && repo.includes('/')) {
|
if (repo && repo.includes('/')) {
|
||||||
@@ -126,9 +252,15 @@
|
|||||||
<i data-lucide="panel-left" class="w-4 h-4"></i>
|
<i data-lucide="panel-left" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="bg-slate-100 p-1.5 rounded-full">
|
<button
|
||||||
<i data-lucide="sun" class="w-4 h-4 text-slate-600"></i>
|
@click="pendingChanges.length ? commitModal = true : null"
|
||||||
</div>
|
:class="pendingChanges.length ? 'bg-orange-500 shadow-lg shadow-orange-200' : 'bg-slate-100'"
|
||||||
|
class="p-1.5 rounded-full transition-all duration-300"
|
||||||
|
>
|
||||||
|
<i :data-lucide="pendingChanges.length ? 'git-commit' : 'sun'"
|
||||||
|
:class="pendingChanges.length ? 'text-white' : 'text-slate-600'"
|
||||||
|
class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button @click="rightSidebar = !rightSidebar" class="p-1.5 hover:bg-gray-50 rounded-md border border-gray-200 text-slate-600">
|
<button @click="rightSidebar = !rightSidebar" class="p-1.5 hover:bg-gray-50 rounded-md border border-gray-200 text-slate-600">
|
||||||
<i data-lucide="panel-right" class="w-4 h-4"></i>
|
<i data-lucide="panel-right" class="w-4 h-4"></i>
|
||||||
@@ -136,10 +268,16 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<main class="flex-1 flex flex-col min-h-0 bg-slate-50/50">
|
<main
|
||||||
|
class="flex-1 flex flex-col min-h-0 bg-slate-50/50 relative"
|
||||||
|
@touchstart="handlePressStart($event, null, 'deadspace')"
|
||||||
|
@touchend="handlePressEnd()"
|
||||||
|
@mousedown="handlePressStart($event, null, 'deadspace')"
|
||||||
|
@mouseup="handlePressEnd()"
|
||||||
|
>
|
||||||
<!-- Explorer Toolbar -->
|
<!-- Explorer Toolbar -->
|
||||||
<template x-if="currentRepo">
|
<template x-if="currentRepo">
|
||||||
<div class="flex items-center px-4 py-2 bg-white border-b border-gray-100 space-x-2 shrink-0">
|
<div class="flex items-center px-4 py-2 bg-white border-b border-gray-100 space-x-2 shrink-0 z-10">
|
||||||
<button @click="goUp()" :disabled="!currentPath" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">
|
<button @click="goUp()" :disabled="!currentPath" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">
|
||||||
<i data-lucide="arrow-up" class="w-4 h-4 text-slate-600"></i>
|
<i data-lucide="arrow-up" class="w-4 h-4 text-slate-600"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -182,6 +320,10 @@
|
|||||||
<template x-for="item in currentItems" :key="item.path">
|
<template x-for="item in currentItems" :key="item.path">
|
||||||
<button
|
<button
|
||||||
@click="navigateTo(item.path)"
|
@click="navigateTo(item.path)"
|
||||||
|
@touchstart.stop="handlePressStart($event, item, 'item')"
|
||||||
|
@touchend.stop="handlePressEnd()"
|
||||||
|
@mousedown.stop="handlePressStart($event, item, 'item')"
|
||||||
|
@mouseup.stop="handlePressEnd()"
|
||||||
class="flex flex-col items-center p-2 rounded-xl hover:bg-white hover:shadow-sm border border-transparent hover:border-slate-200 transition-all group"
|
class="flex flex-col items-center p-2 rounded-xl hover:bg-white hover:shadow-sm border border-transparent hover:border-slate-200 transition-all group"
|
||||||
>
|
>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -200,6 +342,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Context Menu -->
|
||||||
|
<div x-show="contextMenu.show" x-cloak class="fixed inset-0 z-[100]">
|
||||||
|
<div @click="contextMenu.show = false" class="absolute inset-0"></div>
|
||||||
|
<div
|
||||||
|
class="absolute bg-white rounded-xl shadow-2xl border border-slate-200 py-1 w-40 overflow-hidden"
|
||||||
|
:style="`top: ${contextMenu.y}px; left: ${contextMenu.x}px;`"
|
||||||
|
>
|
||||||
|
<template x-if="contextMenu.type === 'item'">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<button @click="renameItem()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
|
||||||
|
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">Rename</span>
|
||||||
|
</button>
|
||||||
|
<button @click="copyItem(false)" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
|
||||||
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">Copy</span>
|
||||||
|
</button>
|
||||||
|
<button @click="copyItem(true)" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
|
||||||
|
<i data-lucide="scissors" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">Cut</span>
|
||||||
|
</button>
|
||||||
|
<div class="h-px bg-slate-100 my-1"></div>
|
||||||
|
<button @click="deleteItem()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-red-600">
|
||||||
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">Delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="contextMenu.type === 'deadspace'">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<button @click="pasteItem()" :disabled="!clipboard.item" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700 disabled:opacity-30">
|
||||||
|
<i data-lucide="clipboard" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">Paste</span>
|
||||||
|
</button>
|
||||||
|
<button @click="createFolder()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
|
||||||
|
<i data-lucide="folder-plus" class="w-3.5 h-3.5"></i>
|
||||||
|
<span class="text-xs font-medium">New Folder</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commit Modal -->
|
||||||
|
<div x-show="commitModal" x-cloak class="fixed inset-0 z-[70] flex items-center justify-center p-4">
|
||||||
|
<div @click="commitModal = false" class="fixed inset-0 bg-black/40 backdrop-blur-sm"></div>
|
||||||
|
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col max-h-[80vh]">
|
||||||
|
<div class="p-6 space-y-4 flex-1 overflow-y-auto custom-scrollbar">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-xl font-semibold text-slate-900">Pending Changes</h3>
|
||||||
|
<span class="bg-orange-100 text-orange-600 text-[10px] font-bold px-2 py-0.5 rounded-full" x-text="`${pendingChanges.length} items`"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<template x-for="change in pendingChanges" :key="change.id">
|
||||||
|
<div class="flex items-center justify-between p-2 bg-slate-50 rounded-lg border border-slate-100">
|
||||||
|
<div class="flex items-center space-x-2 truncate">
|
||||||
|
<span :class="{
|
||||||
|
'text-green-600': change.type === 'add',
|
||||||
|
'text-red-600': change.type === 'delete',
|
||||||
|
'text-blue-600': change.type === 'rename' || change.type === 'move'
|
||||||
|
}" class="text-[10px] font-bold uppercase" x-text="change.type"></span>
|
||||||
|
<span class="text-xs text-slate-600 truncate" x-text="change.path"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 ml-1">Commit Message</label>
|
||||||
|
<textarea x-model="commitMessage" placeholder="What did you change, Meowster?" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all resize-none h-24 text-sm"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 bg-slate-50 border-t border-slate-100 flex flex-col space-y-3">
|
||||||
|
<button @click="commitChanges()" class="w-full py-3 bg-slate-900 text-white font-medium rounded-xl hover:bg-slate-800 transition-colors flex items-center justify-center space-x-2">
|
||||||
|
<i data-lucide="check" class="w-4 h-4"></i>
|
||||||
|
<span>Commit to Branch</span>
|
||||||
|
</button>
|
||||||
|
<button @click="commitModal = false" class="w-full py-2 text-slate-500 text-sm font-medium hover:text-slate-700 transition-colors text-center">Discard</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Left Sidebar (Tree View) -->
|
<!-- Left Sidebar (Tree View) -->
|
||||||
<div x-show="leftSidebar" x-cloak class="fixed inset-0 z-[60] flex">
|
<div x-show="leftSidebar" x-cloak class="fixed inset-0 z-[60] flex">
|
||||||
<div @click="leftSidebar = false" class="fixed inset-0 bg-black/20 backdrop-blur-sm"></div>
|
<div @click="leftSidebar = false" class="fixed inset-0 bg-black/20 backdrop-blur-sm"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user