Initial commit: iOS OTA app store

This commit is contained in:
trey
2026-04-11 11:40:44 -05:00
commit ad2850d664
16 changed files with 1232 additions and 0 deletions

39
views/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App Store</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div class="header-left">
<h1>App Store</h1>
</div>
<nav>
<a href="/" class="active">Apps</a>
<a href="/upload">Upload</a>
<a href="/logout" class="logout">Logout</a>
</nav>
</header>
<main>
<div id="apps" class="app-grid"></div>
<div id="empty" class="empty-state" style="display:none">
<p>No apps yet</p>
<a href="/upload" class="btn">Upload your first IPA</a>
</div>
<!-- App detail modal -->
<div id="modal" class="modal" style="display:none">
<div class="modal-content">
<button class="modal-close" onclick="closeModal()">&times;</button>
<div id="modal-body"></div>
</div>
</div>
</main>
<script src="/js/app.js"></script>
</body>
</html>

29
views/login.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App Store - Login</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="login-page">
<div class="login-card">
<div class="login-icon">📱</div>
<h1>App Store</h1>
<p class="subtitle">OTA Distribution</p>
<form method="POST" action="/login">
<input type="password" name="password" placeholder="Password" autofocus required>
<button type="submit">Sign In</button>
</form>
<script>
if (location.search.includes('error=1')) {
const form = document.querySelector('form');
const err = document.createElement('p');
err.className = 'error';
err.textContent = 'Invalid password';
form.insertBefore(err, form.firstChild);
}
</script>
</div>
</body>
</html>

138
views/upload.html Normal file
View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload - App Store</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div class="header-left">
<h1>App Store</h1>
</div>
<nav>
<a href="/">Apps</a>
<a href="/upload" class="active">Upload</a>
<a href="/logout" class="logout">Logout</a>
</nav>
</header>
<main>
<div class="upload-card">
<h2>Upload IPA</h2>
<form id="upload-form" enctype="multipart/form-data">
<div class="drop-zone" id="drop-zone">
<div class="drop-icon">+</div>
<p>Drop .ipa file here or tap to browse</p>
<input type="file" name="ipa" id="ipa-input" accept=".ipa" required>
</div>
<div id="file-info" class="file-info" style="display:none">
<span id="file-name"></span>
<span id="file-size"></span>
</div>
<textarea name="notes" placeholder="Release notes (optional)" rows="3"></textarea>
<button type="submit" id="upload-btn">Upload</button>
<div id="progress" class="progress" style="display:none">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div id="result" style="display:none"></div>
</form>
<div class="api-info">
<h3>CLI Upload</h3>
<pre><code>curl -X POST ${location.origin}/api/upload \
-H "X-Api-Token: YOUR_TOKEN" \
-F "ipa=@path/to/app.ipa" \
-F "notes=Build notes"</code></pre>
</div>
</div>
</main>
<script>
const form = document.getElementById('upload-form');
const dropZone = document.getElementById('drop-zone');
const ipaInput = document.getElementById('ipa-input');
const fileInfo = document.getElementById('file-info');
const uploadBtn = document.getElementById('upload-btn');
const progress = document.getElementById('progress');
const progressBar = document.getElementById('progress-bar');
const result = document.getElementById('result');
// Update CLI example with actual origin
document.querySelector('.api-info code').textContent =
`curl -X POST ${location.origin}/api/upload \\\n -H "X-Api-Token: YOUR_TOKEN" \\\n -F "ipa=@path/to/app.ipa" \\\n -F "notes=Build notes"`;
// Drag and drop
['dragenter', 'dragover'].forEach(e => {
dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.add('drag-over'); });
});
['dragleave', 'drop'].forEach(e => {
dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.remove('drag-over'); });
});
dropZone.addEventListener('drop', e => {
const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.ipa')) {
ipaInput.files = e.dataTransfer.files;
showFileInfo(file);
}
});
dropZone.addEventListener('click', () => ipaInput.click());
ipaInput.addEventListener('change', () => {
if (ipaInput.files[0]) showFileInfo(ipaInput.files[0]);
});
function showFileInfo(file) {
fileInfo.style.display = 'flex';
document.getElementById('file-name').textContent = file.name;
document.getElementById('file-size').textContent = formatSize(file.size);
}
function formatSize(bytes) {
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
uploadBtn.disabled = true;
uploadBtn.textContent = 'Uploading...';
progress.style.display = 'block';
result.style.display = 'none';
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
progressBar.style.width = (e.loaded / e.total * 100) + '%';
}
};
xhr.onload = () => {
uploadBtn.disabled = false;
uploadBtn.textContent = 'Upload';
const data = JSON.parse(xhr.responseText);
result.style.display = 'block';
if (xhr.status === 200 && data.success) {
result.className = 'result success';
result.innerHTML = `
<strong>${data.app.name}</strong> v${data.build.version} uploaded!
<br><a href="/">View Apps</a>
`;
} else {
result.className = 'result error';
result.textContent = data.error || 'Upload failed';
}
};
xhr.onerror = () => {
uploadBtn.disabled = false;
uploadBtn.textContent = 'Upload';
result.style.display = 'block';
result.className = 'result error';
result.textContent = 'Upload failed — network error';
};
xhr.send(formData);
});
</script>
</body>
</html>