This commit is contained in:
Karl 2025-05-09 16:31:14 +01:00
parent a77c858634
commit 19b9f7fd28
15 changed files with 932 additions and 932 deletions

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
venv/ venv/
config.py config.py
**/*.pyc **/*.pyc

32
.vscode/launch.json vendored
View File

@ -1,17 +1,17 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python Debugger: Current File", "name": "Python Debugger: Current File",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${file}", "program": "${file}",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": false, "justMyCode": false,
"args": ["--host=0.0.0.0"] "args": ["--host=0.0.0.0"]
} }
] ]
} }

372
app.py
View File

@ -1,186 +1,186 @@
# app.py # app.py
from flask import Flask, render_template, request, redirect, url_for, session, send_file, jsonify from flask import Flask, render_template, request, redirect, url_for, session, send_file, jsonify
from flask_caching import Cache from flask_caching import Cache
import requests.auth import requests.auth
import os import os
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
from lib.reqs import get_urls, get_user_accounts, add_user_account, delete_user_account, get_user_accounts_count from lib.reqs import get_urls, get_user_accounts, add_user_account, delete_user_account, get_user_accounts_count
from flask import send_from_directory from flask import send_from_directory
import requests import requests
import base64 import base64
from flask import Flask from flask import Flask
from config import DevelopmentConfig from config import DevelopmentConfig
from paddleocr import PaddleOCR from paddleocr import PaddleOCR
from PIL import Image from PIL import Image
import numpy as np import numpy as np
os.environ["OMP_NUM_THREADS"] = "1" os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1" os.environ["MKL_NUM_THREADS"] = "1"
app = Flask(__name__) app = Flask(__name__)
app.config.from_object( app.config.from_object(
DevelopmentConfig DevelopmentConfig
) )
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"}) cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"})
ocr = PaddleOCR(use_angle_cls=True, lang='en') # Adjust language if needed ocr = PaddleOCR(use_angle_cls=True, lang='en') # Adjust language if needed
app.config['SESSION_COOKIE_SECURE'] = True # Only send cookie over HTTPS app.config['SESSION_COOKIE_SECURE'] = True # Only send cookie over HTTPS
app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent JavaScript access app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent JavaScript access
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Adjust for cross-site requests app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Adjust for cross-site requests
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year in seconds app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year in seconds
cache.clear() # Clears all cache entries cache.clear() # Clears all cache entries
@app.before_request @app.before_request
def make_session_permanent(): def make_session_permanent():
session.permanent = True session.permanent = True
@app.route('/manifest.json') @app.route('/manifest.json')
def serve_manifest(): def serve_manifest():
return send_file('manifest.json', mimetype='application/manifest+json') return send_file('manifest.json', mimetype='application/manifest+json')
@app.route("/favicon.ico") @app.route("/favicon.ico")
def favicon(): def favicon():
return send_from_directory( return send_from_directory(
os.path.join(app.root_path, "static"), os.path.join(app.root_path, "static"),
"favicon.ico", "favicon.ico",
mimetype="image/vnd.microsoft.icon", mimetype="image/vnd.microsoft.icon",
) )
@app.route("/") @app.route("/")
def index(): def index():
# If the user is logged in, redirect to a protected page like /accounts # If the user is logged in, redirect to a protected page like /accounts
if session.get("logged_in"): if session.get("logged_in"):
return redirect(url_for("home")) return redirect(url_for("home"))
return render_template("index.html") return render_template("index.html")
@app.route("/home") @app.route("/home")
@cache.cached(timeout=60) # cache for 120 seconds @cache.cached(timeout=60) # cache for 120 seconds
def home(): def home():
if session.get("logged_in"): if session.get("logged_in"):
base_url = app.config["BASE_URL"] # Access base_url from the config base_url = app.config["BASE_URL"] # Access base_url from the config
all_accounts = get_user_accounts(base_url, session["auth_credentials"]) all_accounts = get_user_accounts(base_url, session["auth_credentials"])
count = len(all_accounts) count = len(all_accounts)
current_month_accounts = filter_accounts_next_30_days(all_accounts) current_month_accounts = filter_accounts_next_30_days(all_accounts)
expired_accounts = filter_accounts_expired(all_accounts) expired_accounts = filter_accounts_expired(all_accounts)
return render_template( return render_template(
"home.html", "home.html",
username=session["username"], username=session["username"],
accounts=count, accounts=count,
current_month_accounts=current_month_accounts, current_month_accounts=current_month_accounts,
expired_accounts=expired_accounts, expired_accounts=expired_accounts,
) )
return render_template("index.html") return render_template("index.html")
@app.route("/login", methods=["POST"]) @app.route("/login", methods=["POST"])
def login(): def login():
username = request.form["username"] username = request.form["username"]
password = request.form["password"] password = request.form["password"]
# Encode the username and password in Base64 # Encode the username and password in Base64
credentials = f"{username}:{password}" credentials = f"{username}:{password}"
encoded_credentials = base64.b64encode(credentials.encode()).decode() encoded_credentials = base64.b64encode(credentials.encode()).decode()
base_url = app.config["BASE_URL"] # Access base_url from the config base_url = app.config["BASE_URL"] # Access base_url from the config
login_url = f"{base_url}/Login" # Construct the full URL login_url = f"{base_url}/Login" # Construct the full URL
# Send GET request to the external login API with Basic Auth # Send GET request to the external login API with Basic Auth
response = requests.get( response = requests.get(
login_url, auth=requests.auth.HTTPBasicAuth(username, password) login_url, auth=requests.auth.HTTPBasicAuth(username, password)
) )
# Check if login was successful # Check if login was successful
if response.status_code == 200 and response.json().get("auth") == "Success": if response.status_code == 200 and response.json().get("auth") == "Success":
# Set session variable to indicate the user is logged in # Set session variable to indicate the user is logged in
session["logged_in"] = True session["logged_in"] = True
session["username"] = username session["username"] = username
session["auth_credentials"] = encoded_credentials session["auth_credentials"] = encoded_credentials
return redirect(url_for("home")) # Redirect to the Accounts page return redirect(url_for("home")) # Redirect to the Accounts page
else: else:
# Show error on the login page # Show error on the login page
error = "Invalid username or password. Please try again." error = "Invalid username or password. Please try again."
return render_template("index.html", error=error) return render_template("index.html", error=error)
@app.route("/urls", methods=["GET"]) @app.route("/urls", methods=["GET"])
@cache.cached(timeout=300) # cache for 5 minutes @cache.cached(timeout=300) # cache for 5 minutes
def urls(): def urls():
# Check if the user is logged in # Check if the user is logged in
if not session.get("logged_in"): if not session.get("logged_in"):
return redirect(url_for("home")) return redirect(url_for("home"))
# Placeholder content for Accounts page # Placeholder content for Accounts page
base_url = app.config["BASE_URL"] # Access base_url from the config base_url = app.config["BASE_URL"] # Access base_url from the config
return render_template( return render_template(
"urls.html", urls=get_urls(base_url, session["auth_credentials"]) "urls.html", urls=get_urls(base_url, session["auth_credentials"])
) )
@app.route("/accounts", methods=["GET"]) @app.route("/accounts", methods=["GET"])
@cache.cached(timeout=120) # cache for 120 seconds @cache.cached(timeout=120) # cache for 120 seconds
def user_accounts(): def user_accounts():
# Check if the user is logged in # Check if the user is logged in
if not session.get("logged_in"): if not session.get("logged_in"):
return redirect(url_for("home")) return redirect(url_for("home"))
# Placeholder content for Accounts page # Placeholder content for Accounts page
base_url = app.config["BASE_URL"] # Access base_url from the config base_url = app.config["BASE_URL"] # Access base_url from the config
return render_template( return render_template(
"user_accounts.html", "user_accounts.html",
username=session["username"], username=session["username"],
user_accounts=get_user_accounts(base_url, session["auth_credentials"]), user_accounts=get_user_accounts(base_url, session["auth_credentials"]),
auth=session["auth_credentials"], auth=session["auth_credentials"],
) )
@app.route("/accounts/add", methods=["GET", "POST"]) @app.route("/accounts/add", methods=["GET", "POST"])
def add_account(): def add_account():
base_url = app.config["BASE_URL"] # Access base_url from the config base_url = app.config["BASE_URL"] # Access base_url from the config
if request.method == "POST": if request.method == "POST":
username = request.form["username"] username = request.form["username"]
password = request.form["password"] password = request.form["password"]
stream = request.form["stream"] stream = request.form["stream"]
if add_user_account( if add_user_account(
base_url, session["auth_credentials"], username, password, stream base_url, session["auth_credentials"], username, password, stream
): ):
cache.clear() # Clears all cache entries cache.clear() # Clears all cache entries
return redirect(url_for("user_accounts")) return redirect(url_for("user_accounts"))
return render_template("add_account.html") return render_template("add_account.html")
return render_template("add_account.html") return render_template("add_account.html")
@app.route("/accounts/delete", methods=["POST"]) @app.route("/accounts/delete", methods=["POST"])
def delete_account(): def delete_account():
stream = request.form.get("stream") stream = request.form.get("stream")
username = request.form.get("username") username = request.form.get("username")
base_url = app.config["BASE_URL"] base_url = app.config["BASE_URL"]
if delete_user_account(base_url, session["auth_credentials"], stream, username): if delete_user_account(base_url, session["auth_credentials"], stream, username):
cache.clear() # Clears all cache entries cache.clear() # Clears all cache entries
return redirect(url_for("user_accounts")) return redirect(url_for("user_accounts"))
return redirect(url_for("user_accounts")) return redirect(url_for("user_accounts"))
@app.route('/OCRupload', methods=['POST']) @app.route('/OCRupload', methods=['POST'])
def OCRupload(): def OCRupload():
if 'image' not in request.files: if 'image' not in request.files:
return jsonify({"error": "No image file found"}), 400 return jsonify({"error": "No image file found"}), 400
# Get the uploaded file # Get the uploaded file
file = request.files['image'] file = request.files['image']
try: try:
image = Image.open(file.stream) image = Image.open(file.stream)
image_np = np.array(image) image_np = np.array(image)
result = ocr.ocr(image_np) result = ocr.ocr(image_np)
# Extract text # Extract text
extracted_text = [] extracted_text = []
for line in result[0]: for line in result[0]:
extracted_text.append(line[1][0]) extracted_text.append(line[1][0])
return render_template("add_account.html", username=extracted_text[2], password=extracted_text[3]) return render_template("add_account.html", username=extracted_text[2], password=extracted_text[3])
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=app.config["DEBUG"], host=app.config["HOST"], port=app.config["PORT"]) app.run(debug=app.config["DEBUG"], host=app.config["HOST"], port=app.config["PORT"])

