diff --git a/pyamll/components/carousel.py b/pyamll/components/carousel.py index 4403bb0..8c98f6a 100644 --- a/pyamll/components/carousel.py +++ b/pyamll/components/carousel.py @@ -2,6 +2,7 @@ from textual.widgets import Label, ListItem, ListView from textual.containers import Horizontal, Vertical from parser import VocalElement, Lyrics +from parser.modify import ModificationType from enum import Enum from utils import convert_seconds_to_format as fsec @@ -139,6 +140,18 @@ def push(self, vocal_element:VocalElement, active:bool=False, first=False) -> No if active: self.active_item = new_item + + def rebuild(self) -> None: + operation = self.app.CURR_LYRICS.modification_stack[-1] + if operation._type == ModificationType.DELETE: + self.remove_children(".active") # Remove the current active element + # If first element then go to next element + if self.active_item.element.line_index == 0 and self.active_item.element.word_index == 0: + self.move(ScrollDirection.forward) + else: + self.move(ScrollDirection.backward) + # Else go back + class VerticalScroller(ListView): diff --git a/pyamll/components/elementedit.py b/pyamll/components/elementedit.py new file mode 100644 index 0000000..7da2c77 --- /dev/null +++ b/pyamll/components/elementedit.py @@ -0,0 +1,32 @@ +from textual.app import ComposeResult +from textual.containers import Grid +from textual.screen import ModalScreen +from textual.widgets import Button, Label, Input, Checkbox +from parser import VocalElement +from parser.modify import LyricModifyOperation, ModificationType + +class ElementEditModal(ModalScreen[LyricModifyOperation]): + """Screen with a dialog to edit a VocalElement.""" + def __init__(self, element:VocalElement): + self.vocal_element = element + super().__init__() + + def compose(self) -> ComposeResult: + yield Grid( + Label("Edit a VocalElement", id="element_edit_label"), + Input("", id="vocal_element_text"), + Checkbox("Is Explicit?", id="is_explicit_checkbox"), + Button("Delete Element", variant="error", id="delete"), + Button("Save", variant="primary"), + Button("Cancel", id="cancel"), + id="element-edit-dialog" + ) + + def on_mount(self) -> None: + self.query_one(Input).value = self.vocal_element.text + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "cancel": + self.dismiss() + elif event.button.id == "delete": + self.dismiss(LyricModifyOperation(ModificationType.DELETE, self.vocal_element)) \ No newline at end of file diff --git a/pyamll/parser/__init__.py b/pyamll/parser/__init__.py index c4a84f9..9026c1c 100644 --- a/pyamll/parser/__init__.py +++ b/pyamll/parser/__init__.py @@ -58,25 +58,57 @@ def __str__(self): class Lyrics(list): element_map = [] init_list:list[Line] + modification_stack = [] def __init__(self, init_list:list[Line], *args): self.init_list = init_list - for i,line in enumerate(init_list): + self.generate_element_map() + + super().__init__(*args) + + def generate_element_map(self) -> None: + for i,line in enumerate(self.init_list): line:Line = line for j, element in enumerate(line.elements): self.element_map.append([element, i, j]) - super().__init__(*args) - - def get_offset_element(self, element:VocalElement, offset:int) -> VocalElement: - for i,map_item in enumerate(self.element_map): - if element == map_item[0]: - return self.element_map[i+offset][0] - + def get_element_map_index(self, element:VocalElement) -> int: for i,map_item in enumerate(self.element_map): if element == map_item[0]: return i return 0 + + def rebuild(self) -> None: + # Check if any empty lines + del_line_list = [] + del_word_list = [] + + for i, line in enumerate(self.init_list): + for j, word in enumerate(line.elements): + if word is None: + del_word_list.append((i,j)) + + # Update word_index for words after it + for word in line.elements[j+1:]: + word.word_index -=1 + + for i,j in del_word_list: + del self.init_list[i].elements[j] + + for i,line in enumerate(self.init_list): + # check if any empty words + if not line.elements: + del_line_list.append(i) + + # Update line_index of all the elements after it + for _line in self.init_list[i+1:]: + for _elements in _line.elements: + _elements.line_index -=1 + + for i in del_line_list: + del self.init_list[i] + + self.generate_element_map() def process_lyrics(lyrics_str:str) -> Lyrics: diff --git a/pyamll/parser/modify.py b/pyamll/parser/modify.py new file mode 100644 index 0000000..20804f5 --- /dev/null +++ b/pyamll/parser/modify.py @@ -0,0 +1,37 @@ +from enum import Enum +from parser import Lyrics, VocalElement + +class ModificationType(Enum): + EDIT = "edit" + DELETE = "delete" + APPEND = "append" + +class OperationStatus(Enum): + PENDING = "pending" + ONGOING = "ongoing" + EXEUTED = "executed" + FAILED = "failed" + +class LyricModifyOperation(): + def __init__(self,_type:ModificationType, modified_element:VocalElement ,lyrics:Lyrics|None=None) -> None: + self._type = _type + self.lyrics = lyrics + self.status:OperationStatus = OperationStatus.PENDING + self.context:str = "" + self.modified_element = modified_element + + def execute(self) -> Lyrics: + + if self._type == ModificationType.DELETE: + for mapping in self.lyrics.element_map: + i_element:VocalElement = mapping[0] + line = mapping[1] + word = mapping[2] + if (i_element.line_index, i_element.word_index) == (self.modified_element.line_index, self.modified_element.word_index): + break + + self.lyrics.init_list[line].elements[word] = None + + self.lyrics.rebuild() + self.status = OperationStatus.EXEUTED + return self.lyrics diff --git a/pyamll/screens/sync.py b/pyamll/screens/sync.py index e901182..d3d0a46 100644 --- a/pyamll/screens/sync.py +++ b/pyamll/screens/sync.py @@ -1,9 +1,9 @@ from components.carousel import Carousel, ScrollDirection, VerticalScroller from components.playerbox import PlayerBox from components.sidebar import Sidebar -from parser import Lyrics, process_lyrics -from screens.edit import EditScreen - +from components.elementedit import ElementEditModal +from parser import Lyrics +from parser.modify import LyricModifyOperation, OperationStatus from textual import events from textual.app import ComposeResult @@ -28,6 +28,7 @@ def compose(self) -> ComposeResult: tooltip="Set timestamp as the end of current word and start time of the next word"), Button("H", id="set_end_time", tooltip="Set Timestamp as the endtime of the current word and stay there"), + Button("✎", id="edit_vocal_element", tooltip="Edit VocalElement"), id="carousel_control" )) yield PlayerBox(id="player_box", player=self.app.PLAYER) @@ -70,6 +71,18 @@ def on_button_pressed(self, event: Button.Pressed): carousel._nodes[active_item_index + 1].update() carousel._nodes[active_item_index].update() + + elif event.button.id == "edit_vocal_element": + def _set_element(modifier:LyricModifyOperation) -> None: + modifier.lyrics = self.app.CURR_LYRICS + self.app.CURR_LYRICS = modifier.execute() + self.app.CURR_LYRICS.modification_stack.append(modifier) + if modifier.status == OperationStatus.EXEUTED: + # Refresh carousel and vertical scroller + self.app.notify("deleted element.") + carousel.rebuild() + + self.app.push_screen(ElementEditModal(carousel.active_item.element), _set_element) def on_screen_resume(self, event: events.ScreenResume): label: Static = self.query_one("#lyrics_label", Static) diff --git a/pyamll/styles/elementedit.tcss b/pyamll/styles/elementedit.tcss new file mode 100644 index 0000000..01202ef --- /dev/null +++ b/pyamll/styles/elementedit.tcss @@ -0,0 +1,38 @@ +ElementEditModal { + align: center middle; +} + +#element-edit-dialog { + grid-size: 2; + grid-rows: 1fr 3; + border: thick $background 80%; + background: $surface; + padding: 0 1; + width: 60; + height: 20; + grid-gutter: 1 2; +} + +#element_edit_label { + column-span: 2; + width: 1fr; + margin-top: 1; + content-align: center middle; +} + +#vocal_element_text { + column-span: 2; +} + +#is_explicit_checkbox { + column-span: 2; + width: 1fr; +} + +#element-edit-dialog > Button { + width: 1fr; +} + +#element-edit-dialog > #delete { + column-span: 2; +} diff --git a/pyamll/tui.py b/pyamll/tui.py index 5593753..594eae4 100644 --- a/pyamll/tui.py +++ b/pyamll/tui.py @@ -17,6 +17,7 @@ class TTMLApp(App): "styles/sidebar.tcss", "styles/carousel.tcss", "styles/playerbox.tcss", + "styles/elementedit.tcss", ] CURR_LYRICS: Lyrics = None