Compare commits

...

24 Commits

Author SHA1 Message Date
marc
443c1b1391 Canviar el preview de la pàgina de tema 2026-01-05 10:40:43 +01:00
marc
4e7a6b18d6 Afegir control visual de les lletres per a partitures 2026-01-05 10:19:25 +01:00
marc
c0624d1e56 Edició de llistes (afegir i esborrar llistes) 2025-12-21 17:30:19 +01:00
marc
fdcda1b566 Nova pàgina de llistes 2025-12-21 17:09:59 +01:00
marc
ac79785cf0 Afegir partitures de llistes (cançoners) 2025-12-21 15:40:21 +01:00
marc
089e61fb0b Canvi de nom a les URLs playlist -> llista 2025-12-20 23:48:54 +01:00
marc
56ab91bd42 Pàgines llista de reproducció 2025-12-20 23:29:11 +01:00
marc
5428d49e89 Crea i esborra playlists quan es creen i esborren sessions 2025-12-20 21:20:58 +01:00
marc
43706af7c4 Afegir funcions especials de lilypond 2025-12-20 20:31:54 +01:00
marc
c1ec268823 Afegir metadades per quan es comparteixen enllaços 2025-11-02 14:22:22 +01:00
marc
2fbdbbf290 Desacoblar playlists de sessions i afegir slow jams 2025-11-01 19:55:24 +01:00
marc
23337f8ab3 Afegir cartells a les jams (i esquelet per a slow jams i notes) 2025-11-01 12:47:04 +01:00
marc
fbfedf4dac Fix set editor 2025-10-30 22:03:01 +01:00
marc
badb73098a Tota la set entry ara linka o bé al set o al tema 2025-10-26 13:00:30 +01:00
marc
98c009ba53 Canvia estil de la playlist i suggerir temes coocurrents 2025-10-25 00:21:05 +02:00
marc
1909af9107 Afegir temes coocurrents 2025-10-24 00:05:56 +02:00
marc
31aeb09dd9 Filtres i ordres als resultats de cerca 2025-10-10 00:14:36 +02:00
marc
aec310c39c Redisseny dels resultats de cerca 2025-10-05 20:54:04 +02:00
marc
664f2e4693 Canvi de color 2025-09-28 17:51:48 +02:00
marc
8cdb9340fd Api routes refactor 2025-07-28 22:45:45 +02:00
marc
6aad7a069e Reestablish brown 2025-07-27 20:59:06 +02:00
marc
92032df7fd Update flake and other small changes 2025-07-27 20:28:11 +02:00
marc
47d18400c3 Search by properties 2025-05-04 22:45:53 +02:00
marc
4911935cdf Change search result appearance 2025-04-27 22:47:52 +02:00
137 changed files with 3449 additions and 1948 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.direnv .direnv
**/**.pyc **/**.pyc
certs

24
AGENTS.md Normal file
View 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

View File

@@ -1,10 +1,10 @@
#+title: Readme #+title: Readme
* Tasques * 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 Suport per a diverses organitzacions (no només jam de Sant Cugat)
** TODO Usuaris i permisos granulars ** TODO Usuaris i permisos granulars
** Lilypond support (o similar) ** Lilypond support
*** TODO Fer cançoners "en directe" *** TODO Fer cançoners "en directe"
*** DONE Suport de caràcters especials (al títol i més llocs?) *** DONE Suport de caràcters especials (al títol i més llocs?)
*** DONE Mostrar partitura als enllaços (resultats de cerca) *** DONE Mostrar partitura als enllaços (resultats de cerca)

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741865919, "lastModified": 1766125104,
"narHash": "sha256-4thdbnP6dlbdq+qZWTsm4ffAwoS8Tiq1YResB+RP6WE=", "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "573c650e8a14b2faa0041645ab18aed7e60f0c9a", "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -60,6 +60,10 @@
lilypond-with-fonts lilypond-with-fonts
# Project tools # Project tools
sqlite sqlite
lazysql
opencode
# Local https
mkcert
]; ];
shellHook = '' shellHook = ''

View File

@@ -1,9 +1,3 @@
from ._router import router from .routes import router
from .auth import *
from .index import *
from .sessio import *
from .sessions import *
from .tema import *
from .temes import *
__all__ = ["router"] __all__ = ["router"]

View File

@@ -1,4 +0,0 @@
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter(default_response_class=HTMLResponse)

View File

@@ -0,0 +1,6 @@
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
def get_router() -> APIRouter:
return APIRouter(default_response_class=HTMLResponse)

View 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"]

View File

@@ -1,11 +1,13 @@
from typing import Annotated from typing import Annotated
from fastapi import Cookie, Form, Request 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.config import auth as config
from folkugat_web.fragments import nota from folkugat_web.fragments import nota
from folkugat_web.services import auth as service from folkugat_web.services import auth as service
router = get_router()
@router.get("/api/nota") @router.get("/api/nota")
def nota_input(request: Request, value: str | None = None): def nota_input(request: Request, value: str | None = None):

