mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
- Add session detail windows with dedicated view per session - Enhance menu bar to show clickable session list (up to 5 sessions) - Implement terminal window focus using AppleScript on macOS - Add project credits to welcome flow - Fix compilation errors in tray_menu.rs All Mac app features are now available in the Tauri version, achieving full feature parity while maintaining Tauri's additional capabilities.
638 lines
No EOL
22 KiB
HTML
638 lines
No EOL
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Welcome to VibeTunnel</title>
|
|
<style>
|
|
:root {
|
|
/* Light mode colors */
|
|
--bg-color: #ffffff;
|
|
--window-bg: #f5f5f7;
|
|
--text-primary: #1d1d1f;
|
|
--text-secondary: #86868b;
|
|
--accent-color: #007aff;
|
|
--accent-hover: #0051d5;
|
|
--border-color: rgba(0, 0, 0, 0.1);
|
|
--shadow-color: rgba(0, 0, 0, 0.15);
|
|
--indicator-inactive: rgba(134, 134, 139, 0.3);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
/* Dark mode colors */
|
|
--bg-color: #1c1c1e;
|
|
--window-bg: #000000;
|
|
--text-primary: #f5f5f7;
|
|
--text-secondary: #98989d;
|
|
--accent-color: #0a84ff;
|
|
--accent-hover: #409cff;
|
|
--border-color: rgba(255, 255, 255, 0.1);
|
|
--shadow-color: rgba(0, 0, 0, 0.5);
|
|
--indicator-inactive: rgba(152, 152, 157, 0.3);
|
|
}
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-primary);
|
|
width: 100vw;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: var(--window-bg);
|
|
}
|
|
|
|
.content {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.page {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
.page.active {
|
|
display: flex;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.app-icon {
|
|
width: 156px;
|
|
height: 156px;
|
|
margin-bottom: 40px;
|
|
filter: drop-shadow(0 10px 20px var(--shadow-color));
|
|
border-radius: 27.6%;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.app-icon:hover {
|
|
transform: scale(1.05);
|
|
filter: drop-shadow(0 15px 30px var(--shadow-color));
|
|
}
|
|
|
|
.text-content {
|
|
text-align: center;
|
|
max-width: 480px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 40px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 16px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.description {
|
|
font-size: 16px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.navigation {
|
|
height: 92px;
|
|
padding: 0 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
border-top: 1px solid var(--border-color);
|
|
background-color: var(--bg-color);
|
|
}
|
|
|
|
.indicators {
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.indicator {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background-color: var(--indicator-inactive);
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.indicator:hover {
|
|
background-color: var(--text-secondary);
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.indicator.active {
|
|
background-color: var(--accent-color);
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
.button-container {
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.next-button {
|
|
min-width: 80px;
|
|
padding: 8px 20px;
|
|
background-color: var(--accent-color);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.next-button:hover {
|
|
background-color: var(--accent-hover);
|
|
}
|
|
|
|
.next-button:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* Additional pages content */
|
|
.feature-list {
|
|
margin-top: 30px;
|
|
text-align: left;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.feature-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
margin-bottom: 16px;
|
|
color: var(--text-secondary);
|
|
font-size: 15px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.feature-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 12px;
|
|
flex-shrink: 0;
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
.code-block {
|
|
background-color: var(--bg-color);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
padding: 16px;
|
|
margin: 20px 0;
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 20px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.secondary-button {
|
|
padding: 8px 20px;
|
|
background-color: transparent;
|
|
color: var(--accent-color);
|
|
border: 1px solid var(--accent-color);
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.secondary-button:hover {
|
|
background-color: var(--accent-color);
|
|
color: white;
|
|
}
|
|
|
|
.terminal-item {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.terminal-item:hover {
|
|
background-color: var(--tab-bg) !important;
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.credits {
|
|
margin-top: 40px;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.credits p {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.credits a {
|
|
color: var(--accent-color);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.credits a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.success-checkmark {
|
|
color: var(--success-color);
|
|
margin-right: 8px;
|
|
}
|
|
|
|
/* Add status colors */
|
|
:root {
|
|
--success-color: #34c759;
|
|
--error-color: #ff3b30;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--success-color: #32d74b;
|
|
--error-color: #ff453a;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="content">
|
|
<!-- Page 1: Welcome -->
|
|
<div class="page active" id="page-0">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Welcome to VibeTunnel</h1>
|
|
<p class="subtitle">Turn any browser into your terminal. Command your agents on the go.</p>
|
|
<p class="description">
|
|
You'll be quickly guided through the basics of VibeTunnel.<br>
|
|
This screen can always be opened from the settings.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 2: VT Command -->
|
|
<div class="page" id="page-1">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Install the VT Command</h1>
|
|
<p class="subtitle">The <code>vt</code> command lets you quickly create terminal sessions</p>
|
|
<div class="code-block">
|
|
$ vt<br>
|
|
# Creates a new terminal session in your browser
|
|
</div>
|
|
<div class="button-group">
|
|
<button class="secondary-button" onclick="installCLI()" id="installCLIBtn">Install CLI Tool</button>
|
|
</div>
|
|
<p class="description" id="cliStatus" style="margin-top: 20px; color: var(--text-secondary);"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 3: Request Permissions -->
|
|
<div class="page" id="page-2">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Grant Permissions</h1>
|
|
<p class="subtitle">VibeTunnel needs permissions to function properly</p>
|
|
<div class="feature-list">
|
|
<div class="feature-item">
|
|
<svg class="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<span>Terminal Automation - To launch terminal windows</span>
|
|
</div>
|
|
<div class="feature-item">
|
|
<svg class="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<span>Accessibility - To control terminal applications</span>
|
|
</div>
|
|
</div>
|
|
<div class="button-group">
|
|
<button class="secondary-button" onclick="requestPermissions()">Grant Permissions</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 4: Select Terminal -->
|
|
<div class="page" id="page-3">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Select Your Terminal</h1>
|
|
<p class="subtitle">Choose your preferred terminal emulator</p>
|
|
<div class="terminal-list" id="terminalList" style="margin: 20px 0;">
|
|
<!-- Terminal options will be populated here -->
|
|
</div>
|
|
<div class="button-group">
|
|
<button class="secondary-button" onclick="testTerminal()">Test Terminal</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 5: Protect Dashboard -->
|
|
<div class="page" id="page-4">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Protect Your Dashboard</h1>
|
|
<p class="subtitle">Security is important when accessing terminals remotely</p>
|
|
<p class="description">
|
|
We recommend setting a password for your dashboard,<br>
|
|
especially if you plan to access it from outside your local network.
|
|
</p>
|
|
<div class="button-group">
|
|
<button class="secondary-button" onclick="openSettings('dashboard')">Set Password</button>
|
|
<button class="secondary-button" onclick="skipPassword()">Skip for Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 6: Access Dashboard -->
|
|
<div class="page" id="page-5">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>Access Your Dashboard</h1>
|
|
<p class="subtitle">
|
|
To access your terminals from any device, create a tunnel from your device.<br><br>
|
|
This can be done via <strong>ngrok</strong> in settings or <strong>Tailscale</strong> (recommended).
|
|
</p>
|
|
<div class="button-group">
|
|
<button class="secondary-button" onclick="openDashboard()">
|
|
Open Dashboard
|
|
</button>
|
|
<button class="secondary-button" onclick="openTailscale()">
|
|
Learn about Tailscale
|
|
</button>
|
|
</div>
|
|
<div class="credits">
|
|
<p>Made with ❤️ by</p>
|
|
<p>
|
|
<a href="#" onclick="openURL('https://twitter.com/badlogic'); return false;">@badlogic</a>,
|
|
<a href="#" onclick="openURL('https://twitter.com/mitsuhiko'); return false;">@mitsuhiko</a> &
|
|
<a href="#" onclick="openURL('https://twitter.com/steipete'); return false;">@steipete</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page 7: Getting Started -->
|
|
<div class="page" id="page-6">
|
|
<img src="icon.png" alt="VibeTunnel" class="app-icon">
|
|
<div class="text-content">
|
|
<h1>You're All Set!</h1>
|
|
<p class="subtitle">VibeTunnel is now running in your system tray</p>
|
|
<p class="description">
|
|
Click the VibeTunnel icon in your system tray to access settings,<br>
|
|
open the dashboard, or manage your terminal sessions.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="navigation">
|
|
<div class="indicators">
|
|
<button class="indicator active" onclick="goToPage(0)"></button>
|
|
<button class="indicator" onclick="goToPage(1)"></button>
|
|
<button class="indicator" onclick="goToPage(2)"></button>
|
|
<button class="indicator" onclick="goToPage(3)"></button>
|
|
<button class="indicator" onclick="goToPage(4)"></button>
|
|
<button class="indicator" onclick="goToPage(5)"></button>
|
|
<button class="indicator" onclick="goToPage(6)"></button>
|
|
</div>
|
|
<div class="button-container">
|
|
<button class="next-button" id="nextButton" onclick="handleNext()">Next</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { invoke } = window.__TAURI__.tauri;
|
|
const { open } = window.__TAURI__.shell;
|
|
const { appWindow } = window.__TAURI__.window;
|
|
|
|
let currentPage = 0;
|
|
const totalPages = 7;
|
|
|
|
|
|
function updateNextButton() {
|
|
const button = document.getElementById('nextButton');
|
|
button.textContent = currentPage === totalPages - 1 ? 'Finish' : 'Next';
|
|
}
|
|
|
|
function handleNext() {
|
|
if (currentPage < totalPages - 1) {
|
|
goToPage(currentPage + 1);
|
|
} else {
|
|
// Close the welcome window
|
|
appWindow.close();
|
|
}
|
|
}
|
|
|
|
async function openDashboard() {
|
|
try {
|
|
await open('http://127.0.0.1:4020');
|
|
} catch (error) {
|
|
console.error('Failed to open dashboard:', error);
|
|
}
|
|
}
|
|
|
|
async function openTailscale() {
|
|
try {
|
|
await open('https://tailscale.com/');
|
|
} catch (error) {
|
|
console.error('Failed to open Tailscale:', error);
|
|
}
|
|
}
|
|
|
|
async function openURL(url) {
|
|
try {
|
|
await open(url);
|
|
} catch (error) {
|
|
console.error('Failed to open URL:', error);
|
|
}
|
|
}
|
|
|
|
async function installCLI() {
|
|
const button = document.getElementById('installCLIBtn');
|
|
const status = document.getElementById('cliStatus');
|
|
|
|
button.disabled = true;
|
|
status.textContent = 'Installing CLI tool...';
|
|
|
|
try {
|
|
await invoke('install_cli');
|
|
status.textContent = '✓ CLI tool installed successfully';
|
|
status.style.color = 'var(--success-color)';
|
|
button.textContent = 'Installed';
|
|
} catch (error) {
|
|
status.textContent = '✗ Installation failed';
|
|
status.style.color = 'var(--error-color)';
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function requestPermissions() {
|
|
try {
|
|
await invoke('request_all_permissions');
|
|
} catch (error) {
|
|
console.error('Failed to request permissions:', error);
|
|
}
|
|
}
|
|
|
|
async function loadTerminals() {
|
|
try {
|
|
const terminals = await invoke('detect_terminals');
|
|
const container = document.getElementById('terminalList');
|
|
container.innerHTML = '';
|
|
|
|
terminals.forEach(terminal => {
|
|
const item = document.createElement('label');
|
|
item.className = 'terminal-item';
|
|
item.style = 'display: flex; align-items: center; padding: 12px; margin-bottom: 8px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer;';
|
|
item.innerHTML = `
|
|
<input type="radio" name="terminal" value="${terminal.id}" style="margin-right: 12px;">
|
|
<span>${terminal.name}</span>
|
|
`;
|
|
container.appendChild(item);
|
|
});
|
|
|
|
// Select first terminal by default
|
|
if (terminals.length > 0) {
|
|
container.querySelector('input[type="radio"]').checked = true;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load terminals:', error);
|
|
}
|
|
}
|
|
|
|
async function testTerminal() {
|
|
const selected = document.querySelector('input[name="terminal"]:checked');
|
|
if (selected) {
|
|
try {
|
|
await invoke('test_terminal', { terminal: selected.value });
|
|
} catch (error) {
|
|
console.error('Failed to test terminal:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function openSettings(tab) {
|
|
try {
|
|
await invoke('open_settings_window', { tab });
|
|
} catch (error) {
|
|
console.error('Failed to open settings:', error);
|
|
}
|
|
}
|
|
|
|
function skipPassword() {
|
|
// Just go to next page
|
|
goToPage(currentPage + 1);
|
|
}
|
|
|
|
// Load terminals when reaching that page
|
|
function goToPage(pageIndex) {
|
|
if (pageIndex < 0 || pageIndex >= totalPages) return;
|
|
|
|
// Hide current page
|
|
document.getElementById(`page-${currentPage}`).classList.remove('active');
|
|
document.querySelectorAll('.indicator')[currentPage].classList.remove('active');
|
|
|
|
// Show new page
|
|
currentPage = pageIndex;
|
|
document.getElementById(`page-${currentPage}`).classList.add('active');
|
|
document.querySelectorAll('.indicator')[currentPage].classList.add('active');
|
|
|
|
// Load data for specific pages
|
|
if (currentPage === 3) {
|
|
loadTerminals();
|
|
}
|
|
|
|
// Update button text
|
|
updateNextButton();
|
|
}
|
|
|
|
// Handle keyboard navigation
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
handleNext();
|
|
} else if (e.key === 'ArrowRight' && currentPage < totalPages - 1) {
|
|
goToPage(currentPage + 1);
|
|
} else if (e.key === 'ArrowLeft' && currentPage > 0) {
|
|
goToPage(currentPage - 1);
|
|
}
|
|
});
|
|
|
|
// Check CLI status on page load
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
try {
|
|
const isInstalled = await invoke('is_cli_installed');
|
|
if (isInstalled) {
|
|
const button = document.getElementById('installCLIBtn');
|
|
const status = document.getElementById('cliStatus');
|
|
button.textContent = 'Installed';
|
|
button.disabled = true;
|
|
status.textContent = '✓ CLI tool is already installed';
|
|
status.style.color = 'var(--success-color)';
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to check CLI status:', error);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |