Compare commits

..

4 commits

Author SHA1 Message Date
bbf662fb6d Added .gitignore 2025-01-20 02:19:56 -06:00
5240aec346 Dark mode by default. 2025-01-20 02:17:55 -06:00
1282bf2bda Added docker compose file and as well as made changes to LICENSE and how
settings panel handles things.

Also removed open in new tab. CORS blocks any ability to do anything
cool with iframes so it will likely have to be reserved for a extension
or something of that kind.
2025-01-20 02:01:40 -06:00
ad05afbb83 Added settings side panel.
Working on open in new tab.
2025-01-20 01:10:03 -06:00
7 changed files with 996 additions and 268 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
docker-compose.yml
docker-compose.override.yml
tailscale
tailscale-config

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Michael Maurakis
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

View file

@ -1,12 +1,77 @@
// Create the button
const button = document.createElement('button');
button.id = 'fumbleButton';
button.textContent = 'Fumble!';
// Create the floating button
const floatingButton = document.createElement('button');
floatingButton.id = 'openInNewTab';
floatingButton.className = 'floating-button';
floatingButton.title = 'Open in new window';
floatingButton.innerHTML = `
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
`;
// Add click handler
button.addEventListener('click', () => {
window.location.href = 'https://wiby.me/surprise/';
floatingButton.addEventListener('click', () => {
window.open(window.location.href, '_blank');
});
// Add button to page
document.body.appendChild(button);
// Add styles
const style = document.createElement('style');
style.textContent = `
.floating-button {
position: fixed;
right: 20px;
top: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 69, 0, 0.9);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
z-index: 999999;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.floating-button svg {
width: 20px;
height: 20px;
color: white;
}
body:hover .floating-button {
opacity: 1;
transform: scale(1);
}
.floating-button:hover {
background: rgba(255, 69, 0, 1);
transform: scale(1.1);
}
@media (hover: none) {
.floating-button {
opacity: 0.9;
transform: scale(1);
}
.floating-button:hover {
transform: scale(1);
}
}
@media (max-width: 768px) {
.floating-button {
bottom: 20px;
top: auto;
}
}
`;
// Add elements to page
document.head.appendChild(style);
document.body.appendChild(floatingButton);

33
docker-compose.yml Normal file
View file

@ -0,0 +1,33 @@
services:
tailscale-fumble:
image: tailscale/tailscale:latest
hostname: fumblearound # Assign a name this so that you can set your domain name ex. https://tail.penguin-dory.ts.net
ports: # Uncomment the next two lines to expose the container to your local area network.
- 80:80
environment:
- TS_AUTHKEY= # BE SURE TO ADD YOUR KEY AS NOTHING WILL WORK
#- TS_EXTRA_ARGS=--advertise-tags=tag:container # Uncomment this if you want to add a tag to your node. Useful for access control lists.
#- TS_FUNNEL_CONFIG=/config/funnel.json # Uncomment this and comment the next line if you want to host your app publicly.
#- TS_SERVE_CONFIG=/config/serve.json # Comment this line if you uncomment the one above.
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=true
- TS_ACCEPT_DNS=false
volumes:
- ${PWD}/tailserve-config:/config
- ${PWD}/tailscale/state:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
fumblearound:
image: nginx:alpine
volumes:
- .:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
restart: unless-stopped
network_mode: service:tailscale-fumble
depends_on:
- tailscale-fumble

View file

@ -1,302 +1,470 @@
<!DOCTYPE html>
<!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">
<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">
<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">
<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">
<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">
<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="logo">
<img
src="/Assets/smily.png"
alt="FumbleAround Logo"
class="logo-icon"
/>
FumbleAround
</div>
<div class="header-buttons">
<div class="social-buttons">
<button id="helpButton"></button>
<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">
<div class="theme-toggle">
<button id="darkModeToggle">🌙</button>
</div>
<button id="fumbleButton">Fumble!</button>
</div>
<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="helpModal" class="modal">
<div class="modal-content">
<span class="close-button">&times;</span>
<h2>Disclaimer</h2>
<p>FumbleAround uses Wiby.me's search engine to provide random web pages. We are not responsible for the content that appears. Use at your own discretion.</p>
<p>This project is a homage to the classic StumbleUpon, reimagined for the modern web.</p>
<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">
<span class="close-button" style="display: none;">&times;</span>
<h2>⚠️ Slow Down!</h2>
<p>Clicking too quickly may get you flagged as spam by Wiby.me.</p>
<p>Please wait a moment between fumbles.</p>
<div id="cooldownTimer" class="cooldown-timer">
You can close this warning in: <span id="cooldownSeconds">5</span>s
</div>
<button id="warningCloseBtn" class="warning-close-btn" disabled>I Understand</button>
<div class="modal-content">
<span class="close-button" style="display: none">&times;</span>
<h2>⚠️ Slow Down!</h2>
<p>Clicking too quickly may get you flagged as spam by Wiby.me.</p>
<p>Please wait a moment between fumbles.</p>
<div id="cooldownTimer" class="cooldown-timer">
You can close this warning in: <span id="cooldownSeconds">5</span>s
</div>
<button id="warningCloseBtn" class="warning-close-btn" disabled>
I Understand
</button>
</div>
</div>
<main>
<iframe
id="contentFrame"
src="landing.html"
title="Content"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>
<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')) {
window.stop();
requestAnimationFrame(() => fumble());
if (performance.navigation.type === performance.navigation.TYPE_RELOAD) {
if (sessionStorage.getItem("hasVisited")) {
window.stop();
requestAnimationFrame(() => fumble());
}
} else if (!sessionStorage.getItem("hasVisited")) {
sessionStorage.setItem("hasVisited", "true");
}
let lastFumbleTime = 0;
const cooldownPeriod = 5000; // 5 seconds cooldown
let cooldownTimer = null;
const fumble = () => {
const currentTime = Date.now();
const timeSinceLastFumble = currentTime - lastFumbleTime;
if (timeSinceLastFumble < cooldownPeriod) {
// Show warning modal
const warningModal = document.getElementById("warningModal");
const cooldownSeconds = document.getElementById("cooldownSeconds");
const closeBtn = document.getElementById("warningCloseBtn");
warningModal.classList.add("show");
closeBtn.disabled = true;
// Update countdown timer
let remainingTime = 5; // 5 second countdown for closing
cooldownSeconds.textContent = remainingTime;
if (cooldownTimer) clearInterval(cooldownTimer);
cooldownTimer = setInterval(() => {
remainingTime--;
if (remainingTime <= 0) {
cooldownSeconds.textContent = "0";
closeBtn.disabled = false;
clearInterval(cooldownTimer);
} else {
cooldownSeconds.textContent = remainingTime;
}
} else if (!sessionStorage.getItem('hasVisited')) {
sessionStorage.setItem('hasVisited', 'true');
}, 1000);
return;
}
let lastFumbleTime = 0;
const cooldownPeriod = 5000; // 5 seconds cooldown
let cooldownTimer = null;
lastFumbleTime = currentTime;
const frame = document.getElementById("contentFrame");
frame.src = "https://wiby.me/surprise/";
const fumble = () => {
const currentTime = Date.now();
const timeSinceLastFumble = currentTime - lastFumbleTime;
if (timeSinceLastFumble < cooldownPeriod) {
// Show warning modal
const warningModal = document.getElementById('warningModal');
const cooldownSeconds = document.getElementById('cooldownSeconds');
const closeBtn = document.getElementById('warningCloseBtn');
warningModal.classList.add('show');
closeBtn.disabled = true;
// Update countdown timer
let remainingTime = 5; // 5 second countdown for closing
cooldownSeconds.textContent = remainingTime;
if (cooldownTimer) clearInterval(cooldownTimer);
cooldownTimer = setInterval(() => {
remainingTime--;
if (remainingTime <= 0) {
cooldownSeconds.textContent = '0';
closeBtn.disabled = false;
clearInterval(cooldownTimer);
} else {
cooldownSeconds.textContent = remainingTime;
}
}, 1000);
return;
// Wait for the page to load then focus
frame.onload = () => {
// Check if we landed on a blocked/error page
try {
const title = frame.contentWindow.document.title.toLowerCase();
if (
title.includes("blocked") ||
title.includes("error") ||
title.includes("refused") ||
title.includes("cannot") ||
title.includes("denied")
) {
// Try again if we hit an error page
fumble();
return;
}
} catch (e) {
// Can't access title due to CORS - assume page is OK
}
lastFumbleTime = currentTime;
const frame = document.getElementById('contentFrame');
frame.src = 'https://wiby.me/surprise/';
// Wait for the page to load then focus
frame.onload = () => {
// Check if we landed on a blocked/error page
try {
const title = frame.contentWindow.document.title.toLowerCase();
if (title.includes('blocked') ||
title.includes('error') ||
title.includes('refused') ||
title.includes('cannot') ||
title.includes('denied')) {
// Try again if we hit an error page
fumble();
return;
}
} catch (e) {
// Can't access title due to CORS - assume page is OK
}
frame.focus();
try {
frame.contentWindow.focus();
} catch (e) {
// Ignore cross-origin errors
}
};
// Handle load errors
frame.onerror = () => {
fumble(); // Try again if loading fails
};
frame.focus();
try {
frame.contentWindow.focus();
} catch (e) {
// Ignore cross-origin errors
}
};
document.getElementById('fumbleButton').addEventListener('click', fumble);
// Handle load errors
frame.onerror = () => {
fumble(); // Try again if loading fails
};
};
document.getElementById("fumbleButton").addEventListener("click", fumble);
// Dark mode toggle
const darkModeToggle = document.getElementById('darkModeToggle');
const body = document.body;
// Check for saved preference
if (localStorage.getItem('darkMode') === 'true') {
body.classList.add('dark-mode');
darkModeToggle.textContent = '☀️';
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);
}
});
darkModeToggle.addEventListener('click', () => {
body.classList.toggle('dark-mode');
const isDark = body.classList.contains('dark-mode');
darkModeToggle.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('darkMode', isDark);
});
// 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');
});
// Add help button click handler
const modal = document.getElementById('helpModal');
const helpButton = document.getElementById('helpButton');
const closeButton = document.querySelector('.close-button');
helpButton.addEventListener('click', () => {
modal.classList.add('show');
});
closeButton.addEventListener('click', () => {
modal.classList.remove('show');
});
// 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 === modal) { // Only for help modal
modal.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;
}
// 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");
}
});
// 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);
}
}
// 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;
// Initialize shake detection when page loads
if ('DeviceMotionEvent' in window) {
// Add a button to request permission on iOS
if (typeof DeviceMotionEvent.requestPermission === 'function') {
const modal = document.getElementById('helpModal');
const modalContent = modal.querySelector('.modal-content');
const permissionButton = document.createElement('button');
permissionButton.textContent = 'Enable Shake to Fumble';
permissionButton.className = 'permission-button';
permissionButton.addEventListener('click', initShakeDetection);
modalContent.appendChild(permissionButton);
} else {
// Automatically start for non-iOS devices
initShakeDetection();
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>
</html>

35
nginx.conf Normal file
View file

@ -0,0 +1,35 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
location / {
try_files $uri $uri/ /index.html;
expires 1h;
add_header Cache-Control "public, no-transform";
}
# Prevent access to .git and other hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Assets caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 7d;
add_header Cache-Control "public, no-transform";
}
}

