melhorado a identificação de temporadas e episódios, e axclução de pastas vazias
This commit is contained in:
Binary file not shown.
@@ -2,40 +2,86 @@ from nicegui import ui
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT_DIR = "/downloads"
|
# ==============================================================================
|
||||||
|
# 1. CONFIGURAÇÕES GERAIS
|
||||||
|
# ==============================================================================
|
||||||
|
ROOT_DIR = "/downloads" # Diretório fixo conforme solicitado
|
||||||
|
|
||||||
# --- UTILITÁRIOS ---
|
# Extensões consideradas para manter a pasta viva (não apagar se sobrarem)
|
||||||
|
VIDEO_EXTENSIONS = ('.mkv', '.mp4', '.avi', '.mov', '.iso', '.wmv', '.flv', '.webm')
|
||||||
|
# Extensões de legendas para mover junto
|
||||||
|
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.ass', '.vtt')
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2. SISTEMA DE DETECÇÃO (REGEX)
|
||||||
|
# ==============================================================================
|
||||||
def extract_season_episode(filename):
|
def extract_season_episode(filename):
|
||||||
"""Detecta Temporada e Episódio usando vários padrões"""
|
"""
|
||||||
|
Detecta Temporada e Episódio.
|
||||||
|
Suporta padrões internacionais (S01E01), Brasileiros (Temp/Ep) e Ingleses (Season/Episode).
|
||||||
|
"""
|
||||||
patterns = [
|
patterns = [
|
||||||
|
# --- PADRÕES UNIVERSAIS (S01E01, s1e1, S01.E01, S01_E01) ---
|
||||||
r'(?i)S(\d{1,4})[\s._-]*E(\d{1,4})',
|
r'(?i)S(\d{1,4})[\s._-]*E(\d{1,4})',
|
||||||
r'(?i)S(\d{1,4})[\s._-]*EP(\d{1,4})',
|
|
||||||
|
# --- PADRÃO X (1x01, 01x01) ---
|
||||||
r'(?i)(\d{1,4})x(\d{1,4})',
|
r'(?i)(\d{1,4})x(\d{1,4})',
|
||||||
r'(?i)Season[\s._-]*(\d{1,4})[\s._-]*Episode[\s._-]*(\d{1,4})',
|
|
||||||
r'(?i)S(\d{1,4})[\s._-]*-\s*(\d{1,4})',
|
# --- INGLÊS VERBOSO (Season 1 Episode 1, Season 01 - Episode 05) ---
|
||||||
|
# O '.*?' permite textos no meio ex: "Season 1 [1080p] Episode 5"
|
||||||
|
r'(?i)Season[\s._-]*(\d{1,4}).*?Episode[\s._-]*(\d{1,4})',
|
||||||
|
|
||||||
|
# --- PORTUGUÊS VERBOSO (Temporada 1 Episódio 1) ---
|
||||||
|
r'(?i)Temporada[\s._-]*(\d{1,4}).*?Epis[oó]dio[\s._-]*(\d{1,4})',
|
||||||
|
|
||||||
|
# --- ABREVIAÇÕES (Temp 1 Ep 1, T01 E01, S1 Ep1) ---
|
||||||
|
r'(?i)(?:Temp|T|S)[\s._-]*(\d{1,4})[\s._-]*E(?:p)?[\s._-]*(\d{1,4})',
|
||||||
|
|
||||||
|
# --- PADRÃO EPISÓDIO ISOLADO (S01EP01) ---
|
||||||
|
r'(?i)S(\d{1,4})[\s._-]*EP(\d{1,4})',
|
||||||
|
|
||||||
|
# --- COLCHETES ([1x01]) ---
|
||||||
r'(?i)\[(\d{1,4})x(\d{1,4})\]',
|
r'(?i)\[(\d{1,4})x(\d{1,4})\]',
|
||||||
]
|
]
|
||||||
|
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
match = re.search(pattern, filename)
|
match = re.search(pattern, filename)
|
||||||
if match: return match.group(1), match.group(2)
|
if match:
|
||||||
|
return match.group(1), match.group(2)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
def is_video(filename):
|
||||||
|
return filename.lower().endswith(VIDEO_EXTENSIONS)
|
||||||
|
|
||||||
|
def is_subtitle(filename):
|
||||||
|
return filename.lower().endswith(SUBTITLE_EXTENSIONS)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 3. CLASSE DE GERENCIAMENTO (ESTADO)
|
||||||
|
# ==============================================================================
|
||||||
class RenamerManager:
|
class RenamerManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.path = ROOT_DIR
|
self.path = ROOT_DIR
|
||||||
self.container = None
|
self.container = None
|
||||||
self.preview_data = []
|
self.preview_data = []
|
||||||
self.view_mode = 'explorer'
|
self.folders_to_clean = set() # Lista de pastas candidatas à exclusão
|
||||||
|
self.view_mode = 'explorer' # Alterna entre 'explorer' e 'preview'
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 4. NAVEGAÇÃO
|
||||||
|
# ==========================================================================
|
||||||
def navigate(self, path):
|
def navigate(self, path):
|
||||||
|
"""Muda o path atual e atualiza a tela."""
|
||||||
if os.path.exists(path) and os.path.isdir(path):
|
if os.path.exists(path) and os.path.isdir(path):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.refresh()
|
self.refresh()
|
||||||
else:
|
else:
|
||||||
ui.notify('Erro ao acessar pasta', type='negative')
|
ui.notify(f'Erro ao acessar: {path}', type='negative')
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
|
"""Atualiza a UI baseada no modo atual."""
|
||||||
if self.container:
|
if self.container:
|
||||||
self.container.clear()
|
self.container.clear()
|
||||||
with self.container:
|
with self.container:
|
||||||
@@ -45,137 +91,282 @@ class RenamerManager:
|
|||||||
else:
|
else:
|
||||||
self.render_preview()
|
self.render_preview()
|
||||||
|
|
||||||
def analyze_folder(self):
|
def cancel(self):
|
||||||
|
"""Reseta o estado para o modo Explorer."""
|
||||||
|
self.view_mode = 'explorer'
|
||||||
self.preview_data = []
|
self.preview_data = []
|
||||||
for root, dirs, files in os.walk(self.path):
|
self.folders_to_clean = set()
|
||||||
if "finalizados" in root: continue
|
self.refresh()
|
||||||
for file in files:
|
|
||||||
if file.lower().endswith(('.mkv', '.mp4', '.avi')):
|
# ==========================================================================
|
||||||
season, episode = extract_season_episode(file)
|
# 5. ANÁLISE E PREPARAÇÃO (CORE)
|
||||||
if season and episode:
|
# ==========================================================================
|
||||||
|
async def analyze_folder(self):
|
||||||
|
"""Lê arquivos recursivamente e prepara a lista de movimentos."""
|
||||||
|
self.preview_data = []
|
||||||
|
self.folders_to_clean = set()
|
||||||
|
|
||||||
|
# Feedback visual
|
||||||
|
n = ui.notification(message='Analisando arquivos...', spinner=True, timeout=None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
# Ignora pasta de destino para não entrar em loop
|
||||||
|
if "finalizados" in root.lower(): continue
|
||||||
|
|
||||||
|
# Cria conjunto para busca rápida de legendas
|
||||||
|
files_in_dir = set(files)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
# 1. Filtra apenas vídeos
|
||||||
|
if not is_video(file): continue
|
||||||
|
|
||||||
|
# 2. Tenta extrair S/E
|
||||||
|
season, episode = extract_season_episode(file)
|
||||||
|
if not season or not episode: continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 3. Define Nomes e Caminhos
|
||||||
s_fmt = f"{int(season):02d}"
|
s_fmt = f"{int(season):02d}"
|
||||||
e_fmt = f"{int(episode):02d}"
|
e_fmt = f"{int(episode):02d}"
|
||||||
|
|
||||||
ext = os.path.splitext(file)[1]
|
ext = os.path.splitext(file)[1]
|
||||||
|
|
||||||
# Estrutura: Temporada XX / Episódio YY.mkv
|
# Estrutura Final: /downloads/Temporada XX/Episódio YY.mkv
|
||||||
new_struct = f"Temporada {s_fmt}/Episódio {e_fmt}{ext}"
|
# Nota: Cria pasta Temporada XX dentro de ROOT_DIR (ou self.path se preferir relativo)
|
||||||
|
# Aqui estou criando relativo ao ROOT_DIR atual para organização centralizada
|
||||||
|
target_season_folder = f"Temporada {s_fmt}"
|
||||||
|
target_filename = f"Episódio {e_fmt}{ext}"
|
||||||
|
|
||||||
src = os.path.join(root, file)
|
src_full = os.path.join(root, file)
|
||||||
dst = os.path.join(self.path, f"Temporada {s_fmt}", f"Episódio {e_fmt}{ext}")
|
dst_full = os.path.join(self.path, target_season_folder, target_filename)
|
||||||
|
|
||||||
if src != dst:
|
# Verifica se origem e destino são iguais
|
||||||
|
if os.path.normpath(src_full) == os.path.normpath(dst_full):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Verifica conflito
|
||||||
|
status = 'OK'
|
||||||
|
if os.path.exists(dst_full):
|
||||||
|
status = 'CONFLITO (Já existe)'
|
||||||
|
|
||||||
|
# Adiciona à lista de preview
|
||||||
self.preview_data.append({
|
self.preview_data.append({
|
||||||
|
'type': 'Vídeo',
|
||||||
'original': file,
|
'original': file,
|
||||||
'new': new_struct,
|
'new_path': os.path.join(target_season_folder, target_filename),
|
||||||
'src': src,
|
'src': src_full,
|
||||||
'dst': dst
|
'dst': dst_full,
|
||||||
|
'status': status
|
||||||
})
|
})
|
||||||
except: pass
|
self.folders_to_clean.add(root)
|
||||||
|
|
||||||
|
# 4. Processamento de Legendas Associadas
|
||||||
|
video_stem = Path(file).stem # Nome do vídeo sem extensão
|
||||||
|
|
||||||
|
for f in files_in_dir:
|
||||||
|
if f == file: continue # Pula o próprio vídeo
|
||||||
|
if not is_subtitle(f): continue
|
||||||
|
|
||||||
|
# Se a legenda começa com o nome do arquivo de vídeo
|
||||||
|
if f.startswith(video_stem):
|
||||||
|
# Pega o que vem depois do nome do vídeo (ex: .forced.srt, .pt.srt)
|
||||||
|
suffix = f[len(video_stem):]
|
||||||
|
|
||||||
|
sub_target_name = f"Episódio {e_fmt}{suffix}"
|
||||||
|
sub_dst_full = os.path.join(self.path, target_season_folder, sub_target_name)
|
||||||
|
|
||||||
|
sub_status = 'OK'
|
||||||
|
if os.path.exists(sub_dst_full):
|
||||||
|
sub_status = 'CONFLITO'
|
||||||
|
|
||||||
|
self.preview_data.append({
|
||||||
|
'type': 'Legenda',
|
||||||
|
'original': f,
|
||||||
|
'new_path': os.path.join(target_season_folder, sub_target_name),
|
||||||
|
'src': os.path.join(root, f),
|
||||||
|
'dst': sub_dst_full,
|
||||||
|
'status': sub_status
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao processar arquivo {file}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
ui.notify(f'Erro fatal na análise: {str(e)}', type='negative')
|
||||||
|
|
||||||
|
n.dismiss()
|
||||||
|
|
||||||
if not self.preview_data:
|
if not self.preview_data:
|
||||||
ui.notify('Nenhum padrão encontrado.', type='warning')
|
ui.notify('Nenhum padrão de Temporada/Episódio encontrado.', type='warning')
|
||||||
else:
|
else:
|
||||||
self.view_mode = 'preview'
|
self.view_mode = 'preview'
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def execute_rename(self):
|
# ==========================================================================
|
||||||
count = 0
|
# 6. EXECUÇÃO E LIMPEZA
|
||||||
|
# ==========================================================================
|
||||||
|
async def execute_rename(self):
|
||||||
|
"""Move os arquivos e limpa pastas 'lixo'."""
|
||||||
|
count_moved = 0
|
||||||
|
errors = 0
|
||||||
|
|
||||||
|
n = ui.notification(message='Movendo e Organizando...', spinner=True, timeout=None)
|
||||||
|
|
||||||
|
# 1. MOVER ARQUIVOS
|
||||||
for item in self.preview_data:
|
for item in self.preview_data:
|
||||||
|
if item['status'] != 'OK': continue # Ignora conflitos
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(item['dst']), exist_ok=True)
|
os.makedirs(os.path.dirname(item['dst']), exist_ok=True)
|
||||||
if not os.path.exists(item['dst']):
|
|
||||||
shutil.move(item['src'], item['dst'])
|
shutil.move(item['src'], item['dst'])
|
||||||
count += 1
|
count_moved += 1
|
||||||
except: pass
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
ui.notify(f"Erro ao mover {item['original']}: {e}", type='negative')
|
||||||
|
|
||||||
ui.notify(f'{count} Arquivos Organizados!', type='positive')
|
# 2. LIMPEZA DE PASTAS (SAFE CLEANUP)
|
||||||
self.view_mode = 'explorer'
|
cleaned_folders = 0
|
||||||
self.preview_data = []
|
if self.folders_to_clean:
|
||||||
self.refresh()
|
# Ordena do caminho mais longo para o mais curto (apaga subpastas antes das pais)
|
||||||
|
sorted_folders = sorted(list(self.folders_to_clean), key=len, reverse=True)
|
||||||
|
|
||||||
def cancel(self):
|
for folder in sorted_folders:
|
||||||
self.view_mode = 'explorer'
|
# Segurança: Nunca apagar a raiz ou pastas inexistentes
|
||||||
self.preview_data = []
|
if not os.path.exists(folder) or os.path.normpath(folder) == os.path.normpath(self.path):
|
||||||
self.refresh()
|
continue
|
||||||
|
|
||||||
# --- RENDERIZADOR: BARRA DE NAVEGAÇÃO (CADEIA) ---
|
try:
|
||||||
|
# Verifica o conteúdo restante
|
||||||
|
remaining = os.listdir(folder)
|
||||||
|
has_video = False
|
||||||
|
|
||||||
|
for f in remaining:
|
||||||
|
full_p = os.path.join(folder, f)
|
||||||
|
|
||||||
|
# Se tiver subpasta, assumimos que tem algo importante dentro (pela lógica recursiva,
|
||||||
|
# se a subpasta estivesse vazia/lixo, ela já teria sido apagada no loop anterior).
|
||||||
|
# Se sobrou subpasta, NÃO APAGA a pasta pai.
|
||||||
|
if os.path.isdir(full_p):
|
||||||
|
has_video = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Se tiver arquivo de vídeo, NÃO APAGA.
|
||||||
|
if is_video(f):
|
||||||
|
has_video = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_video:
|
||||||
|
# Se só sobrou lixo (txt, nfo, imagens, etc), apaga tudo.
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
cleaned_folders += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Impossível limpar {folder}: {e}")
|
||||||
|
|
||||||
|
n.dismiss()
|
||||||
|
|
||||||
|
msg_type = 'positive' if errors == 0 else 'warning'
|
||||||
|
ui.notify(f'Sucesso! {count_moved} arquivos movidos. {cleaned_folders} pastas limpas.', type=msg_type)
|
||||||
|
|
||||||
|
self.cancel() # Volta para o explorer
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 7. INTERFACE (UI)
|
||||||
|
# ==========================================================================
|
||||||
def render_breadcrumbs(self):
|
def render_breadcrumbs(self):
|
||||||
|
"""Barra de topo com caminho e botão de ação."""
|
||||||
with ui.row().classes('w-full items-center bg-gray-100 p-2 rounded gap-1'):
|
with ui.row().classes('w-full items-center bg-gray-100 p-2 rounded gap-1'):
|
||||||
# Botão Raiz
|
ui.button('ROOT', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense text-color=grey-8')
|
||||||
ui.button('🏠', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense text-color=grey-8')
|
|
||||||
|
|
||||||
# Divide o caminho atual para criar os botões
|
# Divide o caminho para criar botões clicáveis
|
||||||
if self.path != ROOT_DIR:
|
if self.path != ROOT_DIR:
|
||||||
rel = os.path.relpath(self.path, ROOT_DIR)
|
rel = os.path.relpath(self.path, ROOT_DIR)
|
||||||
parts = rel.split(os.sep)
|
parts = rel.split(os.sep)
|
||||||
|
|
||||||
acc = ROOT_DIR
|
acc = ROOT_DIR
|
||||||
for part in parts:
|
for part in parts:
|
||||||
ui.icon('chevron_right', color='grey')
|
ui.icon('chevron_right', color='grey')
|
||||||
acc = os.path.join(acc, part)
|
acc = os.path.join(acc, part)
|
||||||
# Botão da Pasta
|
|
||||||
ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps text-color=primary')
|
ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps text-color=primary')
|
||||||
|
|
||||||
ui.space()
|
ui.space()
|
||||||
# Botão de Ação Principal
|
|
||||||
ui.button("🔍 Analisar Pasta Atual", on_click=self.analyze_folder).props('push color=primary')
|
ui.button("🔍 Analisar Pasta Atual", on_click=self.analyze_folder).props('push color=primary')
|
||||||
|
|
||||||
# --- RENDERIZADOR: LISTA DE PASTAS ---
|
|
||||||
def render_folder_list(self):
|
def render_folder_list(self):
|
||||||
|
"""Lista de arquivos/pastas (Explorer)."""
|
||||||
try:
|
try:
|
||||||
# Lista apenas diretórios, ignora arquivos
|
# Ordena: Pastas primeiro, depois arquivos alfabeticamente
|
||||||
entries = sorted([e for e in os.scandir(self.path) if e.is_dir() and not e.name.startswith('.')], key=lambda e: e.name.lower())
|
entries = sorted(list(os.scandir(self.path)), key=lambda e: (not e.is_dir(), e.name.lower()))
|
||||||
except:
|
except Exception as e:
|
||||||
ui.label("Erro ao ler pasta").classes('text-red')
|
ui.label(f"Erro de permissão ou leitura: {e}").classes('text-red font-bold')
|
||||||
return
|
return
|
||||||
|
|
||||||
with ui.column().classes('w-full gap-1 mt-2'):
|
with ui.column().classes('w-full gap-1 mt-2'):
|
||||||
# Botão para subir nível (se não estiver na raiz)
|
# Botão Voltar
|
||||||
if self.path != ROOT_DIR:
|
if self.path != ROOT_DIR:
|
||||||
with ui.item(on_click=lambda: self.navigate(os.path.dirname(self.path))).classes('bg-blue-50 hover:bg-blue-100 cursor-pointer rounded'):
|
with ui.item(on_click=lambda: self.navigate(os.path.dirname(self.path))).classes('bg-blue-50 hover:bg-blue-100 cursor-pointer rounded'):
|
||||||
with ui.item_section().props('avatar'):
|
with ui.item_section().props('avatar'):
|
||||||
ui.icon('arrow_upward', color='grey')
|
ui.icon('arrow_upward', color='grey')
|
||||||
with ui.item_section():
|
with ui.item_section():
|
||||||
ui.item_label('Voltar / Subir Nível')
|
ui.item_label('.. (Subir Nível)')
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
ui.label("Nenhuma subpasta aqui.").classes('text-gray-400 italic ml-4 mt-2')
|
ui.label("Pasta vazia.").classes('text-gray-400 italic ml-4')
|
||||||
|
|
||||||
# Lista de Subpastas
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
if entry.name.startswith('.'): continue # Ignora ocultos
|
||||||
|
|
||||||
|
if entry.is_dir():
|
||||||
with ui.item(on_click=lambda p=entry.path: self.navigate(p)).classes('hover:bg-gray-100 cursor-pointer rounded'):
|
with ui.item(on_click=lambda p=entry.path: self.navigate(p)).classes('hover:bg-gray-100 cursor-pointer rounded'):
|
||||||
with ui.item_section().props('avatar'):
|
with ui.item_section().props('avatar'):
|
||||||
ui.icon('folder', color='amber')
|
ui.icon('folder', color='amber')
|
||||||
with ui.item_section():
|
with ui.item_section():
|
||||||
ui.item_label(entry.name).classes('font-medium')
|
ui.item_label(entry.name).classes('font-medium')
|
||||||
|
else:
|
||||||
|
# Visualização simples de arquivos
|
||||||
|
icon = 'movie' if is_video(entry.name) else ('subtitles' if is_subtitle(entry.name) else 'description')
|
||||||
|
color = 'blue' if is_video(entry.name) else ('green' if is_subtitle(entry.name) else 'grey')
|
||||||
|
|
||||||
|
with ui.item().classes('hover:bg-gray-50 rounded pl-8'):
|
||||||
|
with ui.item_section().props('avatar'):
|
||||||
|
ui.icon(icon, color=color).props('size=sm')
|
||||||
|
with ui.item_section():
|
||||||
|
ui.item_label(entry.name).classes('text-sm text-gray-600')
|
||||||
|
|
||||||
# --- RENDERIZADOR: PREVIEW ---
|
|
||||||
def render_preview(self):
|
def render_preview(self):
|
||||||
with ui.column().classes('w-full items-center gap-4'):
|
"""Tabela de confirmação."""
|
||||||
ui.label(f'Detectados {len(self.preview_data)} arquivos para renomear').classes('text-xl font-bold text-green-700')
|
with ui.column().classes('w-full h-full gap-4'):
|
||||||
|
|
||||||
|
# Cabeçalho
|
||||||
|
with ui.row().classes('w-full items-center justify-between'):
|
||||||
|
ui.label(f'Detectados {len(self.preview_data)} arquivos').classes('text-xl font-bold text-gray-700')
|
||||||
with ui.row():
|
with ui.row():
|
||||||
ui.button('Cancelar', on_click=self.cancel).props('outline color=red')
|
ui.button('Cancelar', on_click=self.cancel).props('outline color=red')
|
||||||
ui.button('Confirmar Tudo', on_click=self.execute_rename).props('push color=green icon=check')
|
ui.button('CONFIRMAR ORGANIZAÇÃO', on_click=self.execute_rename).props('push color=green icon=check')
|
||||||
|
|
||||||
# Tabela Simples
|
# Tabela (AgGrid para performance)
|
||||||
with ui.card().classes('w-full p-0'):
|
cols = [
|
||||||
with ui.column().classes('w-full gap-0'):
|
{'name': 'type', 'label': 'Tipo', 'field': 'type', 'sortable': True, 'align': 'left', 'classes': 'w-24'},
|
||||||
# Cabeçalho
|
{'name': 'original', 'label': 'Arquivo Original', 'field': 'original', 'sortable': True, 'align': 'left'},
|
||||||
with ui.row().classes('w-full bg-gray-200 p-2 font-bold'):
|
{'name': 'new_path', 'label': 'Destino (Simulado)', 'field': 'new_path', 'sortable': True, 'align': 'left', 'classes': 'text-blue-700 font-mono'},
|
||||||
ui.label('Original').classes('w-1/2')
|
{'name': 'status', 'label': 'Status', 'field': 'status', 'sortable': True, 'align': 'center'},
|
||||||
ui.label('Novo Caminho').classes('w-1/2')
|
]
|
||||||
|
|
||||||
# Itens
|
ui.table(
|
||||||
with ui.scroll_area().classes('h-96 w-full'):
|
columns=cols,
|
||||||
for item in self.preview_data:
|
rows=self.preview_data,
|
||||||
with ui.row().classes('w-full p-2 border-b border-gray-100 hover:bg-gray-50'):
|
pagination=50
|
||||||
ui.label(item['original']).classes('w-1/2 text-sm truncate')
|
).classes('w-full').props('dense flat bordered')
|
||||||
ui.label(item['new']).classes('w-1/2 text-sm text-blue-600 font-mono truncate')
|
|
||||||
|
|
||||||
# --- INICIALIZADOR ---
|
ui.label('* Pastas originais serão excluídas somente se restarem apenas arquivos inúteis.').classes('text-xs text-gray-500 mt-2')
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 8. STARTUP
|
||||||
|
# ==============================================================================
|
||||||
def create_ui():
|
def create_ui():
|
||||||
rm = RenamerManager()
|
rm = RenamerManager()
|
||||||
rm.container = ui.column().classes('w-full h-full p-4 gap-4')
|
rm.container = ui.column().classes('w-full h-full p-4 gap-4')
|
||||||
rm.refresh()
|
rm.refresh()
|
||||||
|
|
||||||
|
if __name__ in {"__main__", "__mp_main__"}:
|
||||||
|
create_ui()
|
||||||
Reference in New Issue
Block a user