Compare commits
22 Commits
47d18400c3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
443c1b1391 | ||
|
|
4e7a6b18d6 | ||
|
|
c0624d1e56 | ||
|
|
fdcda1b566 | ||
|
|
ac79785cf0 | ||
|
|
089e61fb0b | ||
|
|
56ab91bd42 | ||
|
|
5428d49e89 | ||
|
|
43706af7c4 | ||
|
|
c1ec268823 | ||
|
|
2fbdbbf290 | ||
|
|
23337f8ab3 | ||
|
|
fbfedf4dac | ||
|
|
badb73098a | ||
|
|
98c009ba53 | ||
|
|
1909af9107 | ||
|
|
31aeb09dd9 | ||
|
|
aec310c39c | ||
|
|
664f2e4693 | ||
|
|
8cdb9340fd | ||
|
|
6aad7a069e | ||
|
|
92032df7fd |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.direnv
|
||||
|
||||
**/**.pyc
|
||||
|
||||
certs
|
||||
|
||||
24
AGENTS.md
Normal file
24
AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Development Commands
|
||||
|
||||
## Build/Lint/Test
|
||||
- **Run development server**: `JWT_SECRET=12345 uvicorn folkugat_web.main:app --reload --reload-include "*.html" --host 0.0.0.0`
|
||||
- **Build for production**: `./build.sh`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Python
|
||||
- **Type hints**: Use strict typing with `from typing import` and custom types from `folkugat_web.typing`
|
||||
- **Imports**: Standard library first, third-party, then local imports. Use absolute imports for local modules.
|
||||
- **Naming**: snake_case for variables/functions, PascalCase for classes, UPPER_CASE for constants
|
||||
- **Error handling**: Use Result[ResultT, ErrorT] pattern from utils for service functions
|
||||
- **Async**: Use async/await for I/O operations, FastAPI endpoints are async
|
||||
|
||||
### Frontend
|
||||
- **CSS**: Use Tailwind CSS classes with custom brown/beige theme
|
||||
- **Templates**: Jinja2 templates in folkugat_web/assets/templates/
|
||||
- **Static files**: Organized in folkugat_web/assets/static/
|
||||
|
||||
### Architecture
|
||||
- **Structure**: api/routes/, config/, dal/, fragments/, model/, services/
|
||||
- **Database**: SQLite with custom DAL layer in dal/sql/
|
||||
- **Authentication**: JWT-based with cookie storage
|
||||
@@ -1,10 +1,10 @@
|
||||
#+title: Readme
|
||||
|
||||
* Tasques
|
||||
** TODO Ordenar els resultats de la cerca de temes
|
||||
** DONE Ordenar els resultats de la cerca de temes
|
||||
** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat)
|
||||
** TODO Usuaris i permisos granulars
|
||||
** Lilypond support (o similar)
|
||||
** Lilypond support
|
||||
*** TODO Fer cançoners "en directe"
|
||||
*** DONE Suport de caràcters especials (al títol i més llocs?)
|
||||
*** DONE Mostrar partitura als enllaços (resultats de cerca)
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741865919,
|
||||
"narHash": "sha256-4thdbnP6dlbdq+qZWTsm4ffAwoS8Tiq1YResB+RP6WE=",
|
||||
"lastModified": 1766125104,
|
||||
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "573c650e8a14b2faa0041645ab18aed7e60f0c9a",
|
||||
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
lilypond-with-fonts
|
||||
# Project tools
|
||||
sqlite
|
||||
lazysql
|
||||
opencode
|
||||
# Local https
|
||||
mkcert
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
from ._router import router
|
||||
from .auth import *
|
||||
from .index import *
|
||||
from .sessio import *
|
||||
from .sessions import *
|
||||
from .tema import *
|
||||
from .temes import *
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
router = APIRouter(default_response_class=HTMLResponse)
|
||||
6
folkugat_web/api/router.py
Normal file
6
folkugat_web/api/router.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
|
||||
def get_router() -> APIRouter:
|
||||
return APIRouter(default_response_class=HTMLResponse)
|
||||
16
folkugat_web/api/routes/__init__.py
Normal file
16
folkugat_web/api/routes/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import auth, index, llista, llistes, sessio, sessions, tema, temes
|
||||
|
||||
router = get_router()
|
||||
|
||||
router.include_router(auth.router)
|
||||
router.include_router(index.router)
|
||||
router.include_router(llista.router)
|
||||
router.include_router(llistes.router)
|
||||
router.include_router(sessio.router)
|
||||
router.include_router(sessions.router)
|
||||
router.include_router(tema.router)
|
||||
router.include_router(temes.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -1,11 +1,13 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Cookie, Form, Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.config import auth as config
|
||||
from folkugat_web.fragments import nota
|
||||
from folkugat_web.services import auth as service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/nota")
|
||||
def nota_input(request: Request, value: str | None = None):
|
||||
@@ -1,8 +1,10 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def index(request: Request, logged_in: auth.LoggedIn):
|
||||
@@ -11,6 +13,7 @@ def index(request: Request, logged_in: auth.LoggedIn):
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"page_description": "Sessions de folk a Sant Cugat",
|
||||
"content": "/api/content/sessions",
|
||||
"logged_in": logged_in,
|
||||
"animate": True,
|
||||
9
folkugat_web/api/routes/llista/__init__.py
Normal file
9
folkugat_web/api/routes/llista/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import playlist, set_page
|
||||
|
||||
router = get_router()
|
||||
router.include_router(set_page.router)
|
||||
router.include_router(playlist.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
269
folkugat_web/api/routes/llista/playlist.py
Normal file
269
folkugat_web/api/routes/llista/playlist.py
Normal file
@@ -0,0 +1,269 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Form, HTTPException, Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import playlist
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import playlists as playlists_service
|
||||
from folkugat_web.services.temes import write as temes_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/llista/{playlist_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
playlist = playlists_service.get_playlist(playlist_id=playlist_id)
|
||||
if not playlist:
|
||||
raise HTTPException(status_code=404, detail="Could not find playlist")
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Setlist",
|
||||
"page_description": playlist.name or "Llista de temes",
|
||||
"page_card": None,
|
||||
"content": f"/api/content/llista/{playlist_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/llista/{playlist_id}")
|
||||
async def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
return await playlist.pagina(request, playlist_id, logged_in)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/name")
|
||||
def get_name(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
return playlist.name(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/editor/name")
|
||||
def name_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
):
|
||||
return playlist.name_editor(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/name")
|
||||
def set_name(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
name: Annotated[str, Form()],
|
||||
):
|
||||
_ = playlists_service.update_name(playlist_id=playlist_id, name=name)
|
||||
return playlist.name(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.post("/api/llista/{playlist_id}/set")
|
||||
def add_set(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
):
|
||||
return playlist.add_set(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def get_set(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return playlist.get_set(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def delete_set(
|
||||
_: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return playlist.delete_set(
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def add_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return playlist.add_tema(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def get_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return playlist.get_tema(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/editor")
|
||||
def get_tema_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return playlist.get_tema_editor(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def delete_tema(
|
||||
_: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return playlist.delete_tema(
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/busca")
|
||||
def busca_tema(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
query: str,
|
||||
):
|
||||
return playlist.busca_tema(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
query=query,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
tema_id: Annotated[int, Form()],
|
||||
):
|
||||
return playlist.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=tema_id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/unknown")
|
||||
def set_tema_unknown(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return playlist.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema_new(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
):
|
||||
new_tema = temes_service.create_tema(title=title)
|
||||
return playlist.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=new_tema.id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/visible")
|
||||
def set_visible(request: Request, logged_in: auth.RequireLogin, playlist_id: int):
|
||||
new_playlist = playlists_service.set_visibility(playlist_id=playlist_id, hidden=False)
|
||||
return playlist.visibility(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist=new_playlist,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/invisible")
|
||||
def set_invisible(request: Request, logged_in: auth.RequireLogin, playlist_id: int):
|
||||
new_playlist = playlists_service.set_visibility(playlist_id=playlist_id, hidden=True)
|
||||
return playlist.visibility(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist=new_playlist,
|
||||
)
|
||||
55
folkugat_web/api/routes/llista/set_page.py
Normal file
55
folkugat_web/api/routes/llista/set_page.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from fastapi import HTTPException, Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import set_page
|
||||
from folkugat_web.model import playlists
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import playlists as playlists_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
def set_description(set_: playlists.Set) -> str:
|
||||
tema_names = [tis.tema.title if tis.tema else "Desconegut" for tis in set_.temes]
|
||||
return " i ".join(filter(bool, [
|
||||
", ".join(tema_names[:-1]),
|
||||
tema_names[-1]
|
||||
]))
|
||||
|
||||
|
||||
@router.get("/llista/{playlist_id}/set/{set_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
set_ = playlists_service.get_set(playlist_id=playlist_id, set_id=set_id)
|
||||
if not set_:
|
||||
raise HTTPException(status_code=404, detail="Set not found")
|
||||
set_ = playlists_service.add_temes_to_set(set_)
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Set",
|
||||
"page_description": set_description(set_=set_),
|
||||
"content": f"/api/content/llista/{playlist_id}/set/{set_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/llista/{playlist_id}/set/{set_id}")
|
||||
async def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return await set_page.pagina(
|
||||
request=request,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
logged_in=logged_in,
|
||||
)
|
||||
5
folkugat_web/api/routes/llistes/__init__.py
Normal file
5
folkugat_web/api/routes/llistes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
from . import index
|
||||
|
||||
router = get_router()
|
||||
router.include_router(index.router)
|
||||
36
folkugat_web/api/routes/llistes/index.py
Normal file
36
folkugat_web/api/routes/llistes/index.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import llistes
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/llistes")
|
||||
def page(request: Request, logged_in: auth.LoggedIn):
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"content": "/api/content/llistes",
|
||||
"logged_in": logged_in,
|
||||
"animate": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/llistes")
|
||||
def content(request: Request, logged_in: auth.LoggedIn):
|
||||
return llistes.llistes_pagina(request, logged_in)
|
||||
|
||||
|
||||
@router.post("/api/llistes/editor/")
|
||||
def insert_row(request: Request, _: auth.RequireLogin):
|
||||
return llistes.llistes_editor_insert_row(request)
|
||||
|
||||
|
||||
@router.delete("/api/llistes/{playlist_id}")
|
||||
def delete_playlist(playlist_id: int, _: auth.RequireLogin):
|
||||
return llistes.llistes_editor_delete_row(playlist_id)
|
||||
11
folkugat_web/api/routes/sessio/__init__.py
Normal file
11
folkugat_web/api/routes/sessio/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import cartell, index, live, notes
|
||||
|
||||
router = get_router()
|
||||
router.include_router(cartell.router)
|
||||
router.include_router(index.router)
|
||||
router.include_router(live.router)
|
||||
router.include_router(notes.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
71
folkugat_web/api/routes/sessio/cartell.py
Normal file
71
folkugat_web/api/routes/sessio/cartell.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import dataclasses
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi.params import File
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments.sessio import cartell
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import files as files_service
|
||||
from folkugat_web.services import sessions as sessions_service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/cartell")
|
||||
def get_cartell(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
return cartell.cartell(request, session_id, logged_in)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/cartell/editor")
|
||||
def get_cartell_editor(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return cartell.cartell_editor(request, session_id)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/cartell")
|
||||
async def set_cartell(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
upload_file: Annotated[UploadFile, File()],
|
||||
):
|
||||
session = sessions_service.get_session(session_id=session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Could not find session")
|
||||
url = await files_service.store_session_cartell(
|
||||
session_id=session_id,
|
||||
upload_file=upload_file,
|
||||
)
|
||||
new_session = dataclasses.replace(
|
||||
session,
|
||||
cartell_url=url,
|
||||
)
|
||||
sessions_service.set_session(new_session)
|
||||
return cartell.cartell(request, session_id, logged_in)
|
||||
|
||||
|
||||
@router.delete("/api/sessio/{session_id}/cartell")
|
||||
async def delete_cartell(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
session = sessions_service.get_session(session_id=session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Could not find session")
|
||||
if session.cartell_url:
|
||||
new_session = dataclasses.replace(
|
||||
session,
|
||||
cartell_url=None,
|
||||
)
|
||||
sessions_service.set_session(new_session)
|
||||
files_service.clean_orphan_files()
|
||||
return cartell.cartell(request, session_id, logged_in)
|
||||
75
folkugat_web/api/routes/sessio/index.py
Normal file
75
folkugat_web/api/routes/sessio/index.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from fastapi import HTTPException, Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import live
|
||||
from folkugat_web.fragments.sessio import page as sessio
|
||||
from folkugat_web.model.sessions import Session
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import sessions as sessions_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
def session_description(session: Session) -> str:
|
||||
date_names = sessions_service.get_date_names(session.date)
|
||||
date_str = f"{date_names.day_name} {date_names.day} {date_names.month_name} de {date_names.year}"
|
||||
location_str = f"A {session.venue.name}" if session.venue else ""
|
||||
time_str = f"de {session.start_time.strftime('%H:%M')} a {session.end_time.strftime('%H:%M')}"
|
||||
location_time_str = " ".join([location_str, time_str])
|
||||
if location_time_str:
|
||||
location_time_str = location_time_str[0].upper() + location_time_str[1:]
|
||||
notes_str = session.notes or ""
|
||||
return "\n".join(filter(bool, [
|
||||
date_str,
|
||||
location_time_str,
|
||||
notes_str,
|
||||
]))
|
||||
|
||||
|
||||
@router.get("/sessio/{session_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
session = sessions_service.get_session(session_id=session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Could not find session")
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Sessió",
|
||||
"page_description": session_description(session),
|
||||
"page_card": session.cartell_url,
|
||||
"content": f"/api/content/sessio/{session_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/sessio/{session_id}")
|
||||
def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
return sessio.pagina(request, session_id, logged_in)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/live")
|
||||
def set_live(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return live.start_live_session(request=request, session_id=session_id)
|
||||
|
||||
|
||||
@router.delete("/api/sessio/{session_id}/live")
|
||||
def stop_live(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return live.stop_live_session(request=request, session_id=session_id)
|
||||
@@ -1,9 +1,11 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import set_page
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/live")
|
||||
def page(
|
||||
47
folkugat_web/api/routes/sessio/notes.py
Normal file
47
folkugat_web/api/routes/sessio/notes.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import dataclasses
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments.sessio import notes
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import sessions as sessions_service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/notes")
|
||||
def get_notes(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
return notes.notes(request, session_id, logged_in)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/notes/editor")
|
||||
def get_notes_editor(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return notes.notes_editor(request, session_id)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/notes")
|
||||
async def set_notes(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
content: Annotated[str, Form()],
|
||||
):
|
||||
session = sessions_service.get_session(session_id=session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Could not find session")
|
||||
new_session = dataclasses.replace(
|
||||
session,
|
||||
notes=content.strip(),
|
||||
)
|
||||
sessions_service.set_session(new_session)
|
||||
return notes.notes(request, session_id, logged_in)
|
||||
9
folkugat_web/api/routes/sessions/__init__.py
Normal file
9
folkugat_web/api/routes/sessions/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import editor, index
|
||||
|
||||
router = get_router()
|
||||
router.include_router(editor.router)
|
||||
router.include_router(index.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -2,10 +2,12 @@ import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Form, Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import sessions
|
||||
from folkugat_web.services import auth
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.post("/api/sessions/editor/")
|
||||
def insert_row(request: Request, _: auth.RequireLogin):
|
||||
@@ -1,10 +1,12 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.config import calendari as calendari_conf
|
||||
from folkugat_web.fragments import live, sessions
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/sessions")
|
||||
def page(request: Request, logged_in: auth.LoggedIn):
|
||||
13
folkugat_web/api/routes/tema/__init__.py
Normal file
13
folkugat_web/api/routes/tema/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import editor, index, links, lyrics, properties, scores
|
||||
|
||||
router = get_router()
|
||||
router.include_router(editor.router)
|
||||
router.include_router(index.router)
|
||||
router.include_router(links.router)
|
||||
router.include_router(lyrics.router)
|
||||
router.include_router(properties.router)
|
||||
router.include_router(scores.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -1,8 +1,10 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import tema
|
||||
from folkugat_web.services import auth
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/editor/title")
|
||||
def title_editor(
|
||||
@@ -1,10 +1,12 @@
|
||||
import dataclasses
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import tema, temes
|
||||
from folkugat_web.model.temes import Tema
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import files as files_service
|
||||
from folkugat_web.services.temes import links as links_service
|
||||
@@ -16,20 +18,56 @@ from folkugat_web.services.temes import write as temes_w
|
||||
from folkugat_web.templates import templates
|
||||
from folkugat_web.utils import FnChain
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
def _build_page_description(tema: Tema) -> str:
|
||||
info_lines = []
|
||||
if composer := tema.composer():
|
||||
info_lines.append(f"Autor: {composer}")
|
||||
if origin := tema.origin():
|
||||
info_lines.append(f"Orígen: {origin}")
|
||||
return "\n".join(info_lines)
|
||||
|
||||
|
||||
@router.get("/tema/{tema_id}")
|
||||
def page(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if not tema:
|
||||
raise HTTPException(status_code=404, detail="Could not find tune")
|
||||
tema = (
|
||||
FnChain.transform(tema) |
|
||||
scores_service.add_scores_to_tema |
|
||||
properties_service.add_properties_to_tema
|
||||
).result()
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"page_title": f"Folkugat - {tema.title}",
|
||||
"page_description": _build_page_description(tema),
|
||||
"page_card": "",
|
||||
"content": f"/api/tema/{tema_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def augment_played_with(tema: Tema) -> Tema:
|
||||
if tema.played_with:
|
||||
tema.played_with = [
|
||||
dataclasses.replace(
|
||||
co_tema,
|
||||
tema=(
|
||||
FnChain.transform(co_tema.tema) |
|
||||
scores_service.add_scores_to_tema |
|
||||
properties_service.add_properties_to_tema
|
||||
).result()
|
||||
) for co_tema in tema.played_with
|
||||
]
|
||||
return tema
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}")
|
||||
def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
@@ -38,6 +76,8 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||
tema = (
|
||||
FnChain.transform(tema) |
|
||||
temes_q.tema_compute_stats |
|
||||
temes_q.tema_compute_played_with |
|
||||
augment_played_with |
|
||||
links_service.add_links_to_tema |
|
||||
lyrics_service.add_lyrics_to_tema |
|
||||
scores_service.add_scores_to_tema |
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi.params import File, Form, Param
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments.tema import links as links_fragments
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.services import auth
|
||||
@@ -11,6 +11,8 @@ from folkugat_web.services import files as files_service
|
||||
from folkugat_web.services.temes import links as links_service
|
||||
from folkugat_web.services.temes import query as temes_q
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/link/{link_id}")
|
||||
def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int):
|
||||
@@ -32,7 +34,7 @@ async def set_link(
|
||||
upload_file: Annotated[UploadFile | None, File()] = None,
|
||||
):
|
||||
if upload_file:
|
||||
url = await files_service.store_file(tema_id=tema_id, upload_file=upload_file)
|
||||
url = await files_service.store_tema_file(tema_id=tema_id, upload_file=upload_file)
|
||||
|
||||
link_type = links_service.guess_link_type(url or '')
|
||||
new_link = model.Link(
|
||||
@@ -63,7 +65,7 @@ def create_link(
|
||||
|
||||
@router.delete("/api/tema/{tema_id}/link/{link_id}")
|
||||
def delete_link(
|
||||
_logged_in: auth.RequireLogin,
|
||||
_: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
link_id: int,
|
||||
):
|
||||
@@ -4,11 +4,13 @@ from typing import Annotated
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments.tema import lyrics as lyrics_fragments
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services.temes import lyrics as lyrics_service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/lyric/{lyric_id}")
|
||||
def lyric(
|
||||
@@ -31,11 +33,25 @@ def set_lyric(
|
||||
lyric_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
content: Annotated[str, Form()],
|
||||
max_columns: Annotated[str | None, Form()] = None,
|
||||
):
|
||||
lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
|
||||
if not lyric:
|
||||
raise HTTPException(status_code=404, detail="Could not find lyric!")
|
||||
new_lyric = lyrics_service.update_lyric(lyric=lyric, title=title, content=content)
|
||||
|
||||
# Parse max_columns from string to int if provided
|
||||
max_columns_int = None
|
||||
if max_columns is not None and max_columns.strip():
|
||||
try:
|
||||
max_columns_int = int(max_columns.strip())
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="max_columns must be a valid integer")
|
||||
|
||||
try:
|
||||
new_lyric = lyrics_service.update_lyric(lyric=lyric, title=title, content=content, max_columns=max_columns_int)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=new_lyric)
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ from typing import Annotated
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments.tema import properties as properties_fragments
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services.temes import properties as properties_service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/property/{property_id}")
|
||||
def property_(request: Request, logged_in: auth.LoggedIn, tema_id: int, property_id: int):
|
||||
@@ -4,7 +4,7 @@ from typing import Annotated
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import temes
|
||||
from folkugat_web.fragments.tema import scores as scores_fragments
|
||||
from folkugat_web.services import auth, files
|
||||
@@ -13,6 +13,8 @@ from folkugat_web.services.lilypond import render as lilypond_render
|
||||
from folkugat_web.services.lilypond import source as lilypond_source
|
||||
from folkugat_web.services.temes import scores as scores_service
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/score/{score_id}")
|
||||
def score(request: Request, logged_in: auth.LoggedIn, tema_id: int, score_id: int):
|
||||
8
folkugat_web/api/routes/temes/__init__.py
Normal file
8
folkugat_web/api/routes/temes/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import index
|
||||
|
||||
router = get_router()
|
||||
router.include_router(index.router)
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -3,11 +3,14 @@ from typing import Annotated
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Param
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import temes
|
||||
from folkugat_web.model.search import Order, OrderBy, OrderParams
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/temes")
|
||||
def page(
|
||||
@@ -15,14 +18,27 @@ def page(
|
||||
logged_in: auth.LoggedIn,
|
||||
query: Annotated[str, Param()] = "",
|
||||
properties: Annotated[list[str] | None, Param()] = None,
|
||||
order_by: OrderBy | None = None,
|
||||
order: Order = Order.DESC,
|
||||
):
|
||||
properties = properties or []
|
||||
content_url = f"/api/content/temes?{temes.build_temes_params(query=query, properties=properties)}"
|
||||
order_params = OrderParams(order_by=order_by, order=order) if order_by else None
|
||||
temes_params = temes.build_temes_params(
|
||||
query=query,
|
||||
properties=properties,
|
||||
order_params=order_params,
|
||||
)
|
||||
content_url = f"/api/content/temes?{temes_params}"
|
||||
if query:
|
||||
additional_description = f" - Resultats per a '{query}'"
|
||||
else:
|
||||
additional_description = ""
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"page_title": "Folkugat - Temes",
|
||||
"page_description": f"Buscador de temes{additional_description}",
|
||||
"content": content_url,
|
||||
"logged_in": logged_in,
|
||||
"animate": False,
|
||||
@@ -36,13 +52,19 @@ def content(
|
||||
logged_in: auth.LoggedIn,
|
||||
query: Annotated[str, Param()] = "",
|
||||
properties: Annotated[list[str] | None, Param()] = None,
|
||||
order_by: OrderBy | None = None,
|
||||
order: Order = Order.DESC,
|
||||
):
|
||||
properties = properties or []
|
||||
if not query and not order_by:
|
||||
order_by = OrderBy.TIMES_PLAYED
|
||||
order_params = OrderParams(order_by=order_by, order=order) if order_by else None
|
||||
return temes.temes_pagina(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
query=query,
|
||||
properties=properties,
|
||||
order_params=order_params,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,13 +74,17 @@ def busca(
|
||||
logged_in: auth.LoggedIn,
|
||||
query: Annotated[str, Param()],
|
||||
properties: Annotated[list[str] | None, Param()] = None,
|
||||
order_by: OrderBy | None = None,
|
||||
order: Order = Order.DESC,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
):
|
||||
order_params = OrderParams(order_by=order_by, order=order) if order_by else None
|
||||
return temes.temes_busca(
|
||||
request=request,
|
||||
query=query,
|
||||
properties=properties or [],
|
||||
order_params=order_params,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
logged_in=logged_in,
|
||||
@@ -1 +0,0 @@
|
||||
from . import index, live, set_page
|
||||
@@ -1,199 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Form, Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.fragments import live, sessio
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services.temes import write as temes_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
@router.get("/sessio/{session_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"content": f"/api/content/sessio/{session_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/sessio/{session_id}")
|
||||
def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
):
|
||||
return sessio.pagina(request, session_id, logged_in)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/live")
|
||||
def set_live(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return live.start_live_session(request=request, session_id=session_id)
|
||||
|
||||
|
||||
@router.delete("/api/sessio/{session_id}/live")
|
||||
def stop_live(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return live.stop_live_session(request=request, session_id=session_id)
|
||||
|
||||
|
||||
@router.post("/api/sessio/{session_id}/set")
|
||||
def add_set(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
):
|
||||
return sessio.add_set(request=request, session_id=session_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/set/{set_id}")
|
||||
def get_set(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return sessio.get_set(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.delete("/api/sessio/{session_id}/set/{set_id}")
|
||||
def delete_set(
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return sessio.delete_set(session_id=session_id, set_id=set_id)
|
||||
|
||||
|
||||
@router.post("/api/sessio/{session_id}/set/{set_id}")
|
||||
def add_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return sessio.add_tema(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
|
||||
def get_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return sessio.get_tema(
|
||||
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/editor")
|
||||
def get_tema_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return sessio.get_tema_editor(
|
||||
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.delete("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
|
||||
def delete_tema(
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return sessio.delete_tema(session_id=session_id, set_id=set_id, entry_id=entry_id)
|
||||
|
||||
|
||||
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/busca")
|
||||
def busca_tema(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
query: str,
|
||||
):
|
||||
return sessio.busca_tema(
|
||||
request=request,
|
||||
session_id=session_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
query=query,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
tema_id: Annotated[int, Form()],
|
||||
):
|
||||
return sessio.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
session_id=session_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=tema_id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/unknown")
|
||||
def set_tema_unknown(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
):
|
||||
return sessio.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
session_id=session_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema_new(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
):
|
||||
new_tema = temes_service.create_tema(title=title)
|
||||
return sessio.set_tema(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
session_id=session_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=new_tema.id,
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.fragments import set_page
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
@router.get("/sessio/{session_id}/set/{set_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"content": f"/api/content/sessio/{session_id}/set/{set_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/sessio/{session_id}/set/{set_id}")
|
||||
async def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
session_id: int,
|
||||
set_id: int,
|
||||
):
|
||||
return await set_page.pagina(request, session_id, set_id, logged_in)
|
||||
@@ -1 +0,0 @@
|
||||
from . import editor, index
|
||||
@@ -1 +0,0 @@
|
||||
from . import editor, index, links, lyrics, properties, scores
|
||||
@@ -1 +0,0 @@
|
||||
from . import index
|
||||
File diff suppressed because one or more lines are too long
BIN
folkugat_web/assets/static/img/folkugat_card.jpg
Normal file
BIN
folkugat_web/assets/static/img/folkugat_card.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
@@ -1,8 +1,10 @@
|
||||
<div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center">
|
||||
<a href="/">
|
||||
<img src="{{ url_for(request, 'static', path='img/folkugat.svg') }}"
|
||||
class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
|
||||
width="100"
|
||||
alt="Folkugat"/>
|
||||
<h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">{{ page_title }}</h1>
|
||||
</a>
|
||||
<h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">Folkugat</h1>
|
||||
<h2 class="text-center sm:text-3xl m-6 {% if animate %} opacity-0 animate-fade-in-two {% endif %}">Sessions de folk a Sant Cugat</h2>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="flex flex-row flex-wrap justify-center"
|
||||
id="llista-name">
|
||||
<form>
|
||||
<input name="name"
|
||||
placeholder="Nom de la playlist"
|
||||
value="{{ playlist.name if playlist.name else '' }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded text-3xl
|
||||
bg-brown p-2 m-0"
|
||||
/>
|
||||
<button title="Desa"
|
||||
class="text-beige text-3xl mx-1"
|
||||
hx-put="/api/llista/{{ playlist.id }}/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Descarta"
|
||||
class="text-beige text-3xl mx-1"
|
||||
hx-get="/api/llista/{{ playlist.id }}/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
20
folkugat_web/assets/templates/fragments/llista/name.html
Normal file
20
folkugat_web/assets/templates/fragments/llista/name.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="flex flex-row flex-wrap justify-center"
|
||||
id="llista-name">
|
||||
<h3 class="text-3xl text-center p-4">
|
||||
{% if playlist.name %}
|
||||
{{ playlist.name }}
|
||||
{% else %}
|
||||
Llista de temes
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% if logged_in %}
|
||||
<button title="Canvia el nom"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-get="/api/llista/{{ playlist.id }}/editor/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% include "fragments/llista/visibility.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
14
folkugat_web/assets/templates/fragments/llista/pagina.html
Normal file
14
folkugat_web/assets/templates/fragments/llista/pagina.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
<div class="flex justify-center">
|
||||
<div class="m-12 grow max-w-4xl">
|
||||
<div id="playlist-name">
|
||||
{% include "fragments/llista/name.html" %}
|
||||
</div>
|
||||
<div class="text-left">
|
||||
{% set playlist_id = playlist.id %}
|
||||
{% set playlist = playlist %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% include "fragments/llista/score.html" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,13 @@
|
||||
<ul id="playlist-{{ session.id }}" class="">
|
||||
<ul id="llista-{{ playlist_id }}" class="">
|
||||
{% for set_entry in playlist.sets %}
|
||||
{% set set_id = set_entry.id %}
|
||||
{% include "fragments/sessio/set_entry.html" %}
|
||||
{% endfor %}
|
||||
{% include "fragments/llista/set_entry.html" %} {% endfor %}
|
||||
</ul>
|
||||
{% if logged_in %}
|
||||
<div class="flex flex-col items-center">
|
||||
<button class="text-beige mt-2"
|
||||
hx-post="/api/sessio/{{ session.id }}/set"
|
||||
hx-target="#playlist-{{ session.id }}"
|
||||
hx-post="/api/llista/{{ playlist_id }}/set"
|
||||
hx-target="#llista-{{ playlist_id }}"
|
||||
hx-swap="beforeend transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix set
|
||||
@@ -0,0 +1,9 @@
|
||||
{% if playlist.playlist_score is not none and playlist.playlist_score.pdf_url is not none %}
|
||||
<div class="flex flex-col items-center mt-4 mb-6">
|
||||
<div class="bg-beige border rounded border-beige m-2 p-2">
|
||||
<a href="{{ playlist.playlist_score.pdf_url }}" target="_blank" class="text-white">
|
||||
Obre en PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
{% include "fragments/llista/set/set_page.html" %}
|
||||
@@ -13,14 +13,14 @@
|
||||
{% for tema_in_set in set.temes %}
|
||||
{% if tema_in_set.tema is not none %}
|
||||
{% set tema = tema_in_set.tema %}
|
||||
{% include "fragments/sessio/set/tema_title.html" %}
|
||||
{% include "fragments/llista/set/tema_title.html" %}
|
||||
{% else %}
|
||||
<h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mx-12">
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
{% include "fragments/sessio/set/set_score.html"%}
|
||||
{% include "fragments/llista/set/set_score.html"%}
|
||||
</div>
|
||||
{% else %}
|
||||
{% for tema_in_set in set.temes %}
|
||||
@@ -31,7 +31,7 @@
|
||||
{% endif %}
|
||||
{% if tema_in_set.tema is not none %}
|
||||
{% set tema = tema_in_set.tema %}
|
||||
{% include "fragments/sessio/set/tema.html"%}
|
||||
{% include "fragments/llista/set/tema.html"%}
|
||||
{% else %}
|
||||
<h3 class="text-center text-3xl p-4">
|
||||
<i>Desconegut</i>
|
||||
@@ -1,4 +1,4 @@
|
||||
{% include "fragments/sessio/set/tema_title.html" %}
|
||||
{% include "fragments/llista/set/tema_title.html" %}
|
||||
<div class="mx-12 text-left">
|
||||
|
||||
{% if tema.main_score() is not none %}
|
||||
@@ -0,0 +1,61 @@
|
||||
<li class="flex flex-row grow items-center
|
||||
m-4 rounded-lg bg-white
|
||||
px-2 py-1 my-1"
|
||||
id="set-entry-{{ set_id }}"
|
||||
hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="reload-set-{{ set_id }}">
|
||||
{% if set_entry.temes | length > 1 or not set_entry.temes[0].tema %}
|
||||
{% set set_url = "/llista/%d/set/%d" | format(playlist_id, set_id) %}
|
||||
{% else %}
|
||||
{% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %}
|
||||
{% endif %}
|
||||
{% if logged_in %}
|
||||
<div class="flex flex-row grow items-center">
|
||||
{% else %}
|
||||
<a href="{{ set_url }}"
|
||||
class="flex flex-row grow items-center cursor-pointer"
|
||||
title="Mostra els temes">
|
||||
{% endif %}
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex flex-col
|
||||
items-start
|
||||
py-2 mx-2
|
||||
w-full max-w-[655px]">
|
||||
{% if logged_in %}
|
||||
<button class="text-beige w-full"
|
||||
hx-delete="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
Esborra el set
|
||||
</button>
|
||||
{% endif %}
|
||||
<ol id="set-entry-{{ set_id }}-list"
|
||||
class="flex flex-col items-start w-full">
|
||||
{% for tema_entry in set_entry.temes %}
|
||||
{% if new_entry %}
|
||||
{% include "fragments/llista/tema_editor.html" %}
|
||||
{% else %}
|
||||
{% include "fragments/llista/tema_entry.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% if logged_in %}
|
||||
<button class="text-beige mt-2 w-full"
|
||||
hx-post="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}-list"
|
||||
hx-swap="beforeend transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix un tema
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-1 text-beige">
|
||||
{% if logged_in %}<a href="{{ set_url }}" class="text-beige">{% endif %}
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
{% if logged_in %}</a>{% endif %}
|
||||
</div>
|
||||
{% if logged_in %}</div>{% else %}</a>{% endif %}
|
||||
</li>
|
||||
@@ -1,5 +1,5 @@
|
||||
<li id="tune-entry-{{ tema_entry.id }}"
|
||||
class="flex flex-col items-center my-1">
|
||||
class="flex flex-col items-center my-1 w-full">
|
||||
<div class="flex flex-row">
|
||||
<input
|
||||
name="query"
|
||||
@@ -10,15 +10,15 @@
|
||||
{% endif %}
|
||||
placeholder="Busca un tema..."
|
||||
class="border border-beige focus:outline-none
|
||||
rounded text-center
|
||||
bg-brown p-1 m-1"
|
||||
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
|
||||
rounded text-center text-black
|
||||
p-1 m-1"
|
||||
hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
|
||||
hx-trigger="revealed, keyup delay:500ms changed"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}-search-results"
|
||||
hx-swap="outerHTML"/>
|
||||
<button title="Descarta els canvis"
|
||||
class="text-beige mx-1"
|
||||
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
@@ -0,0 +1,39 @@
|
||||
<li id="tune-entry-{{ tema_entry.id }}"
|
||||
class="flex flex-col w-full">
|
||||
<div class="flex flex-row my-1 w-full">
|
||||
<div class="flex-1 min-w-0
|
||||
text-black font-bold
|
||||
break-words block">
|
||||
{% if tema_entry.tema %}
|
||||
{{ tema_entry.tema.title }}
|
||||
{% else %}
|
||||
<i class="fa fa-question" aria-hidden="true"></i>
|
||||
<i>(Desconegut)</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if logged_in %}
|
||||
<div class="flex-none flex flex-row shrink-0">
|
||||
<button title="Edita el tema"
|
||||
class="text-beige mx-1"
|
||||
hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Esborra el tema"
|
||||
class="text-beige mx-1"
|
||||
hx-delete="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="my-1 w-full">
|
||||
{% if tema_entry.tema and tema_entry.tema.main_score() and tema_entry.tema.main_score().preview_url %}
|
||||
<img class="pb-2"
|
||||
src="{{ tema_entry.tema.main_score().preview_url }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
@@ -4,7 +4,7 @@
|
||||
<li>
|
||||
<button class="bg-beige text-brown rounded
|
||||
m-1 px-2"
|
||||
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-put="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-vals='{"tema_id": "{{ tema.id }}"}'
|
||||
hx-target="#tune-entry-{{ entry_id }}"
|
||||
hx-swap="outerHTML">
|
||||
@@ -15,7 +15,7 @@
|
||||
<li>
|
||||
<button class="border border-beige text-beige rounded
|
||||
m-1 px-2"
|
||||
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}/unknown"
|
||||
hx-put="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}/unknown"
|
||||
hx-target="#tune-entry-{{ entry_id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-question" aria-hidden="true"></i>
|
||||
@@ -25,7 +25,7 @@
|
||||
<li>
|
||||
<button class="border border-beige text-beige rounded
|
||||
m-1 px-2"
|
||||
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-post="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-vals='{"title": "{{ query }}"}'
|
||||
hx-target="#tune-entry-{{ entry_id }}"
|
||||
hx-swap="outerHTML">
|
||||
@@ -0,0 +1,15 @@
|
||||
{% if not playlist.hidden %}
|
||||
<button title="Llista visible (amaga-la)"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/invisible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Llista invisible (mostra-la)"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/visible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
26
folkugat_web/assets/templates/fragments/llistes/pagina.html
Normal file
26
folkugat_web/assets/templates/fragments/llistes/pagina.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
<div class="p-12 text-center">
|
||||
<h3 class="text-3xl text-beige p-4">Llistes de Repertori</h3>
|
||||
{% if logged_in %}
|
||||
<button title="Afegeix una llista"
|
||||
class="text-beige m-2"
|
||||
hx-post="/api/llistes/editor/"
|
||||
hx-target="#{{ playlist_list_id }}"
|
||||
hx-swap="afterbegin transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix una llista
|
||||
</button>
|
||||
{% endif %}
|
||||
<div id="{{ playlist_list_id }}"
|
||||
class="flex flex-col items-center">
|
||||
{% if playlists %}
|
||||
{% for playlist in playlists %}
|
||||
{% include "fragments/llistes/playlist_entry.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<p class="text-beige/70">No hi ha llistes disponibles.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="
|
||||
border rounded border-beige
|
||||
p-2 m-2 w-full max-w-xl
|
||||
relative"
|
||||
id="playlist-entry-{{ playlist.id }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<p class="text-xl text-white">
|
||||
<a href="/llista/{{ playlist.id }}">
|
||||
{% if playlist.name %}
|
||||
{{ playlist.name }}
|
||||
{% else %}
|
||||
Llista de temes
|
||||
{% endif %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if logged_in %}
|
||||
<div class="ml-auto">
|
||||
{% include "fragments/llistes/playlist_visibility.html" %}
|
||||
<button title="Esborra la llista"
|
||||
class="text-beige mx-1"
|
||||
hx-delete="/api/llistes/{{ playlist.id }}"
|
||||
hx-target="#playlist-entry-{{ playlist.id }}"
|
||||
hx-swap="outerHTML swap:0.5s">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% if not playlist.hidden %}
|
||||
<button title="Llista visible (amaga-la)"
|
||||
class="text-beige mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/invisible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Llista invisible (mostra-la)"
|
||||
class="text-beige mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/visible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -27,6 +27,19 @@
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{% if menu_selected_id == Pages.Llistes %}
|
||||
<li class="p-2 text-beige">
|
||||
<button>
|
||||
{% else %}
|
||||
<li class="p-2">
|
||||
<button hx-get="/api/content/llistes"
|
||||
hx-replace-url="/llistes"
|
||||
hx-target="#content">
|
||||
{% endif %}
|
||||
Llistes
|
||||
</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<hr class="h-px bg-beige border-0">
|
||||
<div hx-get="/api/sessions/live"
|
||||
|
||||
36
folkugat_web/assets/templates/fragments/sessio/cartell.html
Normal file
36
folkugat_web/assets/templates/fragments/sessio/cartell.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div id="cartell"
|
||||
class="flex flex-col items-center">
|
||||
{% if session.cartell_url %}
|
||||
<div class="max-w-[655px] w-full
|
||||
flex flex-col items-center">
|
||||
{% if logged_in %}
|
||||
<div class="my-2 w-full text-beige text-right">
|
||||
<button title="Modifica el cartell"
|
||||
class="mx-1"
|
||||
hx-get="/api/sessio/{{ session_id }}/cartell/editor"
|
||||
hx-target="#cartell"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Esborra el cartell"
|
||||
class="mx-1"
|
||||
hx-delete="/api/sessio/{{ session_id }}/cartell"
|
||||
hx-target="#cartell"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<img class="w-full h-auto"
|
||||
src="{{ session.cartell_url }}"/>
|
||||
</div>
|
||||
{% elif logged_in %}
|
||||
<button class="text-beige mt-2"
|
||||
hx-get="/api/sessio/{{ session_id }}/cartell/editor"
|
||||
hx-target="#cartell"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix cartell
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<form id="cartell-editor"
|
||||
class="flex flex-row gap-2"
|
||||
hx-encoding="multipart/form-data">
|
||||
<input type='file'
|
||||
class="border border-beige focus:outline-none
|
||||
rounded grow
|
||||
bg-brown p-1 my-1"
|
||||
name='upload_file'/>
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1 text-beige"
|
||||
hx-put="/api/sessio/{{ session_id }}/cartell"
|
||||
hx-target="#cartell-editor"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Descarta els canvis"
|
||||
class="mx-1 text-beige"
|
||||
hx-get="/api/sessio/{{ session_id }}/cartell"
|
||||
hx-target="#cartell-editor"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
13
folkugat_web/assets/templates/fragments/sessio/notes.html
Normal file
13
folkugat_web/assets/templates/fragments/sessio/notes.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<h4 class="py-4 text-xl text-beige">
|
||||
Notes
|
||||
{% if logged_in %}
|
||||
<button title="Edita les notes"
|
||||
class="mx-1"
|
||||
hx-get="/api/sessio/{{ session_id }}/notes/editor"
|
||||
hx-target="#notes"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</h4>
|
||||
{{ notes | safe }}
|
||||
@@ -0,0 +1,28 @@
|
||||
<h4 class="py-4 text-xl text-beige">Notes</h4>
|
||||
<form id="notes-form"
|
||||
class="w-full">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1"
|
||||
hx-put="/api/sessio/{{session_id}}/notes"
|
||||
hx-target="#notes"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Descarta els canvis"
|
||||
class="mx-1"
|
||||
hx-get="/api/sessio/{{session_id}}/notes"
|
||||
hx-target="#notes"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
<textarea name="content"
|
||||
placeholder="Notes"
|
||||
rows="{{ max(notes.count('\n') + 1, 5) }}"
|
||||
class="border border-beige focus:outline-none
|
||||
w-full text-left
|
||||
rounded
|
||||
bg-brown p-2 m-0">{{notes}}</textarea>
|
||||
</form>
|
||||
@@ -12,6 +12,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% include "fragments/sessio/cartell.html" %}
|
||||
<div class="text-left">
|
||||
<h4 class="pt-4 text-xl text-beige">Horari i lloc</h4>
|
||||
De {{ session.start_time.strftime("%H:%M") }}
|
||||
@@ -29,10 +30,35 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if logged_in or playlist.sets %}
|
||||
{% if logged_in or session.notes %}
|
||||
<div id="notes"
|
||||
class="text-left">
|
||||
{% set notes = session.notes if session.notes else "" %}
|
||||
{% include "fragments/sessio/notes.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if logged_in or (session.slowjam and session.slowjam.sets) %}
|
||||
<div class="text-left">
|
||||
<h4 class="pt-4 text-xl text-beige mt-2">Temes tocats</h4>
|
||||
{% include "fragments/sessio/playlist.html" %}
|
||||
<h4 class="py-4 text-xl text-beige mt-2">
|
||||
<a href="/llista/{{ session.slowjam.id }}" class="text-beige">
|
||||
Slow Jam
|
||||
</a>
|
||||
</h4>
|
||||
{% set playlist_id = session.slowjam.id %}
|
||||
{% set playlist = session.slowjam %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if logged_in or (session.setlist and session.setlist.sets) %}
|
||||
<div class="text-left">
|
||||
<h4 class="py-4 text-xl text-beige mt-2">
|
||||
<a href="/llista/{{ session.setlist.id }}" class="text-beige">
|
||||
Temes tocats
|
||||
</a>
|
||||
</h4>
|
||||
{% set playlist_id = session.setlist.id %}
|
||||
{% set playlist = session.setlist %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
{% include "fragments/sessio/set/set_page.html" %}
|
||||
@@ -1,46 +0,0 @@
|
||||
<li class="flex flex-row grow items-center
|
||||
border border-beige rounded
|
||||
px-2 py-1 my-1"
|
||||
id="set-entry-{{ set_id }}"
|
||||
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="reload-set-{{ set_id }}">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
{% if logged_in %}
|
||||
<button class="text-beige mt-2"
|
||||
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
Esborra el set
|
||||
</button>
|
||||
{% endif %}
|
||||
<ol id="set-entry-{{ set_id }}-list"
|
||||
class="flex flex-col items-center">
|
||||
{% for tema_entry in set_entry.temes %}
|
||||
{% if new_entry %}
|
||||
{% include "fragments/sessio/tema_editor.html" %}
|
||||
{% else %}
|
||||
{% include "fragments/sessio/tema_entry.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% if logged_in %}
|
||||
<button class="text-beige mt-2"
|
||||
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}-list"
|
||||
hx-swap="beforeend transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix un tema
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a title="Mostra els temes"
|
||||
class="text-beige"
|
||||
href="/sessio/{{ session_id }}/set/{{ set_id }}">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1,29 +0,0 @@
|
||||
<li id="tune-entry-{{ tema_entry.id }}"
|
||||
class="flex flex-row my-1">
|
||||
<div class="mx-1 text-center">
|
||||
{% if tema_entry.tema is none %}
|
||||
<i class="fa fa-question" aria-hidden="true"></i>
|
||||
<i>(Desconegut)</i>
|
||||
{% else %}
|
||||
<a href="/sessio/{{ session_id }}/set/{{ set_id }}">
|
||||
{{ tema_entry.tema.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if logged_in %}
|
||||
<button title="Edita el tema"
|
||||
class="text-beige mx-1"
|
||||
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Esborra el tema"
|
||||
class="text-beige mx-1"
|
||||
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
@@ -4,7 +4,7 @@
|
||||
class="border border-beige focus:outline-none
|
||||
rounded grow
|
||||
bg-brown p-1 my-1"
|
||||
name='upload_file'>
|
||||
name='upload_file'/>
|
||||
<button title="Afegeix un enllaç"
|
||||
class="border border-beige rounded px-2 py-1 my-1"
|
||||
hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
rounded
|
||||
bg-brown px-2 "
|
||||
/>
|
||||
<input name="max_columns"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
placeholder="3 (default)"
|
||||
value="{{ lyric.max_columns or '' }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded
|
||||
bg-brown px-2 ml-2 w-16 "
|
||||
/>
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<div id="tema-lyric-{{ lyric.id }}">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
{{ lyric.title }}
|
||||
{% if logged_in and lyric.max_columns %}
|
||||
<span class="text-xs ml-2">{{ lyric.max_columns }} columnes</span>
|
||||
{% endif %}
|
||||
{% if logged_in %}
|
||||
<button title="Modifica la lletra"
|
||||
class="mx-1"
|
||||
@@ -20,6 +23,6 @@
|
||||
</h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
<div class="text-center">
|
||||
{{ lyric.content.replace('\n', '<br>') | safe }}
|
||||
{{ (lyric.content.replace('~', '')).replace('\n', '<br>') | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<li class="flex flex-col
|
||||
items-center
|
||||
m-4 rounded-lg
|
||||
bg-white">
|
||||
<div class="flex flex-col items-start py-4 text-left w-full max-w-[655px]">
|
||||
<div class="px-4 text-black font-bold">
|
||||
<a href="/tema/{{ tema.id }}">
|
||||
{{ tema.title }}
|
||||
</a>
|
||||
</div>
|
||||
{% if tema.properties %}
|
||||
<ul class="flex flex-wrap text-sm px-3">
|
||||
{% for property in tema.properties %}
|
||||
<a class="bg-beige text-white rounded
|
||||
m-1 px-2"
|
||||
href="/temes?properties={{ property.value }}">
|
||||
{{ property.value }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if tema.main_score() and tema.main_score().preview_url %}
|
||||
<a href="/tema/{{ tema.id }}">
|
||||
<img class="px-4 pb-2"
|
||||
src="{{ tema.main_score().preview_url }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-row items-center
|
||||
row-0 px-4 text-sm text-gray-400">
|
||||
<i class="mx-1">{% include "icons/music-box.svg" %}</i>
|
||||
{% if co_tema.count == 1 %}
|
||||
1 cop
|
||||
{% else %}
|
||||
{{ co_tema.count }} cops
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1,11 +1,12 @@
|
||||
<div id="tema-property-{{ property.id }}"
|
||||
class="flex flex-row">
|
||||
class="flex flex-row items-center">
|
||||
<div class="px-2">
|
||||
<i>{{ property.field.value.capitalize() }}:</i>
|
||||
</div>
|
||||
<div>
|
||||
<a class="bg-beige text-white rounded m-1 px-2"
|
||||
href="/temes?query=&properties={{ property.value }}">
|
||||
{{ property.value }}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{% if logged_in %}
|
||||
<div class="grow"></div>
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
<h4 class="pt-4 text-xl mt-3 text-beige">Estadístiques</h4>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
{% if tema.stats %}
|
||||
<h4 class="pt-4 text-xl mt-3 text-beige">Estadístiques</h4>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
<div>
|
||||
<div class="flex flex-col">
|
||||
{% if tema.played_with %}
|
||||
<div class="my-2 flex flex-row items-center">
|
||||
<i class="mx-2 flex-none">{% include "icons/notes-small.svg" %}</i>
|
||||
<p>
|
||||
Aquest tema ha sigut tocat en
|
||||
S'ha tocat juntament amb:
|
||||
</p>
|
||||
</div>
|
||||
<ol class="ml-4 flex flex-col justify-center">
|
||||
{% for co_tema in tema.played_with %}
|
||||
{% set tema = co_tema.tema %}
|
||||
{% include "fragments/tema/played_with.html" %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
<div class="my-2 flex flex-row items-center">
|
||||
<i class="mx-2 flex-none">{% include "icons/music-box.svg" %}</i>
|
||||
<p>
|
||||
S'ha tocat en
|
||||
{% if tema.stats.times_played == 1%}
|
||||
una sessió.
|
||||
una sessió:
|
||||
{% else %}
|
||||
{{ tema.stats.times_played }} sessions.
|
||||
{{ tema.stats.times_played }} sessions:
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="py-2">
|
||||
S'ha tocat a les sessions següents:
|
||||
<ol class="flex flex-col items-center justify-center">
|
||||
</div>
|
||||
<ol class="ml-4 flex flex-col justify-center">
|
||||
{% for session in tema.stats.sessions_played %}
|
||||
<li class="border rounded border-beige
|
||||
flex flex-row grow
|
||||
p-2 m-2 w-full max-w-xl
|
||||
relative">
|
||||
<a href="/session/{{session.id}}">
|
||||
<div class="flex flex-row grow items-center">
|
||||
<div class="flex-1">
|
||||
<a href="/sessio/{{session.id}}/">
|
||||
{% set dn = date_names(session.date) %}
|
||||
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
|
||||
<li class="flex flex-row grow
|
||||
my-1">
|
||||
<a class="flex flex-row grow items-center"
|
||||
href="/sessio/{{session.id}}">
|
||||
<i class="mx-2 text-beige flex-none">{% include "icons/calendar.svg" %}</i>
|
||||
<p>
|
||||
{% set dn = date_names(session.date) %} {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a title="Més informació"
|
||||
class="text-beige mx-1"
|
||||
href="/sessio/{{session.id}}/">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<i>No s'ha tocat a cap jam (encara)</i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
<div class="p-12 text-center flex flex-col items-center justify-center">
|
||||
<h3 class="text-3xl text-beige p-4">Temes</h3>
|
||||
{% set hx_vars = build_hx_vars({"properties": properties_str}, order_params_dict) %}
|
||||
<input id="query-input"
|
||||
type="text" name="query" value="{{ query }}"
|
||||
placeholder="Busca una tema..."
|
||||
@@ -11,7 +12,7 @@
|
||||
hx-get="/api/temes/busca"
|
||||
hx-trigger="revealed, keyup delay:500ms changed"
|
||||
hx-target="#search-results"
|
||||
hx-vars="properties:{{ properties_str }}"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-swap="outerHTML">
|
||||
<div id="search-results" class="flex-col"></div>
|
||||
</div>
|
||||
|
||||
@@ -2,28 +2,22 @@
|
||||
items-center
|
||||
m-4 rounded-lg
|
||||
bg-white">
|
||||
<div class="py-2 px-4 text-brown font-bold">
|
||||
<div class="flex flex-col items-start py-4 text-left w-full max-w-[655px]">
|
||||
<div class="px-4 text-black font-bold">
|
||||
<a href="/tema/{{ tema.id }}">
|
||||
{{ tema.title }}
|
||||
</a>
|
||||
</div>
|
||||
{% if tema.main_score() and tema.main_score().preview_url %}
|
||||
<a href="/tema/{{ tema.id }}"
|
||||
class="max-w">
|
||||
<img class="p-2 max-w"
|
||||
src="{{ tema.main_score().preview_url }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if tema.properties %}
|
||||
<ul class="flex flex-wrap text-sm
|
||||
py-2 px-4">
|
||||
<ul class="flex flex-wrap text-sm px-3">
|
||||
{% for property in tema.properties %}
|
||||
{% set hx_vars = build_hx_vars({"properties": add_property_str(property.value)}, order_params_dict) %}
|
||||
<button class="bg-beige text-white rounded
|
||||
m-1 px-2"
|
||||
hx-get="/api/content/temes"
|
||||
hx-target="#content"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="properties:{{ add_property_str(property.value) }}"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ property.value }}
|
||||
@@ -31,4 +25,36 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if tema.main_score() and tema.main_score().preview_url %}
|
||||
<a href="/tema/{{ tema.id }}">
|
||||
<img class="px-4 pb-2"
|
||||
src="{{ tema.main_score().preview_url }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-row items-center
|
||||
row-0 px-4 text-sm text-gray-400">
|
||||
<i class="mx-1">{% include "icons/music-box.svg" %}</i>
|
||||
{% if tema.stats %}
|
||||
Tocat {{ tema.stats.times_played }}
|
||||
{% if tema.stats.times_played == 1 %}
|
||||
cop
|
||||
{% else %}
|
||||
cops
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No s'ha tocat mai
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-1 grow"></div>
|
||||
<div class="flex flex-row items-center
|
||||
row-0 px-4 text-sm text-gray-400">
|
||||
{% if tema.stats and tema.stats.sessions_played %}
|
||||
<i class="mx-1">{% include "icons/calendar.svg" %}</i>
|
||||
{% set dn = get_date_names(tema.stats.sessions_played[0].date) %}
|
||||
{{ dn.day }} {{ dn.month_name }} de {{ dn.year }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -12,38 +12,14 @@
|
||||
<i>{{ query }}</i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<ul class="flex flex-wrap justify-center my-4">
|
||||
{% for property in properties %}
|
||||
<li>
|
||||
<button class="bg-beige rounded
|
||||
text-white
|
||||
m-1 px-2"
|
||||
hx-get="/api/content/temes"
|
||||
hx-target="#content"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="properties:{{ remove_property_str(property) }}"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ property }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for property in property_results %}
|
||||
<li>
|
||||
<button class="border border-beige rounded
|
||||
text-white
|
||||
m-1 px-2"
|
||||
hx-get="/api/content/temes"
|
||||
hx-target="#content"
|
||||
hx-vars="properties:{{ add_property_str(property) }}"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{{ property }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
|
||||
<div class="mx-4 flex flex-col">
|
||||
{% include "fragments/temes/search_params/order_by.html" %}
|
||||
{% include "fragments/temes/search_params/filter.html" %}
|
||||
</div>
|
||||
|
||||
<hr class="h-px mt-3 mb-3 bg-beige border-0">
|
||||
<ul class="min-w-full w-full">
|
||||
{% for tema in temes %}
|
||||
{% include "fragments/temes/result.html" %}
|
||||
@@ -52,16 +28,22 @@
|
||||
{% if prev_offset is not none or next_offset is not none %}
|
||||
<div class="py-2">
|
||||
{% if prev_offset is not none %}
|
||||
{% set hx_vars = build_hx_vars({"properties": properties_str, "offset": prev_offset}, order_params_dict) %}
|
||||
<button class="text-beige mx-2"
|
||||
hx-get="/api/temes/busca?query={{ query }}&offset={{ prev_offset }}"
|
||||
hx-get="/api/temes/busca"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if next_offset is not none %}
|
||||
{% set hx_vars = build_hx_vars({"properties": properties_str, "offset": next_offset}, order_params_dict) %}
|
||||
<button class="text-beige mx-2"
|
||||
hx-get="/api/temes/busca?query={{ query }}&offset={{ next_offset }}"
|
||||
hx-get="/api/temes/busca"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{% if properties or property_results %}
|
||||
<div class="flex flex-row items-center">
|
||||
<p class="pr-2 py-2
|
||||
{% if properties %} text-beige {% else %} text-gray-400 {% endif %}
|
||||
">Filtres:</p>
|
||||
<ul class="flex flex-wrap justify-center my-1">
|
||||
{% for property in properties %}
|
||||
<li>
|
||||
{% set hx_vars = build_hx_vars({"properties": remove_property_str(property)}, order_params_dict) %}
|
||||
<button class="bg-beige rounded
|
||||
text-white
|
||||
m-1 px-2"
|
||||
hx-get="/api/content/temes"
|
||||
hx-target="#content"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-swap="innerhtml"
|
||||
>
|
||||
{{ property }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for property in property_results %}
|
||||
<li>
|
||||
{% set hx_vars = build_hx_vars({"properties": add_property_str(property)}, order_params_dict) %}
|
||||
<button class="border border-beige rounded
|
||||
text-white
|
||||
m-1 px-2"
|
||||
hx-get="/api/content/temes"
|
||||
hx-target="#content"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-swap="innerhtml"
|
||||
>
|
||||
{{ property }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,67 @@
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
{% set times_played_color = "text-gray-400" %}
|
||||
{% set times_played_caret = "off" %}
|
||||
{% set times_played_target_params = {"order_by": "'times_played'", "order": "'desc'"} %}
|
||||
|
||||
{% set last_played_color = "text-gray-400" %}
|
||||
{% set last_played_caret = "off" %}
|
||||
{% set last_played_target_params = {"order_by": "'last_played'", "order": "'desc'"} %}
|
||||
|
||||
{% if order_params is not none %}
|
||||
{% if order_params.order_by == OrderBy.TIMES_PLAYED %}
|
||||
{% set times_played_color = "text-beige" %}
|
||||
{% if order_params.order == Order.ASC %}
|
||||
{% set times_played_caret = "asc" %}
|
||||
{% set times_played_target_params = {} %}
|
||||
{% elif order_params.order == Order.DESC %}
|
||||
{% set times_played_caret = "desc" %}
|
||||
{% set times_played_target_params = {"order_by": "'times_played'", "order": "'asc'"} %}
|
||||
{% endif %}
|
||||
{% elif order_params.order_by == OrderBy.LAST_PLAYED %}
|
||||
{% set last_played_color = "text-beige" %}
|
||||
{% if order_params.order == Order.ASC %}
|
||||
{% set last_played_caret = "asc" %}
|
||||
{% set last_played_target_params = {} %}
|
||||
{% elif order_params.order == Order.DESC %}
|
||||
{% set last_played_caret = "desc" %}
|
||||
{% set last_played_target_params = {"order_by": "'last_played'", "order": "'asc'"} %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% set hx_vars = build_hx_vars({"properties": properties_str}, times_played_target_params) %}
|
||||
<button title="Ordena per cops tocat"
|
||||
class="{{ times_played_color }} mx-2 flex flex-row"
|
||||
hx-get="/api/temes/busca"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML">
|
||||
<i class="mr-1 flex-none">{% include "icons/music-box.svg" %}</i>
|
||||
{% if times_played_caret == "asc" %}
|
||||
<i class="fa fa-caret-up flex-1 w-0" aria-hidden="true"></i>
|
||||
{% elif times_played_caret == "desc" %}
|
||||
<i class="fa fa-caret-down flex-1 w-0" aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<div class="flex-1 w-0"></div>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
{% set hx_vars = build_hx_vars({"properties": properties_str}, last_played_target_params) %}
|
||||
<button title="Ordena per últim cop tocat"
|
||||
class="{{ last_played_color }} mx-2 flex flex-row"
|
||||
hx-get="/api/temes/busca"
|
||||
hx-include="[name=query]"
|
||||
hx-vars="{{ hx_vars }}"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML">
|
||||
<i class="mr-1">{% include "icons/calendar.svg" %}</i>
|
||||
{% if last_played_caret == "asc" %}
|
||||
<i class="fa fa-caret-up flex-1 w-0" aria-hidden="true"></i>
|
||||
{% elif last_played_caret == "desc" %}
|
||||
<i class="fa fa-caret-down flex-1 w-0" aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<div class="flex-1 w-0"></div>
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
19
folkugat_web/assets/templates/icons/calendar.svg
Normal file
19
folkugat_web/assets/templates/icons/calendar.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-spotify"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<path
|
||||
d="M 5.6841749,7.1762614 V 8.7837442 H 4.0766921 V 7.1762614 Z m 3.1766921,0 V 8.7837442 H 7.2916577 V 7.1762614 Z m 3.214966,0 V 8.7837442 H 10.46835 V 7.1762614 Z m 1.569209,-5.5687796 q 0.669784,0 1.129065,0.4784175 0.478418,0.4592808 0.478418,1.1290653 V 14.352524 q 0,0.669784 -0.478418,1.129065 -0.459281,0.478418 -1.129065,0.478418 H 2.5074827 q -0.6697844,0 -1.1482019,-0.478418 Q 0.89999998,15.022308 0.89999998,14.352524 V 3.2149646 q 0,-0.6697845 0.45928082,-1.1290653 Q 1.8376983,1.6074818 2.5074827,1.6074818 H 3.2920874 V -9.4223022e-7 H 4.8995702 V 1.6074818 H 11.252955 V -9.4223022e-7 h 1.607482 V 1.6074818 Z m 0,12.7450422 V 5.6070521 H 2.5074827 V 14.352524 Z M 5.6841749,10.391227 v 1.569209 H 4.0766921 v -1.569209 z m 3.1766921,0 v 1.569209 H 7.2916577 v -1.569209 z m 3.214966,0 v 1.569209 H 10.46835 v -1.569209 z"
|
||||
id="text50"
|
||||
style="font-size:19.1367px;line-height:1.25;stroke-width:1.4355"
|
||||
aria-label="" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
19
folkugat_web/assets/templates/icons/music-box.svg
Normal file
19
folkugat_web/assets/templates/icons/music-box.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-spotify"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<path
|
||||
d="M 1.5769257,3.1922874 H 0 V 14.423075 q 0,0.634616 0.46153922,1.096156 0.48077001,0.48077 1.11538648,0.48077 H 12.807713 V 14.423075 H 1.5769257 Z M 12.807713,3.9807502 h -2.40385 v 4.4230842 q 0,0.8269244 -0.5961547,1.4230792 -0.576924,0.5769244 -1.4038484,0.5769244 -0.8076937,0 -1.4230793,-0.5769244 -0.5961548,-0.5961548 -0.5961548,-1.4038484 0,-0.8269244 0.5961548,-1.4230793 Q 7.5961662,6.3846003 8.4038599,6.3846003 9.0384763,6.3653695 9.6154003,6.7884471 V 2.4038246 H 12.807713 Z M 14.4231,-2.5497437e-5 H 4.8077002 q -0.6730781,0 -1.1538481,0.480770017437 Q 3.1923129,0.94228373 3.1923129,1.5769002 V 11.1923 q 0,0.673078 0.4615392,1.153849 0.48077,0.461539 1.1538481,0.461539 H 14.4231 q 0.634617,0 1.096156,-0.461539 0.48077,-0.480771 0.48077,-1.153849 V 1.5769002 q 0,-0.63461647 -0.48077,-1.09615568 Q 15.057717,-2.5497437e-5 14.4231,-2.5497437e-5 Z"
|
||||
id="text48"
|
||||
style="font-size:19.2308px;line-height:1.25;stroke-width:1.44231"
|
||||
aria-label="" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
folkugat_web/assets/templates/icons/notes-small.svg
Normal file
5
folkugat_web/assets/templates/icons/notes-small.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-music-note-beamed" viewBox="0 0 16 16">
|
||||
<path d="M6 13c0 1.105-1.12 2-2.5 2S1 14.105 1 13c0-1.104 1.12-2 2.5-2s2.5.896 2.5 2m9-2c0 1.105-1.12 2-2.5 2s-2.5-.895-2.5-2 1.12-2 2.5-2 2.5.895 2.5 2"/>
|
||||
<path fill-rule="evenodd" d="M14 11V2h1v9zM6 3v10H5V3z"/>
|
||||
<path d="M5 2.905a1 1 0 0 1 .9-.995l8-.8a1 1 0 0 1 1.1.995V3L5 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
@@ -4,6 +4,16 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||
|
||||
<!-- Essential Open Graph (Facebook, WhatsApp, etc.) -->
|
||||
<meta property="og:title" content="{{ page_title }}">
|
||||
<meta property="og:description" content="{{ page_description }}">
|
||||
<meta property="og:image" content="{{ page_card or url_for(request, 'static', path='img/folkugat_card.jpg') }}">
|
||||
|
||||
<!-- Twitter Card (optional but recommended) -->
|
||||
<meta name="twitter:title" content="{{ page_title }}">
|
||||
<meta name="twitter:description" content="{{ page_description }}">
|
||||
<meta name="twitter:image" content="{{ page_card or url_for(request, 'static', path='img/folkugat_card.jpg') }}">
|
||||
|
||||
<title>{{ page_title }}</title>
|
||||
<meta name="description" content="{{ page_description }}"/>
|
||||
|
||||
|
||||
84
folkugat_web/assets/templates/lilypond/lib.ly
Normal file
84
folkugat_web/assets/templates/lilypond/lib.ly
Normal file
@@ -0,0 +1,84 @@
|
||||
% -------------------------------------------------------------------------
|
||||
% Extra custom functions
|
||||
% -------------------------------------------------------------------------
|
||||
|
||||
% Alternative chords
|
||||
|
||||
#(define (pitches-bass-inversion-context-list music context)
|
||||
"Creates a list containing relevant pitches, bass, inversion and context to
|
||||
serve as argument for `ignatzek-chord-names', derived from @var{music}
|
||||
"
|
||||
(let* (;; work on a copy of `music', warranting not to change it
|
||||
(m-add (ly:music-deep-copy music))
|
||||
(ev-notes-orig (event-chord-notes m-add))
|
||||
;; if chord is an inversion some pitches are below root, in this case
|
||||
;; 'octavation-property is set.
|
||||
;; reset pitch of them
|
||||
;; TODO this is an ugly hack, find better method
|
||||
(ev-notes
|
||||
(map
|
||||
(lambda (m)
|
||||
(if (and (not (null? (ly:music-property m 'octavation)))
|
||||
(= (ly:music-property m 'octavation) -1))
|
||||
(begin
|
||||
(ly:music-set-property! m 'pitch
|
||||
(ly:pitch-transpose
|
||||
(ly:music-property m 'pitch)
|
||||
(ly:make-pitch 1 0 0)))
|
||||
m)
|
||||
m))
|
||||
ev-notes-orig))
|
||||
;; sort the ptches ascending as expected by `ignatzek-chord-names'
|
||||
(ev-pitches
|
||||
(sort
|
||||
(map (lambda (m) (ly:music-property m 'pitch)) ev-notes)
|
||||
ly:pitch<?))
|
||||
;; get bass
|
||||
(bass-note
|
||||
(filter
|
||||
(lambda (m)
|
||||
(not (null? (ly:music-property m 'bass))))
|
||||
ev-notes))
|
||||
(bass-pitch
|
||||
(if (not (null? bass-note))
|
||||
(ly:music-property (car bass-note) 'pitch)
|
||||
'()))
|
||||
;; get inversion
|
||||
(inversion-note
|
||||
(filter
|
||||
(lambda (m)
|
||||
(not (null? (ly:music-property m 'inversion))))
|
||||
ev-notes))
|
||||
(inversion-pitch
|
||||
(if (not (null? inversion-note))
|
||||
(ly:music-property (car inversion-note) 'pitch)
|
||||
'()))
|
||||
;; TODO why is this needed?
|
||||
(in-pitches
|
||||
(if (null? inversion-pitch)
|
||||
(delq bass-pitch ev-pitches)
|
||||
ev-pitches)))
|
||||
|
||||
(list in-pitches bass-pitch inversion-pitch context)))
|
||||
|
||||
altChords =
|
||||
#(define-music-function (m1 m2)(ly:music? ly:music?)
|
||||
"Return the default ChordName of @var{m1}, with an added parenthesized ChordName
|
||||
derived from @var{m2}"
|
||||
#{
|
||||
\applyOutput ChordNames.ChordName
|
||||
#(lambda (g ctx p)
|
||||
(let ((main-text (ly:grob-property g 'text))
|
||||
(alt-text
|
||||
(apply
|
||||
ignatzek-chord-names
|
||||
(pitches-bass-inversion-context-list m2 ctx))))
|
||||
(ly:grob-set-property! g 'text
|
||||
#{
|
||||
\markup
|
||||
{ $main-text \hspace #0.4 \fontsize #-3 \parenthesize $alt-text }
|
||||
#})))
|
||||
$m1
|
||||
#})
|
||||
|
||||
% -------------------------------------------------------------------------
|
||||
49
folkugat_web/assets/templates/lilypond/playlist.ly
Normal file
49
folkugat_web/assets/templates/lilypond/playlist.ly
Normal file
@@ -0,0 +1,49 @@
|
||||
{{ score_beginning }}
|
||||
\version "2.24.4"
|
||||
|
||||
{% include "lilypond/lib.ly" %}
|
||||
|
||||
\book {
|
||||
\paper {
|
||||
top-margin = 10
|
||||
left-margin = 15
|
||||
right-margin = 15
|
||||
ragged-bottom = ##f
|
||||
|
||||
scoreTitleMarkup = \markup {
|
||||
\center-column {
|
||||
\fontsize #3 \bold \fromproperty #'header:piece
|
||||
\fill-line {
|
||||
\null
|
||||
\right-column {
|
||||
\fromproperty #'header:composer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\header {
|
||||
title = \markup { "{{ playlist.title | safe }}" }
|
||||
tagline = "Partitura generada amb LilyPond"
|
||||
copyright = "Folkugat"
|
||||
}
|
||||
|
||||
{% for set in playlist.sets %}
|
||||
{% if set.tunes|length > 1 %}
|
||||
\markup {
|
||||
\vspace #2
|
||||
\fill-line {
|
||||
\center-column {
|
||||
\fontsize #2 \bold "{{ set.title | safe }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
\noPageBreak
|
||||
{% endif %}
|
||||
|
||||
{% for tune in set.tunes %}
|
||||
{% include "lilypond/tune_in_set.ly" %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
\version "2.24.4"
|
||||
{% include "lilypond/lib.ly" %}
|
||||
{% if tune.score_source is not none %}
|
||||
\score {
|
||||
\language "english"
|
||||
|
||||
@@ -7,13 +7,25 @@
|
||||
% -----------------------------------------------------
|
||||
|
||||
% A
|
||||
\repeat volta 2 {
|
||||
\alternative {
|
||||
\volta 1 { }
|
||||
\volta 2 { }
|
||||
}
|
||||
}
|
||||
|
||||
% B
|
||||
\repeat volta 2 {
|
||||
\alternative {
|
||||
\volta 1 { }
|
||||
\volta 2 { }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
\new Staff {
|
||||
\relative {
|
||||
\relative c' {
|
||||
% -----------------------------------------------------
|
||||
% Compàs
|
||||
% -----------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
\version "2.24.4"
|
||||
|
||||
{% include "lilypond/lib.ly" %}
|
||||
|
||||
\paper {
|
||||
top-margin = 10
|
||||
left-margin = 15
|
||||
@@ -35,7 +38,7 @@
|
||||
\hspace #1
|
||||
\center-column {
|
||||
{% for line in paragraph.lines %}
|
||||
\line { {{ line | safe }} }
|
||||
{% if line %} \line { {{ line | safe }} } {% else %} \vspace #1 {% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
\noPageBreak
|
||||
{% if tune.score_source is not none %}
|
||||
\score {
|
||||
\language "english"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{{ score_beginning }}
|
||||
\version "2.24.4"
|
||||
|
||||
{% include "lilypond/lib.ly" %}
|
||||
|
||||
\book {
|
||||
\paper {
|
||||
top-margin = 10
|
||||
|
||||
@@ -12,13 +12,16 @@ logger.info(f"Using DB_DIR: {DB_DIR}")
|
||||
|
||||
DB_FILES_DIR = DB_DIR / "fitxer"
|
||||
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema"
|
||||
DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio"
|
||||
DB_FILES_SET_DIR = DB_FILES_DIR / "set"
|
||||
DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes"
|
||||
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"
|
||||
|
||||
for path in [
|
||||
DB_FILES_DIR,
|
||||
DB_FILES_TEMA_DIR,
|
||||
DB_FILES_SET_DIR,
|
||||
DB_FILES_PLAYLIST_DIR,
|
||||
DB_FILES_SET_DIR,
|
||||
]:
|
||||
path.mkdir(exist_ok=True)
|
||||
|
||||
@@ -2,12 +2,13 @@ from typing import TypedDict
|
||||
|
||||
from folkugat_web.model import playlists as model
|
||||
|
||||
PlaylistRowTuple = tuple[int, int, int, int | None]
|
||||
PlaylistRowTuple = tuple[int, str | None, int]
|
||||
PlaylistEntryRowTuple = tuple[int, int, int, int | None]
|
||||
|
||||
|
||||
class PlaylistRowDict(TypedDict):
|
||||
id: int | None
|
||||
session_id: int
|
||||
playlist_id: int
|
||||
set_id: int
|
||||
tema_id: int | None
|
||||
|
||||
@@ -15,16 +16,25 @@ class PlaylistRowDict(TypedDict):
|
||||
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
|
||||
return {
|
||||
'id': tema_in_set.id,
|
||||
'session_id': tema_in_set.session_id,
|
||||
'playlist_id': tema_in_set.playlist_id,
|
||||
'set_id': tema_in_set.set_id,
|
||||
'tema_id': tema_in_set.tema_id,
|
||||
}
|
||||
|
||||
|
||||
def row_to_playlist_entry(row: PlaylistRowTuple) -> model.PlaylistEntry:
|
||||
def row_to_playlist_entry(row: PlaylistEntryRowTuple) -> model.PlaylistEntry:
|
||||
return model.PlaylistEntry(
|
||||
id=row[0],
|
||||
session_id=row[1],
|
||||
playlist_id=row[1],
|
||||
set_id=row[2],
|
||||
tema_id=row[3],
|
||||
)
|
||||
|
||||
|
||||
def row_to_playlist(row: PlaylistRowTuple) -> model.Playlist:
|
||||
return model.Playlist(
|
||||
id=row[0],
|
||||
name=row[1],
|
||||
sets=[], # Empty for list view
|
||||
hidden=bool(row[2]),
|
||||
)
|
||||
|
||||
@@ -4,15 +4,30 @@ from folkugat_web.dal.sql import Connection, get_connection
|
||||
def create_db(con: Connection | None = None):
|
||||
with get_connection(con) as con:
|
||||
create_playlists_table(con)
|
||||
create_playlist_entries_table(con)
|
||||
|
||||
|
||||
def create_playlists_table(con: Connection):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS playlists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
session_id INTEGER NOT NULL,
|
||||
set_id INTEGER NOT NULL,
|
||||
tema_id INTEGER
|
||||
name TEXT,
|
||||
hidden INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
|
||||
def create_playlist_entries_table(con: Connection):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS playlist_entries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
playlist_id INTEGER NOT NULL,
|
||||
set_id INTEGER NOT NULL,
|
||||
tema_id INTEGER,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tema_id) REFERENCES temes(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Iterable, Iterator
|
||||
from collections.abc import Iterator
|
||||
from typing import TypedDict
|
||||
|
||||
from folkugat_web.dal.sql import Connection, get_connection
|
||||
@@ -13,13 +13,13 @@ from . import conversion
|
||||
class QueryData(TypedDict, total=False):
|
||||
id: int
|
||||
set_id: int
|
||||
session_id: int
|
||||
playlist_id: int
|
||||
|
||||
|
||||
def _filter_clause(
|
||||
entry_id: int | None = None,
|
||||
set_id: int | None = None,
|
||||
session_id: int | None = None,
|
||||
playlist_id: int | None = None,
|
||||
) -> tuple[str, QueryData]:
|
||||
filter_clauses: list[str] = []
|
||||
query_data: QueryData = {}
|
||||
@@ -30,9 +30,9 @@ def _filter_clause(
|
||||
if set_id is not None:
|
||||
filter_clauses.append("set_id = :set_id")
|
||||
query_data["set_id"] = set_id
|
||||
if session_id is not None:
|
||||
filter_clauses.append("session_id = :session_id")
|
||||
query_data["session_id"] = session_id
|
||||
if playlist_id is not None:
|
||||
filter_clauses.append("playlist_id = :playlist_id")
|
||||
query_data["playlist_id"] = playlist_id
|
||||
|
||||
return " AND ".join(filter_clauses), query_data
|
||||
|
||||
@@ -40,14 +40,14 @@ def _filter_clause(
|
||||
def get_playlist_entries(
|
||||
entry_id: int | None = None,
|
||||
set_id: int | None = None,
|
||||
session_id: int | None = None,
|
||||
playlist_id: int | None = None,
|
||||
con: Connection | None = None,
|
||||
) -> Iterator[model.PlaylistEntry]:
|
||||
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, session_id=session_id)
|
||||
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, playlist_id=playlist_id)
|
||||
query = f"""
|
||||
SELECT
|
||||
id, session_id, set_id, tema_id
|
||||
FROM playlists
|
||||
id, playlist_id, set_id, tema_id
|
||||
FROM playlist_entries
|
||||
WHERE {filter_clause}
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
@@ -57,22 +57,54 @@ def get_playlist_entries(
|
||||
return map(conversion.row_to_playlist_entry, cur.fetchall())
|
||||
|
||||
|
||||
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, bool]
|
||||
|
||||
|
||||
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]:
|
||||
placeholders = ", ".join(["?" for _ in tema_ids])
|
||||
query = f"""
|
||||
SELECT p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name, s.venue_url, s.is_live
|
||||
FROM playlists p JOIN sessions s ON p.session_id = s.id
|
||||
WHERE p.tema_id IN ({placeholders})
|
||||
def get_playlist_name(playlist_id: int, con: Connection | None = None) -> str | None:
|
||||
query = """
|
||||
SELECT name
|
||||
FROM playlists
|
||||
WHERE id = :playlist_id
|
||||
"""
|
||||
data = dict(playlist_id=playlist_id)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, tema_ids)
|
||||
result_rows: Iterable[GetTuneSessionsRow] = cur.fetchall()
|
||||
return dict(groupby(
|
||||
result_rows,
|
||||
key_fn=lambda row: row[0],
|
||||
group_fn=lambda rows: list(sessions_conversion.row_to_session(row[1:]) for row in rows)
|
||||
))
|
||||
_ = cur.execute(query, data)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def get_all_playlists(logged_in: bool = False, con: Connection | None = None) -> Iterator[model.Playlist]:
|
||||
if logged_in:
|
||||
# Show all playlists for logged in users, except session-associated ones
|
||||
query = """
|
||||
SELECT id, name, hidden
|
||||
FROM playlists
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT playlist_id
|
||||
FROM session_playlists
|
||||
)
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
else:
|
||||
# Show only visible playlists for non-logged in users, except session-associated ones
|
||||
query = """
|
||||
SELECT id, name, hidden
|
||||
FROM playlists
|
||||
WHERE hidden = 0 AND id NOT IN (
|
||||
SELECT DISTINCT playlist_id
|
||||
FROM session_playlists
|
||||
)
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
|
||||
for row in rows:
|
||||
# Convert to Playlist model without sets for list view
|
||||
yield model.Playlist(
|
||||
id=row[0],
|
||||
name=row[1],
|
||||
sets=[], # Empty sets list for now
|
||||
hidden=bool(row[2])
|
||||
)
|
||||
|
||||
@@ -4,27 +4,66 @@ from folkugat_web.model import playlists as model
|
||||
from . import conversion
|
||||
|
||||
|
||||
def insert_playlist_entry(pl_entry: model.PlaylistEntry, con: Connection | None = None) -> model.PlaylistEntry:
|
||||
def create_playlist(
|
||||
name: str | None = None,
|
||||
con: Connection | None = None,
|
||||
) -> int:
|
||||
query = """
|
||||
INSERT INTO playlists
|
||||
(id, session_id, set_id, tema_id)
|
||||
(name)
|
||||
VALUES
|
||||
(:id, :session_id, :set_id, :tema_id)
|
||||
(:name)
|
||||
RETURNING *
|
||||
"""
|
||||
data = dict(name=name)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
row: conversion.PlaylistRowTuple = cur.fetchone()
|
||||
return row[0]
|
||||
|
||||
|
||||
def delete_playlist(
|
||||
playlist_id: int,
|
||||
con: Connection | None = None,
|
||||
) -> None:
|
||||
query = """
|
||||
DELETE FROM playlists
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=playlist_id)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
|
||||
|
||||
def insert_playlist_entry(
|
||||
pl_entry: model.PlaylistEntry,
|
||||
con: Connection | None = None,
|
||||
) -> model.PlaylistEntry:
|
||||
query = """
|
||||
INSERT INTO playlist_entries
|
||||
(id, playlist_id, set_id, tema_id)
|
||||
VALUES
|
||||
(:id, :playlist_id, :set_id, :tema_id)
|
||||
RETURNING *
|
||||
"""
|
||||
data = conversion.playlist_entry_to_row(pl_entry)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
row: conversion.PlaylistRowTuple = cur.fetchone()
|
||||
row: conversion.PlaylistEntryRowTuple = cur.fetchone()
|
||||
return conversion.row_to_playlist_entry(row)
|
||||
|
||||
|
||||
def update_playlist_entry(entry: model.PlaylistEntry, con: Connection | None = None):
|
||||
def update_playlist_entry(
|
||||
entry: model.PlaylistEntry,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
UPDATE playlists
|
||||
UPDATE playlist_entries
|
||||
SET
|
||||
id = :id, session_id = :session_id, set_id = :set_id, tema_id = :tema_id
|
||||
id = :id, playlist_id = :playlist_id, set_id = :set_id, tema_id = :tema_id
|
||||
WHERE
|
||||
id = :id
|
||||
"""
|
||||
@@ -35,9 +74,12 @@ def update_playlist_entry(entry: model.PlaylistEntry, con: Connection | None = N
|
||||
return
|
||||
|
||||
|
||||
def delete_playlist_entry(entry_id: int, con: Connection | None = None):
|
||||
def delete_playlist_entry(
|
||||
entry_id: int,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
DELETE FROM playlists
|
||||
DELETE FROM playlist_entries
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=entry_id)
|
||||
@@ -47,12 +89,50 @@ def delete_playlist_entry(entry_id: int, con: Connection | None = None):
|
||||
return
|
||||
|
||||
|
||||
def delete_playlist_set(session_id: int, set_id: int, con: Connection | None = None):
|
||||
def delete_playlist_set(
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
DELETE FROM playlists
|
||||
WHERE session_id = :session_id AND set_id = :set_id
|
||||
DELETE FROM playlist_entries
|
||||
WHERE playlist_id = :playlist_id AND set_id = :set_id
|
||||
"""
|
||||
data = dict(session_id=session_id, set_id=set_id)
|
||||
data = dict(playlist_id=playlist_id, set_id=set_id)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return
|
||||
|
||||
|
||||
def update_playlist_name(
|
||||
playlist_id: int,
|
||||
name: str | None,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
UPDATE playlists SET
|
||||
name = :name
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=playlist_id, name=name)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return
|
||||
|
||||
|
||||
def update_playlist_visibility(
|
||||
playlist_id: int,
|
||||
hidden: bool,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
UPDATE playlists SET
|
||||
hidden = :hidden
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=playlist_id, hidden=1 if hidden else 0)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
|
||||
@@ -3,7 +3,17 @@ from typing import TypedDict
|
||||
|
||||
from folkugat_web.model import sessions as model
|
||||
|
||||
SessionRowTuple = tuple[int, str, str, str, str | None, str | None, bool]
|
||||
SessionRowTuple = tuple[
|
||||
int, # id
|
||||
str, # date
|
||||
str, # start_time
|
||||
str, # end_time
|
||||
str | None, # venue_name
|
||||
str | None, # venue_url
|
||||
str | None, # notes
|
||||
str | None, # cartell_url
|
||||
bool, # is_live
|
||||
]
|
||||
|
||||
|
||||
class SessionRowDict(TypedDict):
|
||||
@@ -13,6 +23,8 @@ class SessionRowDict(TypedDict):
|
||||
end_time: str
|
||||
venue_name: str | None
|
||||
venue_url: str | None
|
||||
notes: str | None
|
||||
cartell_url: str | None
|
||||
is_live: bool
|
||||
|
||||
|
||||
@@ -24,6 +36,8 @@ def session_to_row(sessio: model.Session) -> SessionRowDict:
|
||||
'end_time': sessio.end_time.isoformat(),
|
||||
'venue_name': sessio.venue.name,
|
||||
'venue_url': sessio.venue.url,
|
||||
'notes': sessio.notes,
|
||||
'cartell_url': sessio.cartell_url,
|
||||
'is_live': sessio.is_live,
|
||||
}
|
||||
|
||||
@@ -38,5 +52,7 @@ def row_to_session(row: SessionRowTuple) -> model.Session:
|
||||
name=row[4],
|
||||
url=row[5],
|
||||
),
|
||||
is_live=row[6],
|
||||
notes=row[6],
|
||||
cartell_url=row[7],
|
||||
is_live=row[8],
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from folkugat_web.dal.sql import Connection, get_connection
|
||||
def create_db(con: Connection | None = None):
|
||||
with get_connection(con) as con:
|
||||
create_sessions_table(con)
|
||||
create_session_playlists_table(con)
|
||||
|
||||
|
||||
def create_sessions_table(con: Connection):
|
||||
@@ -15,8 +16,25 @@ def create_sessions_table(con: Connection):
|
||||
end_time TEXT NOT NULL,
|
||||
venue_name TEXT,
|
||||
venue_url TEXT,
|
||||
notes TEXT,
|
||||
cartell_url TEXT,
|
||||
is_live BOOLEAN DEFAULT false
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
|
||||
def create_session_playlists_table(con: Connection):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS session_playlists (
|
||||
session_id INTEGER,
|
||||
playlist_type TEXT,
|
||||
playlist_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (session_id, playlist_type),
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
138
folkugat_web/dal/sql/sessions/playlists.py
Normal file
138
folkugat_web/dal/sql/sessions/playlists.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from folkugat_web.dal.sql import Connection, get_connection
|
||||
from folkugat_web.dal.sql.temes.conversion import row_to_tema
|
||||
from folkugat_web.model import sessions as sessions_model
|
||||
from folkugat_web.model import temes as temes_model
|
||||
from folkugat_web.model.playlists import PlaylistType
|
||||
from folkugat_web.utils import groupby
|
||||
|
||||
from . import conversion
|
||||
|
||||
|
||||
def get_playlist_id(
|
||||
session_id: int,
|
||||
playlist_type: PlaylistType,
|
||||
con: Connection | None = None,
|
||||
) -> int | None:
|
||||
query = f"""
|
||||
SELECT playlist_id
|
||||
FROM session_playlists
|
||||
WHERE
|
||||
session_id = :session_id AND
|
||||
playlist_type = :playlist_type
|
||||
"""
|
||||
data = dict(
|
||||
session_id=session_id,
|
||||
playlist_type=playlist_type.value,
|
||||
)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
if row := cur.fetchone():
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
|
||||
def insert_playlist(
|
||||
session_id: int,
|
||||
playlist_type: PlaylistType,
|
||||
playlist_id: int,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = f"""
|
||||
INSERT INTO session_playlists
|
||||
(session_id, playlist_type, playlist_id)
|
||||
VALUES
|
||||
(:session_id, :playlist_type, :playlist_id)
|
||||
"""
|
||||
data = dict(
|
||||
session_id=session_id,
|
||||
playlist_type=playlist_type.value,
|
||||
playlist_id=playlist_id,
|
||||
)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
|
||||
|
||||
def delete_playlist(
|
||||
session_id: int,
|
||||
playlist_type: PlaylistType,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = f"""
|
||||
DELETE FROM session_playlists
|
||||
WHERE
|
||||
session_id = :session_id AND
|
||||
playlist_type = :playlist_type
|
||||
"""
|
||||
data = dict(
|
||||
session_id=session_id,
|
||||
playlist_type=playlist_type.value,
|
||||
)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
|
||||
|
||||
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, str | None, str | None, bool]
|
||||
|
||||
|
||||
def get_tune_sessions(
|
||||
tema_ids: list[int],
|
||||
con: Connection | None = None,
|
||||
) -> dict[int, list[sessions_model.Session]]:
|
||||
placeholders = ", ".join(["?" for _ in tema_ids])
|
||||
query = f"""
|
||||
SELECT
|
||||
p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name,
|
||||
s.venue_url, s.notes, s.cartell_url, s.is_live
|
||||
FROM playlist_entries p
|
||||
JOIN session_playlists sp ON p.playlist_id = sp.playlist_id
|
||||
JOIN sessions s ON sp.session_id = s.id
|
||||
WHERE
|
||||
p.tema_id IN ({placeholders}) AND
|
||||
sp.playlist_type = "{PlaylistType.SESSION_SETLIST.value}"
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, tema_ids)
|
||||
result_rows: list[GetTuneSessionsRow] = cur.fetchall()
|
||||
return dict(groupby(
|
||||
result_rows,
|
||||
key_fn=lambda row: row[0],
|
||||
group_fn=lambda rows: list(conversion.row_to_session(row[1:]) for row in rows)
|
||||
))
|
||||
|
||||
|
||||
CommonlyPlayedTuneRow = tuple[int, str, str, str, str, int, int]
|
||||
|
||||
|
||||
def get_commonly_played_tunes(
|
||||
tema_id: int,
|
||||
con: Connection | None = None,
|
||||
) -> list[temes_model.CommonlyPlayedTema]:
|
||||
query = f"""
|
||||
SELECT
|
||||
id, title, alternatives, creation_date, modification_date, hidden, count
|
||||
FROM (
|
||||
SELECT tema_id, count(*) count FROM playlist_entries p JOIN (
|
||||
SELECT pe.playlist_id, pe.set_id
|
||||
FROM playlist_entries pe JOIN session_playlists sp USING (playlist_id)
|
||||
WHERE tema_id = :tema_id AND playlist_type = :playlist_type
|
||||
) s
|
||||
ON p.playlist_id == s.playlist_id AND p.set_id == s.set_id
|
||||
WHERE tema_id != :tema_id
|
||||
GROUP BY tema_id
|
||||
) common JOIN temes t ON common.tema_id == t.id
|
||||
"""
|
||||
data = dict(tema_id=tema_id, playlist_type=PlaylistType.SESSION_SETLIST.value)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
result_rows: list[CommonlyPlayedTuneRow] = cur.fetchall()
|
||||
return [
|
||||
temes_model.CommonlyPlayedTema(
|
||||
tema=row_to_tema(row[:6]),
|
||||
count=row[6],
|
||||
) for row in result_rows
|
||||
]
|
||||
@@ -82,7 +82,7 @@ def get_sessions(session_id: int | None = None,
|
||||
|
||||
clauses_str = " ".join(clauses)
|
||||
query = f"""
|
||||
SELECT id, date, start_time, end_time, venue_name, venue_url, is_live
|
||||
SELECT id, date, start_time, end_time, venue_name, venue_url, notes, cartell_url, is_live
|
||||
FROM sessions
|
||||
{clauses_str}
|
||||
"""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user