View file

@ -100,6 +100,7 @@ header {
main {
flex: 1;
position: relative;
overflow: hidden;
}
#contentFrame {
@ -507,4 +508,405 @@ button {
.dark-mode .warning-close-btn:not(:disabled):hover {
background-color: #ff7c5c;
}
.settings-panel {
position: fixed;
top: 0;
right: -400px;
width: 400px;
height: 100vh;
background-color: #ffffff;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
transition: right 0.3s ease;
z-index: 2000;
overflow-y: auto;
}
.dark-mode .settings-panel {
background-color: #1a1a1a;
color: #ffffff;
}
.settings-panel.show {
right: 0;
}
.settings-content {
padding: 20px;
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.dark-mode .settings-header {
border-bottom-color: #333;
}
.close-settings {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 5px;
}
.dark-mode .close-settings {
color: #999;
}
.settings-section {
margin-bottom: 30px;
}
.settings-section h3 {
margin-bottom: 15px;
color: #ff4500;
}
.dark-mode .settings-section h3 {
color: #ff6b4a;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
background: #f5f5f5;
border-radius: 8px;
}
.dark-mode .setting-item {
background: #2a2a2a;
}
.settings-button {
width: 100%;
padding: 10px;
background-color: #ff4500;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.dark-mode .settings-button {
background-color: #ff6b4a;
}
.settings-button:hover {
background-color: #ff5722;
}
@media (max-width: 768px) {
.settings-panel {
width: 100%;
right: -100%;
}
}
#settingsButton {
padding: 8px;
background-color: transparent;
border: none;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
}
#settingsButton svg {
width: 24px;
height: 24px;
color: #333;
transition: color 0.3s ease, transform 0.3s ease;
}
.dark-mode #settingsButton svg {
color: #fff;
}
#settingsButton:hover svg {
transform: rotate(30deg);
}
#settingsButton:active svg {
transform: rotate(180deg);
}
@media (hover: none) {
#settingsButton:hover svg {
transform: none;
}
}
.about-section {
flex-direction: column;
padding: 20px;
}
.about-content {
width: 100%;
text-align: center;
}
.about-logo {
margin-bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.about-logo img {
width: 64px;
height: 64px;
border-radius: 12px;
}
.about-logo h4 {
font-size: 1.2em;
margin: 0;
color: #ff4500;
}
.dark-mode .about-logo h4 {
color: #ff6b4a;
}
.about-text {
display: flex;
flex-direction: column;
gap: 20px;
}
.about-text p {
margin: 0;
line-height: 1.5;
}
.disclaimer {
background: rgba(255, 69, 0, 0.1);
padding: 15px;
border-radius: 8px;
margin: 10px 0;
}
.dark-mode .disclaimer {
background: rgba(255, 107, 74, 0.1);
}
.disclaimer h5 {
color: #ff4500;
margin: 0 0 10px 0;
font-size: 1em;
}
.dark-mode .disclaimer h5 {
color: #ff6b4a;
}
.tribute {
font-style: italic;
opacity: 0.8;
padding-top: 10px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.dark-mode .tribute {
border-top-color: rgba(255, 255, 255, 0.1);
}
.floating-button {
position: fixed;
right: 20px;
top: 80px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 69, 0, 0.9);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
z-index: 1000;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
pointer-events: auto;
}
.floating-button svg {
width: 20px;
height: 20px;
color: white;
}
main:hover .floating-button {
opacity: 1;
transform: scale(1);
}
.floating-button:hover {
background: rgba(255, 69, 0, 1);
transform: scale(1.1);
}
.dark-mode .floating-button {
background: rgba(255, 107, 74, 0.9);
}
.dark-mode .floating-button:hover {
background: rgba(255, 107, 74, 1);
}
@media (hover: none) {
.floating-button {
opacity: 0.9;
transform: scale(1);
}
.floating-button:hover {
transform: scale(1);
}
}
@media (max-width: 768px) {
.floating-button {
top: auto;
bottom: 80px;
right: 20px;
}
}
.setting-item#darkModeToggleArea {
cursor: pointer;
transition: background-color 0.3s ease;
}
.setting-item#darkModeToggleArea:hover {
background: #eaeaea;
}
.dark-mode .setting-item#darkModeToggleArea:hover {
background: #333;
}
.interactions-list {
flex-direction: column;
gap: 15px;
padding: 15px;
}
.interaction-item {
display: flex;
align-items: flex-start;
gap: 15px;
}
.interaction-icon {
font-size: 24px;
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 69, 0, 0.1);
border-radius: 8px;
}
.dark-mode .interaction-icon {
background: rgba(255, 107, 74, 0.1);
}
.interaction-details {
flex: 1;
}
.interaction-details h4 {
margin: 0 0 5px 0;
color: #ff4500;
font-size: 1em;
}
.dark-mode .interaction-details h4 {
color: #ff6b4a;
}
.interaction-details p {
margin: 0;
font-size: 0.9em;
line-height: 1.4;
opacity: 0.8;
}
.license-section {
flex-direction: column;
padding: 15px;
}
.license-content {
width: 100%;
}
.license-content p {
margin: 0 0 10px 0;
font-size: 0.9em;
line-height: 1.5;
}
.license-details {
margin-top: 15px;
}
.license-toggle {
background: none;
border: none;
color: #ff4500;
cursor: pointer;
padding: 0;
font-size: 0.9em;
text-decoration: underline;
}
.dark-mode .license-toggle {
color: #ff6b4a;
}
.full-license {
margin-top: 15px;
padding: 15px;
background: rgba(255, 69, 0, 0.1);
border-radius: 8px;
}
.dark-mode .full-license {
background: rgba(255, 107, 74, 0.1);
}
.full-license a {
color: #ff4500;
text-decoration: none;
}
.dark-mode .full-license a {
color: #ff6b4a;
}
.full-license a:hover {
text-decoration: underline;
}