View File

@ -1,19 +1,19 @@
from flask import Flask, jsonify from flask import Flask, jsonify
from config import DevelopmentConfig from config import DevelopmentConfig
from lib.mysql import execute_query from lib.mysql import execute_query
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(DevelopmentConfig) app.config.from_object(DevelopmentConfig)
@app.route('/getUserAccounts', methods=['GET']) @app.route('/getUserAccounts', methods=['GET'])
def get_user_accounts(): def get_user_accounts():
# Use the execute_query function to get user accounts # Use the execute_query function to get user accounts
data = execute_query("SELECT COUNT(*) AS account_count FROM userAccounts WHERE userID = %s;", (1,)) data = execute_query("SELECT COUNT(*) AS account_count FROM userAccounts WHERE userID = %s;", (1,))
if data is None: if data is None:
return jsonify({"error": "Database query failed"}), 500 return jsonify({"error": "Database query failed"}), 500
return jsonify(data), 200 return jsonify(data), 200
# Run the app # Run the app
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=app.config["DEBUG"], port=app.config["PORT"]) app.run(debug=app.config["DEBUG"], port=app.config["PORT"])

View File

@ -1,37 +1,37 @@
import mysql.connector import mysql.connector
from flask import current_app from flask import current_app
def execute_query(query, params=None, fetch_one=False): def execute_query(query, params=None, fetch_one=False):
"""Execute a SQL query and optionally fetch results.""" """Execute a SQL query and optionally fetch results."""
try: try:
# Get database configuration from the current app context # Get database configuration from the current app context
db_config = { db_config = {
"host": current_app.config['DBHOST'], "host": current_app.config['DBHOST'],
"user": current_app.config['DBUSER'], "user": current_app.config['DBUSER'],
"password": current_app.config['DBPASS'], "password": current_app.config['DBPASS'],
"database": current_app.config['DATABASE'], "database": current_app.config['DATABASE'],
} }
# Establish database connection # Establish database connection
connection = mysql.connector.connect(**db_config) connection = mysql.connector.connect(**db_config)
cursor = connection.cursor(dictionary=True) cursor = connection.cursor(dictionary=True)
# Execute the query with optional parameters # Execute the query with optional parameters
cursor.execute(query, params) cursor.execute(query, params)
# Fetch results if it's a SELECT query # Fetch results if it's a SELECT query
if query.strip().upper().startswith("SELECT"): if query.strip().upper().startswith("SELECT"):
result = cursor.fetchone() if fetch_one else cursor.fetchall() result = cursor.fetchone() if fetch_one else cursor.fetchall()
else: else:
# Commit changes for INSERT, UPDATE, DELETE # Commit changes for INSERT, UPDATE, DELETE
connection.commit() connection.commit()
result = cursor.rowcount # Number of affected rows result = cursor.rowcount # Number of affected rows
# Close the database connection # Close the database connection
cursor.close() cursor.close()
connection.close() connection.close()
return result return result
except mysql.connector.Error as err: except mysql.connector.Error as err:
print("Error: ", err) print("Error: ", err)
return None return None

