Huge update.

This commit is contained in:
2025-06-11 03:31:59 +12:00
parent 699b829696
commit d1390bd770
14 changed files with 429 additions and 148 deletions

View File

@@ -19,6 +19,10 @@ registry = RoomRegistry()
def index(): def index():
return render_template("home.html") return render_template("home.html")
@app.route('/settings')
def settings():
return render_template("settings.html")
@app.route('/room/<room_code>') @app.route('/room/<room_code>')
def room(room_code): def room(room_code):
return render_template('room.html', room_code=room_code) return render_template('room.html', room_code=room_code)

View File

@@ -6,14 +6,33 @@
--text-highlight-color: white; --text-highlight-color: white;
} }
/* Navigation */ html, body {
height: 100%;
margin: 0px;
color: var(--text-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--primary-color);
}
body.dark {
--primary-color: #121212;
--secondary-color: rgb(7, 155, 152);
--tertiary-color: #1e1e1e;
--text-color: #e0e0e0;
--text-highlight-color: rgb(19, 18, 18);
}
html.transition, body.transition {
transition: background-color 0.3s ease, color 0.3s ease;
}
.navbar { .navbar {
position: fixed; position: fixed;
top:0px; top:0px;
left:0px; left:0px;
right:0px; right:0px;
background-color: var(--tertiary-color); background-color: var(--tertiary-color);
border-bottom: 1px solid var(--secondary-color); /* Changed color to lightsteelblue */ border-bottom: 1px solid var(--secondary-color);
overflow: hidden; overflow: hidden;
z-index: 1000; z-index: 1000;
} }
@@ -24,14 +43,14 @@
text-align: center; text-align: center;
padding: 8px 16px; padding: 8px 16px;
text-decoration: none; text-decoration: none;
transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; /* Only transition the background-color */ transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease;
color: var(--text-color); /* Set default text color */ color: var(--text-color);
} }
.navbar a:hover { .navbar a:hover {
background-color: var(--secondary-color); background-color: var(--secondary-color);
color: var(--text-highlight-color); /* Change text color on hover for better contrast */ color: var(--text-highlight-color);
padding: 8px 20px; /* Increase padding on hover */ padding: 8px 20px;
} }
.navname{ .navname{
@@ -43,19 +62,14 @@
background-color: var(--primary-color) !important; background-color: var(--primary-color) !important;
} }
/* Body */
body {
background-color: var(--primary-color);
margin: 0px;
color: var(--text-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Container */
.container { .container {
padding: 35px 10px; padding: 35px 10px 0px 10px;
margin: auto; margin: auto;
max-width: 800px; max-width: 800px;
min-height: calc(100% - 35px - 10px);
display: flex;
flex-direction: column;
height: 87vh;
} }
.room-join-box { .room-join-box {
@@ -102,12 +116,13 @@ body {
.chat-container { .chat-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 70vh; /* or whatever height works well for your layout */ flex-grow: 1;
min-height: 0; /* Allow it to shrink */
} }
#chat-box { #chat-box {
flex: 1; flex: 1 1 auto;
overflow-y: auto; overflow-y: auto;
padding: 10px; padding: 10px;
border: 1px solid var(--secondary-color); border: 1px solid var(--secondary-color);
@@ -122,9 +137,9 @@ body {
#chat-box p { #chat-box p {
margin: 0 0 5px 0; /* Adjust message spacing */ margin: 0 0 5px 0;
line-height: 1.4; line-height: 1.4;
word-wrap: break-word; /* Ensures long words break and wrap */ word-wrap: break-word;
} }
.chat-input-container { .chat-input-container {
@@ -142,7 +157,7 @@ body {
font-size: 16px; font-size: 16px;
outline: none; outline: none;
transition: padding 0.15s ease, border-color 0.15s ease; transition: padding 0.15s ease, border-color 0.15s ease;
flex: 1; /* Makes the input take up available horizontal space */ flex: 1;
} }
#send-button { #send-button {
@@ -154,7 +169,7 @@ body {
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease;
flex-shrink: 0; /* Prevents the button from shrinking */ flex-shrink: 0;
} }
#send-button:hover { #send-button:hover {
@@ -163,3 +178,45 @@ body {
padding-left: 18px; padding-left: 18px;
padding-right: 18px; padding-right: 18px;
} }
.setting-item {
padding: 15px;
}
.setting-label {
gap: 10px;
color: var(--text-color);
font-size: 16px;
cursor: pointer;
transition: color 0.3s ease;
}
.setting-toggle {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--secondary-color);
}
.setting-select {
padding: 8px 12px;
border: 1px solid var(--secondary-color);
border-radius: 5px;
background-color: var(--tertiary-color);
color: var(--text-color);
font-size: 16px;
outline: none;
transition: all 0.3s ease;
margin-top: 8px;
width: 100%;
max-width: 300px;
}
.setting-select:focus {
border-color: var(--secondary-color);
}
.no-transitions * {
transition: none !important;
animation: none !important;
}

View File

@@ -0,0 +1,67 @@
const STORAGE_VERSION = '11'; // Update this when changing storage structure
if (localStorage.getItem('version') !== STORAGE_VERSION) {
localStorage.clear();
localStorage.setItem('version', STORAGE_VERSION);
}
const savedSettingDarkPersist = localStorage.getItem('dark-mode-toggle');
if (savedSettingDarkPersist === 'true') {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
document.documentElement.classList.add('no-transitions');
setTimeout(function() {
document.documentElement.classList.remove('no-transitions');
}, 100);
function updateNavbarWithRooms() {
const navbar = document.querySelector('.navbar');
if (!navbar) return;
const linksToRemove = [];
navbar.querySelectorAll('a').forEach(link => {
if (link.href && link.href.includes('/room/')) {
linksToRemove.push(link);
}
});
linksToRemove.forEach(link => link.remove());
const allUsernames = JSON.parse(localStorage.getItem('usernames') || '{}');
const rooms = Object.keys(allUsernames);
rooms.forEach(roomCode => {
const roomLink = document.createElement('a');
roomLink.href = `/room/${roomCode}`;
roomLink.textContent = `Room: ${roomCode}`;
const removeBtn = document.createElement('span');
removeBtn.textContent = ' ×';
removeBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
removeRoom(roomCode);
};
roomLink.appendChild(removeBtn);
const settingsLink = navbar.querySelector('a[href="/settings"]');
if (settingsLink) {
settingsLink.before(roomLink);
} else {
navbar.appendChild(roomLink);
}
});
}
function removeRoom(roomCode) {
const allUsernames = JSON.parse(localStorage.getItem('usernames')) || '{}';
delete allUsernames[roomCode];
localStorage.setItem('usernames', JSON.stringify(allUsernames));
updateNavbarWithRooms();
}
document.addEventListener('DOMContentLoaded', updateNavbarWithRooms);

View File

@@ -0,0 +1,33 @@
let pendingUsername = null;
function getUsername() {
const username = document.getElementById('username-input').value.trim();
if (!username) {
alert("Please enter your name.");
throw new Error("Username required");
}
pendingUsername = username;
return username;
}
document.getElementById('create-button').addEventListener('click', async () => {
try {
const username = getUsername();
const res = await fetch('/api/room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await res.json();
if (data.code) {
const allUsernames = JSON.parse(localStorage.getItem('usernames') || '{}');
allUsernames[data.code] = pendingUsername;
localStorage.setItem('usernames', JSON.stringify(allUsernames));
window.location.href = `/room/${data.code}`;
} else {
alert("Failed to create room.");
}
} catch (_) {
}
});

147
resource/static/js/room.js Normal file
View File

@@ -0,0 +1,147 @@
const socket = io();
let originalTitle = document.title;
let notificationActive = false;
let notificationInterval = null;
// The 'room' variable is now directly available from the template,
// so you don't need to get it from the URL.
function getUsername(roomCode) {
const allUsernames = JSON.parse(localStorage.getItem('usernames') || '{}');
let storedUsername = allUsernames[roomCode];
// If a username exists and is not just whitespace, return it
if (storedUsername && storedUsername.trim()) {
return storedUsername;
}
// If no stored username, or it's empty, prompt the user
let newUsername = prompt("Please enter your name:");
// If the user cancels the prompt or enters only whitespace
if (newUsername === null || !newUsername.trim()) {
return null; // Indicates user didn't provide a valid name
}
newUsername = newUsername.trim();
// Store for future use
allUsernames[roomCode] = newUsername;
localStorage.setItem('usernames', JSON.stringify(allUsernames));
return newUsername;
}
// Use the 'room' variable provided by the template directly
const name = getUsername(room); // 'room' is now defined by your template script
if (!name) {
// If the user cancels or provides no name, redirect them
window.location.href = '/';
} else {
socket.emit('join', { room, sender: name });
loadMessages();
}
socket.on('message', (data) => {
const chatBox = document.getElementById('chat-box');
const msgElem = document.createElement('p');
msgElem.textContent = `${data.sender}: ${data.text}`;
if (data.sender === 'System') {
msgElem.style.fontStyle = 'italic';
msgElem.style.color = 'gray';
}
chatBox.appendChild(msgElem);
chatBox.scrollTop = chatBox.scrollHeight;
showNotification("Echo | New Message", `${data.sender}: ${data.text}`);
});
function showNotification(title, body) {
const notificationSetting = localStorage.getItem('notification-setting') || 'off';
if (notificationSetting === 'off') {
return;
}
if (notificationSetting === 'system') {
if (document.hasFocus()) return;
if (Notification.permission === 'granted') {
new Notification(title, { body });
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification(title, { body });
}
});
}
} else if (notificationSetting === 'tab') {
if (!document.hasFocus() && !notificationActive) {
originalTitle = document.title;
notificationInterval = setInterval(() => {
document.title = document.title === originalTitle
? "Echo | New Message!"
: originalTitle;
}, 1000);
notificationActive = true;
if (!window.hasNotificationListener) {
const returnToNormal = () => {
clearInterval(notificationInterval);
document.title = originalTitle;
notificationActive = false;
};
window.addEventListener('focus', returnToNormal);
window.hasNotificationListener = true;
}
}
}
}
document.getElementById('send-button').onclick = () => {
const text = document.getElementById('chat-input').value;
const allUsernames = JSON.parse(localStorage.getItem('usernames') || '{}');
const sender = allUsernames[room] || 'Anonymous';
if (text.trim()) {
socket.emit('message', {
room,
text,
sender
});
document.getElementById('chat-input').value = '';
}
};
document.getElementById('chat-input').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
document.getElementById('send-button').click();
}
});
function loadMessages() {
fetch(`/api/room/${room}/messages`)
.then(res => res.json())
.then(data => {
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML = '';
if (Array.isArray(data)) {
data.forEach(msg => {
const msgElem = document.createElement('p');
msgElem.textContent = `${msg.sender}: ${msg.text}`;
chatBox.appendChild(msgElem);
});
chatBox.scrollTop = chatBox.scrollHeight;
}
});
}
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
loadMessages();
}
});