View File

@@ -1,8 +1,10 @@
from fastapi import Request 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.services import auth
from folkugat_web.templates import templates from folkugat_web.templates import templates
router = get_router()
@router.get("/") @router.get("/")
def index(request: Request, logged_in: auth.LoggedIn): def index(request: Request, logged_in: auth.LoggedIn):
@@ -11,6 +13,7 @@ def index(request: Request, logged_in: auth.LoggedIn):
{ {
"request": request, "request": request,
"page_title": "Folkugat", "page_title": "Folkugat",
"page_description": "Sessions de folk a Sant Cugat",
"content": "/api/content/sessions", "content": "/api/content/sessions",
"logged_in": logged_in, "logged_in": logged_in,
"animate": True, "animate": True,

View 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"]

View 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,
)

View 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,
)

View File

@@ -0,0 +1,5 @@
from folkugat_web.api.router import get_router
from . import index
router = get_router()
router.include_router(index.router)

View 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)

View 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"]

View 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)

View 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)

View File

@@ -1,9 +1,11 @@
from fastapi import Request 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.fragments import set_page
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.templates import templates from folkugat_web.templates import templates
router = get_router()
@router.get("/live") @router.get("/live")
def page( def page(
@@ -22,16 +24,16 @@ def page(
@router.get("/api/content/live") @router.get("/api/content/live")
def contingut( async def contingut(
request: Request, request: Request,
logged_in: auth.LoggedIn, logged_in: auth.LoggedIn,
): ):
return set_page.live(request, logged_in) return await set_page.live(request, logged_in)
@router.get("/api/content/live/set") @router.get("/api/content/live/set")
def get_set_page( async def get_set_page(
request: Request, request: Request,
logged_in: auth.LoggedIn, logged_in: auth.LoggedIn,
): ):
return set_page.live_set(request, logged_in) return await set_page.live_set(request, logged_in)

View 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)

View 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"]

View File

@@ -2,10 +2,12 @@ import datetime
from typing import Annotated from typing import Annotated
from fastapi import Form, Request 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.fragments import sessions
from folkugat_web.services import auth from folkugat_web.services import auth
router = get_router()
@router.post("/api/sessions/editor/") @router.post("/api/sessions/editor/")
def insert_row(request: Request, _: auth.RequireLogin): def insert_row(request: Request, _: auth.RequireLogin):

View File

@@ -1,10 +1,12 @@
from fastapi import Request 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.config import calendari as calendari_conf
from folkugat_web.fragments import live, sessions from folkugat_web.fragments import live, sessions
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.templates import templates from folkugat_web.templates import templates
router = get_router()
@router.get("/sessions") @router.get("/sessions")
def page(request: Request, logged_in: auth.LoggedIn): def page(request: Request, logged_in: auth.LoggedIn):

View 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"]

View File

@@ -1,8 +1,10 @@
from fastapi import Request 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.fragments import tema
from folkugat_web.services import auth from folkugat_web.services import auth
router = get_router()
@router.get("/api/tema/{tema_id}/editor/title") @router.get("/api/tema/{tema_id}/editor/title")
def title_editor( def title_editor(

View File

@@ -1,10 +1,12 @@
import dataclasses
from typing import Annotated from typing import Annotated
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from fastapi.params import Form from fastapi.params import Form
from fastapi.responses import HTMLResponse 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.fragments import tema, temes
from folkugat_web.model.temes import Tema
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services import files as files_service 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 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.templates import templates
from folkugat_web.utils import FnChain 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}") @router.get("/tema/{tema_id}")
def page(request: Request, logged_in: auth.LoggedIn, tema_id: int): 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( return templates.TemplateResponse(
"index.html", "index.html",
{ {
"request": request, "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}", "content": f"/api/tema/{tema_id}",
"logged_in": logged_in, "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}") @router.get("/api/tema/{tema_id}")
def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int): def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id) 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 = ( tema = (
FnChain.transform(tema) | FnChain.transform(tema) |
temes_q.tema_compute_stats | temes_q.tema_compute_stats |
temes_q.tema_compute_played_with |
augment_played_with |
links_service.add_links_to_tema | links_service.add_links_to_tema |
lyrics_service.add_lyrics_to_tema | lyrics_service.add_lyrics_to_tema |
scores_service.add_scores_to_tema | scores_service.add_scores_to_tema |

View File

@@ -3,7 +3,7 @@ from typing import Annotated
from fastapi import HTTPException, Request, UploadFile from fastapi import HTTPException, Request, UploadFile
from fastapi.params import File, Form, Param from fastapi.params import File, Form, Param
from fastapi.responses import HTMLResponse 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.fragments.tema import links as links_fragments
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.services import auth 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 links as links_service
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
router = get_router()
@router.get("/api/tema/{tema_id}/link/{link_id}") @router.get("/api/tema/{tema_id}/link/{link_id}")
def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int): 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, upload_file: Annotated[UploadFile | None, File()] = None,
): ):
if upload_file: 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 '') link_type = links_service.guess_link_type(url or '')
new_link = model.Link( new_link = model.Link(
@@ -63,7 +65,7 @@ def create_link(
@router.delete("/api/tema/{tema_id}/link/{link_id}") @router.delete("/api/tema/{tema_id}/link/{link_id}")
def delete_link( def delete_link(
_logged_in: auth.RequireLogin, _: auth.RequireLogin,
tema_id: int, tema_id: int,
link_id: int, link_id: int,
): ):

View File

@@ -4,11 +4,13 @@ from typing import Annotated
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from fastapi.params import Form from fastapi.params import Form
from fastapi.responses import HTMLResponse 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.fragments.tema import lyrics as lyrics_fragments
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services.temes import lyrics as lyrics_service from folkugat_web.services.temes import lyrics as lyrics_service
router = get_router()
@router.get("/api/tema/{tema_id}/lyric/{lyric_id}") @router.get("/api/tema/{tema_id}/lyric/{lyric_id}")
def lyric( def lyric(
@@ -31,11 +33,25 @@ def set_lyric(
lyric_id: int, lyric_id: int,
title: Annotated[str, Form()], title: Annotated[str, Form()],
content: 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) lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
if not lyric: if not lyric:
raise HTTPException(status_code=404, detail="Could not find 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) return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=new_lyric)

View File

@@ -4,12 +4,14 @@ from typing import Annotated
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from fastapi.params import Form from fastapi.params import Form
from fastapi.responses import HTMLResponse 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.fragments.tema import properties as properties_fragments
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services.temes import properties as properties_service from folkugat_web.services.temes import properties as properties_service
router = get_router()
@router.get("/api/tema/{tema_id}/property/{property_id}") @router.get("/api/tema/{tema_id}/property/{property_id}")
def property_(request: Request, logged_in: auth.LoggedIn, tema_id: int, property_id: int): def property_(request: Request, logged_in: auth.LoggedIn, tema_id: int, property_id: int):

View File

@@ -4,7 +4,7 @@ from typing import Annotated
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from fastapi.params import Form from fastapi.params import Form
from fastapi.responses import HTMLResponse 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 import temes
from folkugat_web.fragments.tema import scores as scores_fragments from folkugat_web.fragments.tema import scores as scores_fragments
from folkugat_web.services import auth, files 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.lilypond import source as lilypond_source
from folkugat_web.services.temes import scores as scores_service from folkugat_web.services.temes import scores as scores_service
router = get_router()
@router.get("/api/tema/{tema_id}/score/{score_id}") @router.get("/api/tema/{tema_id}/score/{score_id}")
def score(request: Request, logged_in: auth.LoggedIn, tema_id: int, score_id: int): def score(request: Request, logged_in: auth.LoggedIn, tema_id: int, score_id: int):

View 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"]

View File

@@ -0,0 +1,91 @@
import urllib.parse
from typing import Annotated
from fastapi import Request
from fastapi.params import Param
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(
request: Request,
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 []
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 - Temes",
"page_description": f"Buscador de temes{additional_description}",
"content": content_url,
"logged_in": logged_in,
"animate": False,
}
)
@router.get("/api/content/temes")
def content(
request: Request,
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,
)
@router.get("/api/temes/busca")
def busca(
request: Request,
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,
)

View File

@@ -1 +0,0 @@
from . import index, live, set_page

View File

@@ -1,224 +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,
)
# @router.get("/api/sessio/{session_id}/set/{set_id}/score")
# async def render(
# request: Request,
# _: auth.RequireLogin,
# session_id: int,
# set_id: int,
# ):
# set_entry = playlists_service.get_set(session_id=session_id, set_id=set_id)
# if not set_entry:
# raise HTTPException(status_code=404, detail="Could not find set!")
# set_entry = playlists_service.add_temes_to_set(set_entry)
# tune_set = lilypond_build.set_from_set(set_entry=set_entry)
# set_source = lilypond_source.set_source(tune_set=tune_set)
# pdf_result = await lilypond_render.render(
# source=set_source,
# output=lilypond_render.RenderOutput.PDF,
# )
# if output_filename := pdf_result.result:
# score_render_url = files.get_db_file_path(output_filename)
# # return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url)
# return HTMLResponse(content=score_render_url)
# else:
# return HTMLResponse()

View File

@@ -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)

View File

@@ -1 +0,0 @@
from . import editor, index

View File

@@ -1 +0,0 @@
from . import editor, index, links, lyrics, properties, scores

View File

@@ -1 +0,0 @@
from . import index

View File

@@ -1,40 +0,0 @@
from typing import Annotated
from fastapi import Request
from fastapi.params import Param
from folkugat_web.api import router
from folkugat_web.fragments import temes
from folkugat_web.services import auth
from folkugat_web.templates import templates
@router.get("/temes")
def page(
request: Request,
logged_in: auth.LoggedIn,
query: Annotated[str, Param()] = "",
):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"page_title": "Folkugat",
"content": f"/api/content/temes?query={query}",
"logged_in": logged_in,
"animate": False,
}
)
@router.get("/api/content/temes")
def content(
request: Request,
logged_in: auth.LoggedIn,
query: Annotated[str, Param()] = "",
):
return temes.temes_pagina(request, logged_in, query)
@router.get("/api/temes/busca")
def busca(request: Request, query: str, logged_in: auth.LoggedIn, limit: int = 10, offset: int = 0):
return temes.temes_busca(request, query=query, limit=limit, offset=offset, logged_in=logged_in)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -1,8 +1,10 @@
<div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center"> <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') }}" <img src="{{ url_for(request, 'static', path='img/folkugat.svg') }}"
class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3" class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
width="100" width="100"
alt="Folkugat"/> 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> <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> </div>

View File

@@ -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>

View 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>

View 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>

View File

@@ -1,14 +1,13 @@
<ul id="playlist-{{ session.id }}" class=""> <ul id="llista-{{ playlist_id }}" class="">
{% for set_entry in playlist.sets %} {% for set_entry in playlist.sets %}
{% set set_id = set_entry.id %} {% set set_id = set_entry.id %}
{% include "fragments/sessio/set_entry.html" %} {% include "fragments/llista/set_entry.html" %} {% endfor %}
{% endfor %}
</ul> </ul>
{% if logged_in %} {% if logged_in %}
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<button class="text-beige mt-2" <button class="text-beige mt-2"
hx-post="/api/sessio/{{ session.id }}/set" hx-post="/api/llista/{{ playlist_id }}/set"
hx-target="#playlist-{{ session.id }}" hx-target="#llista-{{ playlist_id }}"
hx-swap="beforeend transition:true"> hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i> <i class="fa fa-plus" aria-hidden="true"></i>
Afegeix set Afegeix set

View File

@@ -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 %}

View File

@@ -0,0 +1,2 @@
{% include "fragments/menu.html" %}
{% include "fragments/llista/set/set_page.html" %}

View File

@@ -13,14 +13,14 @@
{% for tema_in_set in set.temes %} {% for tema_in_set in set.temes %}
{% if tema_in_set.tema is not none %} {% if tema_in_set.tema is not none %}
{% set tema = tema_in_set.tema %} {% set tema = tema_in_set.tema %}
{% include "fragments/sessio/set/tema_title.html" %} {% include "fragments/llista/set/tema_title.html" %}
{% else %} {% else %}
<h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3> <h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="mx-12"> <div class="mx-12">
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <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> </div>
{% else %} {% else %}
{% for tema_in_set in set.temes %} {% for tema_in_set in set.temes %}
@@ -31,7 +31,7 @@
{% endif %} {% endif %}
{% if tema_in_set.tema is not none %} {% if tema_in_set.tema is not none %}
{% set tema = tema_in_set.tema %} {% set tema = tema_in_set.tema %}
{% include "fragments/sessio/set/tema.html"%} {% include "fragments/llista/set/tema.html"%}
{% else %} {% else %}
<h3 class="text-center text-3xl p-4"> <h3 class="text-center text-3xl p-4">
<i>Desconegut</i> <i>Desconegut</i>

View File

@@ -1,4 +1,4 @@
{% include "fragments/sessio/set/tema_title.html" %} {% include "fragments/llista/set/tema_title.html" %}
<div class="mx-12 text-left"> <div class="mx-12 text-left">
{% if tema.main_score() is not none %} {% if tema.main_score() is not none %}

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<li id="tune-entry-{{ tema_entry.id }}" <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"> <div class="flex flex-row">
<input <input
name="query" name="query"
@@ -10,15 +10,15 @@
{% endif %} {% endif %}
placeholder="Busca un tema..." placeholder="Busca un tema..."
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded text-center rounded text-center text-black
bg-brown p-1 m-1" p-1 m-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca" hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
hx-trigger="revealed, keyup delay:500ms changed" hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#tune-entry-{{ tema_entry.id }}-search-results" hx-target="#tune-entry-{{ tema_entry.id }}-search-results"
hx-swap="outerHTML"/> hx-swap="outerHTML"/>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="text-beige mx-1" 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-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -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>

View File

@@ -4,7 +4,7 @@
<li> <li>
<button class="bg-beige text-brown rounded <button class="bg-beige text-brown rounded
m-1 px-2" 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-vals='{"tema_id": "{{ tema.id }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
@@ -15,7 +15,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" 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-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-question" aria-hidden="true"></i> <i class="fa fa-question" aria-hidden="true"></i>
@@ -25,7 +25,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" 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-vals='{"title": "{{ query }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">

View File

@@ -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 %}

View 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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -27,6 +27,19 @@
</button> </button>
</li> </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> </ul>
<hr class="h-px bg-beige border-0"> <hr class="h-px bg-beige border-0">
<div hx-get="/api/sessions/live" <div hx-get="/api/sessions/live"

View 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>

View File

@@ -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>

View 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 }}

View File

@@ -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>

View File

@@ -12,6 +12,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</h3> </h3>
{% include "fragments/sessio/cartell.html" %}
<div class="text-left"> <div class="text-left">
<h4 class="pt-4 text-xl text-beige">Horari i lloc</h4> <h4 class="pt-4 text-xl text-beige">Horari i lloc</h4>
De {{ session.start_time.strftime("%H:%M") }} De {{ session.start_time.strftime("%H:%M") }}
@@ -29,10 +30,35 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </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"> <div class="text-left">
<h4 class="pt-4 text-xl text-beige mt-2">Temes tocats</h4> <h4 class="py-4 text-xl text-beige mt-2">
{% include "fragments/sessio/playlist.html" %} <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> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,2 +0,0 @@
{% include "fragments/menu.html" %}
{% include "fragments/sessio/set/set_page.html" %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -4,7 +4,7 @@
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded grow rounded grow
bg-brown p-1 my-1" bg-brown p-1 my-1"
name='upload_file'> name='upload_file'/>
<button title="Afegeix un enllaç" <button title="Afegeix un enllaç"
class="border border-beige rounded px-2 py-1 my-1" class="border border-beige rounded px-2 py-1 my-1"
hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url" hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url"

View File

@@ -7,6 +7,16 @@
rounded rounded
bg-brown px-2 " 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" <button title="Desa els canvis"
class="mx-1" class="mx-1"
hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}" hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"

View File

@@ -1,6 +1,9 @@
<div id="tema-lyric-{{ lyric.id }}"> <div id="tema-lyric-{{ lyric.id }}">
<h5 class="text-sm text-beige text-right"> <h5 class="text-sm text-beige text-right">
{{ lyric.title }} {{ lyric.title }}
{% if logged_in and lyric.max_columns %}
<span class="text-xs ml-2">{{ lyric.max_columns }} columnes</span>
{% endif %}
{% if logged_in %} {% if logged_in %}
<button title="Modifica la lletra" <button title="Modifica la lletra"
class="mx-1" class="mx-1"
@@ -20,6 +23,6 @@
</h5> </h5>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
<div class="text-center"> <div class="text-center">
{{ lyric.content.replace('\n', '<br>') | safe }} {{ (lyric.content.replace('~', '')).replace('\n', '<br>') | safe }}
</div> </div>
</div> </div>

View File

@@ -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>

View File

@@ -1,11 +1,12 @@
<div id="tema-property-{{ property.id }}" <div id="tema-property-{{ property.id }}"
class="flex flex-row"> class="flex flex-row items-center">
<div class="px-2"> <div class="px-2">
<i>{{ property.field.value.capitalize() }}:</i> <i>{{ property.field.value.capitalize() }}:</i>
</div> </div>
<div> <a class="bg-beige text-white rounded m-1 px-2"
href="/temes?query=&properties={{ property.value }}">
{{ property.value }} {{ property.value }}
</div> </a>
{% if logged_in %} {% if logged_in %}
<div class="grow"></div> <div class="grow"></div>

View File

@@ -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 %} {% if tema.stats %}
<h4 class="pt-4 text-xl mt-3 text-beige">Estadístiques</h4> <div class="flex flex-col">
<hr class="h-px mt-1 mb-3 bg-beige border-0"> {% if tema.played_with %}
<div> <div class="my-2 flex flex-row items-center">
<p> <i class="mx-2 flex-none">{% include "icons/notes-small.svg" %}</i>
Aquest tema ha sigut tocat en <p>
{% if tema.stats.times_played == 1%} S'ha tocat juntament amb:
una sessió. </p>
{% else %} </div>
{{ tema.stats.times_played }} sessions. <ol class="ml-4 flex flex-col justify-center">
{% endif %} {% for co_tema in tema.played_with %}
</p> {% set tema = co_tema.tema %}
<p class="py-2"> {% include "fragments/tema/played_with.html" %}
S'ha tocat a les sessions següents: {% endfor %}
<ol class="flex flex-col items-center justify-center"> </ol>
{% for session in tema.stats.sessions_played %} {% endif %}
<li class="border rounded border-beige <div class="my-2 flex flex-row items-center">
flex flex-row grow <i class="mx-2 flex-none">{% include "icons/music-box.svg" %}</i>
p-2 m-2 w-full max-w-xl <p>
relative"> S'ha tocat en
<a href="/session/{{session.id}}"> {% if tema.stats.times_played == 1%}
<div class="flex flex-row grow items-center"> una sessió:
<div class="flex-1"> {% else %}
<a href="/sessio/{{session.id}}/"> {{ tema.stats.times_played }} sessions:
{% set dn = date_names(session.date) %} {% endif %}
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} </p>
</a> </div>
</div> <ol class="ml-4 flex flex-col justify-center">
<div class="ml-auto"> {% for session in tema.stats.sessions_played %}
<a title="Més informació" <li class="flex flex-row grow
class="text-beige mx-1" my-1">
href="/sessio/{{session.id}}/"> <a class="flex flex-row grow items-center"
<i class="fa fa-chevron-right" aria-hidden="true"></i> href="/sessio/{{session.id}}">
</a> <i class="mx-2 text-beige flex-none">{% include "icons/calendar.svg" %}</i>
</div> <p>
</div> {% set dn = date_names(session.date) %} {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
</li> </p>
{% endfor %} </a>
</ol> </li>
</p> {% endfor %}
</ol>
</div> </div>
{% else %}
<div>
<i>No s'ha tocat a cap jam (encara)</i>
</div>
{% endif %} {% endif %}

View File

@@ -1,6 +1,7 @@
{% include "fragments/menu.html" %} {% include "fragments/menu.html" %}
<div class="p-12 text-center flex flex-col items-center justify-center"> <div class="p-12 text-center flex flex-col items-center justify-center">
<h3 class="text-3xl text-beige p-4">Temes</h3> <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" <input id="query-input"
type="text" name="query" value="{{ query }}" type="text" name="query" value="{{ query }}"
placeholder="Busca una tema..." placeholder="Busca una tema..."
@@ -11,6 +12,7 @@
hx-get="/api/temes/busca" hx-get="/api/temes/busca"
hx-trigger="revealed, keyup delay:500ms changed" hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#search-results" hx-target="#search-results"
hx-vars="{{ hx_vars }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<div id="search-results" class="flex-col"></div> <div id="search-results" class="flex-col"></div>
</div> </div>

View File

@@ -0,0 +1,60 @@
<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 %}
{% 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="{{ hx_vars }}"
hx-swap="innerHTML"
>
{{ property.value }}
</button>
{% 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>

View File

@@ -1,19 +0,0 @@
<ul class="flex flex-row items-center">
<li class="px-2 text-beige">
{% if tema.has_score %}
<a href="/tema/{{ tema.id }}" target="_blank">
<i class="fa fa-music px-1" aria-hidden="true"></i>
</a>
{% endif %}
{% if tema.has_lyrics %}
<a href="/tema/{{ tema.id }}" target="_blank">
<i class="fa fa-font px-1" aria-hidden="true"></i>
</a>
{% endif %}
{% if tema.has_audio %}
<a href="/tema/{{ tema.id }}" target="_blank">
<i class="fa fa-volume-up px-1" aria-hidden="true"></i>
</a>
{% endif %}
</li>
</ul>

View File

@@ -12,47 +12,38 @@
<i>{{ query }}</i> <i>{{ query }}</i>
</button> </button>
{% endif %} {% endif %}
<ul class="text-left min-w-full w-full"> <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 %} {% for tema in temes %}
<li class="flex flex-col {% include "fragments/temes/result.html" %}
m-4 rounded-lg
bg-white">
<div class="py-2 px-4 text-brown font-bold">
<a href="/tema/{{ tema.id }}">
{{ tema.title }}
</a>
</div>
{% if tema.main_score() and tema.main_score().preview_url %}
<img class="p-2 max-w"
src="{{ tema.main_score().preview_url }}" />
{% endif %}
{% if tema.properties %}
<ul class="flex flex-wrap text-sm
py-2 px-4">
{% for property in tema.properties %}
<div class="bg-beige text-white rounded
m-1 px-2">
{{ property.value }}
</div>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if prev_offset is not none or next_offset is not none %} {% if prev_offset is not none or next_offset is not none %}
<div class="py-2"> <div class="py-2">
{% if prev_offset is not none %} {% 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" <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-target="#search-results"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-chevron-left" aria-hidden="true"></i> <i class="fa fa-chevron-left" aria-hidden="true"></i>
</button> </button>
{% endif %} {% endif %}
{% if next_offset is not none %} {% 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" <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-target="#search-results"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-chevron-right" aria-hidden="true"></i> <i class="fa fa-chevron-right" aria-hidden="true"></i>

View File

@@ -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 %}

View File

@@ -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>

View 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

View 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

View 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

View File

@@ -4,6 +4,16 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> <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> <title>{{ page_title }}</title>
<meta name="description" content="{{ page_description }}"/> <meta name="description" content="{{ page_description }}"/>

View 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
#})
% -------------------------------------------------------------------------

View 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 %}
}