View File

@ -1,11 +1,11 @@
# config.py # config.py
class Config: class Config:
DEBUG = False DEBUG = False
BASE_URL = '' # Set your base URL here BASE_URL = '' # Set your base URL here
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
DEBUG = True DEBUG = True
class ProductionConfig(Config): class ProductionConfig(Config):
BASE_URL = '' # Production base URL BASE_URL = '' # Production base URL

View File

@ -1,46 +1,46 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Dict from typing import List, Dict
def filter_accounts_next_30_days(accounts: List[Dict[str, int]]) -> List[Dict[str, int]]: def filter_accounts_next_30_days(accounts: List[Dict[str, int]]) -> List[Dict[str, int]]:
"""Filter accounts whose expiry date falls within the next 30 days. """Filter accounts whose expiry date falls within the next 30 days.
Args: Args:
accounts (List[Dict[str, int]]): A list of account dictionaries, each containing accounts (List[Dict[str, int]]): A list of account dictionaries, each containing
an 'expiaryDate' key with an epoch timestamp as its value. an 'expiaryDate' key with an epoch timestamp as its value.
Returns: Returns:
List[Dict[str, int]]: A list of accounts expiring within the next 30 days. List[Dict[str, int]]: A list of accounts expiring within the next 30 days.
""" """
now = datetime.now() now = datetime.now()
thirty_days_later = now + timedelta(days=30) thirty_days_later = now + timedelta(days=30)
# Convert current time and 30 days later to epoch timestamps # Convert current time and 30 days later to epoch timestamps
now_timestamp = int(now.timestamp()) now_timestamp = int(now.timestamp())
thirty_days_later_timestamp = int(thirty_days_later.timestamp()) thirty_days_later_timestamp = int(thirty_days_later.timestamp())
# Filter accounts with expiryDate within the next 30 days # Filter accounts with expiryDate within the next 30 days
return [ return [
account for account in accounts account for account in accounts
if now_timestamp <= account['expiaryDate'] < thirty_days_later_timestamp if now_timestamp <= account['expiaryDate'] < thirty_days_later_timestamp
] ]
def filter_accounts_expired(accounts: List[Dict[str, int]]) -> List[Dict[str, int]]: def filter_accounts_expired(accounts: List[Dict[str, int]]) -> List[Dict[str, int]]:
"""Filter accounts whose expiry date has passed. """Filter accounts whose expiry date has passed.
Args: Args:
accounts (List[Dict[str, int]]): A list of account dictionaries, each containing accounts (List[Dict[str, int]]): A list of account dictionaries, each containing
an 'expiaryDate' key with an epoch timestamp as its value. an 'expiaryDate' key with an epoch timestamp as its value.
Returns: Returns:
List[Dict[str, int]]: A list of accounts that have expired. List[Dict[str, int]]: A list of accounts that have expired.
""" """
# Get the current epoch timestamp # Get the current epoch timestamp
current_timestamp = int(datetime.now().timestamp()) current_timestamp = int(datetime.now().timestamp())
# Filter accounts where the current date is greater than the expiryDate # Filter accounts where the current date is greater than the expiryDate
expired_accounts = [ expired_accounts = [
account for account in accounts account for account in accounts
if account['expiaryDate'] < current_timestamp if account['expiaryDate'] < current_timestamp
] ]
return expired_accounts return expired_accounts

