corrigido o merger para não perder pastas inteiras
This commit is contained in:
Binary file not shown.
@@ -4,13 +4,13 @@ import shutil
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
from collections import deque # Import necessário para ler as últimas linhas de forma eficiente
|
from collections import deque
|
||||||
|
|
||||||
# --- CONFIGURAÇÕES DE DIRETÓRIOS ---
|
# --- CONFIGURAÇÕES DE DIRETÓRIOS ---
|
||||||
SRC_ROOT = "/downloads"
|
SRC_ROOT = "/downloads"
|
||||||
DST_ROOT = "/media"
|
DST_ROOT = "/media"
|
||||||
CONFIG_PATH = "/app/data/presets.json"
|
CONFIG_PATH = "/app/data/presets.json"
|
||||||
LOG_PATH = "/app/data/history.log" # Novo arquivo de log persistente
|
LOG_PATH = "/app/data/history.log"
|
||||||
|
|
||||||
class DeployManager:
|
class DeployManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -20,123 +20,124 @@ class DeployManager:
|
|||||||
self.container = None
|
self.container = None
|
||||||
self.presets = self.load_presets()
|
self.presets = self.load_presets()
|
||||||
self.pendencies = []
|
self.pendencies = []
|
||||||
# CARREGA OS LOGS DO ARQUIVO AO INICIAR
|
|
||||||
self.logs = self.load_logs_from_file()
|
self.logs = self.load_logs_from_file()
|
||||||
|
|
||||||
# --- NOVO: GERENCIAMENTO DE LOGS PERSISTENTES ---
|
# --- GERENCIAMENTO DE LOGS ---
|
||||||
def load_logs_from_file(self):
|
def load_logs_from_file(self):
|
||||||
"""Lê as últimas 50 linhas do arquivo de log"""
|
|
||||||
if not os.path.exists(LOG_PATH):
|
if not os.path.exists(LOG_PATH):
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
# Lê as últimas 50 linhas do arquivo
|
|
||||||
with open(LOG_PATH, 'r', encoding='utf-8') as f:
|
with open(LOG_PATH, 'r', encoding='utf-8') as f:
|
||||||
# deque(..., maxlen=50) pega automaticamente as últimas 50
|
|
||||||
last_lines = list(deque(f, maxlen=50))
|
last_lines = list(deque(f, maxlen=50))
|
||||||
|
|
||||||
# Inverte a ordem para mostrar o mais recente no topo da lista visual
|
|
||||||
# remove quebras de linha com .strip()
|
|
||||||
return [line.strip() for line in reversed(last_lines)]
|
return [line.strip() for line in reversed(last_lines)]
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def add_log(self, message, type="info"):
|
def add_log(self, message, type="info"):
|
||||||
"""Adiciona log na memória E no arquivo"""
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
full_msg = f"[{timestamp}] {message}"
|
full_msg = f"[{timestamp}] {message}"
|
||||||
|
|
||||||
# 1. Atualiza a lista da memória (para a UI)
|
|
||||||
self.logs.insert(0, full_msg)
|
self.logs.insert(0, full_msg)
|
||||||
if len(self.logs) > 50: self.logs.pop() # Limpa memória excedente
|
if len(self.logs) > 50: self.logs.pop()
|
||||||
|
|
||||||
# 2. Salva no arquivo (Modo 'a' = append/adicionar no fim)
|
|
||||||
try:
|
try:
|
||||||
with open(LOG_PATH, 'a', encoding='utf-8') as f:
|
with open(LOG_PATH, 'a', encoding='utf-8') as f:
|
||||||
f.write(full_msg + "\n")
|
f.write(full_msg + "\n")
|
||||||
except Exception as e:
|
except: pass
|
||||||
print(f"Erro ao salvar log: {e}")
|
|
||||||
|
|
||||||
# --- 1. PERSISTÊNCIA (JSON) ---
|
# --- 1. PERSISTÊNCIA ---
|
||||||
def load_presets(self):
|
def load_presets(self):
|
||||||
if os.path.exists(CONFIG_PATH):
|
if os.path.exists(CONFIG_PATH):
|
||||||
try:
|
try:
|
||||||
with open(CONFIG_PATH, 'r') as f:
|
with open(CONFIG_PATH, 'r') as f: return json.load(f)
|
||||||
return json.load(f)
|
|
||||||
except: return {}
|
except: return {}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def save_preset(self, name):
|
def save_preset(self, name):
|
||||||
if not name: return
|
if not name: return
|
||||||
self.presets[name] = {'src': self.src_path, 'dst': self.dst_path}
|
self.presets[name] = {'src': self.src_path, 'dst': self.dst_path}
|
||||||
with open(CONFIG_PATH, 'w') as f:
|
with open(CONFIG_PATH, 'w') as f: json.dump(self.presets, f)
|
||||||
json.dump(self.presets, f)
|
|
||||||
ui.notify(f'Preset "{name}" salvo!')
|
ui.notify(f'Preset "{name}" salvo!')
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def delete_preset(self, name):
|
def delete_preset(self, name):
|
||||||
if name in self.presets:
|
if name in self.presets:
|
||||||
del self.presets[name]
|
del self.presets[name]
|
||||||
with open(CONFIG_PATH, 'w') as f:
|
with open(CONFIG_PATH, 'w') as f: json.dump(self.presets, f)
|
||||||
json.dump(self.presets, f)
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
# --- 2. DIÁLOGO DE CONFIRMAÇÃO DO PRESET ---
|
# --- 2. DIÁLOGO ---
|
||||||
def confirm_preset_execution(self, name, paths):
|
def confirm_preset_execution(self, name, paths):
|
||||||
src = paths['src']
|
src, dst = paths['src'], paths['dst']
|
||||||
dst = paths['dst']
|
|
||||||
|
|
||||||
if not os.path.exists(src):
|
if not os.path.exists(src):
|
||||||
ui.notify(f'Erro: Pasta de origem não existe: {src}', type='negative')
|
ui.notify(f'Erro: Origem não existe: {src}', type='negative')
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try: count = len([f for f in os.listdir(src) if not f.startswith('.')])
|
||||||
items = [f for f in os.listdir(src) if not f.startswith('.')]
|
|
||||||
count = len(items)
|
|
||||||
except: count = 0
|
except: count = 0
|
||||||
|
|
||||||
with ui.dialog() as dialog, ui.card():
|
with ui.dialog() as dialog, ui.card():
|
||||||
ui.label(f'Executar: {name}?').classes('text-xl font-bold text-blue-900')
|
ui.label(f'Executar: {name}?').classes('text-xl font-bold text-blue-900')
|
||||||
|
ui.label(f'Origem: {src}').classes('text-xs bg-gray-100 p-1 w-full break-all')
|
||||||
|
ui.label(f'Destino: {dst}').classes('text-xs bg-gray-100 p-1 w-full break-all')
|
||||||
|
ui.label(f'{count} itens encontrados.').classes('font-bold text-green-700 mt-2') if count > 0 else None
|
||||||
|
|
||||||
ui.label(f'Origem: {src}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all')
|
|
||||||
ui.label(f'Destino: {dst}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all')
|
|
||||||
|
|
||||||
if count > 0:
|
|
||||||
ui.label(f'{count} itens encontrados.').classes('font-bold text-green-700 mt-2')
|
|
||||||
else:
|
|
||||||
ui.label('Atenção: A pasta de origem parece vazia.').classes('font-bold text-orange-600 mt-2')
|
|
||||||
|
|
||||||
# Função wrapper para rodar o async corretamente
|
|
||||||
async def execute_action():
|
async def execute_action():
|
||||||
dialog.close()
|
dialog.close()
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
await self.move_process_from_preset(paths)
|
await self.move_process_from_preset(paths)
|
||||||
|
|
||||||
with ui.row().classes('w-full justify-end mt-4'):
|
with ui.row().classes('w-full justify-end mt-4'):
|
||||||
ui.button('Cancelar', on_click=dialog.close).props('flat text-color=grey')
|
ui.button('Cancelar', on_click=dialog.close).props('flat')
|
||||||
ui.button('CONFIRMAR', on_click=execute_action).props('color=green icon=check')
|
ui.button('CONFIRMAR', on_click=execute_action).props('color=green')
|
||||||
|
|
||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
# --- 3. MOVIMENTAÇÃO E PENDÊNCIAS ---
|
# --- 3. MOVIMENTAÇÃO BLINDADA (SAFE MERGE) ---
|
||||||
async def move_process(self, items_to_move, target_folder):
|
async def move_process(self, items_to_move, target_folder):
|
||||||
|
"""
|
||||||
|
Nova lógica: Nunca move pastas inteiras.
|
||||||
|
Sempre cria a estrutura no destino e move apenas arquivos.
|
||||||
|
"""
|
||||||
for item_path in items_to_move:
|
for item_path in items_to_move:
|
||||||
if not os.path.exists(item_path): continue
|
if not os.path.exists(item_path): continue
|
||||||
|
|
||||||
name = os.path.basename(item_path)
|
name = os.path.basename(item_path)
|
||||||
destination = os.path.join(target_folder, name)
|
destination = os.path.join(target_folder, name)
|
||||||
|
|
||||||
|
# --- CASO 1: É UMA PASTA? ---
|
||||||
|
if os.path.isdir(item_path):
|
||||||
|
# Não verifica se existe ou não. Simplesmente garante que a pasta
|
||||||
|
# exista no destino (mkdir -p) e entra nela.
|
||||||
|
try:
|
||||||
|
if not os.path.exists(destination):
|
||||||
|
os.makedirs(destination, exist_ok=True)
|
||||||
|
self.apply_permissions(destination) # Garante permissão na nova pasta
|
||||||
|
|
||||||
|
# Pega conteúdo e RECURSIVIDADE (mergulha na pasta)
|
||||||
|
sub_items = [os.path.join(item_path, f) for f in os.listdir(item_path)]
|
||||||
|
await self.move_process(sub_items, destination)
|
||||||
|
|
||||||
|
# Limpeza: Se a pasta de origem ficou vazia, remove ela
|
||||||
|
if not os.listdir(item_path):
|
||||||
|
os.rmdir(item_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"❌ Erro na pasta {name}: {e}", "negative")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- CASO 2: É UM ARQUIVO? ---
|
||||||
|
# Se já existe o arquivo exato no destino -> Pendência
|
||||||
if os.path.exists(destination):
|
if os.path.exists(destination):
|
||||||
self.add_log(f"⚠️ Pendência: {name}", "warning")
|
self.add_log(f"⚠️ Conflito de arquivo: {name}", "warning")
|
||||||
self.pendencies.append({'name': name, 'src': item_path, 'dst': destination})
|
self.pendencies.append({'name': name, 'src': item_path, 'dst': destination})
|
||||||
self.refresh()
|
self.refresh()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Se não existe -> Move o arquivo
|
||||||
try:
|
try:
|
||||||
await run.cpu_bound(shutil.move, item_path, destination)
|
await run.cpu_bound(shutil.move, item_path, destination)
|
||||||
self.apply_permissions(destination)
|
self.apply_permissions(destination)
|
||||||
self.add_log(f"✅ Movido: {name}", "positive")
|
self.add_log(f"✅ Arquivo movido: {name}", "positive")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.add_log(f"❌ Erro em {name}: {e}", "negative")
|
self.add_log(f"❌ Erro arquivo {name}: {e}", "negative")
|
||||||
|
|
||||||
self.selected_items = []
|
self.selected_items = []
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@@ -146,7 +147,7 @@ class DeployManager:
|
|||||||
if os.path.exists(src):
|
if os.path.exists(src):
|
||||||
items = [os.path.join(src, f) for f in os.listdir(src)]
|
items = [os.path.join(src, f) for f in os.listdir(src)]
|
||||||
await self.move_process(items, dst)
|
await self.move_process(items, dst)
|
||||||
else: ui.notify('Origem do preset não encontrada!', type='negative')
|
else: ui.notify('Origem não encontrada!', type='negative')
|
||||||
|
|
||||||
def apply_permissions(self, path):
|
def apply_permissions(self, path):
|
||||||
try:
|
try:
|
||||||
@@ -154,7 +155,7 @@ class DeployManager:
|
|||||||
else: os.chmod(path, 0o777)
|
else: os.chmod(path, 0o777)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# --- 4. AÇÕES EM MASSA PARA PENDÊNCIAS ---
|
# --- 4. AÇÕES PENDÊNCIAS ---
|
||||||
async def handle_all_pendencies(self, action):
|
async def handle_all_pendencies(self, action):
|
||||||
temp_list = list(self.pendencies)
|
temp_list = list(self.pendencies)
|
||||||
for i in range(len(temp_list)):
|
for i in range(len(temp_list)):
|
||||||
@@ -167,6 +168,7 @@ class DeployManager:
|
|||||||
|
|
||||||
if action == 'replace':
|
if action == 'replace':
|
||||||
try:
|
try:
|
||||||
|
# Como agora só arquivos caem aqui, remove e substitui
|
||||||
if os.path.isdir(item['dst']):
|
if os.path.isdir(item['dst']):
|
||||||
await run.cpu_bound(shutil.rmtree, item['dst'])
|
await run.cpu_bound(shutil.rmtree, item['dst'])
|
||||||
else:
|
else:
|
||||||
@@ -184,7 +186,6 @@ class DeployManager:
|
|||||||
def render_breadcrumbs(self, current_path, root_dir, nav_callback):
|
def render_breadcrumbs(self, current_path, root_dir, nav_callback):
|
||||||
with ui.row().classes('items-center gap-1 bg-gray-100 p-1 rounded w-full mb-2'):
|
with ui.row().classes('items-center gap-1 bg-gray-100 p-1 rounded w-full mb-2'):
|
||||||
ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm')
|
ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm')
|
||||||
|
|
||||||
rel = os.path.relpath(current_path, root_dir)
|
rel = os.path.relpath(current_path, root_dir)
|
||||||
if rel != '.':
|
if rel != '.':
|
||||||
acc = root_dir
|
acc = root_dir
|
||||||
@@ -192,7 +193,6 @@ class DeployManager:
|
|||||||
ui.label('/')
|
ui.label('/')
|
||||||
acc = os.path.join(acc, part)
|
acc = os.path.join(acc, part)
|
||||||
ui.button(part, on_click=lambda p=acc: nav_callback(p)).props('flat dense no-caps size=sm')
|
ui.button(part, on_click=lambda p=acc: nav_callback(p)).props('flat dense no-caps size=sm')
|
||||||
|
|
||||||
if current_path != root_dir:
|
if current_path != root_dir:
|
||||||
ui.space()
|
ui.space()
|
||||||
parent = os.path.dirname(current_path)
|
parent = os.path.dirname(current_path)
|
||||||
@@ -200,23 +200,19 @@ class DeployManager:
|
|||||||
|
|
||||||
def navigate_src(self, path):
|
def navigate_src(self, path):
|
||||||
if os.path.exists(path) and os.path.isdir(path):
|
if os.path.exists(path) and os.path.isdir(path):
|
||||||
self.src_path = path
|
self.src_path = path; self.refresh()
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
def navigate_dst(self, path):
|
def navigate_dst(self, path):
|
||||||
if os.path.exists(path) and os.path.isdir(path):
|
if os.path.exists(path) and os.path.isdir(path):
|
||||||
self.dst_path = path
|
self.dst_path = path; self.refresh()
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
# --- 6. INTERFACE PRINCIPAL ---
|
# --- 6. UI ---
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
if self.container:
|
if self.container:
|
||||||
self.container.clear()
|
self.container.clear()
|
||||||
with self.container:
|
with self.container: self.render_layout()
|
||||||
self.render_layout()
|
|
||||||
|
|
||||||
def render_layout(self):
|
def render_layout(self):
|
||||||
# TOPBAR: PRESETS
|
|
||||||
with ui.row().classes('w-full bg-blue-50 p-3 rounded-lg items-center shadow-sm'):
|
with ui.row().classes('w-full bg-blue-50 p-3 rounded-lg items-center shadow-sm'):
|
||||||
ui.icon('bolt', color='blue').classes('text-2xl')
|
ui.icon('bolt', color='blue').classes('text-2xl')
|
||||||
ui.label('SMART DEPLOYS:').classes('font-bold text-blue-900 mr-4')
|
ui.label('SMART DEPLOYS:').classes('font-bold text-blue-900 mr-4')
|
||||||
@@ -224,88 +220,65 @@ class DeployManager:
|
|||||||
with ui.button_group().props('rounded'):
|
with ui.button_group().props('rounded'):
|
||||||
ui.button(name, on_click=lambda n=name, p=paths: self.confirm_preset_execution(n, p)).props('color=blue-6')
|
ui.button(name, on_click=lambda n=name, p=paths: self.confirm_preset_execution(n, p)).props('color=blue-6')
|
||||||
ui.button(on_click=lambda n=name: self.delete_preset(n)).props('icon=delete color=red-4')
|
ui.button(on_click=lambda n=name: self.delete_preset(n)).props('icon=delete color=red-4')
|
||||||
|
|
||||||
ui.button('Salvar Favorito', on_click=self.prompt_save_preset).props('flat icon=add_circle color=green-7').classes('ml-auto')
|
ui.button('Salvar Favorito', on_click=self.prompt_save_preset).props('flat icon=add_circle color=green-7').classes('ml-auto')
|
||||||
|
|
||||||
# CONTEÚDO: NAVEGADORES
|
|
||||||
with ui.row().classes('w-full gap-6 mt-4'):
|
with ui.row().classes('w-full gap-6 mt-4'):
|
||||||
# ORIGEM
|
|
||||||
with ui.column().classes('flex-grow w-1/2'):
|
with ui.column().classes('flex-grow w-1/2'):
|
||||||
ui.label('📂 ORIGEM (Downloads)').classes('text-lg font-bold text-blue-700')
|
ui.label('📂 ORIGEM').classes('text-lg font-bold text-blue-700')
|
||||||
self.render_breadcrumbs(self.src_path, SRC_ROOT, self.navigate_src)
|
self.render_breadcrumbs(self.src_path, SRC_ROOT, self.navigate_src)
|
||||||
self.render_file_list(self.src_path, is_source=True)
|
self.render_file_list(self.src_path, is_source=True)
|
||||||
|
|
||||||
# DESTINO
|
|
||||||
with ui.column().classes('flex-grow w-1/2'):
|
with ui.column().classes('flex-grow w-1/2'):
|
||||||
ui.label('🎯 DESTINO (Mídia)').classes('text-lg font-bold text-green-700')
|
ui.label('🎯 DESTINO').classes('text-lg font-bold text-green-700')
|
||||||
self.render_breadcrumbs(self.dst_path, DST_ROOT, self.navigate_dst)
|
self.render_breadcrumbs(self.dst_path, DST_ROOT, self.navigate_dst)
|
||||||
self.render_file_list(self.dst_path, is_source=False)
|
self.render_file_list(self.dst_path, is_source=False)
|
||||||
|
|
||||||
# SEÇÃO INFERIOR: LOGS E PENDÊNCIAS
|
|
||||||
with ui.row().classes('w-full gap-6 mt-6'):
|
with ui.row().classes('w-full gap-6 mt-6'):
|
||||||
# PAINEL DE PENDÊNCIAS
|
|
||||||
with ui.card().classes('flex-grow h-64 bg-orange-50 border-orange-200 shadow-none'):
|
with ui.card().classes('flex-grow h-64 bg-orange-50 border-orange-200 shadow-none'):
|
||||||
with ui.row().classes('w-full items-center border-b pb-2'):
|
with ui.row().classes('w-full items-center border-b pb-2'):
|
||||||
ui.label(f'⚠️ Pendências ({len(self.pendencies)})').classes('font-bold text-orange-900 text-lg')
|
ui.label(f'⚠️ Pendências ({len(self.pendencies)})').classes('font-bold text-orange-900 text-lg')
|
||||||
if self.pendencies:
|
if self.pendencies:
|
||||||
ui.button('SUBSTITUIR TODOS', on_click=lambda: self.handle_all_pendencies('replace')).props('color=green-8 size=sm icon=done_all')
|
ui.button('SUBSTITUIR TODOS', on_click=lambda: self.handle_all_pendencies('replace')).props('color=green-8 size=sm icon=done_all')
|
||||||
ui.button('IGNORAR TODOS', on_click=lambda: self.handle_all_pendencies('ignore')).props('color=grey-7 size=sm icon=clear_all')
|
ui.button('IGNORAR TODOS', on_click=lambda: self.handle_all_pendencies('ignore')).props('color=grey-7 size=sm icon=clear_all')
|
||||||
|
|
||||||
with ui.scroll_area().classes('w-full h-full'):
|
with ui.scroll_area().classes('w-full h-full'):
|
||||||
for i, p in enumerate(self.pendencies):
|
for i, p in enumerate(self.pendencies):
|
||||||
with ui.row().classes('w-full items-center p-2 border-b bg-white rounded mb-1'):
|
with ui.row().classes('w-full items-center p-2 border-b bg-white rounded mb-1'):
|
||||||
ui.label(p['name']).classes('flex-grow text-xs font-medium')
|
ui.label(p['name']).classes('flex-grow text-xs font-medium')
|
||||||
ui.button(icon='swap_horiz', on_click=lambda idx=i: self.handle_pendency(idx, 'replace')).props('flat dense color=green').tooltip('Substituir')
|
ui.button(icon='swap_horiz', on_click=lambda idx=i: self.handle_pendency(idx, 'replace')).props('flat dense color=green')
|
||||||
ui.button(icon='close', on_click=lambda idx=i: self.handle_pendency(idx, 'ignore')).props('flat dense color=red').tooltip('Manter Original')
|
ui.button(icon='close', on_click=lambda idx=i: self.handle_pendency(idx, 'ignore')).props('flat dense color=red')
|
||||||
|
|
||||||
# PAINEL DE LOGS
|
|
||||||
with ui.card().classes('flex-grow h-64 bg-slate-900 text-slate-200 shadow-none'):
|
with ui.card().classes('flex-grow h-64 bg-slate-900 text-slate-200 shadow-none'):
|
||||||
ui.label('📜 Log de Atividades (Persistente)').classes('font-bold border-b border-slate-700 w-full pb-2')
|
ui.label('📜 Logs').classes('font-bold border-b border-slate-700 w-full pb-2')
|
||||||
with ui.scroll_area().classes('w-full h-full'):
|
with ui.scroll_area().classes('w-full h-full'):
|
||||||
# Renderiza os logs carregados
|
|
||||||
for log in self.logs:
|
for log in self.logs:
|
||||||
# Pinta de vermelho se tiver erro, verde se sucesso
|
color = 'text-red-400' if '❌' in log else 'text-green-400' if '✅' in log else 'text-slate-300'
|
||||||
color_cls = 'text-red-400' if '❌' in log else 'text-green-400' if '✅' in log else 'text-slate-300'
|
ui.label(f"> {log}").classes(f'text-[10px] font-mono leading-tight {color}')
|
||||||
ui.label(f"> {log}").classes(f'text-[10px] font-mono leading-tight {color_cls}')
|
|
||||||
|
|
||||||
# BOTÃO GLOBAL
|
ui.button('INICIAR MOVIMENTAÇÃO', on_click=lambda: self.move_process(self.selected_items, self.dst_path))\
|
||||||
ui.button('INICIAR MOVIMENTAÇÃO DOS SELECIONADOS', on_click=lambda: self.move_process(self.selected_items, self.dst_path))\
|
|
||||||
.classes('w-full py-6 mt-4 text-xl font-black shadow-lg')\
|
.classes('w-full py-6 mt-4 text-xl font-black shadow-lg')\
|
||||||
.props('color=green-7 icon=forward')\
|
.props('color=green-7 icon=forward')\
|
||||||
.bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0)
|
.bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0)
|
||||||
|
|
||||||
# --- AUXILIARES ---
|
|
||||||
def render_file_list(self, path, is_source):
|
def render_file_list(self, path, is_source):
|
||||||
try:
|
try:
|
||||||
entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower()))
|
entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower()))
|
||||||
with ui.scroll_area().classes('h-[400px] border-2 rounded-lg bg-white w-full shadow-inner'):
|
with ui.scroll_area().classes('h-[400px] border-2 rounded-lg bg-white w-full shadow-inner'):
|
||||||
if not entries:
|
if not entries: ui.label('Pasta vazia').classes('p-4 text-gray-400 italic')
|
||||||
ui.label('Pasta vazia').classes('p-4 text-gray-400 italic')
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
is_selected = entry.path in self.selected_items
|
is_selected = entry.path in self.selected_items
|
||||||
bg = "bg-blue-100 border-blue-200" if is_selected else "hover:bg-gray-50 border-gray-100"
|
bg = "bg-blue-100 border-blue-200" if is_selected else "hover:bg-gray-50 border-gray-100"
|
||||||
|
|
||||||
with ui.row().classes(f'w-full p-2 border-b items-center {bg} transition-colors cursor-pointer') as r:
|
with ui.row().classes(f'w-full p-2 border-b items-center {bg} transition-colors cursor-pointer') as r:
|
||||||
if is_source:
|
if is_source: ui.checkbox(value=is_selected, on_change=lambda e, p=entry.path: self.toggle_selection(p)).props('dense')
|
||||||
ui.checkbox(value=is_selected, on_change=lambda e, p=entry.path: self.toggle_selection(p)).props('dense')
|
|
||||||
|
|
||||||
icon = 'folder' if entry.is_dir() else 'movie' if entry.name.lower().endswith(('.mkv','.mp4')) else 'description'
|
icon = 'folder' if entry.is_dir() else 'movie' if entry.name.lower().endswith(('.mkv','.mp4')) else 'description'
|
||||||
ui.icon(icon, color='amber-500' if entry.is_dir() else 'blue-grey-400')
|
ui.icon(icon, color='amber-500' if entry.is_dir() else 'blue-grey-400')
|
||||||
|
ui.label(entry.name).classes('text-sm flex-grow truncate select-none')
|
||||||
lbl = ui.label(entry.name).classes('text-sm flex-grow truncate select-none')
|
if entry.is_dir(): r.on('click', lambda p=entry.path: self.navigate_src(p) if is_source else self.navigate_dst(p))
|
||||||
if entry.is_dir():
|
elif is_source: r.on('click', lambda p=entry.path: self.toggle_selection(p))
|
||||||
r.on('click', lambda p=entry.path: self.navigate_src(p) if is_source else self.navigate_dst(p))
|
except: ui.label('Erro ao acessar diretório.').classes('text-red-500 p-4 font-bold')
|
||||||
elif is_source:
|
|
||||||
r.on('click', lambda p=entry.path: self.toggle_selection(p))
|
|
||||||
except Exception:
|
|
||||||
ui.label('Erro ao acessar diretório.').classes('text-red-500 p-4 font-bold')
|
|
||||||
|
|
||||||
def prompt_save_preset(self):
|
def prompt_save_preset(self):
|
||||||
with ui.dialog() as d, ui.card().classes('p-6'):
|
with ui.dialog() as d, ui.card().classes('p-6'):
|
||||||
ui.label('Criar Novo Smart Deploy').classes('text-lg font-bold')
|
ui.label('Criar Preset').classes('text-lg font-bold')
|
||||||
ui.label(f'Origem: {self.src_path}').classes('text-xs text-gray-500')
|
name_input = ui.input('Nome')
|
||||||
ui.label(f'Destino: {self.dst_path}').classes('text-xs text-gray-500')
|
|
||||||
name_input = ui.input('Nome do Atalho (ex: Filmes, 4K, Séries)')
|
|
||||||
with ui.row().classes('w-full justify-end mt-4'):
|
with ui.row().classes('w-full justify-end mt-4'):
|
||||||
ui.button('Cancelar', on_click=d.close).props('flat')
|
ui.button('Cancelar', on_click=d.close).props('flat')
|
||||||
ui.button('SALVAR', on_click=lambda: [self.save_preset(name_input.value), d.close()]).props('color=green')
|
ui.button('SALVAR', on_click=lambda: [self.save_preset(name_input.value), d.close()]).props('color=green')
|
||||||
|
|||||||
@@ -56,3 +56,12 @@ OSError: [Errno 107] Transport endpoint is not connected: '/media/Jellyfin/onedr
|
|||||||
[2026-02-05 13:23:13] ✅ Movido: Episódio 07.mp4
|
[2026-02-05 13:23:13] ✅ Movido: Episódio 07.mp4
|
||||||
[2026-02-05 13:23:13] ⚠️ Pendência: Episódio 14.mp4
|
[2026-02-05 13:23:13] ⚠️ Pendência: Episódio 14.mp4
|
||||||
[2026-02-05 13:23:27] 🔄 Substituído: Episódio 14.mp4
|
[2026-02-05 13:23:27] 🔄 Substituído: Episódio 14.mp4
|
||||||
|
[2026-02-06 00:20:02] ✅ Movido: 21 SEXTURY - Hot Babe Veronica Leal Gets The Hottest Ass Creampie EVER After Hard Anal Sex.mp4
|
||||||
|
[2026-02-06 05:16:26] ✅ Movido: TUSHY Abella Danger and Lena Paul Dominate Her Boyfriend and Get Gaped.mp4
|
||||||
|
[2026-02-06 05:16:27] ✅ Movido: 21 NATURALS - Hot Redhead Veronica Leal Wants Her Tight Ass Fucked.mp4
|
||||||
|
[2026-02-08 00:51:31] ⚠️ Pendência: Jujutsu Kaisen
|
||||||
|
[2026-02-08 00:52:15] 🔄 Substituído: Jujutsu Kaisen
|
||||||
|
[2026-02-08 20:47:28] ✅ Movido: Step Brother cums too early on sisters Mouth and Swallows Cum.mp4
|
||||||
|
[2026-02-08 20:47:38] ✅ Movido: Horny Couple gets caught in Public Restroom making out.mp4
|
||||||
|
[2026-02-08 22:31:46] ✅ Movido: Frieren e a Jornada para o Além
|
||||||
|
[2026-02-08 22:44:01] ✅ Arquivo movido: Episódio 05.mkv
|
||||||
|
|||||||
Reference in New Issue
Block a user