diff --git a/main.py b/main.py index 974cd2d..dcda38d 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,10 @@ registry = RoomRegistry() def index(): return render_template("home.html") +@app.route('/settings') +def settings(): + return render_template("settings.html") + @app.route('/room/') def room(room_code): return render_template('room.html', room_code=room_code) diff --git a/resource/static/css/styles.css b/resource/static/css/styles.css index 6673d6a..98ac5cc 100644 --- a/resource/static/css/styles.css +++ b/resource/static/css/styles.css @@ -6,14 +6,33 @@ --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 { position: fixed; top:0px; left:0px; right:0px; 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; z-index: 1000; } @@ -24,14 +43,14 @@ text-align: center; padding: 8px 16px; text-decoration: none; - transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; /* Only transition the background-color */ - color: var(--text-color); /* Set default text color */ + transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; + color: var(--text-color); } .navbar a:hover { background-color: var(--secondary-color); - color: var(--text-highlight-color); /* Change text color on hover for better contrast */ - padding: 8px 20px; /* Increase padding on hover */ + color: var(--text-highlight-color); + padding: 8px 20px; } .navname{ @@ -43,19 +62,14 @@ 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 { - padding: 35px 10px; + padding: 35px 10px 0px 10px; margin: auto; max-width: 800px; + min-height: calc(100% - 35px - 10px); + display: flex; + flex-direction: column; + height: 87vh; } .room-join-box { @@ -102,12 +116,13 @@ body { .chat-container { display: flex; 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 { - flex: 1; + flex: 1 1 auto; overflow-y: auto; padding: 10px; border: 1px solid var(--secondary-color); @@ -122,44 +137,86 @@ body { #chat-box p { - margin: 0 0 5px 0; /* Adjust message spacing */ - line-height: 1.4; - word-wrap: break-word; /* Ensures long words break and wrap */ + margin: 0 0 5px 0; + line-height: 1.4; + word-wrap: break-word; } .chat-input-container { - display: flex; - width: 100%; - gap: 10px; + display: flex; + width: 100%; + gap: 10px; } #chat-input { - 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: padding 0.15s ease, border-color 0.15s ease; - flex: 1; /* Makes the input take up available horizontal space */ + 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: padding 0.15s ease, border-color 0.15s ease; + flex: 1; } #send-button { - padding: 8px 16px; - background-color: var(--tertiary-color); - border: 1px solid var(--secondary-color); - border-radius: 5px; - color: var(--text-color); - font-size: 16px; - cursor: pointer; - transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; - flex-shrink: 0; /* Prevents the button from shrinking */ + padding: 8px 16px; + background-color: var(--tertiary-color); + border: 1px solid var(--secondary-color); + border-radius: 5px; + color: var(--text-color); + font-size: 16px; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; + flex-shrink: 0; } #send-button:hover { - background-color: var(--secondary-color); - color: var(--text-highlight-color); - padding-left: 18px; - padding-right: 18px; + background-color: var(--secondary-color); + color: var(--text-highlight-color); + padding-left: 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; } \ No newline at end of file diff --git a/resource/static/js/base.js b/resource/static/js/base.js new file mode 100644 index 0000000..91a283b --- /dev/null +++ b/resource/static/js/base.js @@ -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); \ No newline at end of file diff --git a/resource/static/js/home.js b/resource/static/js/home.js new file mode 100644 index 0000000..372289c --- /dev/null +++ b/resource/static/js/home.js @@ -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 (_) { + } +}); \ No newline at end of file diff --git a/resource/static/js/room.js b/resource/static/js/room.js new file mode 100644 index 0000000..5b12e0d --- /dev/null +++ b/resource/static/js/room.js @@ -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(); + } +}); diff --git a/resource/static/js/settings.js b/resource/static/js/settings.js new file mode 100644 index 0000000..0c3bf06 --- /dev/null +++ b/resource/static/js/settings.js @@ -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); +}); diff --git a/resource/template/base.html b/resource/template/base.html index e041998..6cc36d7 100644 --- a/resource/template/base.html +++ b/resource/template/base.html @@ -5,9 +5,11 @@ + {% block title %}{% endblock %} + {% include 'navbar.html' %}
diff --git a/resource/template/home.html b/resource/template/home.html index 095ab74..ebc6e09 100644 --- a/resource/template/home.html +++ b/resource/template/home.html @@ -1,63 +1,27 @@ {% extends 'base.html' %} -{% block title %}Home{% endblock %} +{% block title %}Echo | Home{% endblock %} {% block content %}

Hello, hello, hello...

Yeah, it's early.

+ +

Firstly, name:

+

Join an Existing Room?

+ +

Create a New Room?

- - - - -{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/resource/template/navbar.html b/resource/template/navbar.html index 82cec73..ddba690 100644 --- a/resource/template/navbar.html +++ b/resource/template/navbar.html @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/resource/template/options.html b/resource/template/options.html deleted file mode 100644 index e69de29..0000000 diff --git a/resource/template/room.html b/resource/template/room.html index ce1ba27..72ca7e5 100644 --- a/resource/template/room.html +++ b/resource/template/room.html @@ -1,5 +1,5 @@ {% extends 'room_base.html' %} -{% block title %}Room {{ room_code }}{% endblock %} +{% block title %}Echo | Room {{ room_code }}{% endblock %} {% block content %}

Room {{ room_code }}

@@ -11,63 +11,8 @@
+ + {% endblock %} diff --git a/resource/template/room_base.html b/resource/template/room_base.html index 1892e81..3986e7d 100644 --- a/resource/template/room_base.html +++ b/resource/template/room_base.html @@ -6,15 +6,20 @@ + + + {% block title %}{% endblock %} + {% include 'navbar.html' %}
{% block content %} {% endblock %}
+ diff --git a/resource/template/settings.html b/resource/template/settings.html new file mode 100644 index 0000000..4cfe05f --- /dev/null +++ b/resource/template/settings.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block title %}Echo | Settings{% endblock %} +{% block content %} +

Settings

+ +
+ +
+ +
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/run.sh b/run.sh index 4da9661..31ab521 100755 --- a/run.sh +++ b/run.sh @@ -1 +1,2 @@ +source venv/bin/activate gunicorn -k eventlet -w 1 -b 0.0.0.0:8997 main:app \ No newline at end of file