diff --git a/app/dependencies.py b/app/dependencies.py index 9c28232c..9a55421d 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -13,6 +13,7 @@ MEDIA_PATH = os.path.join(APP_PATH, config.MEDIA_DIRECTORY) STATIC_PATH = os.path.join(APP_PATH, "static") TEMPLATES_PATH = os.path.join(APP_PATH, "templates") +CURSORS_PATH = os.path.join(MEDIA_PATH, "cursors") SOUNDS_PATH = os.path.join(STATIC_PATH, "tracks") templates = Jinja2Templates(directory=TEMPLATES_PATH) templates.env.add_extension("jinja2.ext.i18n") diff --git a/app/internal/cursor.py b/app/internal/cursor.py new file mode 100644 index 00000000..1d5415e5 --- /dev/null +++ b/app/internal/cursor.py @@ -0,0 +1,54 @@ +from typing import List, Optional, Tuple + +from sqlalchemy.orm.session import Session + +from app.database.models import User, UserSettings + + +def get_cursor_settings( + session: Session, + user_id: int, +) -> Tuple[Optional[List[str]], Optional[int], Optional[str], Optional[int]]: + """Retrieves cursor settings from the database. + + Args: + session (Session): the database. + user_id (int, optional): the users' id. + + Returns: + Tuple[str, Optional[List[str]], Optional[int], + str, Optional[str], Optional[int]]: the cursor settings. + """ + primary_cursor, secondary_cursor = None, None + cursor_settings = ( + session.query(UserSettings).filter_by(user_id=user_id).first() + ) + if cursor_settings: + primary_cursor = cursor_settings.primary_cursor + secondary_cursor = cursor_settings.secondary_cursor + + return primary_cursor, secondary_cursor + + +def save_cursor_settings( + session: Session, + user: User, + cursor_choices: List[str], +): + """Saves cursor choices in the db. + + Args: + session (Session): the database. + user (User): current user. + cursor_choices (List[str]): primary and secondary cursors. + """ + cursor_settings = ( + session.query(UserSettings).filter_by(user_id=user.user_id).first() + ) + if cursor_settings: + session.query(UserSettings).filter_by( + user_id=cursor_settings.user_id, + ).update(cursor_choices) + else: + session.merge(UserSettings(user_id=user.user_id, **cursor_choices)) + session.commit() diff --git a/app/main.py b/app/main.py index 0170198e..e220ca43 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +import app.internal.features as internal_features from app import config from app.database import engine, models from app.dependencies import ( @@ -13,13 +14,12 @@ SOUNDS_PATH, STATIC_PATH, UPLOAD_PATH, + SessionLocal, get_db, logger, templates, - SessionLocal, ) from app.internal import daily_quotes, json_data_loader -import app.internal.features as internal_features from app.internal.languages import set_ui_language from app.internal.security.ouath2 import auth_exception_handler from app.routers.salary import routes as salary @@ -64,6 +64,7 @@ def create_tables(engine, psql_environment): celebrity, credits, currency, + cursor, dayview, email, event, @@ -118,6 +119,7 @@ async def swagger_ui_redirect(): celebrity.router, credits.router, currency.router, + cursor.router, dayview.router, email.router, event.router, diff --git a/app/media/cursors/Link.cur b/app/media/cursors/Link.cur new file mode 100644 index 00000000..985bb26c Binary files /dev/null and b/app/media/cursors/Link.cur differ diff --git a/app/media/cursors/Link2.cur b/app/media/cursors/Link2.cur new file mode 100644 index 00000000..ca1b3e63 Binary files /dev/null and b/app/media/cursors/Link2.cur differ diff --git a/app/media/cursors/Material_3d.cur b/app/media/cursors/Material_3d.cur new file mode 100644 index 00000000..00f3abe0 Binary files /dev/null and b/app/media/cursors/Material_3d.cur differ diff --git a/app/media/cursors/Valentine_Heart.cur b/app/media/cursors/Valentine_Heart.cur new file mode 100644 index 00000000..f6c4414d Binary files /dev/null and b/app/media/cursors/Valentine_Heart.cur differ diff --git a/app/media/cursors/Wand.cur b/app/media/cursors/Wand.cur new file mode 100644 index 00000000..0ec23876 Binary files /dev/null and b/app/media/cursors/Wand.cur differ diff --git a/app/media/cursors/among_us.cur b/app/media/cursors/among_us.cur new file mode 100644 index 00000000..bcfd6dd9 Binary files /dev/null and b/app/media/cursors/among_us.cur differ diff --git a/app/media/cursors/blue_cursor.cur b/app/media/cursors/blue_cursor.cur new file mode 100644 index 00000000..2735a654 Binary files /dev/null and b/app/media/cursors/blue_cursor.cur differ diff --git a/app/media/cursors/blue_finger.cur b/app/media/cursors/blue_finger.cur new file mode 100644 index 00000000..8d5c8328 Binary files /dev/null and b/app/media/cursors/blue_finger.cur differ diff --git a/app/media/cursors/credits.txt b/app/media/cursors/credits.txt new file mode 100644 index 00000000..340168c3 --- /dev/null +++ b/app/media/cursors/credits.txt @@ -0,0 +1,13 @@ +http://www.rw-designer.com/cursor-set/material-amber +http://www.rw-designer.com/cursor-set/windows-xp-style +http://www.rw-designer.com/cursor-set/material-3d +http://www.rw-designer.com/cursor-set/paper-plane +http://www.rw-designer.com/cursor-set/yodalighsaber +http://www.rw-designer.com/cursor-set/valentine-heart +http://www.rw-designer.com/cursor-set/icy-ice +http://www.rw-designer.com/cursor-set/wii-hand-1 +http://www.rw-designer.com/cursor-set/pen-tablet-1 +http://www.rw-designer.com/cursor-set/among-us-mixed-pointer-pack- +http://www.rw-designer.com/cursor-set/nes-smb-mario +http://www.rw-designer.com/cursor-set/mario-world-v64 +http://www.rw-designer.com/cursor-set/the-legend-of-zelda diff --git a/app/media/cursors/fire.cur b/app/media/cursors/fire.cur new file mode 100644 index 00000000..f234a1b8 Binary files /dev/null and b/app/media/cursors/fire.cur differ diff --git a/app/media/cursors/green_cursor.cur b/app/media/cursors/green_cursor.cur new file mode 100644 index 00000000..64ef753a Binary files /dev/null and b/app/media/cursors/green_cursor.cur differ diff --git a/app/media/cursors/green_finger.cur b/app/media/cursors/green_finger.cur new file mode 100644 index 00000000..2523919b Binary files /dev/null and b/app/media/cursors/green_finger.cur differ diff --git a/app/media/cursors/green_lightsaber.cur b/app/media/cursors/green_lightsaber.cur new file mode 100644 index 00000000..24464bc1 Binary files /dev/null and b/app/media/cursors/green_lightsaber.cur differ diff --git a/app/media/cursors/ice.cur b/app/media/cursors/ice.cur new file mode 100644 index 00000000..d155accd Binary files /dev/null and b/app/media/cursors/ice.cur differ diff --git a/app/media/cursors/lime_cursor.cur b/app/media/cursors/lime_cursor.cur new file mode 100644 index 00000000..5a9f2584 Binary files /dev/null and b/app/media/cursors/lime_cursor.cur differ diff --git a/app/media/cursors/nes_mario.cur b/app/media/cursors/nes_mario.cur new file mode 100644 index 00000000..7289954e Binary files /dev/null and b/app/media/cursors/nes_mario.cur differ diff --git a/app/media/cursors/painted_arrow.cur b/app/media/cursors/painted_arrow.cur new file mode 100644 index 00000000..39dae41a Binary files /dev/null and b/app/media/cursors/painted_arrow.cur differ diff --git a/app/media/cursors/painted_finger.cur b/app/media/cursors/painted_finger.cur new file mode 100644 index 00000000..88266eac Binary files /dev/null and b/app/media/cursors/painted_finger.cur differ diff --git a/app/media/cursors/paper-plane.cur b/app/media/cursors/paper-plane.cur new file mode 100644 index 00000000..e23c4701 Binary files /dev/null and b/app/media/cursors/paper-plane.cur differ diff --git a/app/media/cursors/pen.cur b/app/media/cursors/pen.cur new file mode 100644 index 00000000..3e14bc82 Binary files /dev/null and b/app/media/cursors/pen.cur differ diff --git a/app/media/cursors/pink_cursor.cur b/app/media/cursors/pink_cursor.cur new file mode 100644 index 00000000..2214eab4 Binary files /dev/null and b/app/media/cursors/pink_cursor.cur differ diff --git a/app/media/cursors/red_cursor.cur b/app/media/cursors/red_cursor.cur new file mode 100644 index 00000000..5bae48f4 Binary files /dev/null and b/app/media/cursors/red_cursor.cur differ diff --git a/app/media/cursors/red_finger.cur b/app/media/cursors/red_finger.cur new file mode 100644 index 00000000..2403a2ca Binary files /dev/null and b/app/media/cursors/red_finger.cur differ diff --git a/app/media/cursors/snes_mario.cur b/app/media/cursors/snes_mario.cur new file mode 100644 index 00000000..51464525 Binary files /dev/null and b/app/media/cursors/snes_mario.cur differ diff --git a/app/media/cursors/sword.cur b/app/media/cursors/sword.cur new file mode 100644 index 00000000..abc47464 Binary files /dev/null and b/app/media/cursors/sword.cur differ diff --git a/app/media/cursors/xp_arrow.cur b/app/media/cursors/xp_arrow.cur new file mode 100644 index 00000000..7d845078 Binary files /dev/null and b/app/media/cursors/xp_arrow.cur differ diff --git a/app/media/cursors/yellow_cursor.cur b/app/media/cursors/yellow_cursor.cur new file mode 100644 index 00000000..a29b748e Binary files /dev/null and b/app/media/cursors/yellow_cursor.cur differ diff --git a/app/media/cursors/yellow_finger.cur b/app/media/cursors/yellow_finger.cur new file mode 100644 index 00000000..bf88a383 Binary files /dev/null and b/app/media/cursors/yellow_finger.cur differ diff --git a/app/routers/cursor.py b/app/routers/cursor.py new file mode 100644 index 00000000..06dfc7a4 --- /dev/null +++ b/app/routers/cursor.py @@ -0,0 +1,103 @@ +import json +from pathlib import Path + +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.orm.session import Session +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND + +from app.database.models import User +from app.dependencies import CURSORS_PATH, get_db, templates +from app.internal.cursor import get_cursor_settings, save_cursor_settings +from app.internal.security.dependencies import current_user + +router = APIRouter( + prefix="/cursor", + tags=["cursor"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/settings") +def cursor_settings( + request: Request, + user: User = Depends(current_user), + session: Session = Depends(get_db), +) -> templates.TemplateResponse: + """A route to the cursor settings. + + Args: + request (Request): the http request. + session (Session): the database. + + Returns: + templates.TemplateResponse: renders the cursor_settings.html page + with the relevant information. + """ + cursors = ["default"] + [ + path.stem for path in Path(CURSORS_PATH).glob("**/*.cur") + ] + + return templates.TemplateResponse( + "cursor_settings.html", + { + "request": request, + "cursors": cursors, + }, + ) + + +@router.post("/settings") +async def get_cursor_choices( + session: Session = Depends(get_db), + user: User = Depends(current_user), + primary_cursor: str = Form(...), + secondary_cursor: str = Form(...), +) -> RedirectResponse: + """The form in which the user choses primary and secondary + cursors. + + Args: + session (Session, optional): the database. + user (User, optional): [description]. temp user. + primary_cursor (str, optional): name of the primary cursor. + the primary cursor. + secondary_cursor (str, optional): name of the secondary cursor. + + Returns: + RedirectResponse: redirects to the homepage. + """ + cursor_choices = { + "primary_cursor": primary_cursor, + "secondary_cursor": secondary_cursor, + } + save_cursor_settings(session, user, cursor_choices) + + return RedirectResponse("/", status_code=HTTP_302_FOUND) + + +@router.get("/load") +async def load_cursor( + session: Session = Depends(get_db), + user: User = Depends(current_user), +) -> RedirectResponse: + """loads cursors according to cursor settings. + + Args: + session (Session): the database. + user (User): the user. + + Returns: + RedirectResponse: redirect the user to the homepage. + """ + primary_cursor, secondary_cursor = get_cursor_settings( + session, + user.user_id, + ) + + return json.dumps( + { + "primary_cursor": primary_cursor, + "secondary_cursor": secondary_cursor, + }, + ) diff --git a/app/static/cursor.js b/app/static/cursor.js new file mode 100644 index 00000000..2252345f --- /dev/null +++ b/app/static/cursor.js @@ -0,0 +1,87 @@ +const CURSORS_PATH = "/media/cursors/"; + +// These must be global so the mutationObserver could access them. +let primary_cursor; +let secondary_cursor; + +window.addEventListener("load", init); + +/** + * @summary In charge of initialising the customization of the cursor. + */ +function init() { + get_cursor_choices(); + initMutationObserver(); +} + +/** + * @summary This function gets cursor choices from the db. + */ +function get_cursor_choices() { + const request = new XMLHttpRequest(); + request.open("GET", "/cursor/load", true); + request.onload = change_cursor; + request.send(); +} + +/** + * @summary This function changes the primary cursor and the secondary + * cursor according to users' choices. + */ +function change_cursor() { + const cursor_settings = JSON.parse(JSON.parse(this.response)); + const primary_cursor_choice = cursor_settings["primary_cursor"]; + const primary_cursor_path = `url(${CURSORS_PATH}${primary_cursor_choice}), auto`; + const secondary_cursor_choice = cursor_settings["secondary_cursor"]; + const secondary_cursor_path = `url(${CURSORS_PATH}${secondary_cursor_choice}), auto`; + primary_cursor = + primary_cursor_choice !== "default.cur" ? primary_cursor_path : ""; + secondary_cursor = + secondary_cursor_choice !== "default.cur" ? secondary_cursor_path : ""; + document.body.style.cursor = primary_cursor; + const links = document.querySelectorAll("a, button, input, select, label"); + links.forEach((element) => { + element.style.cursor = secondary_cursor; + }); +} + +/** + * @summary Sets up mutation observer to follow dynamically added links + */ +function initMutationObserver() { + const config = { + childList: true, + subtree: true, + }; + const observer = new MutationObserver(mutate); + observer.observe(document, config); +} + +/** + * @summary This function identifies a new element for the secondary cursor, + * and sets it according to the users' choices. + */ +function mutate(mutationList) { + const links = ["a", "button", "input", "select", "label"]; + for (let mutation of mutationList) { + if (mutation.type == "childList") { + handle_potential_links(mutation.addedNodes, links); + } + } +} + +/** + * @summary Helper function to the mutate function which + * on creation of new nodes in the DOM, if it is a link - changes its' + * style. + */ +function handle_potential_links(nodes, links) { + nodes.forEach((element) => { + if ( + typeof(element.tagName) !== "undefined" && + links.includes(element.tagName.toLowerCase()) + ) { + element.setAttribute("style", secondary_cursor); + } + }); +} diff --git a/app/static/style.css b/app/static/style.css index 53ff19bd..273b164d 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -210,6 +210,13 @@ h2.modal-title { font-size: 1.25rem; } +#primary-cursor, +#secondary-cursor { + width: 10em; + height: 2.5em; + margin-top: 0.5em; +} + #sfx { width: 5rem; height: 2.5rem; diff --git a/app/templates/base.html b/app/templates/base.html index 9bf7748d..a3e158c8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -64,6 +64,9 @@
"{{ quote.text }}"
- {% else %} -"{{ quote.text }}" \ {{ quote.author }}
- {% endif %} - {% endif %} -"{{ quote.text }}"
- {% else %} -"{{ quote.text }}" \ {{quote.author}}
- {% endif %} - {% endif %} -