View File

@ -1,107 +1,107 @@
import requests import requests
import json import json
from datetime import datetime from datetime import datetime
from typing import List, Dict, Any from typing import List, Dict, Any
def get_urls(base_url: str, auth: str) -> List[Dict[str, Any]]: def get_urls(base_url: str, auth: str) -> List[Dict[str, Any]]:
"""Retrieve user account streams from the specified base URL. """Retrieve user account streams from the specified base URL.
Args: Args:
base_url (str): The base URL of the API. base_url (str): The base URL of the API.
auth (str): The authorization token for accessing the API. auth (str): The authorization token for accessing the API.
Returns: Returns:
List[Dict[str, Any]]: A list of user account streams. List[Dict[str, Any]]: A list of user account streams.
""" """
url = f"{base_url}/getUserAccounts/streams" url = f"{base_url}/getUserAccounts/streams"
payload = {} payload = {}
headers = {"Authorization": f"Basic {auth}"} headers = {"Authorization": f"Basic {auth}"}
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
return json.loads(response.text) return json.loads(response.text)
def get_user_accounts(base_url: str, auth: str) -> List[Dict[str, Any]]: def get_user_accounts(base_url: str, auth: str) -> List[Dict[str, Any]]:
"""Retrieve user accounts from the specified base URL. """Retrieve user accounts from the specified base URL.
Args: Args:
base_url (str): The base URL of the API. base_url (str): The base URL of the API.
auth (str): The authorization token for accessing the API. auth (str): The authorization token for accessing the API.
Returns: Returns:
List[Dict[str, Any]]: A list of user accounts with their expiration dates rendered. List[Dict[str, Any]]: A list of user accounts with their expiration dates rendered.
""" """
url = f"{base_url}/getUserAccounts" url = f"{base_url}/getUserAccounts"
payload = {} payload = {}
headers = {"Authorization": f"Basic {auth}"} headers = {"Authorization": f"Basic {auth}"}
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
res_json = json.loads(response.text) res_json = json.loads(response.text)
for account in res_json: for account in res_json:
account["expiaryDate_rendered"] = datetime.utcfromtimestamp( account["expiaryDate_rendered"] = datetime.utcfromtimestamp(
account["expiaryDate"] account["expiaryDate"]
).strftime("%d/%m/%Y") ).strftime("%d/%m/%Y")
return res_json return res_json
def delete_user_account(base_url: str, auth: str, stream: str, username: str) -> bool: def delete_user_account(base_url: str, auth: str, stream: str, username: str) -> bool:
"""Delete a user account from the specified base URL. """Delete a user account from the specified base URL.
Args: Args:
base_url (str): The base URL of the API. base_url (str): The base URL of the API.
auth (str): The authorization token for accessing the API. auth (str): The authorization token for accessing the API.
stream (str): The name of the stream associated with the user account. stream (str): The name of the stream associated with the user account.
username (str): The username of the account to delete. username (str): The username of the account to delete.
Returns: Returns:
bool: True if the account was deleted successfully, False otherwise. bool: True if the account was deleted successfully, False otherwise.
""" """
url = f"{base_url}/deleteAccount" url = f"{base_url}/deleteAccount"
payload = {"stream": stream, "user": username} payload = {"stream": stream, "user": username}
headers = {"Authorization": f"Basic {auth}"} headers = {"Authorization": f"Basic {auth}"}
response = requests.request("POST", url, headers=headers, data=payload) response = requests.request("POST", url, headers=headers, data=payload)
return "Deleted" in response.text return "Deleted" in response.text
def add_user_account(base_url: str, auth: str, username: str, password: str, stream: str) -> bool: def add_user_account(base_url: str, auth: str, username: str, password: str, stream: str) -> bool:
"""Add a user account to the specified base URL. """Add a user account to the specified base URL.
Args: Args:
base_url (str): The base URL of the API. base_url (str): The base URL of the API.
auth (str): The authorization token for accessing the API. auth (str): The authorization token for accessing the API.
username (str): The username of the account to add. username (str): The username of the account to add.
password (str): The password of the account to add. password (str): The password of the account to add.
stream (str): The name of the stream associated with the user account. stream (str): The name of the stream associated with the user account.
Returns: Returns:
bool: True if the account was added successfully, False otherwise. bool: True if the account was added successfully, False otherwise.
""" """
url = f"{base_url}/addAccount" url = f"{base_url}/addAccount"
payload = {"username": username, "password": password, "stream": stream} payload = {"username": username, "password": password, "stream": stream}
headers = {"Authorization": f"Basic {auth}"} headers = {"Authorization": f"Basic {auth}"}
response = requests.request("POST", url, headers=headers, data=payload) response = requests.request("POST", url, headers=headers, data=payload)
return "Added successfully" in response.text return "Added successfully" in response.text
def get_user_accounts_count(base_url: str, auth: str) -> int: def get_user_accounts_count(base_url: str, auth: str) -> int:
"""Get the count of user accounts from the specified base URL. """Get the count of user accounts from the specified base URL.
Args: Args:
base_url (str): The base URL of the API. base_url (str): The base URL of the API.
auth (str): The authorization token for accessing the API. auth (str): The authorization token for accessing the API.
Returns: Returns:
int: The count of user accounts. int: The count of user accounts.
""" """
url = f"{base_url}/getUserAccounts/count" url = f"{base_url}/getUserAccounts/count"
payload = {} payload = {}
headers = {"Authorization": f"Basic {auth}"} headers = {"Authorization": f"Basic {auth}"}
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
res_json = json.loads(response.text) res_json = json.loads(response.text)
return res_json['count'] return res_json['count']

