0
1
mirror of https://github.com/radio95-rnt/RadioPlayer.git synced 2026-02-26 21:53:54 +01:00
This commit is contained in:
2025-08-31 18:03:04 +02:00
parent 955a756e6e
commit a88e1af005

View File

@@ -8,7 +8,7 @@ import shutil
import libcache
import argparse
from datetime import datetime
from typing import List, Dict, Set, Tuple, Optional
from typing import List, Dict, Set, Tuple, Optional, Union
from dataclasses import dataclass
# Configuration
@@ -35,10 +35,31 @@ class Config:
def is_custom_mode(self) -> bool:
return self.custom_playlist_file is not None
@dataclass
class FileItem:
"""Represents either a single file or a folder containing files."""
name: str
is_folder: bool
files: List[str] # For folders: list of contained audio files, For files: [filename]
@property
def display_name(self) -> str:
"""Name to show in the GUI."""
if self.is_folder:
return f"📁 {self.name}/"
return self.name
@property
def all_files(self) -> List[str]:
"""Get all file paths for playlist operations."""
if self.is_folder:
return [os.path.join(self.name, f) for f in self.files]
return self.files
class FileManager:
@staticmethod
def get_audio_files(directory: str) -> List[str]:
"""Get all audio files from the specified directory."""
"""Get all audio files from the specified directory (legacy method for compatibility)."""
audio_files = []
try:
for file in os.listdir(directory):
@@ -52,10 +73,72 @@ class FileManager:
print(f"Error: Permission denied for directory '{directory}'.")
return []
@staticmethod
def get_file_items(directory: str) -> List[FileItem]:
"""Get all audio files and folders containing audio files as FileItem objects."""
items = []
try:
entries = sorted(os.listdir(directory))
for entry in entries:
full_path = os.path.join(directory, entry)
if os.path.isfile(full_path) and entry.lower().endswith(FORMATS):
# Single audio file
items.append(FileItem(name=entry, is_folder=False, files=[entry]))
elif os.path.isdir(full_path):
# Directory - check for audio files inside
audio_files = []
try:
for file in os.listdir(full_path):
if file.lower().endswith(FORMATS):
audio_files.append(file)
except (PermissionError, FileNotFoundError):
continue
if audio_files:
# Folder contains audio files
items.append(FileItem(name=entry, is_folder=True, files=sorted(audio_files)))
return items
except FileNotFoundError:
print(f"Error: Directory '{directory}' not found.")
return []
except PermissionError:
print(f"Error: Permission denied for directory '{directory}'.")
return []
class SearchManager:
@staticmethod
def filter_file_items(items: List[FileItem], search_term: str) -> List[FileItem]:
"""Filter and sort FileItem objects based on search term."""
if not search_term:
return items
search_lower = search_term.lower()
# Group items by match type
starts_with = []
contains = []
has_chars = []
for item in items:
item_name_lower = item.name.lower()
if item_name_lower.startswith(search_lower):
starts_with.append(item)
elif search_lower in item_name_lower:
contains.append(item)
elif SearchManager._has_matching_chars(item_name_lower, search_lower):
has_chars.append(item)
# Return sorted results: starts_with first, then contains, then has_chars
return starts_with + contains + has_chars
@staticmethod
def filter_files(files: List[str], search_term: str) -> List[str]:
"""Filter and sort files based on search term."""
"""Filter and sort files based on search term (legacy method for compatibility)."""
if not search_term:
return files
@@ -110,10 +193,11 @@ class PlaylistManager:
for line in f:
line = line.strip()
if line:
filename = os.path.basename(line)
self.custom_playlist_files.add(filename)
# Store relative path for comparison
rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
self.custom_playlist_files.add(rel_path)
# In custom mode, we'll use 'day' as the default period for display
playlists["custom"]["day"].add(filename)
playlists["custom"]["day"].add(rel_path)
return playlists
else:
# Original functionality for weekly playlists
@@ -130,21 +214,20 @@ class PlaylistManager:
for line in f:
line = line.strip()
if line:
filename = os.path.basename(line)
playlists[day][period].add(filename)
# Store relative path for comparison
rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
playlists[day][period].add(rel_path)
return playlists
def update_playlist_file(self, day: str, period: str, filepath: str, add: bool):
"""Update a playlist file by adding or removing a file."""
def update_playlist_file(self, day: str, period: str, file_item: FileItem, add: bool):
"""Update a playlist file by adding or removing files from a FileItem."""
if self.config.is_custom_mode:
self._update_custom_playlist(filepath, add)
self._update_custom_playlist(file_item, add)
else:
self._update_weekly_playlist(day, period, filepath, add)
self._update_weekly_playlist(day, period, file_item, add)
def _update_custom_playlist(self, filepath: str, add: bool):
def _update_custom_playlist(self, file_item: FileItem, add: bool):
"""Update the custom playlist file."""
full_filepath = os.path.join(FILES_DIR, filepath)
# Ensure the directory exists
os.makedirs(os.path.dirname(self.config.custom_playlist_file), exist_ok=True)
@@ -154,22 +237,31 @@ class PlaylistManager:
with open(self.config.custom_playlist_file, 'r') as f:
lines = f.read().splitlines()
if add and full_filepath not in lines:
lines.append(full_filepath)
self.custom_playlist_files.add(filepath)
elif not add and full_filepath in lines:
lines.remove(full_filepath)
self.custom_playlist_files.discard(filepath)
# Get full paths for all files in the item
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
if add:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
# Update tracking set with relative paths
self.custom_playlist_files.update(file_item.all_files)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
# Remove from tracking set
for filepath in file_item.all_files:
self.custom_playlist_files.discard(filepath)
# Write back to file
with open(self.config.custom_playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else ''))
def _update_weekly_playlist(self, day: str, period: str, filepath: str, add: bool):
def _update_weekly_playlist(self, day: str, period: str, file_item: FileItem, add: bool):
"""Update a weekly playlist file (original functionality)."""
playlist_dir = self.ensure_playlist_dir(day)
playlist_file = os.path.join(playlist_dir, period)
full_filepath = os.path.join(FILES_DIR, filepath)
if not os.path.exists(playlist_file):
with open(playlist_file, 'w') as f:
@@ -178,14 +270,29 @@ class PlaylistManager:
with open(playlist_file, 'r') as f:
lines = f.read().splitlines()
if add and full_filepath not in lines:
lines.append(full_filepath)
elif not add and full_filepath in lines:
lines.remove(full_filepath)
# Get full paths for all files in the item
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
if add:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
with open(playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else ''))
def is_file_item_in_playlist(self, file_item: FileItem, day: str, period: str, playlists: Dict) -> bool:
"""Check if ALL files from a FileItem are in the specified playlist."""
if not file_item.all_files:
return False
playlist_set = playlists.get(day, {}).get(period, set())
return all(filepath in playlist_set for filepath in file_item.all_files)
def copy_day_to_all(self, playlists: Dict, source_day: str, days: List[str]) -> Dict:
"""Copy all playlists from source day to all other days."""
if self.config.is_custom_mode:
@@ -210,17 +317,17 @@ class PlaylistManager:
return playlists
def copy_current_file_to_all(self, playlists: Dict, source_day: str,
days: List[str], current_file: str) -> Tuple[Dict, bool]:
"""Copy current file's playlist assignments to all other days."""
def copy_current_item_to_all(self, playlists: Dict, source_day: str,
days: List[str], current_item: FileItem) -> Tuple[Dict, bool]:
"""Copy current item's playlist assignments to all other days."""
if self.config.is_custom_mode:
# No-op in custom mode
return playlists, False
source_periods = {
period: current_file in playlists[source_day][period]
for period in self.periods
}
# Check which periods the item's files are in
source_periods = {}
for period in self.periods:
source_periods[period] = self.is_file_item_in_playlist(current_item, source_day, period, playlists)
if not any(source_periods.values()):
return playlists, False
@@ -231,13 +338,16 @@ class PlaylistManager:
for period, is_present in source_periods.items():
target_set = playlists[target_day][period]
full_path = os.path.join(FILES_DIR, current_file)
if is_present:
target_set.add(current_file)
# Add all files from the item
target_set.update(current_item.all_files)
else:
target_set.discard(current_file)
# Remove all files from the item
for filepath in current_item.all_files:
target_set.discard(filepath)
# Update the playlist file
playlist_dir = self.ensure_playlist_dir(target_day)
playlist_file = os.path.join(playlist_dir, period)
@@ -247,11 +357,16 @@ class PlaylistManager:
else:
lines = []
if is_present and full_path not in lines:
lines.append(full_path)
elif not is_present:
while full_path in lines:
lines.remove(full_path)
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in current_item.all_files]
if is_present:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
with open(playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else ''))
@@ -297,12 +412,12 @@ class TerminalUtils:
class StatsCalculator:
@staticmethod
def calculate_category_percentages(playlists: Dict, current_day: str, config: Config) -> Optional[Tuple]:
def calculate_category_percentages(playlists: Dict, current_day: str, config: Config, all_file_items: List[FileItem]) -> Optional[Tuple]:
"""Calculate category distribution percentages."""
if config.is_custom_mode:
# In custom mode, show simple stats
custom_files = playlists.get("custom", {}).get("day", set())
total_files = len(FileManager.get_audio_files(FILES_DIR))
total_files = sum(len(item.all_files) for item in all_file_items)
assigned_count = len(custom_files)
if total_files == 0:
@@ -359,10 +474,10 @@ class DisplayManager:
self.config = config
def draw_header(self, playlists: Dict, current_day: str, current_day_idx: int,
days: List[str], term_width: int, force_redraw: bool = False,
state: InterfaceState = None):
days: List[str], term_width: int, all_file_items: List[FileItem],
force_redraw: bool = False, state: InterfaceState = None):
"""Draw the header, only if content has changed."""
result = self.stats.calculate_category_percentages(playlists, current_day, self.config)
result = self.stats.calculate_category_percentages(playlists, current_day, self.config, all_file_items)
percentages, polskie_percentages, total_pl = result or ({}, {}, 0)
if self.config.is_custom_mode:
@@ -377,7 +492,6 @@ class DisplayManager:
f"{cat[:4].capitalize()}: {percentages.get(cat, 0):.1f}% (P:{polskie_percentages.get(cat, 0):.1f}%)"
for cat in ['late_night', 'morning', 'day', 'night']
])
# ... (rest of your header logic for weekly mode is fine)
day_bar = " ".join([f"\033[1;44m[{day}]\033[0m" if i == current_day_idx else f"[{day}]" for i, day in enumerate(days)])
header_content = (category_bar, day_bar)
@@ -385,12 +499,17 @@ class DisplayManager:
if force_redraw or state.last_header != header_content:
self.terminal.move_cursor(1)
self.terminal.clear_line()
# ... (your printing logic for the header)
print(header_content[0].center(term_width))
print("\033[1;37mCategory Distribution:\033[0m".center(term_width), end="", flush=True)
self.terminal.move_cursor(2)
self.terminal.clear_line()
print(header_content[0].center(term_width), end="", flush=True)
if not self.config.is_custom_mode:
self.terminal.move_cursor(3)
self.terminal.clear_line()
print(header_content[1])
print(header_content[1], end="", flush=True)
state.last_header = header_content
def get_header_height(self) -> int:
@@ -409,7 +528,7 @@ class DisplayManager:
print(f"\033[1;33m{search_display}\033[0m")
state.last_search = search_term
def draw_files_section(self, audio_files: List[str], playlists: Dict, selected_idx: int,
def draw_files_section(self, file_items: List[FileItem], playlists: Dict, selected_idx: int,
current_day: str, scroll_offset: int, term_width: int, term_height: int,
force_redraw: bool = False, state: InterfaceState = None):
"""Draw the files list, optimized to only redraw when necessary."""
@@ -418,13 +537,13 @@ class DisplayManager:
available_lines = term_height - content_start_row
start_idx = scroll_offset
end_idx = min(start_idx + available_lines, len(audio_files))
end_idx = min(start_idx + available_lines, len(file_items))
# Create a snapshot of the current state to compare against the last one
files_display_state = (
start_idx, end_idx, selected_idx, current_day,
# We also need to know if the playlist data for the visible files has changed
tuple(f in playlists.get(current_day, {}).get('day', set()) for f in audio_files[start_idx:end_idx])
# We also need to know if the playlist data for the visible items has changed
tuple(self._get_item_playlist_status(item, playlists, current_day) for item in file_items[start_idx:end_idx])
)
if force_redraw or state.last_files_display != files_display_state:
@@ -440,44 +559,47 @@ class DisplayManager:
print(" ", end="")
if self.config.is_custom_mode:
position_info = f" Custom | File {selected_idx + 1}/{len(audio_files)} "
position_info = f" Custom | Item {selected_idx + 1}/{len(file_items)} "
else:
position_info = f" {current_day.capitalize()} | File {selected_idx + 1}/{len(audio_files)} "
position_info = f" {current_day.capitalize()} | Item {selected_idx + 1}/{len(file_items)} "
padding = term_width - len(position_info) - 2
print(position_info.center(padding), end="")
if end_idx < len(audio_files):
if end_idx < len(file_items):
print("", end="", flush=True)
else:
print(" ", end="", flush=True)
# File list
for display_row, idx in enumerate(range(start_idx, end_idx)):
file = audio_files[idx]
item = file_items[idx]
line_row = content_start_row + display_row
self.terminal.move_cursor(line_row)
self.terminal.clear_line()
if self.config.is_custom_mode:
# In custom mode, only show 'C' for custom playlist
in_custom = file in playlists.get("custom", {}).get("day", set())
in_custom = all(filepath in playlists.get("custom", {}).get("day", set())
for filepath in item.all_files)
c_color = "\033[1;32m" if in_custom else "\033[1;30m"
row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 6
display_file = file
if len(file) > max_filename_length:
display_file = file[:max_filename_length-3] + "..."
display_name = item.display_name
if len(display_name) > max_filename_length:
display_name = display_name[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row)
self.terminal.clear_line()
print(f"{row_highlight}[{c_color}C\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
print(f"{row_highlight}[{c_color}C\033[0m{row_highlight}] {display_name}\033[0m", end="", flush=True)
else:
# Original weekly mode display
in_late_night = file in playlists[current_day]['late_night']
in_morning = file in playlists[current_day]['morning']
in_day = file in playlists[current_day]['day']
in_night = file in playlists[current_day]['night']
in_late_night = all(filepath in playlists[current_day]['late_night']
for filepath in item.all_files)
in_morning = all(filepath in playlists[current_day]['morning']
for filepath in item.all_files)
in_day = all(filepath in playlists[current_day]['day']
for filepath in item.all_files)
in_night = all(filepath in playlists[current_day]['night']
for filepath in item.all_files)
l_color = "\033[1;32m" if in_late_night else "\033[1;30m"
m_color = "\033[1;32m" if in_morning else "\033[1;30m"
@@ -487,13 +609,11 @@ class DisplayManager:
row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 15
display_file = file
if len(file) > max_filename_length:
display_file = file[:max_filename_length-3] + "..."
display_name = item.display_name
if len(display_name) > max_filename_length:
display_name = display_name[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row)
self.terminal.clear_line()
print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_name}\033[0m", end="", flush=True)
# Clear remaining lines
last_end_idx = state.last_files_display[1] if state.last_files_display else 0
@@ -504,112 +624,18 @@ class DisplayManager:
state.last_files_display = files_display_state
# Day bar
day_bar = ""
for i, day in enumerate(days):
if i == current_day_idx:
day_bar += f"\033[1;44m[{day}]\033[0m "
else:
day_bar += f"[{day}] "
header_content = (category_bar, day_bar.strip())
if force_redraw or (state and state.last_header != header_content):
self.terminal.move_cursor(1)
self.terminal.clear_line()
print("\033[1;37mCategory Distribution:\033[0m".center(term_width), end="", flush=True)
self.terminal.move_cursor(2)
self.terminal.clear_line()
print(category_bar.center(term_width), end="", flush=True)
self.terminal.move_cursor(3)
self.terminal.clear_line()
print(day_bar.strip(), end="", flush=True)
if state:
state.last_header = header_content
def draw_search_bar(self, search_term: str, term_width: int, force_redraw: bool = False,
state: InterfaceState = None):
"""Draw the search bar."""
if force_redraw or (state and state.last_search != search_term):
self.terminal.move_cursor(self.get_header_height() + 3)
self.terminal.clear_line()
search_display = f"Search: {search_term}"
if len(search_display) > term_width - 2:
search_display = search_display[:term_width - 5] + "..."
print(f"\033[1;33m{search_display}\033[0m", end="", flush=True)
if state:
state.last_search = search_term
def draw_files_section(self, audio_files: List[str], playlists: Dict, selected_idx: int,
current_day: str, scroll_offset: int, term_width: int, term_height: int,
force_redraw: bool = False, state: InterfaceState = None):
"""Draw the files list section."""
available_lines = term_height - 4 - self.get_header_height() # Adjusted for search bar
start_idx = max(0, min(scroll_offset, len(audio_files) - available_lines))
end_idx = min(start_idx + available_lines, len(audio_files))
files_display_state = (start_idx, end_idx, selected_idx, current_day)
if force_redraw or (state and (state.last_files_display != files_display_state or
state.last_selected_idx != selected_idx)):
# Position info line
self.terminal.move_cursor(7)
self.terminal.clear_line()
if start_idx > 0:
print("", end="")
else:
print(" ", end="")
position_info = f" {current_day.capitalize()} | File {selected_idx + 1}/{len(audio_files)} "
padding = term_width - len(position_info) - 2
print(position_info.center(padding), end="")
if end_idx < len(audio_files):
print("", end="", flush=True)
else:
print(" ", end="", flush=True)
# File list
for display_row, idx in enumerate(range(start_idx, end_idx)):
file = audio_files[idx]
line_row = 4 + display_row + self.get_header_height() # Start after header and search
in_late_night = file in playlists[current_day]['late_night']
in_morning = file in playlists[current_day]['morning']
in_day = file in playlists[current_day]['day']
in_night = file in playlists[current_day]['night']
l_color = "\033[1;32m" if in_late_night else "\033[1;30m"
m_color = "\033[1;32m" if in_morning else "\033[1;30m"
d_color = "\033[1;32m" if in_day else "\033[1;30m"
n_color = "\033[1;32m" if in_night else "\033[1;30m"
row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 15
display_file = file
if len(file) > max_filename_length:
display_file = file[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row)
self.terminal.clear_line()
if not self.config.is_custom_mode: print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
else: print(f"{row_highlight}[{d_color}C\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
# Clear remaining lines
for clear_row in range(9 + (end_idx - start_idx), term_height):
self.terminal.move_cursor(clear_row)
self.terminal.clear_line()
if state:
state.last_files_display = files_display_state
state.last_selected_idx = selected_idx
def _get_item_playlist_status(self, item: FileItem, playlists: Dict, current_day: str) -> Tuple:
"""Get playlist status for an item to use in display state comparison."""
if self.config.is_custom_mode:
return (all(filepath in playlists.get("custom", {}).get("day", set())
for filepath in item.all_files),)
else:
return (
all(filepath in playlists[current_day]['late_night'] for filepath in item.all_files),
all(filepath in playlists[current_day]['morning'] for filepath in item.all_files),
all(filepath in playlists[current_day]['day'] for filepath in item.all_files),
all(filepath in playlists[current_day]['night'] for filepath in item.all_files)
)
class Application:
def __init__(self, config: Config):
@@ -633,8 +659,8 @@ class Application:
self.in_search_mode = False
# Data
self.all_audio_files = []
self.filtered_files = []
self.all_file_items = []
self.filtered_file_items = []
self.playlists = {}
self.days_of_week = []
@@ -649,12 +675,12 @@ class Application:
def initialize_data(self):
"""Initialize application data."""
self.all_audio_files = self.file_manager.get_audio_files(FILES_DIR)
if not self.all_audio_files:
print("No audio files found. Exiting.")
self.all_file_items = self.file_manager.get_file_items(FILES_DIR)
if not self.all_file_items:
print("No audio files or folders found. Exiting.")
return False
self.filtered_files = self.all_audio_files.copy()
self.filtered_file_items = self.all_file_items.copy()
if self.config.is_custom_mode:
self.days_of_week = ["custom"] # Single "day" for custom mode
@@ -665,18 +691,18 @@ class Application:
return True
def update_search(self, new_search: str):
"""Update search term and filter files."""
"""Update search term and filter file items."""
self.search_term = new_search
self.filtered_files = self.search_manager.filter_files(self.all_audio_files, self.search_term)
self.filtered_file_items = self.search_manager.filter_file_items(self.all_file_items, self.search_term)
# Reset selection if current selection is not in filtered results
if self.selected_idx >= len(self.filtered_files):
self.selected_idx = max(0, len(self.filtered_files) - 1)
elif self.filtered_files and self.selected_idx < len(self.filtered_files):
# Keep current file selected if it's still in results
current_file = self.all_audio_files[self.selected_idx] if self.selected_idx < len(self.all_audio_files) else None
if current_file and current_file in self.filtered_files:
self.selected_idx = self.filtered_files.index(current_file)
if self.selected_idx >= len(self.filtered_file_items):
self.selected_idx = max(0, len(self.filtered_file_items) - 1)
elif self.filtered_file_items and self.selected_idx < len(self.filtered_file_items):
# Keep current item selected if it's still in results
current_item = self.all_file_items[self.selected_idx] if self.selected_idx < len(self.all_file_items) else None
if current_item and current_item in self.filtered_file_items:
self.selected_idx = self.filtered_file_items.index(current_item)
else:
self.selected_idx = 0
@@ -703,20 +729,20 @@ class Application:
if self.config.is_custom_mode:
print("UP/DOWN: Navigate | C: Toggle | /: Search | Q: Quit", end="", flush=True)
else:
print("UP/DOWN: Navigate | D/N/L/M: Toggle | C: Copy day | F: Copy file | /: Search | Q: Quit", end="", flush=True)
print("UP/DOWN: Navigate | D/N/L/M: Toggle | C: Copy day | F: Copy item | /: Search | Q: Quit", end="", flush=True)
self.terminal.move_cursor(keybind_row + 1)
print("ESC: Exit search | ENTER: Apply search", end="", flush=True)
# Draw header
self.display.draw_header(self.playlists, current_day, self.current_day_idx,
self.days_of_week, term_width, force_redraw, self.state)
self.days_of_week, term_width, self.all_file_items, force_redraw, self.state)
# Draw search bar
self.display.draw_search_bar(self.search_term, term_width, force_redraw, self.state)
# Draw files section
self.display.draw_files_section(self.filtered_files, self.playlists, self.selected_idx,
self.display.draw_files_section(self.filtered_file_items, self.playlists, self.selected_idx,
current_day, self.scroll_offset, term_width, term_height,
force_redraw, self.state)
@@ -741,7 +767,7 @@ class Application:
if key == 'A': # Up arrow
self.selected_idx = max(0, self.selected_idx - 1)
elif key == 'B': # Down arrow
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + 1)
self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + 1)
elif key == 'C' and not self.config.is_custom_mode: # Right arrow (disabled in custom mode)
self.current_day_idx = (self.current_day_idx + 1) % len(self.days_of_week)
elif key == 'D' and not self.config.is_custom_mode: # Left arrow (disabled in custom mode)
@@ -749,40 +775,46 @@ class Application:
elif key == '5': # Page Up
self.selected_idx = max(0, self.selected_idx - visible_lines)
elif key == '6': # Page Down
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + visible_lines)
self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + visible_lines)
elif key == '1': # Home
self.selected_idx = 0
elif key == '4': # End
self.selected_idx = len(self.filtered_files) - 1
self.selected_idx = len(self.filtered_file_items) - 1
def toggle_playlist(self, period: str):
"""Toggle current file in specified playlist period."""
if not self.filtered_files:
"""Toggle current file item in specified playlist period."""
if not self.filtered_file_items:
return
current_day = self.days_of_week[self.current_day_idx]
file = self.filtered_files[self.selected_idx]
file_item = self.filtered_file_items[self.selected_idx]
if self.config.is_custom_mode:
# In custom mode, all operations work with the "day" period
is_in_playlist = file in self.playlists["custom"]["day"]
is_in_playlist = self.playlist_manager.is_file_item_in_playlist(file_item, "custom", "day", self.playlists)
if is_in_playlist:
self.playlists["custom"]["day"].remove(file)
# Remove all files from the item
for filepath in file_item.all_files:
self.playlists["custom"]["day"].discard(filepath)
else:
self.playlists["custom"]["day"].add(file)
# Add all files from the item
self.playlists["custom"]["day"].update(file_item.all_files)
self.playlist_manager.update_playlist_file("custom", "day", file, not is_in_playlist)
self.playlist_manager.update_playlist_file("custom", "day", file_item, not is_in_playlist)
else:
# Original weekly mode
is_in_playlist = file in self.playlists[current_day][period]
is_in_playlist = self.playlist_manager.is_file_item_in_playlist(file_item, current_day, period, self.playlists)
if is_in_playlist:
self.playlists[current_day][period].remove(file)
# Remove all files from the item
for filepath in file_item.all_files:
self.playlists[current_day][period].discard(filepath)
else:
self.playlists[current_day][period].add(file)
# Add all files from the item
self.playlists[current_day][period].update(file_item.all_files)
self.playlist_manager.update_playlist_file(current_day, period, file, not is_in_playlist)
self.playlist_manager.update_playlist_file(current_day, period, file_item, not is_in_playlist)
def handle_search_input(self, key: str):
"""Handle search input."""
@@ -872,7 +904,7 @@ class Application:
elif key == ' ':
header_height = self.display.get_header_height()
visible_lines = term_height - (header_height + 5)
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + visible_lines)
self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + visible_lines)
elif key.lower() == 'c':
if self.config.is_custom_mode:
# In custom mode, 'c' toggles the custom playlist
@@ -893,29 +925,30 @@ class Application:
elif key.lower() == 'l' and not self.config.is_custom_mode:
self.toggle_playlist('late_night')
elif key.lower() == 'f' and not self.config.is_custom_mode:
if self.filtered_files:
if self.filtered_file_items:
current_day = self.days_of_week[self.current_day_idx]
current_file = self.filtered_files[self.selected_idx]
current_item = self.filtered_file_items[self.selected_idx]
self.playlists, success = self.playlist_manager.copy_current_file_to_all(
self.playlists, current_day, self.days_of_week, current_file)
self.playlists, success = self.playlist_manager.copy_current_item_to_all(
self.playlists, current_day, self.days_of_week, current_item)
if success:
self.flash_message = f"File '{current_file}' copied to all days!"
item_name = current_item.display_name
self.flash_message = f"Item '{item_name}' copied to all days!"
else:
self.flash_message = f"File not in any playlist! Add it first."
self.flash_message = f"Item not in any playlist! Add it first."
self.message_timer = 0
elif key.isupper() and len(key) == 1 and key.isalpha():
# Jump to file starting with letter
# Jump to item starting with letter
target_letter = key.lower()
found_idx = -1
for i in range(self.selected_idx + 1, len(self.filtered_files)):
if self.filtered_files[i].lower().startswith(target_letter):
for i in range(self.selected_idx + 1, len(self.filtered_file_items)):
if self.filtered_file_items[i].name.lower().startswith(target_letter):
found_idx = i
break
if found_idx == -1:
for i in range(0, self.selected_idx):
if self.filtered_files[i].lower().startswith(target_letter):
if self.filtered_file_items[i].name.lower().startswith(target_letter):
found_idx = i
break
if found_idx != -1: