From 9f905c669d4dff7994db54430a3d67d18bb1c8dc Mon Sep 17 00:00:00 2001 From: Yonokid <37304577+Yonokid@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:10:33 -0500 Subject: [PATCH] add search --- Songs/18 Search/box.def | 4 +++ libs/file_navigator.py | 43 +++++++++++++++++++++++- scenes/song_select.py | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 Songs/18 Search/box.def diff --git a/Songs/18 Search/box.def b/Songs/18 Search/box.def new file mode 100644 index 0000000..30df7c7 --- /dev/null +++ b/Songs/18 Search/box.def @@ -0,0 +1,4 @@ +#TITLE:Search Song +#TITLEJA:曲検索 +#COLLECTION:SEARCH +#BACKCOLOR:#800000 diff --git a/libs/file_navigator.py b/libs/file_navigator.py index 2231806..3b2d1ee 100644 --- a/libs/file_navigator.py +++ b/libs/file_navigator.py @@ -1038,7 +1038,8 @@ class Directory(FileSystemItem): 'RECENT', 'FAVORITE', 'DIFFICULTY', - 'RECOMMENDED' + 'RECOMMENDED', + 'SEARCH' ] def __init__(self, path: Path, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex, genre_index: GenreIndex, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None): super().__init__(path, name) @@ -1143,6 +1144,7 @@ class FileNavigator: self.genre_bg = None self.song_count = 0 self.in_dan_select = False + self.current_search = '' logger.info("FileNavigator initialized") def initialize(self, root_dirs: list[Path]): @@ -1392,6 +1394,43 @@ class FileNavigator: temp_items.append(item) return random.sample(temp_items, min(10, len(temp_items))) + def _levenshtein_distance(self, s1: str, s2: str): + # Create a matrix to store distances + m, n = len(s1), len(s2) + dp = [[0] * (n + 1) for _ in range(m + 1)] + + # Initialize base cases + for i in range(m + 1): + dp[i][0] = i + for j in range(n + 1): + dp[0][j] = j + + # Fill the matrix + for i in range(1, m + 1): + for j in range(1, n + 1): + if s1[i-1] == s2[j-1]: + dp[i][j] = dp[i-1][j-1] # No operation needed + else: + dp[i][j] = 1 + min( + dp[i-1][j], # Deletion + dp[i][j-1], # Insertion + dp[i-1][j-1] # Substitution + ) + + return dp[m][n] + + def search_song(self, search_name: str): + items = [] + for path, song in self.all_song_files.items(): + logger.info(f"{song.name[:-4].lower()}, {search_name.lower()}, {self._levenshtein_distance(song.name[:-4].lower(), search_name.lower())}") + if self._levenshtein_distance(song.name[:-4].lower(), search_name.lower()) < 2: + items.append(song) + if isinstance(song, SongFile): + if self._levenshtein_distance(song.tja.metadata.subtitle["en"].lower(), search_name.lower()) < 2: + items.append(song) + + return items + def load_current_directory(self, selected_item: Optional[Directory] = None): """Load pre-generated items for the current directory (unified for root and subdirs)""" dir_key = str(self.current_dir) @@ -1438,6 +1477,8 @@ class FileNavigator: content_items = self.load_diff_sort_items(selected_item, dir_key) elif selected_item.collection == Directory.COLLECTIONS[4]: content_items = self.load_recommended_items(selected_item, dir_key) + elif selected_item.collection == Directory.COLLECTIONS[5]: + content_items = self.search_song(self.current_search) if content_items == []: self.go_back() diff --git a/scenes/song_select.py b/scenes/song_select.py index 69295f4..115976f 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -45,6 +45,7 @@ class State: BROWSING = 0 SONG_SELECTED = 1 DIFF_SORTING = 2 + SEARCHING = 3 class SongSelectScreen(Screen): BOX_CENTER = 444 @@ -69,6 +70,7 @@ class SongSelectScreen(Screen): self.game_transition = None self.demo_song = None self.diff_sort_selector = None + self.search_box = None self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon() self.indicator = Indicator(Indicator.State.SELECT) @@ -167,6 +169,11 @@ class SongSelectScreen(Screen): self.diff_sort_selector = DiffSortSelect(self.navigator.diff_sort_statistics, self.navigator.diff_sort_diff, self.navigator.diff_sort_level) self.text_fade_in.start() self.text_fade_out.start() + elif action == "search": + self.state = State.SEARCHING + self.search_box = SearchBox() + self.text_fade_in.start() + self.text_fade_out.start() elif action == "select_song": current_song = self.navigator.get_current_item() if isinstance(current_song, Directory) and current_song.box.genre_index == GenreIndex.DAN: @@ -225,6 +232,21 @@ class SongSelectScreen(Screen): self.navigator.diff_sort_level = level self.navigator.select_current_item() + def handle_input_search(self): + if self.search_box is None: + raise Exception("search box was not able to be created") + + result = self.player_1.handle_input_search() + self.search_box.current_search = self.player_1.search_string + + if result is not None: + self.state = State.BROWSING + self.search_box = None + self.text_fade_out.reset() + self.text_fade_in.reset() + self.navigator.current_search = result + self.navigator.select_current_item() + def _cancel_selection(self): """Reset to browsing state""" self.player_1.selected_song = False @@ -324,6 +346,9 @@ class SongSelectScreen(Screen): if self.diff_sort_selector is not None: self.diff_sort_selector.update(current_time) + if self.search_box is not None: + self.search_box.update(current_time) + self.check_for_selection() for song in self.navigator.items: @@ -386,6 +411,9 @@ class SongSelectScreen(Screen): if self.diff_sort_selector is not None: self.diff_sort_selector.draw() + if self.search_box is not None: + self.search_box.draw() + if (self.player_1.selected_song and self.state == State.SONG_SELECTED): tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) elif self.state == State.DIFF_SORTING: @@ -428,6 +456,7 @@ class SongSelectPlayer: self.diff_select_move_right = False self.neiro_selector = None self.modifier_selector = None + self.search_string = '' # References to shared animations self.diff_selector_move_1 = tex.get_animation(26, is_copy=True) @@ -511,6 +540,8 @@ class SongSelectPlayer: return "go_back" elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]: return "diff_sort" + elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[5]: + return "search" else: return "select_song" @@ -538,6 +569,20 @@ class SongSelectPlayer: return None + def handle_input_search(self): + if ray.is_key_pressed(ray.KeyboardKey.KEY_BACKSPACE): + self.search_string = self.search_string[:-1] + elif ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER): + result = self.search_string + self.search_string = '' + return result + key = ray.get_char_pressed() + + while key > 0: + self.search_string += chr(key) + key = ray.get_char_pressed() + return None + def handle_input(self, state, screen): """Main input dispatcher. Delegates to state-specific handlers.""" if self.is_voice_playing() or self.is_ready: @@ -549,6 +594,8 @@ class SongSelectPlayer: screen.handle_input_selected() elif state == State.DIFF_SORTING: screen.handle_input_diff_sort() + elif state == State.SEARCHING: + screen.handle_input_search() def handle_input_selected(self, current_item): """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" @@ -1027,6 +1074,31 @@ class DiffSortSelect: else: self.draw_diff_select() +class SearchBox: + def __init__(self): + self.bg_resize = tex.get_animation(19) + self.diff_fade_in = tex.get_animation(20) + self.bg_resize.start() + self.diff_fade_in.start() + self.current_search = '' + + def update(self, current_ms): + self.bg_resize.update(current_ms) + self.diff_fade_in.update(current_ms) + + def draw(self): + ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.fade(ray.BLACK, 0.6)) + tex.draw_texture('diff_sort', 'background', scale=self.bg_resize.attribute, center=True) + background = tex.textures['diff_sort']['background'] + fade = self.diff_fade_in.attribute + text_box_width, text_box_height = 400 * tex.screen_scale, 60 * tex.screen_scale + x, y = background.width//2 + background.x[0] - text_box_width//2, background.height//2 + background.y[0] - text_box_height//2 + text_box = ray.Rectangle(x, y, text_box_width, text_box_height) + ray.draw_rectangle_rec(text_box, ray.fade(ray.LIGHTGRAY, fade)) + ray.draw_rectangle_lines(int(text_box.x), int(text_box.y), int(text_box.width), int(text_box.height), ray.fade(ray.DARKGRAY, fade)) + text_size = ray.measure_text_ex(global_data.font, self.current_search, int(30 * tex.screen_scale), 1) + ray.draw_text_ex(global_data.font, self.current_search, ray.Vector2(x + text_box_width//2 - text_size.x//2, y + text_box_height//2 - text_size.y//2), int(30 * tex.screen_scale), 1, ray.BLACK) + class NeiroSelector: """The menu for selecting the game hitsounds.""" def __init__(self, player_num: PlayerNum):