Ver Fonte

Merge branch 'development'

deadtom há 1 semana atrás
pai
commit
f81217f17c

+ 6 - 0
README.md

@@ -37,6 +37,8 @@ A chatbot for Owncast, where viewers earn points which can be redeemed for strea
  * A queue manager, where the streamer can mark rewards fulfilled as they go, or refund rewards if needed.
 * Kofi integration.
  * Your viewers can get points for donating via Kofi, and as part of their monthly Kofi membership.
+* To-do list. Add tasks and cross them off as you complete them.
+ * Overlay to display the list on your stream.
 
 ## Screenshots
 #### Votes and Goals Overlays
@@ -179,6 +181,10 @@ Goals ```http://localhost:8081/goals```
 * Recommended height: 210
 * In OBS, check "Refresh browser source when scene becomes active"
 
+To-do list ```http://localhost:8081/todo```
+* Recommended width: 225
+* Recommended height: 350
+
 ## Support
 For support (a bug, feature request/tweak, question or comment), you can contact DeadTOm via the following methods:
 * [Mastodon](https://dice.camp/@deadtom)

+ 1 - 0
TODO.md

@@ -1,3 +1,4 @@
+* Customize CSS for /todo overlay
 * Add celebration route for OBS overlay, for things like new followers and goals/milestones
  * Add customizable celebration emojis for this
 * Add chat notification when a reward is fulfilled

+ 2 - 0
ownchatbot/__init__.py

@@ -29,6 +29,8 @@ def create_app(test_config=None):
     app.config.from_pyfile('kofi.py', silent=True)
     app.config.from_object('ownchatbot.defaults.announce')
     app.config.from_pyfile('announce.py', silent=True)
+    app.config.from_object('ownchatbot.defaults.todo')
+    app.config.from_pyfile('todo.py', silent=True)
 
     if app.config['GUNICORN']:  # Gunicorn logging integration
         gunicorn_logger = logging.getLogger('gunicorn.error')

+ 14 - 0
ownchatbot/defaults/todo.py

@@ -0,0 +1,14 @@
+LIST = [
+    {
+        "name": "This thing",
+        "crossed": "no"
+    },
+    {
+        "name": "That thing",
+        "crossed": "no"
+    },
+    {
+        "name": "That other thing",
+        "crossed": "yes"
+    },
+]

+ 13 - 5
ownchatbot/static/mgmtpanel.js

@@ -17,10 +17,6 @@ function openTab(event, tabName) {
     localStorage.setItem("activeTab", tabName);
 }
 
-function refreshPage() {
-    window.location.reload();
-}
-
 window.onload = function() {
     var activeTab = localStorage.getItem("activeTab");
     if (activeTab) {
@@ -39,4 +35,16 @@ window.onload = function() {
             defaultTabLink.click();
         }
     }
-};
+}
+
+function refreshPage() {
+    window.location.reload();
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    const urlParams = new URLSearchParams(window.location.search);
+    const activeTab = urlParams.get('activeTab');
+    if (activeTab) {
+        openTab(event, activeTab);
+    }
+});

+ 33 - 0
ownchatbot/templates/list.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="refresh" content="10">
+    <title>Today's to-do List</title>
+    <style>
+        {{ css }}
+    </style>
+    <script>
+      function refreshPage() {
+          window.location.reload();
+      }
+
+      setTimeout(refreshPage, 30 * 1000);
+    </script>
+</head>
+<body>
+    {% if items %}
+        <h4>Today's to-do List</h4>
+        <ul>
+            {% for item in items %}
+                <li style="text-decoration: {{ 'line-through' if item.crossed == 'yes' else 'none' }};">
+                    {{ item.name }}
+                </li>
+            {% endfor %}
+        </ul>
+    {% else %}
+    	<h4>Nothing on the to-do list yet</h4>
+    {% endif %}
+</body>
+</html>

+ 42 - 0
ownchatbot/templates/mgmt.html

@@ -14,6 +14,7 @@
             <button class="tablinks" data-tab="categories" onclick="openTab(event, 'categories')">Categories</button>
             <button class="tablinks" data-tab="accounts" onclick="openTab(event, 'accounts')">Manage Accounts</button>
             <button class="tablinks" data-tab="announcements" onclick="openTab(event, 'announcements')">Announcements</button>
+            <button class="tablinks" data-tab="todolist" onclick="openTab(event, 'todolist')">To-Do List</button>
             <button class="tablinks" data-tab="settings" onclick="openTab(event, 'settings')">Settings</button>
             {% if kofi_integration %}
                 <button class="tablinks" data-tab="kofi-settings" onclick="openTab(event, 'kofi-settings')">Kofi Settings</button>
