From a5d568ba342237c475bb891c75742b1b1b4501a9 Mon Sep 17 00:00:00 2001 From: Javanaut Date: Fri, 27 Sep 2024 15:51:34 +0200 Subject: [PATCH] inc streams ui --- bin/ffx/ffx_app.py | 1 + bin/ffx/model/pattern.py | 4 + bin/ffx/model/track.py | 28 +++ bin/ffx/pattern_details_screen.py | 102 ++++++-- bin/ffx/shows_screen.py | 3 +- bin/ffx/track_controller.py | 92 ++++++++ bin/ffx/track_delete_screen.py | 114 +++++++++ ...ream_descriptor.py => track_descriptor.py} | 0 bin/ffx/track_details_screen.py | 222 ++++++++++++++++++ bin/ffx/{stream_type.py => track_type.py} | 2 +- 10 files changed, 544 insertions(+), 24 deletions(-) create mode 100644 bin/ffx/model/track.py create mode 100644 bin/ffx/track_controller.py create mode 100644 bin/ffx/track_delete_screen.py rename bin/ffx/{stream_descriptor.py => track_descriptor.py} (100%) create mode 100644 bin/ffx/track_details_screen.py rename bin/ffx/{stream_type.py => track_type.py} (73%) diff --git a/bin/ffx/ffx_app.py b/bin/ffx/ffx_app.py index 75ca93a..6567f5c 100644 --- a/bin/ffx/ffx_app.py +++ b/bin/ffx/ffx_app.py @@ -8,6 +8,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from ffx.model.show import Base, Show from ffx.model.pattern import Pattern +from ffx.model.track import Track from .shows_screen import ShowsScreen from .warning_screen import WarningScreen diff --git a/bin/ffx/model/pattern.py b/bin/ffx/model/pattern.py index fe8d550..4db1a6c 100644 --- a/bin/ffx/model/pattern.py +++ b/bin/ffx/model/pattern.py @@ -2,6 +2,8 @@ from sqlalchemy import create_engine, Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref from .show import Base +from .track import Track + class Pattern(Base): @@ -22,3 +24,5 @@ class Pattern(Base): # v2.0 # show_id: Mapped[int] = mapped_column(ForeignKey("shows.id", ondelete="CASCADE")) # show: Mapped["Show"] = relationship(back_populates="patterns") + + tracks = relationship('Track', back_populates='pattern', cascade="all, delete") diff --git a/bin/ffx/model/track.py b/bin/ffx/model/track.py new file mode 100644 index 0000000..9bbddc2 --- /dev/null +++ b/bin/ffx/model/track.py @@ -0,0 +1,28 @@ +# from typing import List +from sqlalchemy import create_engine, Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship, declarative_base, sessionmaker + +from .show import Base + + +class Track(Base): + """ + relationship(argument, opt1, opt2, ...) + argument is string of class or Mapped class of the target entity + backref creates a bi-directional corresponding relationship (back_populates preferred) + back_populates points to the corresponding relationship (the actual class attribute identifier) + + See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html + """ + + __tablename__ = 'tracks' + + # v1.x + id = Column(Integer, primary_key=True) + + track_type = Column(Integer) + + # v1.x + pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE")) + pattern = relationship('Pattern', back_populates='tracks') + diff --git a/bin/ffx/pattern_details_screen.py b/bin/ffx/pattern_details_screen.py index 6fb52f9..e1b5f58 100644 --- a/bin/ffx/pattern_details_screen.py +++ b/bin/ffx/pattern_details_screen.py @@ -12,15 +12,19 @@ from ffx.model.pattern import Pattern from .pattern_controller import PatternController from .show_controller import ShowController +from .track_details_screen import TrackDetailsScreen +from .track_delete_screen import TrackDeleteScreen + + # Screen[dict[int, str, int]] class PatternDetailsScreen(Screen): CSS = """ Grid { - grid-size: 2; - grid-rows: 2 auto; - grid-columns: 30 330; + grid-size: 5 11; + grid-rows: 2 2 2 2 6 2 2 6 2 2 2; + grid-columns: 25 25 25 25 25; height: 100%; width: 100%; padding: 1; @@ -32,17 +36,24 @@ class PatternDetailsScreen(Screen): Button { border: none; } + DataTable { - column-span: 2; - min-height: 5; + min-height: 6; } #toplabel { height: 1; } - - .two { - column-span: 2; + + .three { + column-span: 3; + } + + .four { + column-span: 4; + } + .five { + column-span: 5; } .box { @@ -91,9 +102,18 @@ class PatternDetailsScreen(Screen): # self.patternTable.add_row(*map(str, row)) + for subIndex in range(3): + + row4 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),) + self.audioStreamsTable.add_row(*map(str, row4)) + + row5 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),str(subIndex),) + self.subtitleStreamsTable.add_row(*map(str, row5)) + + def compose(self): - self.audioStreamsTable = DataTable() + self.audioStreamsTable = DataTable(classes="five") # Define the columns with headers self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=10) @@ -104,7 +124,7 @@ class PatternDetailsScreen(Screen): self.audioStreamsTable.cursor_type = 'row' - self.subtitleStreamsTable = DataTable() + self.subtitleStreamsTable = DataTable(classes="five") # Define the columns with headers self.column_key_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=10) @@ -116,31 +136,48 @@ class PatternDetailsScreen(Screen): self.subtitleStreamsTable.cursor_type = 'row' - yield Header() with Grid(): - yield Static("Filename pattern" if self.pattern_obj else "New filename pattern", id="toplabel", classes="two") + # 1 + yield Static("Edit filename pattern" if self.pattern_obj else "New filename pattern", id="toplabel") + yield Input(type="text", id="pattern_input", classes="four") + # 2 yield Static("from show") yield Static("", id="showlabel") - yield Static("Pattern") - yield Input(type="text", id="pattern_input") - - yield Static(" ", classes="two") - - yield Static("Audio streams", classes="two") + # 3 + yield Static(" ", classes="five") + # 4 + yield Static(" ", classes="five") + + # 5 + yield Static("Audio streams") + yield Static(" ") + yield Button("Add", id="button_add_audio_stream") + yield Button("Edit", id="button_edit_audio_stream") + yield Button("Delete", id="button_delete_audio_stream") + # 6 yield self.audioStreamsTable - yield Static(" ", classes="two") + # 7 + yield Static(" ", classes="five") - yield Static("Subtitle streams", classes="two") + # 8 + yield Static("Subtitle streams") + yield Static(" ") + yield Button("Add", id="button_add_subtitle_stream") + yield Button("Edit", id="button_edit_subtitle_stream") + yield Button("Delete", id="button_delete_subtitle_stream") + # 9 yield self.subtitleStreamsTable - yield Static(" ", classes="two") + # 10 + yield Static(" ", classes="five") + # 11 yield Button("Save", id="save_button") yield Button("Cancel", id="cancel_button") @@ -172,3 +209,26 @@ class PatternDetailsScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + + + if event.button.id == "button_add_audio_stream": + self.app.push_screen(TrackDetailsScreen(2), self.handle_add_stream) + if event.button.id == "button_edit_audio_stream": + self.app.push_screen(TrackDetailsScreen(2), self.handle_edit_stream) + if event.button.id == "button_delete_audio_stream": + self.app.push_screen(TrackDeleteScreen(), self.handle_delete_stream) + + if event.button.id == "button_add_subtitle_stream": + self.app.push_screen(TrackDetailsScreen(3), self.handle_add_stream) + if event.button.id == "button_edit_subtitle_stream": + self.app.push_screen(TrackDetailsScreen(3), self.handle_edit_stream) + if event.button.id == "button_delete_subtitle_stream": + self.app.push_screen(TrackDeleteScreen(), self.handle_delete_stream) + + + def handle_add_stream(self): + pass + def handle_edit_stream(self): + pass + def handle_delete_stream(self): + pass diff --git a/bin/ffx/shows_screen.py b/bin/ffx/shows_screen.py index a0b1ec0..81acacd 100644 --- a/bin/ffx/shows_screen.py +++ b/bin/ffx/shows_screen.py @@ -121,7 +121,7 @@ class ShowsScreen(Screen): return [(int(s.id), s.name, s.year) for s in q.all()] except Exception as ex: - click.ClickException(f"loadShows(): {repr(ex)}") + raise click.ClickException(f"ShowsScreen.loadShows(): {repr(ex)}") finally: s.close() @@ -131,7 +131,6 @@ class ShowsScreen(Screen): self.table.add_row(*map(str, show)) # Convert each element to a string before adding - def compose(self): # Create the DataTable widget diff --git a/bin/ffx/track_controller.py b/bin/ffx/track_controller.py new file mode 100644 index 0000000..bcc9075 --- /dev/null +++ b/bin/ffx/track_controller.py @@ -0,0 +1,92 @@ +import click + +from ffx.model.track import Track + + +class TrackController(): + + def __init__(self, context): + + self.context = context + self.Session = self.context['database_session'] # convenience + + +# def updatePattern(self, show_id, pattern): +# +# try: +# s = self.Session() +# q = s.query(Pattern).filter(Pattern.show_id == int(show_id), Pattern.pattern == str(pattern)) +# +# if not q.count(): +# pattern = Pattern(show_id = int(show_id), pattern = str(pattern)) +# s.add(pattern) +# s.commit() +# return True +# +# except Exception as ex: +# raise click.ClickException(f"PatternController.updatePattern(): {repr(ex)}") +# finally: +# s.close() +# +# +# +# def findPattern(self, showId, pattern): +# +# try: +# s = self.Session() +# q = s.query(Pattern).filter(Pattern.show_id == int(showId), Pattern.pattern == str(pattern)) +# +# if q.count(): +# pattern = q.first() +# return int(pattern.id) +# else: +# return None +# +# except Exception as ex: +# raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}") +# finally: +# s.close() +# +# +# def getPatternDescriptor(self, patternId): +# +# try: +# s = self.Session() +# q = s.query(Pattern).filter(Pattern.id == int(patternId)) +# +# patternDescriptor = {} +# if q.count(): +# pattern = q.first() +# +# patternDescriptor['id'] = pattern.id +# patternDescriptor['pattern'] = pattern.pattern +# patternDescriptor['show_id'] = pattern.show_id +# +# return patternDescriptor +# +# except Exception as ex: +# raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}") +# finally: +# s.close() +# +# +# def deletePattern(self, patternId): +# try: +# s = self.Session() +# q = s.query(Pattern).filter(Pattern.id == int(patternId)) +# +# if q.count(): +# +# #DAFUQ: https://stackoverflow.com/a/19245058 +# # q.delete() +# pattern = q.first() +# s.delete(pattern) +# +# s.commit() +# return True +# return False +# +# except Exception as ex: +# raise click.ClickException(f"PatternController.deletePattern(): {repr(ex)}") +# finally: +# s.close() diff --git a/bin/ffx/track_delete_screen.py b/bin/ffx/track_delete_screen.py new file mode 100644 index 0000000..0605199 --- /dev/null +++ b/bin/ffx/track_delete_screen.py @@ -0,0 +1,114 @@ +import click + +from textual import events +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input +from textual.containers import Grid, Horizontal + +from ffx.model.pattern import Pattern + +from .show_controller import ShowController +from .pattern_controller import PatternController + + +# Screen[dict[int, str, int]] +class TrackDeleteScreen(Screen): + + CSS = """ + + Grid { + grid-size: 2; + grid-rows: 2 auto; + grid-columns: 30 330; + height: 100%; + width: 100%; + padding: 1; + } + + Input { + border: none; + } + Button { + border: none; + } + #toplabel { + height: 1; + } + + .two { + column-span: 2; + } + + .box { + height: 100%; + border: solid green; + } + """ + + def __init__(self, patternId = None, showId = None): + super().__init__() + + self.context = self.app.getContext() + self.Session = self.context['database_session'] # convenience + + self.__pc = PatternController(context = self.context) + self.__sc = ShowController(context = self.context) + + self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} + self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} + + + def on_mount(self): + if self.show_obj: + self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") + if self.pattern_obj: + self.query_one("#patternlabel", Static).update(str(self.pattern_obj['pattern'])) + + + def compose(self): + + yield Header() + + with Grid(): + + yield Static("Are you sure to delete the following filename pattern?", id="toplabel", classes="two") + + yield Static("", classes="two") + + yield Static("Pattern") + yield Static("", id="patternlabel") + + yield Static("", classes="two") + + yield Static("from show") + yield Static("", id="showlabel") + + yield Static("", classes="two") + + yield Button("Delete", id="delete_button") + yield Button("Cancel", id="cancel_button") + + yield Footer() + + + # Event handler for button press + def on_button_pressed(self, event: Button.Pressed) -> None: + + if event.button.id == "delete_button": + + if self.__pc.deletePattern(self.pattern_obj['id']): + + screenResult = {} + screenResult['show_id'] = self.show_obj['id'] + screenResult['pattern'] = self.pattern_obj['pattern'] + + self.dismiss(screenResult) + + else: + #TODO: Meldung + self.app.pop_screen() + + if event.button.id == "cancel_button": + self.app.pop_screen() + diff --git a/bin/ffx/stream_descriptor.py b/bin/ffx/track_descriptor.py similarity index 100% rename from bin/ffx/stream_descriptor.py rename to bin/ffx/track_descriptor.py diff --git a/bin/ffx/track_details_screen.py b/bin/ffx/track_details_screen.py new file mode 100644 index 0000000..4702307 --- /dev/null +++ b/bin/ffx/track_details_screen.py @@ -0,0 +1,222 @@ +import click + +from textual import events +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input +from textual.containers import Grid, Horizontal + +from ffx.model.show import Show +from ffx.model.pattern import Pattern + +from .track_controller import TrackController +# from .pattern_controller import PatternController +# from .show_controller import ShowController + +from .track_type import TrackType + + +# Screen[dict[int, str, int]] +class TrackDetailsScreen(Screen): + + CSS = """ + + Grid { + grid-size: 5 11; + grid-rows: 2 2 2 2 6 2 2 6 2 2 2; + grid-columns: 25 25 25 25 25; + height: 100%; + width: 100%; + padding: 1; + } + + Input { + border: none; + } + Button { + border: none; + } + + DataTable { + min-height: 6; + } + + #toplabel { + height: 1; + } + + .three { + column-span: 3; + } + + .four { + column-span: 4; + } + .five { + column-span: 5; + } + + .box { + height: 100%; + border: solid green; + } + """ + + STREAM_TYPE_LABELS = [ + 'video', + 'audio', + 'subtitle' + ] + + def __init__(self, trackType : TrackType, streamId = None, patternId = None): + super().__init__() + + self.context = self.app.getContext() + self.Session = self.context['database_session'] # convenience + + self.trackType = trackType + + self.__tc = TrackController(context = self.context) + #self.__pc = PatternController(context = self.context) + #self.__sc = ShowController(context = self.context) + + # self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} + # self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} + self.track_obj = {} + +# def loadPatterns(self, show_id): +# +# try: +# s = self.Session() +# q = s.query(Pattern).filter(Pattern.show_id == int(show_id)) +# +# return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()] +# +# except Exception as ex: +# click.ClickException(f"loadPatterns(): {repr(ex)}") +# finally: +# s.close() + + + def on_mount(self): + pass + # if self.pattern_obj: + # self.query_one("#pattern_input", Input).value = str(self.pattern_obj['pattern']) + + # if self.show_obj: + # self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") + + # for pattern in self.loadPatterns(int(self.show_obj['id'])): + # row = (pattern['pattern'],) + # self.patternTable.add_row(*map(str, row)) + + +# for subIndex in range(3): +# +# row4 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),) +# self.audioStreamsTable.add_row(*map(str, row4)) +# +# row5 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),str(subIndex),) +# self.subtitleStreamsTable.add_row(*map(str, row5)) + + + def compose(self): + +# self.audioStreamsTable = DataTable(classes="five") +# +# # Define the columns with headers +# self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=10) +# self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=10) +# self.column_key_audio_language = self.audioStreamsTable.add_column("Language", width=10) +# self.column_key_audio_title = self.audioStreamsTable.add_column("Title", width=10) +# +# self.audioStreamsTable.cursor_type = 'row' +# +# +# self.subtitleStreamsTable = DataTable(classes="five") +# +# # Define the columns with headers +# self.column_key_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=10) +# self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=10) +# self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=10) +# self.column_key_subtitle_default = self.subtitleStreamsTable.add_column("Default", width=10) +# self.column_key_subtitle_forced = self.subtitleStreamsTable.add_column("Forced", width=10) +# +# self.subtitleStreamsTable.cursor_type = 'row' + + typeLabel = TrackDetailsScreen.STREAM_TYPE_LABELS[self.trackType-1] + + yield Header() + + with Grid(): + + # 1 + yield Static(f"Edit {typeLabel} stream" if self.track_obj else f"New {typeLabel} stream", id="toplabel", classes="five") +# yield Input(type="text", id="pattern_input", classes="four") + + # 2 +# yield Static("from show") +# yield Static("", id="showlabel") +# +# # 3 +# yield Static(" ", classes="five") +# # 4 +# yield Static(" ", classes="five") +# +# # 5 +# yield Static("Audio streams") +# yield Static(" ") +# yield Button("Add", id="button_add_audio_stream") +# yield Button("Edit", id="button_edit_audio_stream") +# yield Button("Delete", id="button_delete_audio_stream") +# # 6 +# yield self.audioStreamsTable +# +# # 7 +# yield Static(" ", classes="five") +# +# # 8 +# yield Static("Subtitle streams") +# yield Static(" ") +# yield Button("Add", id="button_add_subtitle_stream") +# yield Button("Edit", id="button_edit_subtitle_stream") +# yield Button("Delete", id="button_delete_subtitle_stream") +# # 9 +# yield self.subtitleStreamsTable +# + # 10 + yield Static(" ", classes="five") + + # 11 + yield Button("Save", id="save_button") + yield Button("Cancel", id="cancel_button") + + yield Footer() + + + # def getPatternFromInput(self): + # return str(self.query_one("#pattern_input", Input).value) + + + # Event handler for button press + def on_button_pressed(self, event: Button.Pressed) -> None: + # Check if the button pressed is the one we are interested in +# if event.button.id == "save_button": +# +# pattern = self.getPatternFromInput() +# +# if self.__pc.updatePattern(self.show_obj['id'], pattern): +# +# screenResult = {} +# screenResult['show_id'] = self.show_obj['id'] +# screenResult['pattern'] = pattern +# +# self.dismiss(screenResult) +# else: +# #TODO: Meldung +# self.app.pop_screen() + + if event.button.id == "cancel_button": + self.app.pop_screen() + + diff --git a/bin/ffx/stream_type.py b/bin/ffx/track_type.py similarity index 73% rename from bin/ffx/stream_type.py rename to bin/ffx/track_type.py index 4ea60e8..e76ce55 100644 --- a/bin/ffx/stream_type.py +++ b/bin/ffx/track_type.py @@ -1,6 +1,6 @@ from enum import Enum -class StreamType(Enum): +class TrackType(Enum): VIDEO = 1 AUDIO = 2 SUBTITLE = 3