FumbleAround/index.html
2025-01-21 02:45:10 -06:00

523 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>FumbleAround</title>
<link rel="stylesheet" href="styles.css" />
<link rel="icon" type="image/png" href="/Assets/smily.png" />
<!-- PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="FumbleAround" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#ffffff" />
<meta name="application-name" content="FumbleAround" />
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="/Assets/smily.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/Assets/smily.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/Assets/smily.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/Assets/smily.png" />
<!-- Web Manifest -->
<link rel="manifest" href="manifest.json" />
</head>
<body>
<header>
<div class="logo">
<img
src="/Assets/smily.png"
alt="FumbleAround Logo"
class="logo-icon"
/>
FumbleAround
</div>
<div class="header-buttons">
<div class="social-buttons">
<button id="donateButton">💝</button>
<button id="githubButton">
<svg viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"
/>
</svg>
</button>
</div>
<div class="app-controls">
<button id="settingsButton">
<svg viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
/>
</svg>
</button>
<button id="fumbleButton">Fumble!</button>
</div>
</div>
</header>
<div id="settingsPanel" class="settings-panel">
<div class="settings-content">
<div class="settings-header">
<h2>Settings</h2>
<button class="close-settings">×</button>
</div>
<div class="settings-section">
<h3>Display</h3>
<div class="setting-item" id="darkModeToggleArea">
<label>Dark Mode</label>
<button id="darkModeToggle">🌙</button>
</div>
</div>
<div class="settings-section">
<h3>Interactions</h3>
<div class="setting-item interactions-list">
<div class="interaction-item">
<div class="interaction-icon">🖱️</div>
<div class="interaction-details">
<h4>Fumble Button</h4>
<p>Click the "Fumble!" button to discover a new website</p>
</div>
</div>
<div class="interaction-item">
<div class="interaction-icon">🔄</div>
<div class="interaction-details">
<h4>Page Refresh</h4>
<p>Refresh the page to load a new website</p>
</div>
</div>
<div class="interaction-item">
<div class="interaction-icon">📱</div>
<div class="interaction-details">
<h4>Shake to Fumble</h4>
<p>
On mobile devices, shake your phone to discover a new site
</p>
</div>
</div>
</div>
</div>
<div class="settings-section">
<h3>About</h3>
<div class="setting-item about-section">
<div class="about-content">
<div class="about-logo">
<img src="/Assets/smily.png" alt="FumbleAround Logo" />
<h4>FumbleAround</h4>
</div>
<div class="about-text">
<p>
Discover the hidden gems of the internet, powered by Wiby.me's
search engine.
</p>
<div class="disclaimer">
<h5>Disclaimer</h5>
<p>
We are not responsible for the content that appears. Use at
your own discretion.
</p>
</div>
<div class="tribute">
<p>
A homage to the classic StumbleUpon, reimagined for the
modern web.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="settings-section">
<h3>License</h3>
<div class="setting-item license-section">
<div class="license-content">
<p>FumbleAround - A modern web discovery tool</p>
<p>Copyright (c) 2024 Michael Maurakis</p>
<p>Licensed under the MIT License</p>
<div class="license-details">
<button class="license-toggle">View Full License</button>
<div class="full-license" style="display: none">
<p>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so,
subject to the following conditions:
</p>
<p>
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the
Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="warningModal" class="modal warning-modal">
<div class="modal-content">
<h2>⚠️ Rate Limited</h2>
<p>To protect Wiby.me from excessive requests, fumbling is limited to 20 times per minute.</p>
<p>Please wait <span id="cooldownSeconds">0</span> seconds before fumbling again.</p>
<button id="warningCloseBtn" class="warning-close-btn">
I Understand
</button>
</div>
</div>
<div id="blockedModal" class="modal blocked-modal">
<div class="modal-content">
<h2>⚠️ Content Blocked</h2>
<p>This content cannot be displayed due to security restrictions.</p>
<div class="blocked-modal-buttons">
<button id="blockedRetryBtn" class="blocked-retry-btn">Try Another Site</button>
<button id="blockedOpenBtn" class="blocked-open-btn">Open in New Tab</button>
</div>
</div>
</div>
<main>
<iframe
id="contentFrame"
src="landing.html"
title="Content"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>
</main>
<script>
if (performance.navigation.type === performance.navigation.TYPE_RELOAD) {
if (sessionStorage.getItem("hasVisited")) {
// Instead of stopping the page load, wait for assets then redirect
window.addEventListener('load', () => {
const frame = document.getElementById("contentFrame");
frame.src = "https://wiby.me/surprise/";
});
}
} else if (!sessionStorage.getItem("hasVisited")) {
sessionStorage.setItem("hasVisited", "true");
}
let lastFumbleTime = 0;
const minInterval = 2000; // Minimum 2 seconds between fumbles
const maxPerMinute = 20; // Maximum 20 fumbles per minute
let recentFumbles = []; // Track fumble timestamps for rate limiting
const fumble = () => {
const currentTime = Date.now();
const timeSinceLastFumble = currentTime - lastFumbleTime;
// Prevent rapid clicks (2 second minimum interval)
if (timeSinceLastFumble < minInterval) {
return;
}
// Clean up old fumbles (older than 1 minute)
recentFumbles = recentFumbles.filter(time =>
currentTime - time < 60000
);
// Check rate limit (20 per minute)
if (recentFumbles.length >= maxPerMinute) {
const warningModal = document.getElementById('warningModal');
const cooldownSeconds = document.getElementById('cooldownSeconds');
warningModal.classList.add('show');
// Calculate remaining cooldown time
const oldestFumble = recentFumbles[0];
const cooldownRemaining = Math.ceil((60000 - (currentTime - oldestFumble)) / 1000);
cooldownSeconds.textContent = cooldownRemaining;
return;
}
// Record this fumble
lastFumbleTime = currentTime;
recentFumbles.push(currentTime);
// Perform the fumble
const frame = document.getElementById("contentFrame");
frame.src = "https://wiby.me/surprise/";
// Wait for the page to load then focus
frame.onload = () => {
try {
// Check if it's an HTTP URL first
const currentUrl = frame.contentWindow.location.href;
if (currentUrl.startsWith('http:')) {
// Show blocked content modal
const blockedModal = document.getElementById('blockedModal');
const retryBtn = document.getElementById('blockedRetryBtn');
const openBtn = document.getElementById('blockedOpenBtn');
blockedModal.classList.add('show');
retryBtn.onclick = () => {
blockedModal.classList.remove('show');
fumble();
};
openBtn.onclick = () => {
window.open(currentUrl, '_blank');
blockedModal.classList.remove('show');
fumble();
};
return;
}
// Check for error pages
const title = frame.contentWindow.document.title.toLowerCase();
if (
title.includes("blocked") ||
title.includes("error") ||
title.includes("refused") ||
title.includes("cannot") ||
title.includes("denied")
) {
fumble(); // Just try another site for error pages
return;
}
frame.focus();
try {
frame.contentWindow.focus();
} catch (e) {
// Ignore cross-origin errors - these are normal for HTTPS sites
}
} catch (e) {
// Only show blocked modal if it's an HTTP site
if (frame.src.startsWith('http:')) {
const blockedModal = document.getElementById('blockedModal');
const retryBtn = document.getElementById('blockedRetryBtn');
const openBtn = document.getElementById('blockedOpenBtn');
blockedModal.classList.add('show');
retryBtn.onclick = () => {
blockedModal.classList.remove('show');
fumble();
};
openBtn.onclick = () => {
window.open(frame.src, '_blank');
blockedModal.classList.remove('show');
fumble();
};
return;
}
// Otherwise ignore CORS errors - these are normal for HTTPS sites
}
};
// Handle load errors
frame.onerror = () => {
fumble(); // Just try another site for load errors
};
};
document.getElementById("fumbleButton").addEventListener("click", fumble);
// Dark mode toggle
const darkModeToggle = document.getElementById('darkModeToggle');
const darkModeToggleArea = document.getElementById('darkModeToggleArea');
const body = document.body;
// Check for saved preference, default to dark if not set
if (localStorage.getItem('darkMode') === null) {
localStorage.setItem('darkMode', 'true');
}
if (localStorage.getItem('darkMode') === 'true') {
body.classList.add('dark-mode');
darkModeToggle.textContent = '☀️';
}
const toggleDarkMode = () => {
body.classList.toggle("dark-mode");
const isDark = body.classList.contains("dark-mode");
darkModeToggle.textContent = isDark ? "☀️" : "🌙";
localStorage.setItem("darkMode", isDark);
};
darkModeToggle.addEventListener("click", toggleDarkMode);
darkModeToggleArea.addEventListener("click", toggleDarkMode);
// Add GitHub button click handler
document.getElementById("githubButton").addEventListener("click", () => {
window.open("https://git.mauix.bio/michael/FumbleAround", "_blank");
});
// Add donate button click handler
document.getElementById("donateButton").addEventListener("click", () => {
window.open("https://wiby.me/donate/", "_blank");
});
// Update the settings panel click handlers
const settingsPanel = document.getElementById('settingsPanel');
const settingsButton = document.getElementById('settingsButton');
const closeSettings = document.querySelector('.close-settings');
const settingsContent = settingsPanel.querySelector('.settings-content');
const closeSettingsPanel = () => {
settingsPanel.classList.remove('show');
};
settingsButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
settingsPanel.classList.add('show');
});
closeSettings.addEventListener('click', closeSettingsPanel);
// Handle clicks on the document
document.addEventListener('click', (event) => {
if (settingsPanel.classList.contains('show') &&
!settingsContent.contains(event.target) &&
!settingsButton.contains(event.target)) {
closeSettingsPanel();
}
});
// Prevent clicks inside settings from closing
settingsContent.addEventListener('click', (event) => {
event.stopPropagation();
});
// Update warning modal close handler
const warningModal = document.getElementById("warningModal");
const warningCloseBtn = document.getElementById("warningCloseBtn");
warningCloseBtn.addEventListener("click", () => {
if (!warningCloseBtn.disabled) {
warningModal.classList.remove("show");
if (cooldownTimer) clearInterval(cooldownTimer);
}
});
// Remove the click-outside-to-close functionality for warning modal
window.addEventListener("click", (event) => {
if (event.target === warningModal) {
// Only for warning modal
warningModal.classList.remove("show");
}
});
// Add shake detection
let lastX = 0;
let lastY = 0;
let lastZ = 0;
let lastUpdate = 0;
const shakeThreshold = 15; // Adjust sensitivity
const shakeTimeout = 1000; // Prevent multiple shakes
let lastShake = 0;
function handleMotion(event) {
const current = event.accelerationIncludingGravity;
const currentTime = new Date().getTime();
const timeDiff = currentTime - lastUpdate;
if (timeDiff > 100) {
const deltaX = Math.abs(current.x - lastX);
const deltaY = Math.abs(current.y - lastY);
const deltaZ = Math.abs(current.z - lastZ);
if (
((deltaX > shakeThreshold && deltaY > shakeThreshold) ||
(deltaX > shakeThreshold && deltaZ > shakeThreshold) ||
(deltaY > shakeThreshold && deltaZ > shakeThreshold)) &&
currentTime - lastShake > shakeTimeout
) {
// Vibrate if available
if ("vibrate" in navigator) {
navigator.vibrate(200);
}
fumble();
lastShake = currentTime;
}
lastX = current.x;
lastY = current.y;
lastZ = current.z;
lastUpdate = currentTime;
}
}
// Request permission and start shake detection on mobile
function initShakeDetection() {
if (typeof DeviceMotionEvent.requestPermission === "function") {
// iOS 13+ requires permission
DeviceMotionEvent.requestPermission()
.then((permissionState) => {
if (permissionState === "granted") {
window.addEventListener("devicemotion", handleMotion);
}
})
.catch(console.error);
} else {
// Non iOS 13+ devices
window.addEventListener("devicemotion", handleMotion);
}
}
// Initialize shake detection when page loads
if ("DeviceMotionEvent" in window) {
// Add a button to request permission on iOS
if (typeof DeviceMotionEvent.requestPermission === "function") {
const settingsPanel = document.getElementById("settingsPanel");
const settingsContent =
settingsPanel.querySelector(".settings-content");
const permissionButton = document.createElement("button");
permissionButton.textContent = "Enable Shake to Fumble";
permissionButton.className = "permission-button";
permissionButton.addEventListener("click", initShakeDetection);
settingsContent.appendChild(permissionButton);
} else {
// Automatically start for non-iOS devices
initShakeDetection();
}
}
const licenseToggle = document.querySelector(".license-toggle");
const fullLicense = document.querySelector(".full-license");
licenseToggle.addEventListener("click", () => {
const isHidden = fullLicense.style.display === "none";
fullLicense.style.display = isHidden ? "block" : "none";
licenseToggle.textContent = isHidden
? "Hide Full License"
: "View Full License";
});
</script>
</body>
</html>