@@ -419,6 +420,47 @@
 	    <i>Kofi subscription tier support coming soon.</i>
         </body>
     </div>
+
+    <div id='todolist' class="tabcontent">
+        <script>
+            function focusInput() {  // Function to focus on the input field
+                document.getElementById('itemInput').focus();
+            }
+        </script>
+        <body onload="focusInput()">
+        <h1>To-Do List</h1>
+        <form id="todo-item-form" method="POST" onsubmit="focusInput()" action="/mgmt/addtodoitem">
+            <input type="text" id="itemInput" name="item" placeholder="Add a new item">
+            <button id="todo-item-form" class="button button2" type="submit">Add</button>
+        </form>
+        <ul>
+            {% if items %}
+                {% for item in items %}
+                    {% if item.crossed == 'no' %}
+                        <li style="text-decoration:none;">
+                            {{ item.name }}
+                            <a href="{{ url_for('web_panels.cross', item_id=loop.index0) }}">[Cross Off]</a>
+                        </li>
+                    {% else %}
+                        <li> <span style="text-decoration:line-through;">
+                            {{ item.name }}</span>
+                            <a href="{{ url_for('web_panels.uncross', item_id=loop.index0) }}">[Un-Cross]</a>
+                        </li>
+                    {% endif %}
+                {% endfor %}
+            {% endif %}
+        </ul>
+        <form action="/mgmt/clearlist" method="get" style="display: inline;">
+            <button class="button button2" type="submit" class="button">Clear List</button>
+        </form>
+        <br>
+        
+        <h1>Custom CSS for your list overlay</h1>
+        <form id="todo-css-form" method="POST" onsubmit="focusInput()" action="/mgmt/addtodocss">
+            <textarea id="cssInput" name="todo_css" rows="20" cols="80">{{ todo_css }}</textarea>
+            <button id="todo-css-form" class="button button2" type="submit">Save</button>
+        </form>
+    </div>
     
     
 </html>

+ 3 - 2
ownchatbot/templates/userpanel.html

@@ -11,7 +11,7 @@
         <div class="tab">
             <button class="tablinks" data-tab="ocbinfo" onclick="openTab(event, 'ocbinfo')">OwnchatBot Info</button>
             <button class="tablinks" data-tab="rewards" onclick="openTab(event, 'rewards')">Points and Rewards</button>
-            <button class="tablinks" data-tab="queue" onclick="openTab(event, 'queue')">Redeems Queue</button>
+            <button class="tablinks" data-tab="queue" onclick="openTab(event, 'queue')">Rewards Queue</button>
         </div>
         <img src="/static/img/ownchatbotwide.png">
     </div>
@@ -203,7 +203,7 @@
             {% endif %}
         </body>
         <body>
-            <h3>Active Redeems</h3>
+            <h3>Active Rewards</h3>
             {% if rewards %}
             <table>
                 <thead>
@@ -263,6 +263,7 @@
                 </thead>
                 <tbody>
                 {% for row in queue %}