View File

@@ -0,0 +1,32 @@
const darkToggle = document.getElementById('dark-mode-toggle');
const notificationDropDown = document.getElementById('notification-setting');
const savedSettingNotif = localStorage.getItem('notification-setting');
if (savedSettingNotif) {
notificationDropDown.value = savedSettingNotif;
}
const savedSettingDark = localStorage.getItem('dark-mode-toggle');
if (savedSettingDark === 'true') {
darkToggle.checked = true;
document.body.classList.add('dark');
} else {
darkToggle.checked = false;
document.body.classList.remove('dark');
}
darkToggle.addEventListener('change', () => {
if (darkToggle.checked) {
document.body.classList.add('transition');
document.body.classList.add('dark');
localStorage.setItem('dark-mode-toggle', 'true');
} else {
document.body.classList.add('transition');
document.body.classList.remove('dark');
localStorage.setItem('dark-mode-toggle', 'false');
}
});
notificationDropDown.addEventListener('change', () => {
localStorage.setItem('notification-setting', notificationDropDown.value);
});

View File

@@ -5,9 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
</head> </head>
<body> <body>
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
{% include 'navbar.html' %} {% include 'navbar.html' %}
<div class="container"> <div class="container">

View File

@@ -1,63 +1,27 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Home{% endblock %} {% block title %}Echo | Home{% endblock %}
{% block content %} {% block content %}
<h1>Hello, hello, hello...</h1> <h1>Hello, hello, hello...</h1>
<p>Yeah, it's early.</p> <p>Yeah, it's early.</p>
<h3>Firstly, name:</h3>
<div class="room-join-box"> <div class="room-join-box">
<input type="text" id="username-input" class="room-input" placeholder="Your name"> <input type="text" id="username-input" class="room-input" placeholder="Your name">
</div> </div>
<h3>Join an Existing Room?</h3>
<div class="room-join-box"> <div class="room-join-box">
<input type="text" id="room-code-input" class="room-input" placeholder="Enter Room Code" maxlength="10"> <input type="text" id="room-code-input" class="room-input" placeholder="Enter Room Code" maxlength="10">
<button class="room-button" id="join-button">Join</button> <button class="room-button" id="join-button">Join</button>
</div> </div>
<h3>Create a New Room?</h3>
<div class="room-join-box"> <div class="room-join-box">
<button class="room-button" id="create-button">Create New Room</button> <button class="room-button" id="create-button">Create New Room</button>
</div> </div>
<script>
function getUsername() {
const username = document.getElementById('username-input').value.trim();
if (!username) {
alert("Please enter your name.");
throw new Error("Username required");
}
localStorage.setItem('username', username);
return username;
}
document.getElementById('join-button').addEventListener('click', () => { <script src="{{ url_for('static', filename='js/home.js') }}"></script>
try {
getUsername();
const code = document.getElementById('room-code-input').value.trim();
if (code) {
window.location.href = `/room/${code}`;
} else {
alert("Enter a room code.");
}
} catch (_) {}
});
document.getElementById('create-button').addEventListener('click', async () => {
try {
getUsername();
const res = await fetch('/api/room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await res.json();
if (data.code) {
window.location.href = `/room/${data.code}`;
} else {
alert("Failed to create room.");
}
} catch (_) {}
});
</script>
</script>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,6 @@
<div class="navbar"> <div class="navbar">
<a class="navname">Echo</a> <a class="navname">Echo</a>
<a href="/">Home</a> <a href="/">Home</a>
<a href="/settings">Settings</a>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% extends 'room_base.html' %} {% extends 'room_base.html' %}
{% block title %}Room {{ room_code }}{% endblock %} {% block title %}Echo | Room {{ room_code }}{% endblock %}
{% block content %} {% block content %}
<h1>Room {{ room_code }}</h1> <h1>Room {{ room_code }}</h1>
<div class="chat-container"> <div class="chat-container">
@@ -11,63 +11,8 @@
</div> </div>
<script> <script>
const socket = io();
const room = "{{ room_code }}"; const room = "{{ room_code }}";
const name = localStorage.getItem('username') || 'Anonymous';
socket.emit('join', { room, sender: name });
// Fetch message history
fetch(`/api/room/${room}/messages`)
.then(res => res.json())
.then(data => {
const chatBox = document.getElementById('chat-box');
if (data && Array.isArray(data)) {
data.forEach(msg => {
const msgElem = document.createElement('p');
msgElem.textContent = `${msg.sender}: ${msg.text}`;
chatBox.appendChild(msgElem);
});
chatBox.scrollTop = chatBox.scrollHeight;
}
});
// Message listener
socket.on('message', (data) => {
const chatBox = document.getElementById('chat-box');
const msgElem = document.createElement('p');
msgElem.textContent = `${data.sender}: ${data.text}`;
if (data.sender === 'System') {
msgElem.style.fontStyle = 'italic';
msgElem.style.color = 'gray';
}
chatBox.appendChild(msgElem);
chatBox.scrollTop = chatBox.scrollHeight;
});
// Send message on button click
document.getElementById('send-button').onclick = () => {
const text = document.getElementById('chat-input').value;
const sender = localStorage.getItem('username') || 'Anonymous';
if (text.trim()) {
socket.emit('message', {
room,
text,
sender
});
document.getElementById('chat-input').value = '';
}
};
// Send message on Enter key press
document.getElementById('chat-input').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
document.getElementById('send-button').click();
}
});
</script> </script>
<script src="{{ url_for('static', filename='js/room.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -6,15 +6,20 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
</head> </head>
<body> <body>
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
{% include 'navbar.html' %} {% include 'navbar.html' %}
<div class="container"> <div class="container">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block title %}Echo | Settings{% endblock %}
{% block content %}
<h1>Settings</h1>
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" id="dark-mode-toggle" class="setting-toggle">
<span><strong>(EXPERIMENTAL)</strong> Enable Dark Mode</span>
</label>
</div>
<div class="setting-item">
<label for="notification-setting" class="setting-label">Enable Notifications:</label>
<select id="notification-setting" class="setting-select">
<option value="off">Off</option>
<option value="system">System Notifications</option>
<option value="tab">Tab Indicator</option>
</select>
</div>
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
{% endblock %}

1
run.sh
View File

@@ -1 +1,2 @@
source venv/bin/activate
gunicorn -k eventlet -w 1 -b 0.0.0.0:8997 main:app gunicorn -k eventlet -w 1 -b 0.0.0.0:8997 main:app