466 lines
18 KiB
Python
466 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
File Manager Tab for DazedMTLTool GUI
|
|
|
|
Manages input files from 'files' directory and output to 'translated' directory.
|
|
Users can add files through GUI or by copying them to the files folder.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTreeWidget, QTreeWidgetItem,
|
|
QPushButton, QGroupBox, QSplitter, QTextEdit, QFileDialog, QMessageBox,
|
|
QProgressBar, QFrame, QHeaderView
|
|
)
|
|
from PyQt5.QtCore import Qt, QTimer
|
|
from PyQt5.QtGui import QFont
|
|
|
|
|
|
class FileManagerTab(QWidget):
|
|
"""File Manager tab for managing input and output files."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.project_root = Path(__file__).parent.parent
|
|
self.files_dir = self.project_root / "files"
|
|
self.translated_dir = self.project_root / "translated"
|
|
|
|
# Ensure directories exist
|
|
self.files_dir.mkdir(exist_ok=True)
|
|
self.translated_dir.mkdir(exist_ok=True)
|
|
|
|
self.setup_ui()
|
|
self.refresh_file_lists()
|
|
|
|
# Auto-refresh timer
|
|
self.refresh_timer = QTimer()
|
|
self.refresh_timer.timeout.connect(self.refresh_file_lists)
|
|
self.refresh_timer.start(3000) # Refresh every 3 seconds
|
|
|
|
def setup_ui(self):
|
|
"""Set up the user interface."""
|
|
layout = QVBoxLayout()
|
|
|
|
# Title
|
|
title_label = QLabel("File Manager")
|
|
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;")
|
|
layout.addWidget(title_label)
|
|
|
|
# Description
|
|
desc_label = QLabel(
|
|
"Input files are loaded from the 'files' directory. "
|
|
"Translated files will be saved to the 'translated' directory. "
|
|
"You can add files using the GUI or by copying them to the files folder."
|
|
)
|
|
desc_label.setWordWrap(True)
|
|
desc_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;")
|
|
layout.addWidget(desc_label)
|
|
|
|
# Main splitter
|
|
splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Input files section
|
|
input_group = self.create_input_files_section()
|
|
splitter.addWidget(input_group)
|
|
|
|
# Output files section
|
|
output_group = self.create_output_files_section()
|
|
splitter.addWidget(output_group)
|
|
|
|
# Set equal sizes
|
|
splitter.setSizes([400, 400])
|
|
layout.addWidget(splitter)
|
|
|
|
# Action buttons
|
|
button_layout = self.create_action_buttons()
|
|
layout.addLayout(button_layout)
|
|
|
|
# Status bar
|
|
self.status_label = QLabel("Ready")
|
|
self.status_label.setStyleSheet("color: #cccccc; padding: 5px;")
|
|
layout.addWidget(self.status_label)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def create_input_files_section(self):
|
|
"""Create the input files section."""
|
|
group = QGroupBox("Input Files (files directory)")
|
|
layout = QVBoxLayout()
|
|
|
|
# File tree
|
|
self.input_tree = QTreeWidget()
|
|
self.input_tree.setHeaderLabels(["Filename", "Size", "Modified"])
|
|
self.input_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
|
self.input_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
|
self.input_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
self.input_tree.itemSelectionChanged.connect(self.on_input_file_selected)
|
|
layout.addWidget(self.input_tree)
|
|
|
|
# Input buttons
|
|
input_button_layout = QHBoxLayout()
|
|
|
|
self.add_file_btn = QPushButton("Add Files")
|
|
self.add_file_btn.clicked.connect(self.add_files)
|
|
input_button_layout.addWidget(self.add_file_btn)
|
|
|
|
self.remove_file_btn = QPushButton("Remove Selected")
|
|
self.remove_file_btn.clicked.connect(self.remove_input_file)
|
|
self.remove_file_btn.setEnabled(False)
|
|
input_button_layout.addWidget(self.remove_file_btn)
|
|
|
|
self.open_files_folder_btn = QPushButton("Open Files Folder")
|
|
self.open_files_folder_btn.clicked.connect(self.open_files_folder)
|
|
input_button_layout.addWidget(self.open_files_folder_btn)
|
|
|
|
input_button_layout.addStretch()
|
|
layout.addLayout(input_button_layout)
|
|
|
|
group.setLayout(layout)
|
|
return group
|
|
|
|
def create_output_files_section(self):
|
|
"""Create the output files section."""
|
|
group = QGroupBox("Output Files (translated directory)")
|
|
layout = QVBoxLayout()
|
|
|
|
# File tree
|
|
self.output_tree = QTreeWidget()
|
|
self.output_tree.setHeaderLabels(["Filename", "Size", "Modified"])
|
|
self.output_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
|
self.output_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
|
self.output_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
self.output_tree.itemSelectionChanged.connect(self.on_output_file_selected)
|
|
layout.addWidget(self.output_tree)
|
|
|
|
# Output buttons
|
|
output_button_layout = QHBoxLayout()
|
|
|
|
self.view_output_btn = QPushButton("View Selected")
|
|
self.view_output_btn.clicked.connect(self.view_output_file)
|
|
self.view_output_btn.setEnabled(False)
|
|
output_button_layout.addWidget(self.view_output_btn)
|
|
|
|
self.delete_output_btn = QPushButton("Delete Selected")
|
|
self.delete_output_btn.clicked.connect(self.delete_output_file)
|
|
self.delete_output_btn.setEnabled(False)
|
|
output_button_layout.addWidget(self.delete_output_btn)
|
|
|
|
self.open_translated_folder_btn = QPushButton("Open Translated Folder")
|
|
self.open_translated_folder_btn.clicked.connect(self.open_translated_folder)
|
|
output_button_layout.addWidget(self.open_translated_folder_btn)
|
|
|
|
output_button_layout.addStretch()
|
|
layout.addLayout(output_button_layout)
|
|
|
|
group.setLayout(layout)
|
|
return group
|
|
|
|
def create_action_buttons(self):
|
|
"""Create main action buttons."""
|
|
layout = QHBoxLayout()
|
|
|
|
self.refresh_btn = QPushButton("Refresh File Lists")
|
|
self.refresh_btn.clicked.connect(self.refresh_file_lists)
|
|
layout.addWidget(self.refresh_btn)
|
|
|
|
self.clear_translated_btn = QPushButton("Clear All Translated Files")
|
|
self.clear_translated_btn.clicked.connect(self.clear_translated_files)
|
|
layout.addWidget(self.clear_translated_btn)
|
|
|
|
layout.addStretch()
|
|
|
|
# Auto-refresh indicator
|
|
self.auto_refresh_label = QLabel("Auto-refresh: ON")
|
|
self.auto_refresh_label.setStyleSheet("color: #00aa00; font-size: 10px;")
|
|
layout.addWidget(self.auto_refresh_label)
|
|
|
|
return layout
|
|
|
|
def refresh_file_lists(self):
|
|
"""Refresh both input and output file lists."""
|
|
self.refresh_input_files()
|
|
self.refresh_output_files()
|
|
self.update_status()
|
|
|
|
def refresh_input_files(self):
|
|
"""Refresh the input files tree."""
|
|
self.input_tree.clear()
|
|
|
|
if not self.files_dir.exists():
|
|
return
|
|
|
|
supported_extensions = {'.json', '.txt', '.csv', '.js', '.py', '.xml', '.html', '.md'}
|
|
|
|
for file_path in self.files_dir.iterdir():
|
|
if (file_path.is_file() and
|
|
file_path.suffix.lower() in supported_extensions and
|
|
file_path.name != '.gitkeep'):
|
|
item = QTreeWidgetItem()
|
|
item.setText(0, file_path.name)
|
|
|
|
# File size
|
|
size = file_path.stat().st_size
|
|
if size < 1024:
|
|
size_str = f"{size} B"
|
|
elif size < 1024 * 1024:
|
|
size_str = f"{size / 1024:.1f} KB"
|
|
else:
|
|
size_str = f"{size / (1024 * 1024):.1f} MB"
|
|
item.setText(1, size_str)
|
|
|
|
# Modified time
|
|
import datetime
|
|
mtime = datetime.datetime.fromtimestamp(file_path.stat().st_mtime)
|
|
item.setText(2, mtime.strftime("%Y-%m-%d %H:%M"))
|
|
|
|
# Store file path
|
|
item.setData(0, Qt.UserRole, str(file_path))
|
|
|
|
self.input_tree.addTopLevelItem(item)
|
|
|
|
def refresh_output_files(self):
|
|
"""Refresh the output files tree."""
|
|
self.output_tree.clear()
|
|
|
|
if not self.translated_dir.exists():
|
|
return
|
|
|
|
for file_path in self.translated_dir.iterdir():
|
|
if file_path.is_file() and file_path.name != '.gitkeep':
|
|
item = QTreeWidgetItem()
|
|
item.setText(0, file_path.name)
|
|
|
|
# File size
|
|
size = file_path.stat().st_size
|
|
if size < 1024:
|
|
size_str = f"{size} B"
|
|
elif size < 1024 * 1024:
|
|
size_str = f"{size / 1024:.1f} KB"
|
|
else:
|
|
size_str = f"{size / (1024 * 1024):.1f} MB"
|
|
item.setText(1, size_str)
|
|
|
|
# Modified time
|
|
import datetime
|
|
mtime = datetime.datetime.fromtimestamp(file_path.stat().st_mtime)
|
|
item.setText(2, mtime.strftime("%Y-%m-%d %H:%M"))
|
|
|
|
# Store file path
|
|
item.setData(0, Qt.UserRole, str(file_path))
|
|
|
|
self.output_tree.addTopLevelItem(item)
|
|
|
|
def update_status(self):
|
|
"""Update the status label."""
|
|
input_count = self.input_tree.topLevelItemCount()
|
|
output_count = self.output_tree.topLevelItemCount()
|
|
self.status_label.setText(f"Input files: {input_count} | Output files: {output_count}")
|
|
|
|
def on_input_file_selected(self):
|
|
"""Handle input file selection."""
|
|
selected = self.input_tree.selectedItems()
|
|
self.remove_file_btn.setEnabled(len(selected) > 0)
|
|
|
|
def on_output_file_selected(self):
|
|
"""Handle output file selection."""
|
|
selected = self.output_tree.selectedItems()
|
|
self.view_output_btn.setEnabled(len(selected) > 0)
|
|
self.delete_output_btn.setEnabled(len(selected) > 0)
|
|
|
|
def add_files(self):
|
|
"""Add files to the input directory."""
|
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
self,
|
|
"Select Files to Add",
|
|
"",
|
|
"All Supported Files (*.json *.txt *.csv *.js *.py *.xml *.html *.md);;All Files (*.*)"
|
|
)
|
|
|
|
if not file_paths:
|
|
return
|
|
|
|
copied_count = 0
|
|
for file_path in file_paths:
|
|
source_path = Path(file_path)
|
|
dest_path = self.files_dir / source_path.name
|
|
|
|
try:
|
|
# Check if file already exists
|
|
if dest_path.exists():
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"File Exists",
|
|
f"'{source_path.name}' already exists. Overwrite?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
continue
|
|
|
|
shutil.copy2(source_path, dest_path)
|
|
copied_count += 1
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Copy Error",
|
|
f"Failed to copy '{source_path.name}':\n{str(e)}"
|
|
)
|
|
|
|
if copied_count > 0:
|
|
QMessageBox.information(
|
|
self,
|
|
"Files Added",
|
|
f"Successfully added {copied_count} file(s) to the input directory."
|
|
)
|
|
self.refresh_input_files()
|
|
|
|
def remove_input_file(self):
|
|
"""Remove selected input file."""
|
|
selected = self.input_tree.selectedItems()
|
|
if not selected:
|
|
return
|
|
|
|
item = selected[0]
|
|
file_path = Path(item.data(0, Qt.UserRole))
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Removal",
|
|
f"Are you sure you want to remove '{file_path.name}' from the input directory?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
try:
|
|
file_path.unlink()
|
|
self.refresh_input_files()
|
|
QMessageBox.information(self, "File Removed", f"'{file_path.name}' has been removed.")
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to remove file:\n{str(e)}")
|
|
|
|
def view_output_file(self):
|
|
"""View selected output file."""
|
|
selected = self.output_tree.selectedItems()
|
|
if not selected:
|
|
return
|
|
|
|
item = selected[0]
|
|
file_path = Path(item.data(0, Qt.UserRole))
|
|
|
|
try:
|
|
# Open file with default system application
|
|
import subprocess
|
|
import platform
|
|
|
|
if platform.system() == 'Windows':
|
|
os.startfile(str(file_path))
|
|
elif platform.system() == 'Darwin': # macOS
|
|
subprocess.run(['open', str(file_path)])
|
|
else: # Linux
|
|
subprocess.run(['xdg-open', str(file_path)])
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to open file:\n{str(e)}")
|
|
|
|
def delete_output_file(self):
|
|
"""Delete selected output file."""
|
|
selected = self.output_tree.selectedItems()
|
|
if not selected:
|
|
return
|
|
|
|
item = selected[0]
|
|
file_path = Path(item.data(0, Qt.UserRole))
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Deletion",
|
|
f"Are you sure you want to delete '{file_path.name}' from the output directory?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
try:
|
|
file_path.unlink()
|
|
self.refresh_output_files()
|
|
QMessageBox.information(self, "File Deleted", f"'{file_path.name}' has been deleted.")
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to delete file:\n{str(e)}")
|
|
|
|
def clear_translated_files(self):
|
|
"""Clear all files from the translated directory."""
|
|
if self.output_tree.topLevelItemCount() == 0:
|
|
QMessageBox.information(self, "No Files", "There are no files to clear.")
|
|
return
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Clear All",
|
|
"Are you sure you want to delete ALL files from the translated directory?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
try:
|
|
deleted_count = 0
|
|
for file_path in self.translated_dir.iterdir():
|
|
if file_path.is_file() and file_path.name != '.gitkeep':
|
|
file_path.unlink()
|
|
deleted_count += 1
|
|
|
|
self.refresh_output_files()
|
|
QMessageBox.information(
|
|
self,
|
|
"Files Cleared",
|
|
f"Successfully deleted {deleted_count} file(s) from the translated directory."
|
|
)
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to clear files:\n{str(e)}")
|
|
|
|
def open_files_folder(self):
|
|
"""Open the files folder in the system file manager."""
|
|
try:
|
|
import subprocess
|
|
import platform
|
|
|
|
if platform.system() == 'Windows':
|
|
subprocess.run(['explorer', str(self.files_dir)])
|
|
elif platform.system() == 'Darwin': # macOS
|
|
subprocess.run(['open', str(self.files_dir)])
|
|
else: # Linux
|
|
subprocess.run(['xdg-open', str(self.files_dir)])
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to open folder:\n{str(e)}")
|
|
|
|
def open_translated_folder(self):
|
|
"""Open the translated folder in the system file manager."""
|
|
try:
|
|
import subprocess
|
|
import platform
|
|
|
|
if platform.system() == 'Windows':
|
|
subprocess.run(['explorer', str(self.translated_dir)])
|
|
elif platform.system() == 'Darwin': # macOS
|
|
subprocess.run(['open', str(self.translated_dir)])
|
|
else: # Linux
|
|
subprocess.run(['xdg-open', str(self.translated_dir)])
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error", f"Failed to open folder:\n{str(e)}")
|
|
|
|
def get_input_files(self):
|
|
"""Get list of input files for processing."""
|
|
input_files = []
|
|
for i in range(self.input_tree.topLevelItemCount()):
|
|
item = self.input_tree.topLevelItem(i)
|
|
file_path = item.data(0, Qt.UserRole)
|
|
input_files.append(Path(file_path))
|
|
return input_files
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle widget close event."""
|
|
if hasattr(self, 'refresh_timer'):
|
|
self.refresh_timer.stop()
|
|
event.accept()
|