View File

@ -1,15 +1,15 @@
self.addEventListener('install', e => { self.addEventListener('install', e => {
// console.log('[Service Worker] Installed'); // console.log('[Service Worker] Installed');
}); });
self.addEventListener('activate', e => { self.addEventListener('activate', e => {
// console.log('[Service Worker] Activated'); // console.log('[Service Worker] Activated');
}); });
self.addEventListener('fetch', e => { self.addEventListener('fetch', e => {
// e.respondWith( // e.respondWith(
// caches.match(e.request).then(res => { // caches.match(e.request).then(res => {
// return res || fetch(e.request); // return res || fetch(e.request);
// }) // })
// ); // );
}); });

View File

@ -1,30 +1,30 @@
/* Base styles */ /* Base styles */
html, body { html, body {
height: 100%; /* Ensure the body and html elements fill the viewport height */ height: 100%; /* Ensure the body and html elements fill the viewport height */
margin: 0; /* Remove default margin */ margin: 0; /* Remove default margin */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
header { header {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
padding: 1em; padding: 1em;
text-align: center; text-align: center;
} }
main { main {
flex: 1; /* Make the main content area grow to fill the available space */ flex: 1; /* Make the main content area grow to fill the available space */
padding: 1em; padding: 1em;
} }
footer { footer {
background-color: #333; background-color: #333;
color: white; color: white;
text-align: center; text-align: center;
padding: 1em; padding: 1em;
} }

View File

@ -1,106 +1,106 @@
<!-- templates/add_account.html --> <!-- templates/add_account.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Account - KTVManager</title> <title>Add Account - KTVManager</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
<style> <style>
/* Hide the spinner by default */ /* Hide the spinner by default */
#loadingSpinner, #loadingSpinner,
#ocrLoadingSpinner { #ocrLoadingSpinner {
display: none; display: none;
} }
</style> </style>
</head> </head>
<body> <body>
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">KTVManager</a> <a class="navbar-brand" href="/">KTVManager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts">Accounts</a> <a class="nav-link" href="/accounts">Accounts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/urls">URLs</a> <a class="nav-link" href="/urls">URLs</a>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
<!-- Sub-navigation for Accounts --> <!-- Sub-navigation for Accounts -->
<div class="bg-light py-2"> <div class="bg-light py-2">
<div class="container"> <div class="container">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts">List Accounts</a> <a class="nav-link" href="/accounts">List Accounts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="/accounts/add">Add Account</a> <a class="nav-link active" href="/accounts/add">Add Account</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<main class="container mt-5"> <main class="container mt-5">
<h1>Add Account</h1> <h1>Add Account</h1>
<div> <div>
<form action="/accounts/add" method="POST" onsubmit="showLoading()"> <form action="/accounts/add" method="POST" onsubmit="showLoading()">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" value="{{ username }}" required> <input type="text" class="form-control" id="username" name="username" value="{{ username }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input type="text" class="form-control" id="password" name="password" value="{{ password }}" required> <input type="text" class="form-control" id="password" name="password" value="{{ password }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="stream">Stream Name</label> <label for="stream">Stream Name</label>
<input type="text" class="form-control" id="stream" name="stream" required> <input type="text" class="form-control" id="stream" name="stream" required>
</div> </div>
<button type="submit" class="btn btn-primary" id="submitButton"> <button type="submit" class="btn btn-primary" id="submitButton">
<span class="spinner-border spinner-border-sm" id="loadingSpinner" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" id="loadingSpinner" role="status" aria-hidden="true"></span>
<span id="buttonText">Add Account</span> <span id="buttonText">Add Account</span>
</button> </button>
</form> </form>
<hr> <hr>
<h4>Load Details Via OCR</h2> <h4>Load Details Via OCR</h2>
<form action="/OCRupload" method="POST" enctype="multipart/form-data" onsubmit="showLoadingOCR()"> <form action="/OCRupload" method="POST" enctype="multipart/form-data" onsubmit="showLoadingOCR()">
<div class="form-group"> <div class="form-group">
<label for="image">Select Image</label> <label for="image">Select Image</label>
<input type="file" class="form-control-file" id="image" name="image" accept="image/*" required> <input type="file" class="form-control-file" id="image" name="image" accept="image/*" required>
</div> </div>
<button type="submit" class="btn btn-success" id="ocrButton"> <button type="submit" class="btn btn-success" id="ocrButton">
<span class="spinner-border spinner-border-sm" id="ocrLoadingSpinner" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" id="ocrLoadingSpinner" role="status" aria-hidden="true"></span>
<span id="ocrButtonText">Load Details</span> <span id="ocrButtonText">Load Details</span>
</button> </button>
</form> </form>
</main> </main>
<footer class="bg-dark text-white text-center py-3 mt-5"> <footer class="bg-dark text-white text-center py-3 mt-5">
<p></p> <p></p>
</footer> </footer>
<script> <script>
function showLoading() { function showLoading() {
document.getElementById("submitButton").disabled = true; document.getElementById("submitButton").disabled = true;
document.getElementById("loadingSpinner").style.display = "inline-block"; document.getElementById("loadingSpinner").style.display = "inline-block";
document.getElementById("buttonText").textContent = "Working..."; document.getElementById("buttonText").textContent = "Working...";
} }
function showLoadingOCR() { function showLoadingOCR() {
document.getElementById("ocrButton").disabled = true; document.getElementById("ocrButton").disabled = true;
document.getElementById("ocrLoadingSpinner").style.display = "inline-block"; document.getElementById("ocrLoadingSpinner").style.display = "inline-block";
document.getElementById("ocrButtonText").textContent = "Processing..."; document.getElementById("ocrButtonText").textContent = "Processing...";
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,93 +1,93 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KTVManager</title> <title>KTVManager</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head> </head>
<body> <body>
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">KTVManager</a> <a class="navbar-brand" href="/">KTVManager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts">Accounts</a> <a class="nav-link" href="/accounts">Accounts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/urls">URLs</a> <a class="nav-link" href="/urls">URLs</a>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
<!-- Main Content --> <!-- Main Content -->
<main class="container mt-5"> <main class="container mt-5">
<h1>Welcome {{ username }}!</h1> <h1>Welcome {{ username }}!</h1>
<br> <br>
<h2>You have {{ accounts }} active accounts</h2> <h2>You have {{ accounts }} active accounts</h2>
<br> <br>
{% if current_month_accounts %} {% if current_month_accounts %}
<h3>Accounts Expiring Within 30 Days</h3> <h3>Accounts Expiring Within 30 Days</h3>
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead class="thead-dark"> <thead class="thead-dark">
<tr> <tr>
<th>Stream Name</th> <th>Stream Name</th>
<th>Username</th> <th>Username</th>
<th>Expiry Date</th> <th>Expiry Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for account in current_month_accounts %} {% for account in current_month_accounts %}
<tr> <tr>
<td>{{ account.stream }}</td> <td>{{ account.stream }}</td>
<td>{{ account.username }}</td> <td>{{ account.username }}</td>
<td>{{ account.expiaryDate_rendered }}</td> <td>{{ account.expiaryDate_rendered }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if expired_accounts %} {% if expired_accounts %}
<h3>Expired Accounts</h3> <h3>Expired Accounts</h3>
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead class="thead-dark"> <thead class="thead-dark">
<tr> <tr>
<th>Stream Name</th> <th>Stream Name</th>
<th>Username</th> <th>Username</th>
<th>Expiry Date</th> <th>Expiry Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for account in expired_accounts %} {% for account in expired_accounts %}
<tr> <tr>
<td>{{ account.stream }}</td> <td>{{ account.stream }}</td>
<td>{{ account.username }}</td> <td>{{ account.username }}</td>
<td>{{ account.expiaryDate_rendered }}</td> <td>{{ account.expiaryDate_rendered }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="bg-dark text-white text-center py-3 mt-5"> <footer class="bg-dark text-white text-center py-3 mt-5">
<p></p> <p></p>
</footer> </footer>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body> </body>
</html> </html>

View File

@ -1,80 +1,80 @@
<!-- templates/index.html --> <!-- templates/index.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KTVManager</title> <title>KTVManager</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon-96x96.png') }}" sizes="96x96" /> <link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon-96x96.png') }}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}" /> <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}" />
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" /> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}" /> <link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}" />
<meta name="apple-mobile-web-app-title" content="kTvManager" /> <meta name="apple-mobile-web-app-title" content="kTvManager" />
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}" /> <link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head> </head>
<body> <body>
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">KTVManager</a> <a class="navbar-brand" href="/">KTVManager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts">Accounts</a> <a class="nav-link" href="/accounts">Accounts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/urls">URLs</a> <a class="nav-link" href="/urls">URLs</a>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
<!-- Main Content --> <!-- Main Content -->
<main div class="container mt-5"> <main div class="container mt-5">
<h1>Welcome to KTV Manager</h1> <h1>Welcome to KTV Manager</h1>
<!-- Login Form --> <!-- Login Form -->
<form action="/login" method="post" class="mt-3"> <form action="/login" method="post" class="mt-3">
<div class="form-group"> <div class="form-group">
<label for="username">Username:</label> <label for="username">Username:</label>
<input type="text" class="form-control" id="username" name="username" required> <input type="text" class="form-control" id="username" name="username" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password:</label> <label for="password">Password:</label>
<input type="password" class="form-control" id="password" name="password" required> <input type="password" class="form-control" id="password" name="password" required>
</div> </div>
<button type="submit" class="btn btn-primary">Login</button> <button type="submit" class="btn btn-primary">Login</button>
{% if error %} {% if error %}
<div class="alert alert-danger mt-3">{{ error }}</div> <div class="alert alert-danger mt-3">{{ error }}</div>
{% endif %} {% endif %}
</form> </form>
</div> </div>
</main> </main>
<footer class="bg-dark text-white text-center py-3 mt-5"> <footer class="bg-dark text-white text-center py-3 mt-5">
<p></p> <p></p>
</footer> </footer>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script> <script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}') navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}')
// .then(reg => { // .then(reg => {
// console.log('Service worker:', reg); // console.log('Service worker:', reg);
// .catch(err => { // .catch(err => {
// console.log('Service worker:', err); // console.log('Service worker:', err);
// }); // });
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,65 +1,65 @@
<!-- templates/urls.html --> <!-- templates/urls.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KTVManager</title> <title>KTVManager</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head> </head>
<body> <body>
<!-- Navbar (same as index.html) --> <!-- Navbar (same as index.html) -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">KTVManager</a> <a class="navbar-brand" href="/">KTVManager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts">Accounts</a> <!-- Link to the URLs page --> <a class="nav-link" href="/accounts">Accounts</a> <!-- Link to the URLs page -->
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/urls">URLs</a> <!-- Link to the URLs page --> <a class="nav-link" href="/urls">URLs</a> <!-- Link to the URLs page -->
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
<!-- Main Content --> <!-- Main Content -->
<main div class="container mt-5"> <main div class="container mt-5">
<h2>URLs</h2> <h2>URLs</h2>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>URL</th> <th>URL</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for url in urls %} {% for url in urls %}
<tr> <tr>
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td><a href="{{ url }}" target="_blank">{{ url }}</a></td> <td><a href="{{ url }}" target="_blank">{{ url }}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</main> </main>
<footer class="bg-dark text-white text-center py-3 mt-5"> <footer class="bg-dark text-white text-center py-3 mt-5">
<p></p> <p></p>
</footer> </footer>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body> </body>
</html> </html>

