Skip to content

Commit f1d967b

Browse files
committed
feat: add ElementEditModal for editing VocalElements and implement rebuild functionality
1 parent 41e51c9 commit f1d967b

File tree

7 files changed

+177
-11
lines changed

7 files changed

+177
-11
lines changed

pyamll/components/carousel.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from textual.widgets import Label, ListItem, ListView
33
from textual.containers import Horizontal, Vertical
44
from parser import VocalElement, Lyrics
5+
from parser.modify import ModificationType
56
from enum import Enum
67
from utils import convert_seconds_to_format as fsec
78

@@ -139,6 +140,18 @@ def push(self, vocal_element:VocalElement, active:bool=False, first=False) -> No
139140

140141
if active:
141142
self.active_item = new_item
143+
144+
def rebuild(self) -> None:
145+
operation = self.app.CURR_LYRICS.modification_stack[-1]
146+
if operation._type == ModificationType.DELETE:
147+
self.remove_children(".active") # Remove the current active element
148+
# If first element then go to next element
149+
if self.active_item.element.line_index == 0 and self.active_item.element.word_index == 0:
150+
self.move(ScrollDirection.forward)
151+
else:
152+
self.move(ScrollDirection.backward)
153+
# Else go back
154+
142155

143156

144157
class VerticalScroller(ListView):

pyamll/components/elementedit.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from textual.app import ComposeResult
2+
from textual.containers import Grid
3+
from textual.screen import ModalScreen
4+
from textual.widgets import Button, Label, Input, Checkbox
5+
from parser import VocalElement
6+
from parser.modify import LyricModifyOperation, ModificationType
7+
8+
class ElementEditModal(ModalScreen[LyricModifyOperation]):
9+
"""Screen with a dialog to edit a VocalElement."""
10+
def __init__(self, element:VocalElement):
11+
self.vocal_element = element
12+
super().__init__()
13+
14+
def compose(self) -> ComposeResult:
15+
yield Grid(
16+
Label("Edit a VocalElement", id="element_edit_label"),
17+
Input("", id="vocal_element_text"),
18+
Checkbox("Is Explicit?", id="is_explicit_checkbox"),
19+
Button("Delete Element", variant="error", id="delete"),
20+
Button("Save", variant="primary"),
21+
Button("Cancel", id="cancel"),
22+
id="element-edit-dialog"
23+
)
24+
25+
def on_mount(self) -> None:
26+
self.query_one(Input).value = self.vocal_element.text
27+
28+
def on_button_pressed(self, event: Button.Pressed) -> None:
29+
if event.button.id == "cancel":
30+
self.dismiss()
31+
elif event.button.id == "delete":
32+
self.dismiss(LyricModifyOperation(ModificationType.DELETE, self.vocal_element))

pyamll/parser/__init__.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,57 @@ def __str__(self):
5858
class Lyrics(list):
5959
element_map = []
6060
init_list:list[Line]
61+
modification_stack = []
6162

6263
def __init__(self, init_list:list[Line], *args):
6364
self.init_list = init_list
64-
for i,line in enumerate(init_list):
65+
self.generate_element_map()
66+
67+
super().__init__(*args)
68+
69+
def generate_element_map(self) -> None:
70+
for i,line in enumerate(self.init_list):
6571
line:Line = line
6672
for j, element in enumerate(line.elements):
6773
self.element_map.append([element, i, j])
68-
super().__init__(*args)
69-
70-
def get_offset_element(self, element:VocalElement, offset:int) -> VocalElement:
71-
for i,map_item in enumerate(self.element_map):
72-
if element == map_item[0]:
73-
return self.element_map[i+offset][0]
74-
74+
7575
def get_element_map_index(self, element:VocalElement) -> int:
7676
for i,map_item in enumerate(self.element_map):
7777
if element == map_item[0]:
7878
return i
7979
return 0
80+
81+
def rebuild(self) -> None:
82+
# Check if any empty lines
83+
del_line_list = []
84+
del_word_list = []
85+
86+
for i, line in enumerate(self.init_list):
87+
for j, word in enumerate(line.elements):
88+
if word is None:
89+
del_word_list.append((i,j))
90+
91+
# Update word_index for words after it
92+
for word in line.elements[j+1:]:
93+
word.word_index -=1
94+
95+
for i,j in del_word_list:
96+
del self.init_list[i].elements[j]
97+
98+
for i,line in enumerate(self.init_list):
99+
# check if any empty words
100+
if not line.elements:
101+
del_line_list.append(i)
102+
103+
# Update line_index of all the elements after it
104+
for _line in self.init_list[i+1:]:
105+
for _elements in _line.elements:
106+
_elements.line_index -=1
107+
108+
for i in del_line_list:
109+
del self.init_list[i]
110+
111+
self.generate_element_map()
80112

