added python app

This commit is contained in:
2025-02-13 17:06:05 +01:00
parent 96273071bb
commit c468ec7d57
18 changed files with 561 additions and 1 deletions
+163
View File
@@ -0,0 +1,163 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
*.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Flet
storage/
+23
View File
@@ -0,0 +1,23 @@
# Flet app
Flet app using Flet extension.
To run the app:
1. Install dependencies from pyproject.toml:
```
poetry install
```
2. Build app:
```
poetry run flet build macos -v
```
3. Run app:
```
poetry run flet run
```
+41
View File
@@ -0,0 +1,41 @@
[project]
name = "songsheet_generator"
version = "0.1.0"
description = ""
readme = "README.md"
requires-python = ">=3.9"
authors = [
{ name = "Leopold Fajtak", email = "leopold@fajtak.at" }
]
dependencies = [
"flet==0.26.0"
]
[tool.flet]
# org name in reverse domain name notation, e.g. "com.mycompany".
# Combined with project.name to build bundle ID for iOS and Android apps
org = "at.fajtak.songsheet_generator"
# project display name that is used as an app title on Android and iOS home screens,
# shown in window titles and about app dialogs on desktop.
product = "Songsheet Generator"
# company name to display in about app dialogs
company = "Leopold Fajtak"
# copyright text to display in about app dialogs
copyright = "Copyright (C) 2025 by Leopold Fajtak"
[tool.flet.app]
path = "src/songsheet_generator"
[tool.uv]
dev-dependencies = [
"flet[all]==0.26.0",
]
[tool.poetry]
package-mode = false
[tool.poetry.group.dev.dependencies]
flet = {extras = ["all"], version = "0.26.0"}
+2
View File
@@ -0,0 +1,2 @@
flet[all]
dependency_injector
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,24 @@
import flet as ft
from pathlib import PurePath
from songsheet_generator.service.config_service import ConfigService
class ConfigComponent(ft.Column):
# application's root control is a Column containing all other controls
def __init__(self, config_service: ConfigService):
super().__init__()
self.config_service = config_service
self.path = ft.TextField(label="Bibliothek Pfad", value = self.config_service.library_path, on_change=self.textbox_changed)
self.width = 600
self.controls = [
self.path,
ft.ElevatedButton(text="Save config", on_click=self.save_config)
]
def textbox_changed(self, e):
self.config_service.library_path = e.control.value
def save_config(self, e):
self.config_service.save_config_to_storage()
@@ -0,0 +1,91 @@
import flet as ft
from songsheet_generator.types.song_purpose import SongPurpose
from songsheet_generator.types.song import Song
from songsheet_generator.service.song_service import SongService
from pathlib import Path
class SongComponent(ft.Column):
def __init__(self,
song_service: SongService,
song_delete = lambda a : None,
song: Song = Song(),
):
super().__init__()
self.song_service = song_service
self.song = song
self.delete = song_delete
self.display_song = ft.Row(spacing=0, controls=[
ft.Text(self.song.purpose.display_name, weight=ft.FontWeight.BOLD),
ft.Text(self.song.path),
])
self.edit_purpose = ft.Dropdown(
label = "Messteil",
hint_text="Teil der Messe, der gesungen wird",
options=[ ft.dropdown.Option(key=purpose.key, text=purpose.display_name) for purpose in SongPurpose.get_all_purposes() ],
autofocus=True,
)
self.edit_path = ft.Dropdown(
label="Pfad",
options = [ft.dropdown.Option(key=path) for path in self.song_service.all_songs()]
)
self.edit_name = ft.TextField(expand=1)
self.display_view = ft.Row(
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
self.display_song,
ft.Row(
spacing=0,
controls=[
ft.IconButton(
icon=ft.Icons.CREATE_OUTLINED,
tooltip="Lied bearbeiten",
on_click=self.edit_clicked,
),
ft.IconButton(
ft.Icons.DELETE_OUTLINE,
tooltip="Lied entfernen",
on_click=self.delete_clicked,
),
],
),
],
)
self.edit_view = ft.Row(
visible=False,
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
self.edit_purpose,
self.edit_path,
ft.IconButton(
icon=ft.Icons.DONE_OUTLINE_OUTLINED,
icon_color=ft.Colors.GREEN,
tooltip="Update Song",
on_click=self.save_clicked,
),
],
)
self.controls = [self.display_view, self.edit_view]
def edit_clicked(self, e):
self.edit_purpose.value = self.song.purpose.key
self.edit_path.value = self.song.path
self.display_view.visible = False
self.edit_view.visible = True
self.update()
def save_clicked(self, e):
self.song.purpose = SongPurpose(self.edit_purpose.value)
self.display_song.controls[0].value = self.song.purpose.display_name
self.display_song.controls[1].value = Path(self.song.path)
self.display_view.visible = True
self.edit_view.visible = False
self.update()
def delete_clicked(self, e):
self.delete(self)
@@ -0,0 +1,47 @@
import flet as ft
from pathlib import Path
from .song_component import SongComponent
from songsheet_generator.types.song_purpose import SongPurpose
from songsheet_generator.types.song import Song
from songsheet_generator.service.song_service import SongService
from songsheet_generator.containers import Services
class SongsComponent(ft.Column):
# application's root control is a Column containing all other controls
def __init__(self, song_service: SongService):
super().__init__()
self.song_service = song_service
self.songs = ft.Column()
self.purpose = ft.Dropdown(
label = "Messteil",
hint_text="Teil der Messe, der gesungen wird",
options=[ ft.dropdown.Option(key=purpose.key, text=purpose.display_name) for purpose in SongPurpose.get_all_purposes() ],
autofocus=True,
)
self.path = ft.Dropdown(
label="Pfad",
options = [ft.dropdown.Option(key=path) for path in self.song_service.all_songs()]
)
self.width = 600
self.controls = [
ft.Row(
controls=[
self.purpose,
self.path,
ft.FloatingActionButton(
icon=ft.Icons.ADD, on_click=self.add_clicked
),
],
),
self.songs,
]
def add_clicked(self, s):
# todo use dependency injection and the factory design pattern
self.songs.controls.append(SongComponent(song_service = self.song_service, song_delete = self.song_delete, song = Song(SongPurpose(self.purpose.value), Path(self.path.value))))
self.update()
def song_delete(self, song):
self.songs.controls.remove(song)
self.update()
+16
View File
@@ -0,0 +1,16 @@
"""Containers module"""
from dependency_injector import containers, providers
from service.config_service_impl import ConfigServiceImpl
from service.song_service_impl import SongServiceImpl
from components.song_component import SongComponent
class Services(containers.DeclarativeContainer):
config_service = providers.Singleton(ConfigServiceImpl)
song_service = providers.Singleton(SongServiceImpl, config_service = config_service)
song_component_factory = providers.Factory(SongComponent, song_service=song_service)
class Application(containers.DeclarativeContainer):
services = providers.Container(
Services
)
+48
View File
@@ -0,0 +1,48 @@
import flet as ft
from dependency_injector.wiring import Provide, inject
from components.songs_component import SongsComponent
from components.config_component import ConfigComponent
from service.config_service import ConfigService
from service.song_service import SongService
from songsheet_generator.containers import Application
def main(
page: ft.Page,
song_service: SongService = Provide[Application.services.song_service],
config_service: ConfigService = Provide[Application.services.config_service]):
page.title = "Songsheet Generator"
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.update()
config_service.set_page(page)
lieder = SongsComponent(song_service)
config = ConfigComponent(config_service)
tabs = ft.Tabs(
selected_index=1,
animation_duration=300,
tabs = [
ft.Tab(
text="Lieder",
content=lieder,
),
ft.Tab(
text="Events",
content=ft.Text("Events"),
),
ft.Tab(
text="Config",
content=config,
)
]
)
#add application's root control to the page
page.add(tabs)
container1 = Application()
container1.wire(modules=[__name__])
ft.app(main)
@@ -0,0 +1,17 @@
import flet as ft
from abc import ABC, abstractmethod
from pathlib import Path
class ConfigService(ABC):
@property
@abstractmethod
def library_path(self) -> Path:
pass
@abstractmethod
def set_page(self, page: ft.Page):
pass
@abstractmethod
def save_config_to_storage(self):
pass
@@ -0,0 +1,26 @@
import flet as ft
from .config_service import ConfigService
from pathlib import Path
class ConfigServiceImpl(ConfigService):
def __init__(self):
self.library_path: Path = Path.home()
self.page = None
def set_page(self, page: ft.Page):
self.page = page
if self.page.client_storage.contains_key("library_path"):
self.library_path = Path(self.page.client_storage.get("library_path"))
@property
def library_path(self) -> Path:
return self._library_path
@library_path.setter
def library_path(self, value: Path):
self._library_path = value
def save_config_to_storage(self):
if self.page:
self.page.client_storage.set("library_path", self.library_path)
@@ -0,0 +1,2 @@
class Library():
def __init__(self):
@@ -0,0 +1,8 @@
from abc import ABC, abstractmethod
class SongService(ABC):
@abstractmethod
def all_songs():
pass
@@ -0,0 +1,12 @@
from .config_service import ConfigService
from .song_service import SongService
class SongServiceImpl(SongService):
def __init__(self, config_service: ConfigService):
self.config_service = config_service
def all_songs(self):
assignments_paths = list(self.config_service.library_path.rglob('assignments.tt'))
return [ p.parent.relative_to(self.config_service.library_path) for p in assignments_paths ]
@@ -0,0 +1,9 @@
from pathlib import PurePath
from .song_purpose import SongPurpose
class Song():
def __init__(self,
purpose: SongPurpose = SongPurpose(),
path: PurePath = PurePath(),):
self.purpose = purpose
self.path = path
@@ -0,0 +1,27 @@
class SongPurpose():
all_purposes = {
"introitus": ["Eröffnung", "Eröffnung"],
"kyrie": ["Kyrie", "Kyrie"],
"gloria": ["Gloria", "Gloria"],
"kehrvers": ["Antwortpsalm-Kehrvers", "Antwortpsalm-Kehrvers"],
"graduale": ["Graduale", "Graduale"],
"ad_evangelium": ["Ruf vor dem Evangelium/der Passion", "Ruf vor dem Evangelium/der Passion"],
"credo": ["Glaubensbekenntnis", "Glaubensbekenntnis"],
"offertorium": ["Zur Gabenbereitung", "Zur Gabenbereitung"],
"sanctus": ["Sanctus", "Sanctus"],
"agnus": ["Agnus Dei", "Agnus Dei"],
"communio": ["Zur Kommunion", "Zur Kommunion"],
"BMV": ["Marianische Antiphon", "Marianische Antiphon"],
"": ["", "Leer"]
}
def __init__(self, key=""):
self.key = key
try:
[self.print_name, self.display_name] = self.all_purposes.get(key)
except KeyError:
printf(f"{key} is not a purpose option.")
@classmethod
def get_all_purposes(cls):
return [cls(key) for key in cls.all_purposes.keys()]
+5 -1
View File
@@ -7,9 +7,13 @@
purpose_print = purpose_print ? purpose_print : 'Kyrie'; purpose_print = purpose_print ? purpose_print : 'Kyrie';
ELSIF purpose == 'gloria'; ELSIF purpose == 'gloria';
purpose_print = purpose_print ? purpose_print : 'Gloria'; purpose_print = purpose_print ? purpose_print : 'Gloria';
ELSIF purpose == 'graduale'; ELSIF purpose == 'kehrvers';
purpose_print = 'Antwortpsalm-Kehrvers'; purpose_print = 'Antwortpsalm-Kehrvers';
ELSIF purpose == 'graduale';
purpose_print = 'Graduale';
ELSIF purpose == 'tractus'; ELSIF purpose == 'tractus';
purpose_print = 'Traktus';
ELSIF purpose == 'ad_evangelium';
purpose_print = passion ? 'Ruf vor der Passion' : lent ? 'Ruf vor dem Evangelium' : 'Halleluja'; purpose_print = passion ? 'Ruf vor der Passion' : lent ? 'Ruf vor dem Evangelium' : 'Halleluja';
ELSIF purpose == 'credo'; ELSIF purpose == 'credo';
purpose_print = 'Glaubensbekenntnis'; purpose_print = 'Glaubensbekenntnis';