+                    {{ row[2] }}
                     {% if not row[4] %}
                     <tr>
                         <td>{{ row[1].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>

+ 31 - 0
ownchatbot/user_handlers.py

@@ -3,6 +3,37 @@ from sqlite3 import Error
 from re import sub
 import random
 import time
+import json
+import os
+
+
+def save_todocss(todo_css):  # Save todo list custom CSS
+    todo_list = current_app.config['LIST']
+    list_file = os.path.join(current_app.instance_path, 'todo.py')
+    try:
+        with open(list_file, 'w') as f:
+            f.write(f'LIST = {todo_list}\n\nCSS = \"\"\"{todo_css}\"\"\"')
+        f.close
+        current_app.config.from_pyfile('todo.py', silent=True)  # Reread the list into the app
+        return True
+    except Exception as stdcerror:
+        current_app.logger.error(f'Couldn\'t save todo.py: {stdcerror.args[0]}')
+        return False
+
+
+def save_todolist(list_items):  # Save todo list items
+    new_list = json.dumps(list_items, indent=4)
+    todo_css = current_app.config['CSS']
+    list_file = os.path.join(current_app.instance_path, 'todo.py')
+    try:
+        with open(list_file, 'w') as f:
+            f.write(f'LIST = {new_list}\n\nCSS = \"\"\"{todo_css}\"\"\"')
+        f.close
+        current_app.config.from_pyfile('todo.py', silent=True)  # Reread the list into the app
+        return True
+    except Exception as stdlerror:
+        current_app.logger.error(f'Couldn\'t save todo.py: {stdlerror.args[0]}')
+        return False
 
 
 def get_users_points(db, user_id):  # Look up one user's points by user id

+ 84 - 13
ownchatbot/web_panels.py

@@ -2,7 +2,7 @@ from flask import flash, render_template, Blueprint, current_app, redirect, requ
 from datetime import timezone
 from ownchatbot.db import get_db, reread_goals, reread_votes, rem_vote, reset_vote, reset_goal, clear_fulfilled_rewards, clear_reward_queue, rem_cool, rem_from_queue
 from ownchatbot.reward_handlers import all_active_votes, all_active_goals, all_active_rewards, get_queue, fulfill_reward, save_rewards, activate_category, deactivate_category, refund_reward, reread_categories, save_config
-from ownchatbot.user_handlers import get_all_users, get_all_users_by_name, refund_points, adjust_points, change_email, get_email_code, del_email_code
+from ownchatbot.user_handlers import get_all_users, get_all_users_by_name, refund_points, adjust_points, change_email, get_email_code, del_email_code, save_todolist, save_todocss
 from ownchatbot.bot_messages import save_announce
 from ownchatbot.owncast_com import send_private_chat
 import json
@@ -103,6 +103,9 @@ def mgmt():
     announce_enable = current_app.config['ANNOUNCE_ENABLE']
     announce_interval = current_app.config['ANNOUNCE_INTERVAL']
     announcements = current_app.config['ANNOUNCEMENTS']
+    todolist_items = current_app.config['LIST']
+    todo_css = current_app.config['CSS']
+    active_tab = request.args.get('activeTab')
     settings_info = [
         access_id,
         points_interval,
@@ -116,7 +119,7 @@ def mgmt():
         announce_enable,
         announce_interval
         ]
-    
+
     return render_template('mgmt.html',
                            queue=get_queue(db),
                            votes=all_active_votes(db),
@@ -132,7 +135,10 @@ def mgmt():
                            utc_timezone=utc_timezone,
                            active_categories=active_categories,
                            inactive_categories=inactive_categories,
-                           settings_info=settings_info)
+                           settings_info=settings_info,
+                           items=todolist_items,
+                           todo_css=todo_css,
+                           activeTab=active_tab)
 
 
 @ocb.route('/mgmt_queue', methods=['GET'])  # The streamer's management panel
@@ -231,7 +237,7 @@ def edit_account(user_id):
                     current_app.logger.info(f'Removed {name}\'s email')
                 else:
                     current_app.logger.info(f'Changed {name}\'s email to {newemail}')
-        return redirect(url_for('web_panels.mgmt'))
+        return redirect(url_for('web_panels.mgmt', activeTab='accounts'))
 
     return render_template('edit_account.html',
                            name=name,
@@ -251,7 +257,7 @@ def delete(reward_name):
             if reread_votes():
                 if reread_goals():
                     pass
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='managerewards'))
 
 
 @ocb.route('/mgmt/edit/<reward_name>', methods=['GET', 'POST'])
@@ -300,7 +306,7 @@ def edit(reward_name):
             reread_goals()
         if reward_data['type'] == 'vote':
             reread_votes()
-        return redirect(url_for('web_panels.mgmt'))
+        return redirect(url_for('web_panels.mgmt', activeTab='managerewards'))
 
     return render_template('edit.html',
                            all_cats=all_cats,
@@ -312,6 +318,7 @@ def edit(reward_name):
 @ocb.route('/mgmt/settings', methods=['GET', 'POST'])  # OwnchatBot settings panel
 @requires_login
 def settings():
+    todolist_items = current_app.config['LIST']
     points_interval = int(request.form['points_interval'])
     points_award = int(request.form['points_award'])
     gunicorn_logging = 'gunicorn_logging' in request.form
@@ -335,7 +342,7 @@ def settings():
     if save_config(config_dict):  # Save new config.py
         current_app.logger.info('Saved new config.')
 
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='settings'))
 
 
 @ocb.route('/mgmt/announcements', methods=['GET', 'POST'])  # OwnchatBot settings panel
@@ -353,7 +360,7 @@ def announcements():
     if save_announce(announce_dict):  # Save new announce.py
         current_app.logger.info('Saved new announcements.')
 
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='announcements'))
 
 
 @ocb.route('/mgmt/ksettings', methods=['GET', 'POST'])  # OwnchatBot settings panel