View File

@@ -1,4 +1,5 @@
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
{% if tune.score_source is not none %} {% if tune.score_source is not none %}
\score { \score {
\language "english" \language "english"

View File

@@ -7,13 +7,25 @@
% ----------------------------------------------------- % -----------------------------------------------------
% A % A
\repeat volta 2 {
\alternative {
\volta 1 { }
\volta 2 { }
}
}
% B % B
\repeat volta 2 {
\alternative {
\volta 1 { }
\volta 2 { }
}
}
} }
} }
\new Staff { \new Staff {
\relative { \relative c' {
% ----------------------------------------------------- % -----------------------------------------------------
% Compàs % Compàs
% ----------------------------------------------------- % -----------------------------------------------------

View File

@@ -1,4 +1,7 @@
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
\paper { \paper {
top-margin = 10 top-margin = 10
left-margin = 15 left-margin = 15
@@ -35,7 +38,7 @@
\hspace #1 \hspace #1
\center-column { \center-column {
{% for line in paragraph.lines %} {% for line in paragraph.lines %}
\line { {{ line | safe }} } {% if line %} \line { {{ line | safe }} } {% else %} \vspace #1 {% endif %}
{% endfor %} {% endfor %}
} }
{% endfor %} {% endfor %}

View File

@@ -9,6 +9,7 @@
} }
} }
} }
\noPageBreak
{% if tune.score_source is not none %} {% if tune.score_source is not none %}
\score { \score {
\language "english" \language "english"

View File

@@ -1,5 +1,8 @@
{{ score_beginning }} {{ score_beginning }}
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
\book { \book {
\paper { \paper {
top-margin = 10 top-margin = 10

View File

@@ -12,13 +12,16 @@ logger.info(f"Using DB_DIR: {DB_DIR}")
DB_FILES_DIR = DB_DIR / "fitxer" DB_FILES_DIR = DB_DIR / "fitxer"
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" 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_SET_DIR = DB_FILES_DIR / "set"
DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes"
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"
for path in [ for path in [
DB_FILES_DIR, DB_FILES_DIR,
DB_FILES_TEMA_DIR, DB_FILES_TEMA_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
DB_FILES_PLAYLIST_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
]: ]:
path.mkdir(exist_ok=True) path.mkdir(exist_ok=True)

View File

@@ -2,12 +2,13 @@ from typing import TypedDict
from folkugat_web.model import playlists as model 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): class PlaylistRowDict(TypedDict):
id: int | None id: int | None
session_id: int playlist_id: int
set_id: int set_id: int
tema_id: int | None tema_id: int | None
@@ -15,16 +16,25 @@ class PlaylistRowDict(TypedDict):
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict: def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
return { return {
'id': tema_in_set.id, '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, 'set_id': tema_in_set.set_id,
'tema_id': tema_in_set.tema_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( return model.PlaylistEntry(
id=row[0], id=row[0],
session_id=row[1], playlist_id=row[1],
set_id=row[2], set_id=row[2],
tema_id=row[3], 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]),
)

View File

@@ -4,15 +4,30 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None): def create_db(con: Connection | None = None):
with get_connection(con) as con: with get_connection(con) as con:
create_playlists_table(con) create_playlists_table(con)
create_playlist_entries_table(con)
def create_playlists_table(con: Connection): def create_playlists_table(con: Connection):
query = """ query = """
CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
session_id INTEGER NOT NULL, 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, set_id INTEGER NOT NULL,
tema_id INTEGER 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() cur = con.cursor()

View File

@@ -1,4 +1,4 @@
from collections.abc import Iterable, Iterator from collections.abc import Iterator
from typing import TypedDict from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.dal.sql import Connection, get_connection
@@ -13,13 +13,13 @@ from . import conversion
class QueryData(TypedDict, total=False): class QueryData(TypedDict, total=False):
id: int id: int
set_id: int set_id: int
session_id: int playlist_id: int
def _filter_clause( def _filter_clause(
entry_id: int | None = None, entry_id: int | None = None,
set_id: int | None = None, set_id: int | None = None,
session_id: int | None = None, playlist_id: int | None = None,
) -> tuple[str, QueryData]: ) -> tuple[str, QueryData]:
filter_clauses: list[str] = [] filter_clauses: list[str] = []
query_data: QueryData = {} query_data: QueryData = {}
@@ -30,9 +30,9 @@ def _filter_clause(
if set_id is not None: if set_id is not None:
filter_clauses.append("set_id = :set_id") filter_clauses.append("set_id = :set_id")
query_data["set_id"] = set_id query_data["set_id"] = set_id
if session_id is not None: if playlist_id is not None:
filter_clauses.append("session_id = :session_id") filter_clauses.append("playlist_id = :playlist_id")
query_data["session_id"] = session_id query_data["playlist_id"] = playlist_id
return " AND ".join(filter_clauses), query_data return " AND ".join(filter_clauses), query_data
@@ -40,14 +40,14 @@ def _filter_clause(
def get_playlist_entries( def get_playlist_entries(
entry_id: int | None = None, entry_id: int | None = None,
set_id: int | None = None, set_id: int | None = None,
session_id: int | None = None, playlist_id: int | None = None,
con: Connection | None = None, con: Connection | None = None,
) -> Iterator[model.PlaylistEntry]: ) -> 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""" query = f"""
SELECT SELECT
id, session_id, set_id, tema_id id, playlist_id, set_id, tema_id
FROM playlists FROM playlist_entries
WHERE {filter_clause} WHERE {filter_clause}
ORDER BY id ASC ORDER BY id ASC
""" """
@@ -57,22 +57,54 @@ def get_playlist_entries(
return map(conversion.row_to_playlist_entry, cur.fetchall()) return map(conversion.row_to_playlist_entry, cur.fetchall())
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, bool] def get_playlist_name(playlist_id: int, con: Connection | None = None) -> str | None:
query = """
SELECT name
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]: FROM playlists
placeholders = ", ".join(["?" for _ in tema_ids]) WHERE id = :playlist_id
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})
""" """
data = dict(playlist_id=playlist_id)
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, tema_ids) _ = cur.execute(query, data)
result_rows: Iterable[GetTuneSessionsRow] = cur.fetchall() row = cur.fetchone()
return dict(groupby( return row[0] if row else None
result_rows,
key_fn=lambda row: row[0],
group_fn=lambda rows: list(sessions_conversion.row_to_session(row[1:]) for row in rows) 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])
)

View File

@@ -4,27 +4,66 @@ from folkugat_web.model import playlists as model
from . import conversion 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 = """ query = """
INSERT INTO playlists INSERT INTO playlists
(id, session_id, set_id, tema_id) (name)
VALUES 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 * RETURNING *
""" """
data = conversion.playlist_entry_to_row(pl_entry) data = conversion.playlist_entry_to_row(pl_entry)
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, data)
row: conversion.PlaylistRowTuple = cur.fetchone() row: conversion.PlaylistEntryRowTuple = cur.fetchone()
return conversion.row_to_playlist_entry(row) 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 = """ query = """
UPDATE playlists UPDATE playlist_entries
SET 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 WHERE
id = :id id = :id
""" """
@@ -35,9 +74,12 @@ def update_playlist_entry(entry: model.PlaylistEntry, con: Connection | None = N
return return
def delete_playlist_entry(entry_id: int, con: Connection | None = None): def delete_playlist_entry(
entry_id: int,
con: Connection | None = None,
):
query = """ query = """
DELETE FROM playlists DELETE FROM playlist_entries
WHERE id = :id WHERE id = :id
""" """
data = dict(id=entry_id) data = dict(id=entry_id)
@@ -47,12 +89,50 @@ def delete_playlist_entry(entry_id: int, con: Connection | None = None):
return 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 = """ query = """
DELETE FROM playlists DELETE FROM playlist_entries
WHERE session_id = :session_id AND set_id = :set_id 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: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, data)

View File

@@ -3,7 +3,17 @@ from typing import TypedDict
from folkugat_web.model import sessions as model 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): class SessionRowDict(TypedDict):
@@ -13,6 +23,8 @@ class SessionRowDict(TypedDict):
end_time: str end_time: str
venue_name: str | None venue_name: str | None
venue_url: str | None venue_url: str | None
notes: str | None
cartell_url: str | None
is_live: bool is_live: bool
@@ -24,6 +36,8 @@ def session_to_row(sessio: model.Session) -> SessionRowDict:
'end_time': sessio.end_time.isoformat(), 'end_time': sessio.end_time.isoformat(),
'venue_name': sessio.venue.name, 'venue_name': sessio.venue.name,
'venue_url': sessio.venue.url, 'venue_url': sessio.venue.url,
'notes': sessio.notes,
'cartell_url': sessio.cartell_url,
'is_live': sessio.is_live, 'is_live': sessio.is_live,
} }
@@ -38,5 +52,7 @@ def row_to_session(row: SessionRowTuple) -> model.Session:
name=row[4], name=row[4],
url=row[5], url=row[5],
), ),
is_live=row[6], notes=row[6],
cartell_url=row[7],
is_live=row[8],
) )

View File

@@ -4,6 +4,7 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None): def create_db(con: Connection | None = None):
with get_connection(con) as con: with get_connection(con) as con:
create_sessions_table(con) create_sessions_table(con)
create_session_playlists_table(con)
def create_sessions_table(con: Connection): def create_sessions_table(con: Connection):
@@ -15,8 +16,25 @@ def create_sessions_table(con: Connection):
end_time TEXT NOT NULL, end_time TEXT NOT NULL,
venue_name TEXT, venue_name TEXT,
venue_url TEXT, venue_url TEXT,
notes TEXT,
cartell_url TEXT,
is_live BOOLEAN DEFAULT false is_live BOOLEAN DEFAULT false
) )
""" """
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query) _ = 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)

Some files were not shown because too many files have changed in this diff Show More