View File

@ -1,120 +1,120 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KTVManager</title> <title>KTVManager</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.dataTables.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.dataTables.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head> </head>
<body> <body>
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">KTVManager</a> <a class="navbar-brand" href="/">KTVManager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li> <li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/accounts">Accounts</a></li> <li class="nav-item"><a class="nav-link" href="/accounts">Accounts</a></li>
<li class="nav-item"><a class="nav-link" href="/urls">URLs</a></li> <li class="nav-item"><a class="nav-link" href="/urls">URLs</a></li>
</ul> </ul>
</div> </div>
</nav> </nav>
<!-- Sub-navigation for Accounts --> <!-- Sub-navigation for Accounts -->
<div class="bg-light py-2"> <div class="bg-light py-2">
<div class="container"> <div class="container">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="/accounts">List Accounts</a> <a class="nav-link active" href="/accounts">List Accounts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/accounts/add">Add Account</a> <a class="nav-link" href="/accounts/add">Add Account</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<main div class="container mt-5"> <main div class="container mt-5">
<h2>{{ username }}'s Accounts</h2> <h2>{{ username }}'s Accounts</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped" id="accountsTable"> <table class="table table-striped" id="accountsTable">
<thead> <thead>
<tr> <tr>
<!-- <th>#</th> --> <!-- <th>#</th> -->
<th>Username</th> <th>Username</th>
<th>Stream</th> <th>Stream</th>
<th>Stream URL</th> <th>Stream URL</th>
<th>Expiry Date</th> <th>Expiry Date</th>
<th>Password</th> <th>Password</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for account in user_accounts %} {% for account in user_accounts %}
<tr> <tr>
<!-- <td>{{ loop.index }}</td> --> <!-- <td>{{ loop.index }}</td> -->
<td>{{ account.username }}</td> <td>{{ account.username }}</td>
<td>{{ account.stream }}</td> <td>{{ account.stream }}</td>
<td><a href="{{ account.streamURL }}" target="_blank">{{ account.streamURL }}</a></td> <td><a href="{{ account.streamURL }}" target="_blank">{{ account.streamURL }}</a></td>
<td>{{ account.expiaryDate_rendered }}</td> <td>{{ account.expiaryDate_rendered }}</td>
<td>{{ account.password }}</td> <td>{{ account.password }}</td>
<td> <td>
<form action="/accounts/delete" method="POST" style="display:inline;"> <form action="/accounts/delete" method="POST" style="display:inline;">
<input type="hidden" name="stream" value="{{ account.stream }}"> <input type="hidden" name="stream" value="{{ account.stream }}">
<input type="hidden" name="username" value="{{ account.username }}"> <input type="hidden" name="username" value="{{ account.username }}">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this account?');"> <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this account?');">
Delete Delete
</button> </button>
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</main> </main>
<footer class="bg-dark text-white text-center py-3 mt-5"> <footer class="bg-dark text-white text-center py-3 mt-5">
<p></p> <p></p>
</footer> </footer>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.7/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script> <script src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Extend DataTables date sorting for DD/MM/YYYY format // Extend DataTables date sorting for DD/MM/YYYY format
$.fn.dataTable.ext.type.order['date-eu-pre'] = function(data) { $.fn.dataTable.ext.type.order['date-eu-pre'] = function(data) {
const parts = data.split('/'); const parts = data.split('/');
// Check if the data is in the correct format before processing // Check if the data is in the correct format before processing
if (parts.length === 3) { if (parts.length === 3) {
return new Date(parts[2], parts[1] - 1, parts[0]).getTime(); return new Date(parts[2], parts[1] - 1, parts[0]).getTime();
} }
return data; // return as is if the format is unexpected return data; // return as is if the format is unexpected
}; };
// Initialize DataTable with custom sorting and default ordering by Expiry Date // Initialize DataTable with custom sorting and default ordering by Expiry Date
$('#accountsTable').DataTable({ $('#accountsTable').DataTable({
"searching": true, "searching": true,
"ordering": true, "ordering": true,
"responsive": true, "responsive": true,
"order": [[3, 'asc']], // Default order by Expiry Date column in ascending order "order": [[3, 'asc']], // Default order by Expiry Date column in ascending order
"columnDefs": [ "columnDefs": [
{ "type": "date-eu", "targets": 3 } // Use custom date-eu type for the date column { "type": "date-eu", "targets": 3 } // Use custom date-eu type for the date column
] ]
}); });
}); });
</script> </script>
</body> </body>
</html> </html>