@@ -376,7 +383,7 @@ def ksettings():
         if save_kofi_settings(kofi_settings_dict):
             current_app.logger.info(f'Saved Kofi settings')
 
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='kofi-settings'))
 
 
 @ocb.route('/mgmt/add/<reward_type>', methods=['GET', 'POST'])
@@ -459,7 +466,7 @@ def add(reward_type):
             inactive_categories = current_app.config['INACTIVE_CAT']
             inactive_categories.append(name)  # Add it to the INACTIVE_CAT variable
             reread_categories()  # Write it to categories.py
-        return redirect(url_for('web_panels.mgmt'))
+        return redirect(url_for('web_panels.mgmt', activeTab='managerewards'))
 
     return render_template('add.html',
                            all_cats=all_cats,
@@ -491,13 +498,13 @@ def set_viewer_email():
 @ocb.route('/mgmt/activate/<category>', methods=['GET', 'POST'])
 def activate(category):
     activate_category(category)
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='categories'))
 
 
 @ocb.route('/mgmt/deactivate/<category>', methods=['GET', 'POST'])
 def deactivate(category):
     deactivate_category(category)
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='categories'))
 
 
 @ocb.route('/mgmt/delcat/<cat_name>/<cat_act>', methods=['GET', 'POST'])
@@ -514,7 +521,7 @@ def delcat(cat_name, cat_act):
         if cat_name in details['categories']:
             details['categories'].remove(cat_name)
     save_rewards(current_rewards)
-    return redirect(url_for('web_panels.mgmt'))
+    return redirect(url_for('web_panels.mgmt', activeTab='categories'))
 
 
 @ocb.route('/mgmt/reset/<reward_name>/<reward_type>', methods=['GET', 'POST'])  # Reset votes and goals to zero
@@ -544,6 +551,63 @@ def clear_queue():
     return redirect(url_for('web_panels.mgmtqueue'))
 
 
+@ocb.route('/mgmt/addtodocss', methods=['POST'])
+def add_todo_css():
+    if request.method == 'POST':
+        new_css = request.form.get('todo_css')
+        if not new_css:
+            new_css = """body {
+    background-color: transparent;
+    color: white;
+}"""
+        if save_todocss(new_css):  # Save todo list
+            current_app.logger.info('Saved to-do CSS.')
+        return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+    return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+
+
+@ocb.route('/mgmt/addtodoitem', methods=['POST'])
+def add_todo_item():
+    if request.method == 'POST':
+        todolist_items = current_app.config['LIST']
+        item = request.form.get('item')
+        if item:
+            todolist_items.append({'name': item, 'crossed': 'no'})
+            if save_todolist(todolist_items):  # Save todo list
+                current_app.logger.info('Saved to-do list.')
+        return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+    return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+
+
+@ocb.route('/mgmt/cross/<int:item_id>')
+def cross(item_id):
+    todolist_items = current_app.config['LIST']
+    if 0 <= item_id < len(todolist_items):
+        todolist_items[item_id]['crossed'] = 'yes'
+        if save_todolist(todolist_items):  # Save todo list
+            current_app.logger.info('Saved to-do list.')
+    return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+
+
+@ocb.route('/mgmt/uncross/<int:item_id>')
+def uncross(item_id):
+    todolist_items = current_app.config['LIST']
+    if 0 <= item_id < len(todolist_items):
+        todolist_items[item_id]['crossed'] = 'no'
+        if save_todolist(todolist_items):  # Save todo list
+            current_app.logger.info('Saved to-do list.')
+    return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+
+
+@ocb.route('/mgmt/clearlist')
+def clear_list():
+    todolist_items = current_app.config['LIST']
+    todolist_items = []  # Clear the list
+    if save_todolist(todolist_items):  # Save todo list
+        current_app.logger.info('Saved to-do list.')
+    return redirect(url_for('web_panels.mgmt', activeTab='todolist'))
+
+
 @ocb.route('/goals', methods=['GET'])  # Route for goals overlay
 def goals():
     db = get_db()
@@ -557,3 +621,10 @@ def votes():
     db = get_db()
     return render_template('votes.html',
                            votes=all_active_votes(db))
+
+
+@ocb.route('/todo')
+def todo():
+    todolist_items = current_app.config['LIST']
+    todolist_css = current_app.config['CSS']
+    return render_template('list.html', items=todolist_items, css=todolist_css)