ppp
Anonymous
4 x views • 3 months ago
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MD Zone</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg-color: #0d1117;
--panel-bg: #161b22;
--border-color: #30363d;
--text-main: #c9d1d9;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-hover: #1f6feb;
--danger: #f85149;
--danger-hover: #da3633;
--success: #2ea043;
--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* Header & Toolbar */
header {
background-color: var(--panel-bg);
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
flex-wrap: wrap;
gap: 10px;
}
.brand { font-size: 1.2rem; font-weight: bold; color: var(--accent); display: flex; align-items: center; gap: 10px; }
.path-navigator { display: flex; align-items: center; gap: 10px; font-family: monospace; font-size: 14px; background: #000; padding: 5px 15px; border-radius: 20px; border: 1px solid var(--border-color); flex: 1 1 auto; overflow-x: auto;}
.toolbar { padding: 20px; display: flex; gap: 10px; flex-wrap: wrap; }
button {
background-color: var(--panel-bg);
color: var(--text-main);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex; align-items: center; gap: 8px;
transition: var(--transition);
white-space: nowrap;
}
button:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 8px rgba(88, 166, 255, 0.2); }
button.primary { background-color: var(--success); border-color: var(--success); color: white; }
button.primary:hover { background-color: #2c974b; transform: translateY(-2px); color: white; box-shadow: 0 4px 8px rgba(46, 160, 67, 0.3);}
button.danger { background-color: transparent; border-color: var(--danger); color: var(--danger); }
button.danger:hover { background-color: var(--danger); color: white; box-shadow: 0 4px 8px rgba(248, 81, 73, 0.3);}
/* Main Content */
.container { flex: 1; padding: 0 20px 20px 20px; display: flex; flex-direction: column; }
.file-list { background-color: var(--panel-bg); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; animation: slideUp 0.5s ease; }
.file-header, .file-item { display: grid; grid-template-columns: 40px 1fr 200px 150px; align-items: center; padding: 12px 15px; border-bottom: 1px solid var(--border-color); }
.file-header { background-color: rgba(255,255,255,0.02); font-weight: bold; color: var(--text-muted); }
.file-item { transition: var(--transition); cursor: pointer; }
.file-item:hover { background-color: rgba(255,255,255,0.05); }
.file-item:last-child { border-bottom: none; }
.file-name { display: flex; align-items: center; gap: 10px; color: var(--text-main); text-decoration: none; word-break: break-all; }
.file-name i { font-size: 1.2rem; min-width: 20px; text-align: center; }
.file-name i.fa-folder { color: #dcb67a; }
.file-name i.fa-file-code { color: var(--text-muted); }
.file-actions { display: flex; gap: 10px; justify-content: flex-end; }
.icon-btn { background: none; border: none; padding: 5px; color: var(--text-muted); cursor: pointer; transition: var(--transition); box-shadow: none;}
.icon-btn:hover { color: var(--accent); transform: scale(1.1); background: none; box-shadow: none;}
.icon-btn.delete:hover { color: var(--danger); }
/* Custom Checkbox */
input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; cursor: pointer; }
/* Loading Screen Realtime */
.loader-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(13, 17, 23, 0.9); backdrop-filter: blur(5px);
display: flex; flex-direction: column; justify-content: center; align-items: center;
z-index: 9999; opacity: 0; visibility: hidden; transition: var(--transition);
}
.loader-overlay.active { opacity: 1; visibility: visible; }
.spinner { width: 50px; height: 50px; border: 4px solid var(--border-color); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; }
.loading-text { font-size: 1.2rem; font-family: monospace; color: var(--accent); letter-spacing: 2px; text-align: center; padding: 0 20px;}
.progress-bar-container { width: 80%; max-width: 300px; height: 6px; background: var(--border-color); border-radius: 3px; margin-top: 15px; overflow: hidden; }
.progress-bar { width: 0%; height: 100%; background: var(--accent); box-shadow: 0 0 10px var(--accent); transition: width 0.3s ease; }
/* Editor Panel - FIXED RESPONSIVE */
.editor-panel {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-color); z-index: 1000; flex-direction: column; animation: fadeIn 0.3s ease;
}
.editor-header {
padding: 15px 20px;
background: var(--panel-bg);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
gap: 15px;
flex-wrap: wrap; /* Mencegah overflow */
}
.editor-title-group {
display: flex;
align-items: center;
gap: 15px;
flex: 1 1 auto;
}
.editor-filename {
font-weight: bold;
color: var(--accent);
word-break: break-all;
}
.editor-tools {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
flex: 1 1 auto;
justify-content: flex-end;
}
.search-box {
display: flex;
align-items: center;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 10px;
flex: 1 1 auto; /* Kotak pencarian otomatis menyesuaikan lebar */
min-width: 200px;
}
.search-box input {
background: transparent;
border: none;
color: white;
padding: 10px 8px;
outline: none;
width: 100%;
}
.search-box button { background: none; border: none; color: var(--text-muted); box-shadow: none; padding: 5px; }
.search-box button:hover { color: var(--accent); }
.editor-textarea { flex: 1; width: 100%; background: #000; color: #00ff00; font-family: 'Courier New', Courier, monospace; font-size: 14px; padding: 20px; border: none; resize: none; outline: none; line-height: 1.5; }
/* Animations */
@keyframes spin { 100% { transform: rotate(360deg); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
/* Mobile Responsive Upgrades */
@media (max-width: 768px) {
.file-header, .file-item { grid-template-columns: 35px 1fr 80px; }
.file-header div:nth-child(3), .file-item div:nth-child(3) { display: none; } /* Sembunyikan tanggal di mobile */
/* Perbaikan Toolbar */
.toolbar { flex-direction: column; width: 100%; }
.toolbar button { width: 100%; justify-content: center; }
/* Perbaikan Layout Editor untuk Mobile */
.editor-header { flex-direction: column; align-items: stretch; }
.editor-title-group { justify-content: space-between; width: 100%; }
.editor-tools { flex-direction: column; width: 100%; align-items: stretch; }
.search-box { width: 100%; min-width: 100%; }
.editor-tools button.primary { width: 100%; justify-content: center; }
}
</style>
</head>
<body>
<div class="loader-overlay" id="loader">
<div class="spinner"></div>
<div class="loading-text" id="loader-text">Memproses Permintaan...</div>
<div class="progress-bar-container">
<div class="progress-bar" id="loader-progress"></div>
</div>
</div>
<header>
<div class="brand">
<i class="fab fa-github"></i>MD Zone
</div>
<div class="path-navigator">
<button class="icon-btn" onclick="goBack()" title="Kembali"><i class="fas fa-level-up-alt"></i></button>
<span id="current-path">/</span>
</div>
</header>
<div class="toolbar">
<button class="primary" onclick="triggerUpload()"><i class="fas fa-upload"></i> Upload File</button>
<input type="file" id="file-upload" style="display: none;" multiple onchange="handleFileUpload(event)">
<button onclick="promptCreateFolder()"><i class="fas fa-folder-plus"></i> Buat Folder</button>
<button class="danger" onclick="deleteSelected()"><i class="fas fa-trash-alt"></i> Hapus Terpilih</button>
<button onclick="loadFiles()"><i class="fas fa-sync-alt"></i> Refresh</button>
</div>
<div class="container">
<div class="file-list">
<div class="file-header">
<div><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)"></div>
<div>Nama File / Folder</div>
<div>Terakhir Dimodifikasi</div>
<div style="text-align: right;">Aksi</div>
</div>
<div id="file-container">
</div>
</div>
</div>
<div class="editor-panel" id="editor-panel">
<div class="editor-header">
<div class="editor-title-group">
<button onclick="closeEditor()"><i class="fas fa-arrow-left"></i> Kembali</button>
<span id="editor-filename" class="editor-filename">filename.js</span>
</div>
<div class="editor-tools">
<div class="search-box">
<input type="text" id="search-input" placeholder="Cari kode dalam file...">
<button onclick="findInEditor()"><i class="fas fa-search"></i></button>
</div>
<button class="primary" onclick="saveEditedFile()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
<textarea class="editor-textarea" id="editor-textarea" spellcheck="false"></textarea>
</div>
<script>
// --- KREDENSIAL ---
const GITHUB_TOKEN = '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';
const GITHUB_USER = 'modora-official';
const GITHUB_REPO = '2';
// --- STATE MANAGER ---
let currentPath = '';
let currentFiles = [];
let currentEditFile = null;
// --- API BASE ---
const API_BASE = `https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/contents`;
const headers = {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
};
// --- UTILITAS LOADING REALTIME ---
function showLoading(text) {
document.getElementById('loader').classList.add('active');
updateLoading(text, 10);
}
function updateLoading(text, percent) {
document.getElementById('loader-text').innerText = text;
document.getElementById('loader-progress').style.width = `${percent}%`;
}
function hideLoading() {
updateLoading('Selesai!', 100);
setTimeout(() => {
document.getElementById('loader').classList.remove('active');
document.getElementById('loader-progress').style.width = '0%';
}, 500);
}
// --- BASE64 ENCODE/DECODE (Mendukung UTF-8) ---
function encodeBase64(text) {
return btoa(String.fromCharCode(...new TextEncoder().encode(text)));
}
function decodeBase64(base64) {
const bin = atob(base64);
const bytes = new Uint8Array(bin.length);
for(let i=0; i<bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
// --- INIT ---
window.onload = () => {
loadFiles();
};
// --- FITUR UTAMA: BACA FILE & FOLDER ---
async function loadFiles(path = currentPath) {
showLoading('Mengambil data dari GitHub API...');
currentPath = path;
document.getElementById('current-path').innerText = '/' + currentPath;
try {
updateLoading('Menghubungkan ke repositori...', 40);
const res = await fetch(`${API_BASE}${path ? '/' + path : ''}`, { headers });
if (!res.ok) throw new Error('Gagal mengambil data. Cek Token atau Repo.');
const data = await res.json();
currentFiles = Array.isArray(data) ? data : [data];
updateLoading('Mengambil riwayat tanggal...', 70);
await renderFiles(currentFiles);
hideLoading();
} catch (error) {
alert(error.message);
hideLoading();
}
}
async function renderFiles(files) {
const container = document.getElementById('file-container');
container.innerHTML = '';
document.getElementById('selectAll').checked = false;
// Sort: Folder pertama, lalu File
files.sort((a, b) => {
if (a.type === b.type) return a.name.localeCompare(b.name);
return a.type === 'dir' ? -1 : 1;
});
for (const file of files) {
// Sembunyikan file dummy [@] yang digunakan untuk pembuatan folder
if (file.name === '[@]') continue;
const isDir = file.type === 'dir';
const icon = isDir ? '<i class="fas fa-folder"></i>' : '<i class="fas fa-file-code"></i>';
// Ambil tanggal modifikasi asinkron agar UI tidak hang
const dateId = `date-${file.sha}`;
const row = document.createElement('div');
row.className = 'file-item';
row.innerHTML = `
<div><input type="checkbox" class="file-checkbox" value="${file.path}" data-sha="${file.sha}" data-type="${file.type}"></div>
<div class="file-name" onclick="${isDir ? `loadFiles('${file.path}')` : `openEditor('${file.path}', '${file.sha}')`}">
${icon} ${file.name}
</div>
<div id="${dateId}" style="font-size: 12px; color: var(--text-muted);">Memuat...</div>
<div class="file-actions">
${!isDir ? `<button class="icon-btn" onclick="openEditor('${file.path}', '${file.sha}')"><i class="fas fa-edit"></i></button>` : ''}
<button class="icon-btn delete" onclick="deleteSingle('${file.path}', '${file.sha}', '${file.type}')"><i class="fas fa-trash"></i></button>
</div>
`;
container.appendChild(row);
// Fetch Date via Commits API
fetchCommitDate(file.path, dateId);
}
if(files.length === 0 || (files.length === 1 && files[0].name === '[@]')) {
container.innerHTML = '<div style="padding: 20px; text-align:center; color: var(--text-muted);">Folder ini kosong.</div>';
}
}
async function fetchCommitDate(filePath, elementId) {
try {
const res = await fetch(`https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/commits?path=${filePath}&page=1&per_page=1`, { headers });
if(res.ok) {
const data = await res.json();
if(data.length > 0) {
const dateObj = new Date(data[0].commit.committer.date);
document.getElementById(elementId).innerText = dateObj.toLocaleDateString('id-ID', { year: 'numeric', month: 'short', day: 'numeric', hour:'2-digit', minute:'2-digit' });
} else {
document.getElementById(elementId).innerText = '-';
}
}
} catch (e) {
document.getElementById(elementId).innerText = 'Error';
}
}
function goBack() {
if (!currentPath) return;
const parts = currentPath.split('/');
parts.pop();
loadFiles(parts.join('/'));
}
// --- FITUR BUAT FOLDER DENGAN FILE DUMMY [@] ---
async function promptCreateFolder() {
const folderName = prompt('Masukkan nama folder baru:');
if (!folderName) return;
showLoading('Membuat struktur folder...');
const path = currentPath ? `${currentPath}/${folderName}/[@]` : `${folderName}/[@]`;
try {
updateLoading('Menyimpan ke GitHub...', 60);
const res = await fetch(`${API_BASE}/${path}`, {
method: 'PUT',
headers,
body: JSON.stringify({
message: `Create folder ${folderName}`,
content: encodeBase64('Init Folder') // Konten file dummy
})
});
if(res.ok) {
updateLoading('Berhasil!', 100);
setTimeout(() => loadFiles(currentPath), 500);
} else {
throw new Error('Gagal membuat folder');
}
} catch (error) {
alert(error.message);
hideLoading();
}
}
// --- FITUR UPLOAD FILE ---
function triggerUpload() { document.getElementById('file-upload').click(); }
async function handleFileUpload(event) {
const files = event.target.files;
if (files.length === 0) return;
showLoading('Mempersiapkan upload...');
for (let i = 0; i < files.length; i++) {
const file = files[i];
updateLoading(`Membaca file ${file.name}...`, 20);
const reader = new FileReader();
reader.onload = async function(e) {
// Konversi binary string to Base64
const base64Content = e.target.result.split(',')[1];
const path = currentPath ? `${currentPath}/${file.name}` : file.name;
updateLoading(`Mengunggah ${file.name} ke server...`, 60);
try {
await fetch(`${API_BASE}/${path}`, {
method: 'PUT',
headers,
body: JSON.stringify({
message: `Upload file ${file.name}`,
content: base64Content
})
});
if(i === files.length - 1) {
updateLoading('Semua file berhasil diunggah!', 100);
setTimeout(() => { loadFiles(currentPath); }, 500);
}
} catch (error) {
alert(`Gagal upload ${file.name}`);
hideLoading();
}
};
reader.readAsDataURL(file);
}
document.getElementById('file-upload').value = '';
}
// --- FITUR HAPUS ---
async function deleteSingle(path, sha, type) {
if (!confirm(`Yakin ingin menghapus ${type === 'dir' ? 'folder' : 'file'} ini?`)) return;
showLoading('Menghapus item...');
if (type === 'dir') {
await deleteFolderRecursively(path);
} else {
await deleteApiCall(path, sha);
}
loadFiles(currentPath);
}
async function deleteApiCall(path, sha) {
updateLoading(`Menghapus ${path}...`, 50);
await fetch(`${API_BASE}/${path}`, {
method: 'DELETE',
headers,
body: JSON.stringify({ message: `Delete ${path}`, sha: sha })
});
}
// GitHub tidak memiliki API hapus folder langsung, kita harus hapus semua isi filenya
async function deleteFolderRecursively(folderPath) {
try {
updateLoading(`Memindai isi folder ${folderPath}...`, 30);
const res = await fetch(`${API_BASE}/${folderPath}`, { headers });
const items = await res.json();
for (let i = 0; i < items.length; i++) {
updateLoading(`Menghapus isi folder: ${Math.round((i/items.length)*100)}%`, 30 + ((i/items.length)*60));
if (items[i].type === 'dir') {
await deleteFolderRecursively(items[i].path);
} else {
await deleteApiCall(items[i].path, items[i].sha);
}
}
} catch(e) { console.error('Error delete recursive', e); }
}
// --- FITUR HAPUS BANYAK (CHECKBOX) ---
function toggleSelectAll(source) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = source.checked);
}
async function deleteSelected() {
const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'));
if (selected.length === 0) return alert('Pilih file/folder yang ingin dihapus');
if (!confirm(`Hapus ${selected.length} item terpilih?`)) return;
showLoading('Menghapus massal...');
for (let i = 0; i < selected.length; i++) {
updateLoading(`Menghapus item ${i+1} dari ${selected.length}...`, (i/selected.length)*90);
const path = selected[i].value;
const sha = selected[i].dataset.sha;
const type = selected[i].dataset.type;
if (type === 'dir') {
await deleteFolderRecursively(path);
} else {
await deleteApiCall(path, sha);
}
}
updateLoading('Selesai menghapus!', 100);
setTimeout(() => loadFiles(currentPath), 500);
}
// --- FITUR EDITOR & PENCARIAN ---
async function openEditor(path, sha) {
showLoading('Membaca isi file...');
try {
const res = await fetch(`${API_BASE}/${path}`, { headers });
const data = await res.json();
currentEditFile = { path: path, sha: data.sha }; // Update SHA terbaru
const content = decodeBase64(data.content);
document.getElementById('editor-filename').innerText = path;
document.getElementById('editor-textarea').value = content;
hideLoading();
document.getElementById('editor-panel').style.display = 'flex';
} catch (e) {
alert('Gagal membuka file. Mungkin bukan file teks.');
hideLoading();
}
}
function closeEditor() {
document.getElementById('editor-panel').style.display = 'none';
currentEditFile = null;
}
async function saveEditedFile() {
if (!currentEditFile) return;
showLoading('Menyimpan perubahan kode...');
const newContent = document.getElementById('editor-textarea').value;
const base64Content = encodeBase64(newContent);
try {
updateLoading('Mengirim data ke GitHub...', 60);
const res = await fetch(`${API_BASE}/${currentEditFile.path}`, {
method: 'PUT',
headers,
body: JSON.stringify({
message: `Update ${currentEditFile.path} via Dashboard`,
content: base64Content,
sha: currentEditFile.sha
})
});
if (res.ok) {
updateLoading('Berhasil disimpan!', 100);
setTimeout(() => { closeEditor(); loadFiles(currentPath); }, 500);
} else {
throw new Error('Gagal menyimpan file.');
}
} catch (e) {
alert(e.message);
hideLoading();
}
}
// Pencarian Teks di dalam Editor
function findInEditor() {
const keyword = document.getElementById('search-input').value;
if (!keyword) return;
const textarea = document.getElementById('editor-textarea');
const text = textarea.value;
const startPos = textarea.selectionEnd; // Mulai dari posisi kursor saat ini
let index = text.indexOf(keyword, startPos);
// Jika tidak ketemu, cari dari awal
if (index === -1) {
index = text.indexOf(keyword, 0);
}
if (index !== -1) {
textarea.focus();
textarea.setSelectionRange(index, index + keyword.length);
// Auto scroll ke posisi teks
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 21;
const linesBefore = text.substring(0, index).split('\n').length;
textarea.scrollTop = (linesBefore - 1) * lineHeight - (textarea.clientHeight / 2);
} else {
alert('Teks tidak ditemukan!');
}
}
// Tangkap tombol Enter pada kolom pencarian
document.getElementById('search-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault(); // Mencegah reload form
findInEditor();
}
});
</script>
</body>
</html>