81113

82114
def process_lyrics(lyrics_str:str) -> Lyrics:

pyamll/parser/modify.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from enum import Enum
2+
from parser import Lyrics, VocalElement
3+
4+
class ModificationType(Enum):
5+
EDIT = "edit"
6+
DELETE = "delete"
7+
APPEND = "append"
8+
9+
class OperationStatus(Enum):
10+
PENDING = "pending"
11+
ONGOING = "ongoing"
12+
EXEUTED = "executed"
13+
FAILED = "failed"
14+
15+
class LyricModifyOperation():
16+
def __init__(self,_type:ModificationType, modified_element:VocalElement ,lyrics:Lyrics|None=None) -> None:
17+
self._type = _type
18+
self.lyrics = lyrics
19+
self.status:OperationStatus = OperationStatus.PENDING
20+
self.context:str = ""
21+
self.modified_element = modified_element
22+
23+
def execute(self) -> Lyrics:
24+
25+
if self._type == ModificationType.DELETE:
26+
for mapping in self.lyrics.element_map:
27+
i_element:VocalElement = mapping[0]
28+
line = mapping[1]
29+
word = mapping[2]
30+
if (i_element.line_index, i_element.word_index) == (self.modified_element.line_index, self.modified_element.word_index):
31+
break
32+
33+
self.lyrics.init_list[line].elements[word] = None
34+
35+
self.lyrics.rebuild()
36+
self.status = OperationStatus.EXEUTED
37+
return self.lyrics

pyamll/screens/sync.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from components.carousel import Carousel, ScrollDirection, VerticalScroller
22
from components.playerbox import PlayerBox
33
from components.sidebar import Sidebar
4-
from parser import Lyrics, process_lyrics
5-
from screens.edit import EditScreen
6-
4+
from components.elementedit import ElementEditModal
5+
from parser import Lyrics
6+
from parser.modify import LyricModifyOperation, OperationStatus
77

88
from textual import events
99
from textual.app import ComposeResult
@@ -28,6 +28,7 @@ def compose(self) -> ComposeResult:
2828
tooltip="Set timestamp as the end of current word and start time of the next word"),
2929
Button("H", id="set_end_time",
3030
tooltip="Set Timestamp as the endtime of the current word and stay there"),
31+
Button("✎", id="edit_vocal_element", tooltip="Edit VocalElement"),
3132
id="carousel_control"
3233
))
3334
yield PlayerBox(id="player_box", player=self.app.PLAYER)
@@ -70,6 +71,18 @@ def on_button_pressed(self, event: Button.Pressed):
7071
carousel._nodes[active_item_index + 1].update()
7172

7273
carousel._nodes[active_item_index].update()
74+
75+
elif event.button.id == "edit_vocal_element":
76+
def _set_element(modifier:LyricModifyOperation) -> None:
77+
modifier.lyrics = self.app.CURR_LYRICS
78+
self.app.CURR_LYRICS = modifier.execute()
79+
self.app.CURR_LYRICS.modification_stack.append(modifier)
80+
if modifier.status == OperationStatus.EXEUTED:
81+
# Refresh carousel and vertical scroller
82+
self.app.notify("deleted element.")
83+
carousel.rebuild()
84+
85+
self.app.push_screen(ElementEditModal(carousel.active_item.element), _set_element)
7386

7487
def on_screen_resume(self, event: events.ScreenResume):
7588
label: Static = self.query_one("#lyrics_label", Static)

pyamll/styles/elementedit.tcss

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
ElementEditModal {
2+
align: center middle;
3+
}
4+
5+
#element-edit-dialog {
6+
grid-size: 2;
7+
grid-rows: 1fr 3;
8+
border: thick $background 80%;
9+
background: $surface;
10+
padding: 0 1;
11+
width: 60;
12+
height: 20;
13+
grid-gutter: 1 2;
14+
}
15+
16+
#element_edit_label {
17+
column-span: 2;
18+
width: 1fr;
19+
margin-top: 1;
20+
content-align: center middle;
21+
}
22+
23+
#vocal_element_text {
24+
column-span: 2;
25+
}
26+
27+
#is_explicit_checkbox {
28+
column-span: 2;
29+
width: 1fr;
30+
}
31+
32+
#element-edit-dialog > Button {
33+
width: 1fr;
34+
}
35+
36+
#element-edit-dialog > #delete {
37+
column-span: 2;
38+
}

pyamll/tui.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class TTMLApp(App):
1717
"styles/sidebar.tcss",
1818
"styles/carousel.tcss",
1919
"styles/playerbox.tcss",
20+
"styles/elementedit.tcss",
2021
]
2122

2223
CURR_LYRICS: Lyrics = None

0 commit comments

Comments
 (0)