Compare commits
4 commits
fd88979c32
...
bbf662fb6d
Author | SHA1 | Date | |
---|---|---|---|
bbf662fb6d | |||
5240aec346 | |||
1282bf2bda | |||
ad05afbb83 |
7 changed files with 996 additions and 268 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
docker-compose.yml
|
||||
docker-compose.override.yml
|
||||
tailscale
|
||||
tailscale-config
|
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
81
content.js
81
content.js
|
@ -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
33
docker-compose.yml
Normal 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
|
||||
|
348
index.html
348
index.html
|
@ -1,73 +1,197 @@
|
|||
<!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">
|
||||
<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"/>
|
||||
<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="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">×</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;">×</span>
|
||||
<span class="close-button" style="display: none">×</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>
|
||||
<button id="warningCloseBtn" class="warning-close-btn" disabled>
|
||||
I Understand
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -82,12 +206,12 @@
|
|||
|
||||
<script>
|
||||
if (performance.navigation.type === performance.navigation.TYPE_RELOAD) {
|
||||
if (sessionStorage.getItem('hasVisited')) {
|
||||
if (sessionStorage.getItem("hasVisited")) {
|
||||
window.stop();
|
||||
requestAnimationFrame(() => fumble());
|
||||
}
|
||||
} else if (!sessionStorage.getItem('hasVisited')) {
|
||||
sessionStorage.setItem('hasVisited', 'true');
|
||||
} else if (!sessionStorage.getItem("hasVisited")) {
|
||||
sessionStorage.setItem("hasVisited", "true");
|
||||
}
|
||||
|
||||
let lastFumbleTime = 0;
|
||||
|
@ -100,10 +224,10 @@
|
|||
|
||||
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');
|
||||
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
|
||||
|
@ -115,7 +239,7 @@
|
|||
remainingTime--;
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
cooldownSeconds.textContent = '0';
|
||||
cooldownSeconds.textContent = "0";
|
||||
closeBtn.disabled = false;
|
||||
clearInterval(cooldownTimer);
|
||||
} else {
|
||||
|
@ -127,19 +251,21 @@
|
|||
}
|
||||
|
||||
lastFumbleTime = currentTime;
|
||||
const frame = document.getElementById('contentFrame');
|
||||
frame.src = 'https://wiby.me/surprise/';
|
||||
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')) {
|
||||
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;
|
||||
|
@ -162,63 +288,91 @@
|
|||
};
|
||||
};
|
||||
|
||||
document.getElementById('fumbleButton').addEventListener('click', fumble);
|
||||
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
|
||||
// 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 = '☀️';
|
||||
}
|
||||
|
||||
darkModeToggle.addEventListener('click', () => {
|
||||
body.classList.toggle('dark-mode');
|
||||
const isDark = body.classList.contains('dark-mode');
|
||||
darkModeToggle.textContent = isDark ? '☀️' : '🌙';
|
||||
localStorage.setItem('darkMode', isDark);
|
||||
});
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
// 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');
|
||||
|
||||
helpButton.addEventListener('click', () => {
|
||||
modal.classList.add('show');
|
||||
const closeSettingsPanel = () => {
|
||||
settingsPanel.classList.remove('show');
|
||||
};
|
||||
|
||||
settingsButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
settingsPanel.classList.add('show');
|
||||
});
|
||||
|
||||
closeButton.addEventListener('click', () => {
|
||||
modal.classList.remove('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');
|
||||
const warningModal = document.getElementById("warningModal");
|
||||
const warningCloseBtn = document.getElementById("warningCloseBtn");
|
||||
|
||||
warningCloseBtn.addEventListener('click', () => {
|
||||
warningCloseBtn.addEventListener("click", () => {
|
||||
if (!warningCloseBtn.disabled) {
|
||||
warningModal.classList.remove('show');
|
||||
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');
|
||||
window.addEventListener("click", (event) => {
|
||||
if (event.target === warningModal) {
|
||||
// Only for warning modal
|
||||
warningModal.classList.remove("show");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -241,13 +395,14 @@
|
|||
const deltaY = Math.abs(current.y - lastY);
|
||||
const deltaZ = Math.abs(current.z - lastZ);
|
||||
|
||||
if (((deltaX > shakeThreshold && deltaY > shakeThreshold) ||
|
||||
if (
|
||||
((deltaX > shakeThreshold && deltaY > shakeThreshold) ||
|
||||
(deltaX > shakeThreshold && deltaZ > shakeThreshold) ||
|
||||
(deltaY > shakeThreshold && deltaZ > shakeThreshold)) &&
|
||||
(currentTime - lastShake > shakeTimeout)) {
|
||||
|
||||
currentTime - lastShake > shakeTimeout
|
||||
) {
|
||||
// Vibrate if available
|
||||
if ('vibrate' in navigator) {
|
||||
if ("vibrate" in navigator) {
|
||||
navigator.vibrate(200);
|
||||
}
|
||||
|
||||
|
@ -264,39 +419,52 @@
|
|||
|
||||
// Request permission and start shake detection on mobile
|
||||
function initShakeDetection() {
|
||||
if (typeof DeviceMotionEvent.requestPermission === 'function') {
|
||||
if (typeof DeviceMotionEvent.requestPermission === "function") {
|
||||
// iOS 13+ requires permission
|
||||
DeviceMotionEvent.requestPermission()
|
||||
.then(permissionState => {
|
||||
if (permissionState === 'granted') {
|
||||
window.addEventListener('devicemotion', handleMotion);
|
||||
.then((permissionState) => {
|
||||
if (permissionState === "granted") {
|
||||
window.addEventListener("devicemotion", handleMotion);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
// Non iOS 13+ devices
|
||||
window.addEventListener('devicemotion', handleMotion);
|
||||
window.addEventListener("devicemotion", handleMotion);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize shake detection when page loads
|
||||
if ('DeviceMotionEvent' in window) {
|
||||
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');
|
||||
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);
|
||||
const permissionButton = document.createElement("button");
|
||||
permissionButton.textContent = "Enable Shake to Fumble";
|
||||
permissionButton.className = "permission-button";
|
||||
permissionButton.addEventListener("click", initShakeDetection);
|
||||
|
||||
modalContent.appendChild(permissionButton);
|
||||
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>
|
||||
|
||||
|
|
35
nginx.conf
Normal file
35
nginx.conf
Normal 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";
|
||||
}
|
||||
}
|
402
styles.css
402
styles.css
|
@ -100,6 +100,7 @@ header {
|
|||
main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#contentFrame {
|
||||
|
@ -508,3 +509,404 @@ 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;
|
||||
}
|
Loading…
Reference in a new issue