commit 76add2a45aa9e484917569ceacdd2f241695eb30 Author: zenaku Date: Wed May 21 10:32:03 2025 -0500 first commit diff --git a/.config_app/code/open_zip.py b/.config_app/code/open_zip.py new file mode 100644 index 0000000..f5ee0d1 --- /dev/null +++ b/.config_app/code/open_zip.py @@ -0,0 +1,99 @@ +import os +import zipfile +import hashlib +from io import BytesIO +from PyQt6.QtWidgets import QFileDialog, QMessageBox +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding + +public_key_path = os.path.join(".config_app","data","public_key.pem") + +with open(public_key_path, "rb") as key_file: + public_key = serialization.load_pem_public_key(key_file.read()) + +def calcular_hash_zip_sin_firma(zip_data): + hash_sha256 = hashlib.sha256() + with zipfile.ZipFile(zip_data, 'r') as zipf: + for name in sorted(zipf.namelist()): + if name == "signature.sig": + continue + data = zipf.read(name) + hash_sha256.update(data) + return hash_sha256.digest() + +def extraer_zip(zip_data, destino): + os.makedirs(destino, exist_ok=True) + with zipfile.ZipFile(zip_data) as zip_file: + for member in zip_file.namelist(): + if member == "signature.sig": + continue + zip_file.extract(member, destino) + print(f"Files extracted to: {destino}") + +def open_zip(): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles) + file_dialog.setNameFilter("ZIP files (*.zip)") + file_dialog.setViewMode(QFileDialog.ViewMode.List) + + if file_dialog.exec(): + selected_files = file_dialog.selectedFiles() + if selected_files: + zip_file_path = selected_files[0] + + if not zip_file_path.lower().endswith(".zip"): + QMessageBox.warning(None, "Invalid File", "Please select a valid ZIP file.") + return False + + try: + with open(zip_file_path, 'rb') as f: + zip_data = BytesIO(f.read()) + + with zipfile.ZipFile(zip_data) as zip_file: + file_list = zip_file.namelist() + warning = False + + if "signature.sig" not in file_list: + print("Warning: 'signature.sig' not found in the ZIP.") + warning = True + else: + signature = zip_file.read("signature.sig") + file_hash = calcular_hash_zip_sin_firma(zip_data) + + try: + public_key.verify( + signature, + file_hash, + padding.PKCS1v15(), + hashes.SHA256() + ) + print("ZIP is valid and correctly signed.") + destination = os.path.expanduser(".config_app/templates") + extraer_zip(zip_data, destination) + return True + except Exception as e: + print("ZIP is altered or invalid:", e) + warning = True + + if warning: + reply = QMessageBox.question( + None, + "Signature Warning", + "This file may be tampered or unofficial.\nDo you want to continue at your own risk?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.Yes: + destination = os.path.expanduser(".config_app/templates") + extraer_zip(zip_data, destination) + return True + else: + return False + + except Exception as e: + print(f"Error processing the ZIP file → {e}") + QMessageBox.critical(None, "Error", "An error occurred while processing the ZIP file.") + return False + else: + return False + else: + return False diff --git a/.config_app/data/public_key.pem b/.config_app/data/public_key.pem new file mode 100644 index 0000000..a8ea3e3 --- /dev/null +++ b/.config_app/data/public_key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoWIJ82SIaUETU8UraRW2 +yD3UhHoud1Y0FZyPIUG2YICCAXLt1peg7NEaoQ/Hi29iJpini/9W92Gvf0ofLaQr +yNP1klQI51rG6sr+JKn4iZYsyErFWNIy0KhllOdBrpS/wJCWN+OPRIfuBy0y9+Wf +ZDKz5J9cLlKhsC2upPrf7rWEoknu1VeIlBuRG1mEsnmI+22FQ2EcKlzJlJJinXsy +ofUK92ZrH3WQLdRLplh6M0PsEphiRknW7AIIuU0eDJLJ/ZufRJl+Ru4L3EXW8eX3 +fn4Im8R9bPj66jOiO47eh1Eu0LUnyTvcQV0te46uNzzPZv5q8qCAc1xE63JGHapQ +Jy5n2ud1XzR5Lzif+T/mAJNYHGRGa2g3zY10caaKHTOtpVxlDX23tjBbmC+TigwV +jSheNLbe/VdRTu9e+pI4KdbPtYVsqxdGTk+h5O/2eP5y5A6kW2n9rOJ4VXwGb9xY +hn5ZKNrfLN1UnL1T1l1waKBLtYdIk47ia4xsR/1A/cffvoLwYVtf0nfW2pEmbA/i +BTZW3+4UdnDpFmRc4fO/edTh7iMPhX6mJBMx12h3OVmyRRds91xFkDYpPuBJ8v9f +zIFAi00cxWWTDbDGXa+eqYFNNQ+g2/xz8QeY/5+6YKsx6C8XyemNBQza+JqFNwda +lKNuiudtwpJUUprKfup/jMUCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/.config_app/images/bastyon.svg b/.config_app/images/bastyon.svg new file mode 100755 index 0000000..fe711c7 --- /dev/null +++ b/.config_app/images/bastyon.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.config_app/images/nostr.svg b/.config_app/images/nostr.svg new file mode 100644 index 0000000..1888bfa --- /dev/null +++ b/.config_app/images/nostr.svg @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.config_app/images/session.svg b/.config_app/images/session.svg new file mode 100644 index 0000000..86434ab --- /dev/null +++ b/.config_app/images/session.svg @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/.config_app/images/signal.svg b/.config_app/images/signal.svg new file mode 100644 index 0000000..dbae6ba --- /dev/null +++ b/.config_app/images/signal.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/.config_app/layout.css b/.config_app/layout.css new file mode 100644 index 0000000..baa0754 --- /dev/null +++ b/.config_app/layout.css @@ -0,0 +1,85 @@ +/*General style*/ +* { + color: rgb(29, 29, 29); + font-size: 11pt; +} +QMainWindow{ + background: rgb(235, 235, 235); + +} +#progress_bar { + border: none; + background-color: transparent; +} +#progress_bar::chunk { + background-color: #808080; + border-radius: 20%; + border: 2px solid rgb(134, 134, 134); +} + +/*Label*/ +QLabel{ + color: rgb(29, 29, 29); +} +/*Label*/ +QPushButton{ + background-color: rgb(231, 231, 231); + color: rgb(22, 22, 22); + border: 1px solid rgb(4, 122, 10); + border-radius: 8px; +} +QPushButton:hover{ + background-color: rgb(82, 82, 82); + color: rgb(233, 233, 233); + border: 1px solid rgb(90, 207, 96); + +} +QPlainTextEdit{ + background-color: rgb(231, 231, 231); + color: rgb(22, 22, 22); + border: 1px solid rgb(4, 122, 10); + border-radius: 8px; + +} +QLineEdit{ + background-color: rgb(231, 231, 231); + color: rgb(22, 22, 22); + border: 1px solid rgb(4, 122, 10); + border-radius: 8px; + +} +/*BasePage························································*/ +#widget_up,#widget_center,#widget_down{ + background-color: rgb(255, 255, 255); + border: 1px solid rgba(136, 135, 135, 0.1); /* Borde sutil para dar profundidad */ + border-radius: 12px; +} +#label_title{ + color: rgb(19, 143, 2); + font-weight: 400; +} +#label_balance{ + color: black; +} + +/*PageCreateWebsite························································*/ +#line_project, +#line_website_name, +#plain_website_description, +#plain_website_seo { + background-color: rgb(231, 231, 231); + color: rgb(22, 22, 22); + border: 1px solid rgb(4, 122, 10); + border-radius: 8px; +} +#footer_nostr, +#footer_session, +#footer_bastyon, +#footer_signal { + background-color: rgb(231, 231, 231); + color: rgb(22, 22, 22); + border: 1px solid rgb(4, 122, 10); + border-radius: 8px; +} + +/*PageEditWebsite························································*/ diff --git a/.config_app/urbanist.ttf b/.config_app/urbanist.ttf new file mode 100644 index 0000000..eda2a96 Binary files /dev/null and b/.config_app/urbanist.ttf differ diff --git a/appimagetool-x86_64.AppImage b/appimagetool-x86_64.AppImage new file mode 100755 index 0000000..89ff93c Binary files /dev/null and b/appimagetool-x86_64.AppImage differ diff --git a/arweb.png b/arweb.png new file mode 100644 index 0000000..ff68e0c Binary files /dev/null and b/arweb.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..f704598 --- /dev/null +++ b/main.py @@ -0,0 +1,1211 @@ +# === Standard Library === +import glob +import importlib +import json +import os +import re +import shutil +import subprocess +import sys +import webbrowser +# === PyQt6 === +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont, QFontDatabase, QPixmap +from PyQt6.QtWidgets import ( + QApplication, QFileDialog, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QMessageBox, QPushButton, QPlainTextEdit, + QProgressBar, QSizePolicy, QSpacerItem, QStackedLayout, + QStackedWidget, QVBoxLayout, QWidget +) +# === Local Modules === +from worker import WorkerThread +# === App Configuration === +config_path = os.path.join(os.path.dirname(__file__), ".config_app") + +class MiVentana(QMainWindow): + def __init__(self): + super().__init__() + self.setMinimumSize(960, 540) + font_id = QFontDatabase.addApplicationFont(".config_app/urbanist.ttf") + font_families = QFontDatabase.applicationFontFamilies(font_id) + font = QFont(font_families[0]) + QApplication.setFont(font) + self.setStyleSheet(open('.config_app/layout.css', 'r').read()) + self.stacked_widget = QStackedWidget(self) + self.pages = {} + print("Style default load") + print("Font default load:", font_families) + #PageImportWallet -->Page start + #PageWebsites -->Page Two + #PageTemplates -->Page three + #PageCreateWebsite -->Page four + #PagePublic -->Page five + #PageWebsitesEditor -->Page six + #PageEditWebsite -->Page seven + + for page_class in [PageImportWallet,PageWebsites,PageTemplates,PageCreateWebsite,PagePublic,PageWebsitesEditor,PageEditWebsite]: + page_instance = page_class(self.stacked_widget) + self.stacked_widget.addWidget(page_instance) + self.pages[page_class.__name__] = page_instance + + self.setCentralWidget(self.stacked_widget) + # Cargador de arranque -- importante! + perfiles = os.listdir('.config_app/profiles') if os.path.exists('.config_app/profiles') else [] + if any(os.path.isdir(os.path.join('.config_app/profiles', p)) for p in perfiles): + self.stacked_widget.setCurrentWidget(self.pages["PageWebsites"]) + print("Wallet Json detected") + else: + self.stacked_widget.setCurrentWidget(self.pages["PageImportWallet"]) + print("No detected Wallet Json") + print("Welcome Neo") +class BasePage(QWidget): + def __init__(self, stacked_widget, container_width_percentage, layout_type): + super().__init__() + + self.stacked_widget = stacked_widget + self.container_width_percentage = container_width_percentage + BasePage.profile_selected = 'default' + + layout = QVBoxLayout(self) + + for object_name, widget_type, box, height in [ + ('widget_up', QWidget, QHBoxLayout, 30), + ('widget_center', QWidget, layout_type, None), + ('widget_down', QWidget, QHBoxLayout, 35) + ]: + widget = widget_type() + layout_widget = box() + widget.setLayout(layout_widget) + layout_widget.setContentsMargins(10, 0, 10, 0) + widget.setObjectName(object_name) + if height is not None: + widget.setFixedHeight(height) + setattr(self, object_name, widget) + layout.addWidget(widget) + #widget_up === + if object_name == 'widget_up': + for index, (label_name, text) in enumerate([ + ('label_title', 'ArWeb'), + ('label_balance', 'Balance') + ]): + label = QLabel(text) + label.setObjectName(label_name) + setattr(self, label_name, label) + widget.layout().addWidget(label) + if index == 0: + spacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + widget.layout().addItem(spacer) + #widget_center === + elif object_name == 'widget_center': + self.container = QWidget() + self.container_layout = layout_type(self.container) + self.container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.container.setLayout(self.container_layout) + + self.loading_group = QWidget() + loading_group_layout = QVBoxLayout(self.loading_group) + + for obj_name, widget_type, h in [ + ('label_loading', QLabel, 20), + ('progress_bar', QProgressBar, 4) + ]: + w = widget_type() + w.setObjectName(obj_name) + w.setFixedHeight(h) + + if isinstance(w, QLabel): + w.setAlignment(Qt.AlignmentFlag.AlignCenter) + w.setText("Loading...") + elif isinstance(w, QProgressBar): + w.setRange(0, 0) + w.setTextVisible(True) + + setattr(self, obj_name, w) + loading_group_layout.addWidget(w) + + self.container_layout.addWidget(self.loading_group) + widget.layout().addWidget(self.container) + self.loading_group.hide() + + # Evento resize personalizado + self.widget_center.resizeEvent = self.resize_event_custom + + # === Zona widget_down === + elif object_name == 'widget_down': + self.btn_settings = QPushButton('Settings') + self.btn_settings.setObjectName('button_settings') + self.btn_settings.setFixedWidth(80) + self.btn_settings.clicked.connect(self.settings) + widget.layout().addWidget(self.btn_settings) + + spacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + widget.layout().addItem(spacer) + + for btn_name, text, func, width in [ + ('button_back', 'Back', self.go_back, 40), + ('button_next', 'Next', self.go_next, 40) + ]: + btn = QPushButton(text) + btn.setObjectName(btn_name) + btn.setFixedWidth(width) + btn.clicked.connect(func) + setattr(self, btn_name, btn) + widget.layout().addWidget(btn) + self.button_back.hide() + self.button_next.hide() + + + self.setLayout(layout) + + def settings(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsites"]) + def go_next(self): + pass + def go_back(self): + pass + def show_balance(self): + try: + out = subprocess.run( + ["ardrive", "get-balance", "-w", f".config_app/profiles/{BasePage.profile_selected}/wallet_{BasePage.profile_selected}.json"], + capture_output=True, text=True, check=True, timeout=5 + ).stdout.strip().splitlines() + lines = [" ".join(line.split()) for line in out if line.strip()] + text = " // ".join(lines) if len(lines) == 2 else "Invalid balance format" + except Exception: + text = "Error getting balance" + self.label_balance.setText(f"Balance: {text}") + + def resize_event_custom(self, event): + self.container.setFixedWidth(int(self.widget_center.width() * self.container_width_percentage)) + self.container.setFixedHeight(self.widget_center.height()) + QWidget.resizeEvent(self.widget_center, event) + + def showEvent(self, event): + super().showEvent(event) + self.show_balance() + print('Refresh balance') + +class PageImportWallet(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.6, QVBoxLayout) + self.label_balance.hide() + + for object_name, widget_type, height, text, action in [ + ('label_select', QLabel, 60, + "Eternal Legacy allows you to create or import an Arweave wallet for your website.\n" + "How would you like to proceed?", None), + ('button_create', QPushButton, 40, "Create Arweave Wallet", self.create_wallet), + ('button_import', QPushButton, 40, "Import Arweave Wallet", self.import_wallet), + ]: + widget = widget_type(text) + widget.setObjectName(object_name) + widget.setFixedHeight(height) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + if widget_type == QLabel: + widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + if widget_type == QPushButton and action: + widget.clicked.connect(action) + + setattr(self, object_name, widget) + self.container_layout.addWidget(widget) + + self.widget_center.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.widget_center.layout().addWidget(self.container) + + def create_wallet(self): + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.hide() + self.loading_group.show() + self.label_loading.setText("Creating wallet...") + + self.worker = WorkerThread('create_wallet', BasePage.profile_selected) + self.worker.result.connect(self.verify_worker) + self.worker.start() + + def import_wallet(self): + base_dir = f".config_app/profiles/{BasePage.profile_selected}/" + destino = os.path.join(base_dir, f"wallet_{BasePage.profile_selected}.json") + + def copiar_archivo(origen): + if not os.path.isfile(origen): + print("The selected file does not exist") + return False + os.makedirs(base_dir, exist_ok=True) + shutil.copy(origen, destino) + print(f"File copied as: {destino}") + return True + + def verify_wallet(): + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.hide() + self.loading_group.show() + self.label_loading.setText("Verifying wallet...") + + self.worker = WorkerThread('verify_wallet', BasePage.profile_selected) + self.worker.result.connect(self.verify_worker) + self.worker.start() + + file_dialog = QFileDialog(self) + if file_dialog.exec(): + file_path = file_dialog.selectedFiles()[0] + if copiar_archivo(file_path): + verify_wallet() + else: + print("No file was selected") + + def verify_worker(self, success: bool): + base_dir = f".config_app/profiles/{BasePage.profile_selected}/" + destino = os.path.join(base_dir, f"wallet_{BasePage.profile_selected}.json") + + if success: + print("Wallet verified successfully") + self.go_next() + else: + if os.path.isfile(destino): + os.remove(destino) + print(f"Wallet not verified, file {destino} removed.") + else: + print(f"The file {destino} does not exist to be removed.") + + self.loading_group.hide() + + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget and widget is not self.loading_group: + widget.show() + + if success: + print("Wallet verified successfully") + self.go_next() + else: + if os.path.isfile(destino): + os.remove(destino) + print(f"Wallet not verified, file {destino} removed.") + else: + print(f"The file {destino} does not exist to be removed.") + + def go_next(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsites"]) + +class PageWebsites(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.6, QVBoxLayout) + self.button_back.show() + for object_name, widget_type, height, text, action in [ + ('label_select', QLabel, 60, "What would you like to do?", None), + ('button_edit', QPushButton, 40, "View or Edit Website", self.edit_website), + ('button_new_website', QPushButton, 40, "Create New Website", self.new_website), + ]: + widget = widget_type(text) + widget.setObjectName(object_name) + widget.setFixedHeight(height) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + if widget_type == QLabel: + widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + if widget_type == QPushButton and action: + widget.clicked.connect(action) + + setattr(self, object_name, widget) + self.container_layout.addWidget(widget) + + self.widget_center.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.widget_center.layout().addWidget(self.container) + + def edit_website(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsitesEditor"]) + + def new_website(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageTemplates"]) + + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageImportWallet"]) + +class PageTemplates(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.98, QHBoxLayout) + self.button_back.show() + self.container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.container_layout.setContentsMargins(0, 0, 0, 0) + + def showEvent(self, event): + super().showEvent(event) + if not hasattr(self, "widget_rotary"): + self.build_template_view() + + def build_template_view(self): + if hasattr(self, "widget_rotary"): + self.container_layout.removeWidget(self.button_before) + self.container_layout.removeWidget(self.widget_rotary) + self.container_layout.removeWidget(self.button_after) + self.widget_rotary.deleteLater() + self.button_before.deleteLater() + self.button_after.deleteLater() + del self.widget_rotary, self.button_before, self.button_after + self.elements.clear() + + for name, widget_type, text, callback in [ + ("button_before", QPushButton, "◀", self.go_before), + ("widget_rotary", QWidget, None, ""), + ("button_after", QPushButton, "▶", self.go_after), + ]: + widget = widget_type(text) if text else widget_type() + widget.setObjectName(name) + if isinstance(widget, QPushButton): + widget.setFixedSize(40, 40) + widget.clicked.connect(callback) + widget.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + setattr(self, name, widget) + else: + widget.setContentsMargins(0, 0, 0, 0) + self.widget_rotary_layout = QStackedLayout(widget) + self.widget_rotary_layout.setContentsMargins(0, 0, 0, 0) + self.widget_rotary_layout.setSpacing(0) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.widget_rotary = widget + + self.elements = [] + self.current_index = 0 + template_dir = ".config_app/templates" + if os.path.exists(template_dir): + for folder_name in os.listdir(template_dir): + folder_path = os.path.join(template_dir, folder_name) + if os.path.isdir(folder_path): + for file_name in os.listdir(folder_path): + if file_name.lower().endswith(".png"): + label = QLabel() + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + label.setStyleSheet("background-color: transparent;") + label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + label.setPixmap(QPixmap(os.path.join(folder_path, file_name))) + label.setScaledContents(True) + self.elements.append((folder_name, label)) + + self.template_button = QPushButton("Import your template") + self.template_button.clicked.connect(self.open_zip) + self.template_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.elements.append(self.template_button) + + for element in self.elements: + widget = element if isinstance(element, QPushButton) else element[1] + self.widget_rotary_layout.addWidget(widget) + + self.widget_rotary_layout.setCurrentIndex(self.current_index) + self.container_layout.addWidget(self.button_before) + self.container_layout.addWidget(self.widget_rotary) + self.container_layout.addWidget(self.button_after) + + layout = self.widget_center.layout() + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.container) + + self._update_next_button_visibility() + + def _update_next_button_visibility(self): + current_element = self.elements[self.current_index] + if isinstance(current_element, tuple): + self.button_next.show() + else: + self.button_next.hide() + + def go_before(self): + self.current_index = (self.current_index - 1) % len(self.elements) + self.widget_rotary_layout.setCurrentIndex(self.current_index) + self._update_next_button_visibility() + + def go_after(self): + self.current_index = (self.current_index + 1) % len(self.elements) + self.widget_rotary_layout.setCurrentIndex(self.current_index) + self._update_next_button_visibility() + + def open_zip(self): + try: + base_path = os.path.dirname(os.path.abspath(__file__)) + zip_script_path = os.path.join(base_path, ".config_app", "code", "open_zip.py") + print(f"Buscando script en: {zip_script_path}") + + if os.path.exists(zip_script_path): + print(f"Script encontrado en: {zip_script_path}") + + name = os.path.splitext(os.path.basename(zip_script_path))[0] + print(f"Nombre del módulo: {name}") + + # Cargar el módulo dinámicamente + spec = importlib.util.spec_from_file_location(name, zip_script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + print("Módulo cargado correctamente.") + + # Verificar si la función 'open_zip' está en el módulo + if hasattr(module, 'open_zip'): + print("Función 'open_zip' encontrada en el módulo.") + result = module.open_zip() + if result: + print("Template imported successfully.") + self.build_template_view() + else: + print("Failed to import template.") + else: + print(f"'open_zip' function not found in {zip_script_path}") + else: + print(f"{zip_script_path} no encontrado.") + except Exception as e: + print(f"Error loading {zip_script_path}: {e}") + def go_next(self): + try: + item = self.elements[self.current_index] + if isinstance(item, QPushButton): + raise Exception + BasePage.template_selected = item[0] + print(f"Template selected: {BasePage.template_selected}") + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageCreateWebsite"]) + except Exception: + QMessageBox.warning(self, "Warning", "Please select or import a template first.") + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsites"]) + +class PageCreateWebsite(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.98, QHBoxLayout) + self.button_back.show() + self._last_template_loaded = None + self.conclave = {} + self.hugo_memory = {} + + for object_name in [("widget_left"), ("widget_right")]: + widget = QWidget() + widget.setObjectName(object_name) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + layout = QVBoxLayout(widget) + layout.setContentsMargins(0, 0, 0, 0) + setattr(self, object_name, widget) + setattr(self, f"{object_name}_layout", layout) + self.container_layout.addWidget(widget, 1) + + for object_name, widget_type, text, action, height in [ + ("label_website", QLabel, "General website config", None, 30), + ("line_project", QLineEdit, "Project Name", None, 30), + ("line_website_name", QLineEdit, "Website Name", None, 30), + ("plain_website_description", QPlainTextEdit, "Website Description", None, 65), + ("plain_website_seo", QPlainTextEdit, "Website Seo (separated with commas)", None, 65), + ("button_favicon", QPushButton, "Select Favicon", self.favicon, 30) + ]: + widget = widget_type() + widget.setObjectName(object_name) + widget.setFixedHeight(height) + + if isinstance(widget, (QLabel, QPushButton)): + widget.setText(text) + + if isinstance(widget, QPushButton) and action: + widget.clicked.connect(action) + self.conclave[object_name] = False + + if isinstance(widget, (QLineEdit, QPlainTextEdit)): + widget.setPlaceholderText(text) + widget.textChanged.connect(self.check_conclave) + self.conclave[object_name] = False + + self.widget_left_layout.addWidget(widget) + + for icon_name, placeholder in [ + ("footer_nostr", "Nostr link"), + ("footer_bastyon", "Bastyon link"), + ("footer_session", "Session link"), + ("footer_signal", "Signal link") + ]: + widget = QWidget() + widget.setObjectName(f"widget_footer_{icon_name}") + widget.setFixedHeight(30) + widget.setLayout(QHBoxLayout()) + widget.layout().setContentsMargins(0, 0, 0, 0) + widget.setStyleSheet("background-color: white; border: 0px; margin: 0px;") + + label = QLabel() + label.setFixedSize(30, 30) + label.setPixmap(QPixmap(f".config_app/images/{icon_name}.svg")) + label.setScaledContents(True) + line_edit = QLineEdit() + line_edit.setFixedHeight(30) + + line_edit.setPlaceholderText(placeholder) + line_edit.setObjectName(icon_name) # Añadido aquí + + widget.layout().addWidget(label) + widget.setFixedHeight(30) + widget.layout().addWidget(line_edit) + + self.widget_left_layout.addWidget(widget) + + self.widget_center.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.widget_center.layout().addWidget(self.container) + + def showEvent(self, event): + super().showEvent(event) + print(f"Layout is: {BasePage.template_selected}") + self.reset_widgets() + + + if self._last_template_loaded == BasePage.template_selected: + print("La plantilla ya fue cargada anteriormente, omitiendo carga de widgets.") + return + for i in reversed(range(self.widget_right_layout.count())): + widget = self.widget_right_layout.itemAt(i).widget() + if widget: + widget.setParent(None) + + self._last_template_loaded = BasePage.template_selected + template_dir = f".config_app/templates/{BasePage.template_selected}/" + + # Limpiar widgets anteriores + for i in reversed(range(self.widget_right_layout.count())): + widget = self.widget_right_layout.itemAt(i).widget() + if widget: + widget.setParent(None) + + self._last_template_loaded = BasePage.template_selected + + # Ruta del template seleccionado + template_dir = f".config_app/templates/{BasePage.template_selected}/" + print(f"Buscando archivos en: {template_dir}") + + # Buscar y cargar el .py + for file_path in glob.glob(f"{template_dir}*.py"): + print(f"Encontrado archivo Python: {file_path}") + try: + name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, 'add_right_widgets'): + print(f"Llamando a add_right_widgets desde {file_path}") + module.add_right_widgets(self) + else: + print(f"'add_right_widgets' no encontrado en {file_path}") + + except Exception as e: + print(f"Error cargando {file_path} → {e}") + template_dir = f".config_app/templates/{BasePage.template_selected}/" + + # Buscar y cargar archivos .css + css_files = glob.glob(os.path.join(template_dir, "*.css")) + + if css_files: + css_path = css_files[0] + print(f"Hoja de estilo encontrada: {css_path}") + try: + with open(css_path, "r", encoding="utf-8") as f: + self.setStyleSheet(f.read()) + print(f"Estilo aplicado desde: {css_path}") + except Exception as e: + print(f"Error al cargar el estilo desde {css_path} → {e}") + else: + print(f"No se encontró ningún archivo .css en: {template_dir}") + + + def reset_widgets(self): + for container in [self.widget_left, self.widget_right]: + for widget in container.findChildren(QWidget): + if isinstance(widget, QLineEdit): + widget.clear() + elif isinstance(widget, QPlainTextEdit): + widget.setPlainText("") + elif isinstance(widget, QPushButton): + widget.setStyleSheet("") # Restablecer estilos de botones + elif widget.objectName().startswith("widget_footer_"): + line_edit = widget.findChild(QLineEdit) + if line_edit: + line_edit.clear() + widget.setStyleSheet("") # Restablecer estilos generales + + def favicon(self): + file_path, _ = QFileDialog.getOpenFileName( + self, + "Select a Favicon (.ico)", + "", + "Icon files (*.ico)" + ) + if file_path: + self.hugo_memory['favicon'] = file_path + self.conclave['button_favicon'] = True + button = self.findChild(QPushButton, "button_favicon") + if button: + button.setStyleSheet("background-color: #4CAF50; color: white;") + print(self.hugo_memory) + else: + self.conclave['button_favicon'] = False + button = self.findChild(QPushButton, "button_favicon") + if button: + button.setStyleSheet("") + self.check_conclave() + + def check_conclave(self): + all_completed = True + + for object_name in self.conclave.keys(): + widget = self.findChild(QWidget, object_name) + if isinstance(widget, (QLineEdit, QPlainTextEdit)): + text = widget.toPlainText() if isinstance(widget, QPlainTextEdit) else widget.text() + self.conclave[object_name] = bool(text.strip()) + + if not self.conclave[object_name]: + all_completed = False + + if all_completed: + self.button_next.show() + else: + self.button_next.hide() + pass + + def go_next(self): + for widget in self.findChildren((QLineEdit, QPlainTextEdit)): + name = widget.objectName() + if not name: + parent = widget.parent() + if parent and parent.objectName().startswith("widget_footer_"): + name = parent.objectName().replace("widget_footer_", "") + else: + continue + text = widget.text().strip() if isinstance(widget, QLineEdit) else widget.toPlainText().strip() + self.hugo_memory[name] = text + if "line_project" in self.hugo_memory: + BasePage.project_selected = self.hugo_memory["line_project"] + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.hide() + + self.loading_group.show() + self.label_loading.setText("Creating website...") + self.hugo_memory = {k: v for k, v in self.hugo_memory.items() if not (k.startswith("footer_") and not v.strip())} + + self.worker = WorkerThread('create_website', BasePage.profile_selected, self.hugo_memory, BasePage.template_selected, BasePage.project_selected) + self.worker.result.connect(self.veify_worker) + self.worker.start() + def veify_worker(self, result): + if result: + print("Next steps") + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.show() + + self.loading_group.hide() + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PagePublic"]) + else: + print("Error: no se pudo crear el sitio.") + QMessageBox.warning(self, "Warning", "Please select or import a template first.") + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.show() + + self.loading_group.hide() + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageTemplates"]) + +class PagePublic(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.6, QVBoxLayout) + + for object_name, widget_type, height, text, action in [ + ('label_select', QLabel, 60, + "Your website has been successfully generated. What would you like to do?", None), + ('button_edit_website', QPushButton, 40, "Edit Website", self.edit_website), + ('button_view_website', QPushButton, 40, "View Website", self.view_website), + ('button_public_website', QPushButton, 40, "Public Website on arweave", self.public_website) + ]: + widget = widget_type(text) + widget.setObjectName(object_name) + widget.setFixedHeight(height) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + if widget_type == QLabel: + widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + if widget_type == QPushButton and action: + widget.clicked.connect(action) + + setattr(self, object_name, widget) + self.container_layout.addWidget(widget) + + self.widget_center.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.widget_center.layout().addWidget(self.container) + + self._worker_key = 'public_arweave' # default fallback + + def showEvent(self, event): + super().showEvent(event) + manifest_path = os.path.abspath( + f".config_app/profiles/{BasePage.profile_selected}/{BasePage.project_selected}/manifest.json" + ) + + if os.path.isfile(manifest_path): + self.label_loading.setText("Refresh Website on arweave...") + print("refreesh on arweave") + self._worker_key = 'refresh_arweave' + else: + self.label_loading.setText("Upload Website on arweave...") + self._worker_key = 'public_arweave' + print("refreesh on arweave") + + + def edit_website(self): + p = os.path.abspath(f".config_app/profiles/{BasePage.profile_selected}/{BasePage.project_selected}") + if os.path.exists(p): + if sys.platform == "win32": + os.startfile(p) + else: + subprocess.run(["open" if sys.platform == "darwin" else "xdg-open", p]) + + def view_website(self): + index_path = f".config_app/profiles/{BasePage.profile_selected}/{BasePage.project_selected}/public/index.html" + absolute_path = os.path.abspath(index_path) + if os.path.exists(absolute_path): + webbrowser.open(f"file://{absolute_path}") + + def public_website(self): + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.hide() + self.loading_group.show() + + self.worker = WorkerThread( + key=self._worker_key, + profile_selected=BasePage.profile_selected, + project_selected=BasePage.project_selected + ) + self.worker.result.connect(self.go_next) + self.worker.start() + + def go_next(self, result): + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.show() + + self.loading_group.hide() + if result: + manifest_path = os.path.join( + ".config_app", "profiles", + BasePage.profile_selected, + BasePage.project_selected, + "manifest.json" + ) + + if os.path.exists(manifest_path): + with open(manifest_path, "r", encoding="utf-8") as file: + data = json.load(file) + created = data.get("created", []) + if created and "dataTxId" in created[0]: + data_tx_id = created[0]["dataTxId"] + webbrowser.open(f"https://arweave.net/{data_tx_id}") + + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsites"]) + else: + QMessageBox.critical(self, "Error", "La publicación en Arweave falló. Revisa tu conexión, claves o drive.") + + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageTemplates"]) + +class PageWebsitesEditor(BasePage):#reformular toda la pagina en aras de que muestre en nombre + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.98, QHBoxLayout) + self.button_back.show() + self.container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.container_layout.setContentsMargins(0, 0, 0, 0) + self.current_index = 0 + + def showEvent(self, event): + super().showEvent(event) + + while self.container_layout.count(): + item = self.container_layout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + profile_path = os.path.join(".config_app", "profiles", BasePage.profile_selected) + self.proyectos = {} + + if os.path.exists(profile_path): + for project_name in os.listdir(profile_path): + project_path = os.path.join(profile_path, project_name) + if os.path.isdir(project_path): + themes_path = os.path.join(project_path, "themes") + plantilla = None + if os.path.exists(themes_path): + template_folders = [ + f for f in os.listdir(themes_path) + if os.path.isdir(os.path.join(themes_path, f)) + ] + if template_folders: + plantilla = template_folders[0] + self.proyectos[project_name] = plantilla + + if not self.proyectos: + label = QLabel("No websites yet") + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.container_layout.addWidget(label) + else: + self.create_structure() + + + + def create_structure(self): + self.button_next.show() + + + # Botón anterior + self.button_before = QPushButton("◀") + self.button_before.setFixedSize(40, 40) + self.button_before.clicked.connect(self.go_before) + self.button_before.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.container_layout.addWidget(self.button_before) + + # Contenedor central con QStackedLayout + self.widget_rotary = QLabel() + self.widget_rotary.setContentsMargins(0, 0, 0, 0) + self.widget_rotary_layout = QStackedLayout(self.widget_rotary) + self.widget_rotary_layout.setContentsMargins(0, 0, 0, 0) + self.widget_rotary_layout.setSpacing(0) + self.widget_rotary.setStyleSheet("background-color: black;") + + self.widget_rotary.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.container_layout.addWidget(self.widget_rotary) + + self.images = [] # Almacenaremos las imágenes aquí + + # Cargar imágenes según plantilla + for project_name, plantilla in self.proyectos.items(): + plantilla_dir = os.path.join(".config_app", "templates", plantilla) + image_path = None + + + # Imprimir la ruta donde se está buscando la imagen + print(f"Buscando imágenes en: {plantilla_dir}") + + # Buscar cualquier archivo de imagen en la carpeta de la plantilla + if os.path.exists(plantilla_dir): + for f in os.listdir(plantilla_dir): + if f.lower().endswith((".png", ".jpg", ".jpeg", ".gif")): + image_path = os.path.join(plantilla_dir, f) + print(f"Imagen encontrada: {image_path}") + break # Solo tomamos la primera imagen válida + + # Crear el QLabel y cargar la imagen + image_label = QLabel() + image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Establecer fondo transparente y expandir el QLabel + image_label.setStyleSheet("background-color: transparent;") + image_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + image_label.setScaledContents(True) # Asegurar que la imagen se escale al tamaño del QLabel + + if image_path and os.path.exists(image_path): + # Establecer la imagen en el QLabel + image_label.setPixmap(QPixmap(image_path)) + else: + image_label.setText(f"No image for '{plantilla}'") + + # Añadir la imagen al layout y almacenarla para control + self.widget_rotary_layout.addWidget(image_label) + self.images.append((project_name, image_label)) # Guardar el proyecto y su imagen + self.label_balance.setText(project_name) + + # Botón siguiente + self.button_after = QPushButton("▶") + self.button_after.setFixedSize(40, 40) + self.button_after.clicked.connect(self.go_after) # Usamos go_next para guardar el nombre del proyecto + self.button_after.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.container_layout.addWidget(self.button_after) + + self.current_index = 0 # Empezamos con la primera imagen + self.widget_rotary_layout.setCurrentIndex(self.current_index) + + def go_next(self): + # Ahora guardamos BasePage.project_selected como el nombre del proyecto de la imagen que esté viéndose + selected_project = self.images[self.current_index][0] # Nombre del proyecto + BasePage.project_selected = selected_project + print(f"Proyecto seleccionado: {selected_project}") + + # Guardar también la plantilla seleccionada + plantilla = self.proyectos[selected_project] # Obtener la plantilla del proyecto + BasePage.template_selected = plantilla # Guardar la plantilla seleccionada + print(f"Plantilla seleccionada: {plantilla}") + print(BasePage.project_selected) + + + # Opcional: Cambiar a la página de edición, sin modificar la imagen + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageEditWebsite"]) + + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageWebsites"]) + + def go_before(self): + if self.widget_rotary_layout.count() > 0: + self.current_index = (self.current_index - 1) % self.widget_rotary_layout.count() + self.widget_rotary_layout.setCurrentIndex(self.current_index) + + project_name = self.images[self.current_index][0] + plantilla = self.proyectos[project_name] + print(f"[◀ Anterior] Proyecto: {project_name} | Plantilla: {plantilla}") + self.label_balance.setText(project_name) + + + def go_after(self): + if self.widget_rotary_layout.count() > 0: + self.current_index = (self.current_index + 1) % self.widget_rotary_layout.count() + self.widget_rotary_layout.setCurrentIndex(self.current_index) + + project_name = self.images[self.current_index][0] + plantilla = self.proyectos[project_name] + print(f"[▶ Siguiente] Proyecto: {project_name} | Plantilla: {plantilla}") + self.label_balance.setText(project_name) + + +class PageEditWebsite(BasePage): + def __init__(self, stacked_widget): + super().__init__(stacked_widget, 0.98, QHBoxLayout) + + self.button_back.show() + self._last_template_loaded = None + self.conclave = {} + self.hugo_memory = {} + + + for object_name in [("widget_left"), ("widget_right")]: + widget = QWidget() + widget.setObjectName(object_name) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + layout = QVBoxLayout(widget) + layout.setContentsMargins(0, 0, 0, 0) + setattr(self, object_name, widget) + setattr(self, f"{object_name}_layout", layout) + self.container_layout.addWidget(widget, 1) + + for object_name, widget_type, text, action, height in [ + ("label_website", QLabel, "General website config", None, 30), + ("button_open_website", QPushButton, "Open website in browser", self.open_website, 30), + ("button_edit_website", QPushButton, "Edit manually website (public folder)", self.edit, 30), + ("button_view_manifest", QPushButton, "View manifest", self.view_manifest, 30), + ("label_qr", QLabel, "", None, 80), + ("phantom", QLabel, None, None, 30), + ("phantom", QLabel, None, None, 80), + ("phantom", QLabel, None, None, 30), + ("phantom", QLabel, None, None, 30) + + ]: + widget = widget_type() + widget.setObjectName(object_name) + widget.setFixedHeight(height) + + if isinstance(widget, (QLabel, QPushButton)): + widget.setText(text) + + if isinstance(widget, QPushButton) and action: + widget.clicked.connect(action) + + self.widget_left_layout.addWidget(widget) + + self.widget_center.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.widget_center.layout().addWidget(self.container) + + def showEvent(self, event): + super().showEvent(event) + print(BasePage.project_selected) + print(f"Layout is: {BasePage.template_selected}") + self.reset_widgets() + + if self._last_template_loaded == BasePage.template_selected: + print("La plantilla ya fue cargada anteriormente, omitiendo carga de widgets.") + return + for i in reversed(range(self.widget_right_layout.count())): + widget = self.widget_right_layout.itemAt(i).widget() + if widget: + widget.setParent(None) + self._last_template_loaded = BasePage.template_selected + + template_dir = f".config_app/templates/{BasePage.template_selected}/" + for file_path in glob.glob(f"{template_dir}*.py"): + try: + name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, 'add_right_widgets'): + print(f"Cargando: {file_path}") + module.add_right_widgets(self) + + except Exception as e: + print(f"Error en {file_path} → {e}") + template_dir = f".config_app/templates/{BasePage.template_selected}/" + + # Buscar y cargar archivos .css + css_files = glob.glob(os.path.join(template_dir, "*.css")) + + if css_files: + css_path = css_files[0] + print(f"Hoja de estilo encontrada: {css_path}") + try: + with open(css_path, "r", encoding="utf-8") as f: + self.setStyleSheet(f.read()) + print(f"Estilo aplicado desde: {css_path}") + except Exception as e: + print(f"Error al cargar el estilo desde {css_path} → {e}") + else: + print(f"No se encontró ningún archivo .css en: {template_dir}") + def reset_widgets(self): + for container in [self.widget_left, self.widget_right]: + for widget in container.findChildren(QWidget): + if isinstance(widget, QLineEdit): + widget.clear() + elif isinstance(widget, QPlainTextEdit): + widget.setPlainText("") + elif isinstance(widget, QPushButton): + widget.setStyleSheet("") # Restablecer estilos de botones + elif widget.objectName().startswith("widget_footer_"): + line_edit = widget.findChild(QLineEdit) + if line_edit: + line_edit.clear() + widget.setStyleSheet("") # Restablecer estilos generales + + def reset_left_widgets(self): + for widget in self.widget_left.findChildren(QWidget): + if isinstance(widget, QLineEdit): + widget.clear() + elif isinstance(widget, QPlainTextEdit): + widget.setPlainText("") + elif isinstance(widget, QPushButton): + widget.setStyleSheet("") # Restablecer estilos de botones + elif widget.objectName().startswith("widget_footer_"): + line_edit = widget.findChild(QLineEdit) + if line_edit: + line_edit.clear() + widget.setStyleSheet("") # Restablecer estilos generales + + def view_manifest(self): + manifest_path = os.path.join( + ".config_app", "profiles", + BasePage.profile_selected, + BasePage.project_selected, + "manifest.json" + ) + if os.path.exists(manifest_path): + webbrowser.open(f"file://{os.path.abspath(manifest_path)}") + def open_website(self): + + manifest_path = os.path.join( + ".config_app", "profiles", + BasePage.profile_selected, + BasePage.project_selected, + "manifest.json" + ) + + if os.path.exists(manifest_path): + with open(manifest_path, "r", encoding="utf-8") as file: + data = json.load(file) + created = data.get("created", []) + if created and "dataTxId" in created[0]: + data_tx_id = created[0]["dataTxId"] + webbrowser.open(f"https://arweave.net/{data_tx_id}") + + def edit(self): + import os, subprocess, sys + + manifest_path = os.path.join( + ".config_app", "profiles", + BasePage.profile_selected, + BasePage.project_selected + ) + + if os.path.exists(manifest_path): + if sys.platform.startswith('darwin'): + subprocess.Popen(['open', manifest_path]) + elif os.name == 'nt': + os.startfile(manifest_path) + elif os.name == 'posix': + subprocess.Popen(['xdg-open', manifest_path]) + + def check_conclave(self): + all_completed = True + + for object_name in self.conclave.keys(): + widget = self.findChild(QWidget, object_name) + if isinstance(widget, (QLineEdit, QPlainTextEdit)): + text = widget.toPlainText() if isinstance(widget, QPlainTextEdit) else widget.text() + self.conclave[object_name] = bool(text.strip()) + + if not self.conclave[object_name]: + all_completed = False + + if all_completed: + self.button_next.show() + else: + self.button_next.hide() + pass + + def go_next(self): + + for widget in self.findChildren((QLineEdit, QPlainTextEdit)): + name = widget.objectName() + if not name: + parent = widget.parent() + if parent and parent.objectName().startswith("widget_footer_"): + name = parent.objectName().replace("widget_footer_", "") + else: + continue + text = widget.text().strip() if isinstance(widget, QLineEdit) else widget.toPlainText().strip() + self.hugo_memory[name] = text + if "line_project" in self.hugo_memory: + BasePage.project_selected = self.hugo_memory["line_project"] + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.hide() + + self.loading_group.show() + self.label_loading.setText("edit website...") + self.hugo_memory = {k: v for k, v in self.hugo_memory.items() if not (k.startswith("footer_") and not v.strip())} + + self.worker = WorkerThread('edit_website', BasePage.profile_selected, self.hugo_memory, BasePage.template_selected, BasePage.project_selected) + self.worker.result.connect(self.veify_worker) + self.worker.start() + def veify_worker(self, result): + if result: + print("Next steps") + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.show() + + self.loading_group.hide() + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PagePublic"]) + else: + print("Error: no se pudo crear el sitio.") + QMessageBox.warning(self, "Warning", "Please select or import a template first.") + for i in reversed(range(self.container_layout.count())): + widget = self.container_layout.itemAt(i).widget() + if widget: + widget.show() + + self.loading_group.hide() + def go_back(self): + self.stacked_widget.setCurrentWidget(self.stacked_widget.parent().pages["PageTemplates"]) + + +if __name__ == "__main__": + app = QApplication([]) + ventana = MiVentana() + ventana.show() + app.exec() diff --git a/readme-español.md b/readme-español.md new file mode 100644 index 0000000..692e5f7 --- /dev/null +++ b/readme-español.md @@ -0,0 +1,181 @@ +--- +title: "ArWeb " +description: "Lgado eterno de sitios webs. Una web sin dominio, universal y resistente a la censura, bloqueos y ataques" +image: "" +categories: + - "" + - "" +tags: + - "" +date: 2025-03-03 +authors: + - "Zenaku" +--- +**ArWeb** es un creador de páginas web estáticas para la red **Arweave**, también conocido como **Eternal Legacy (arWeb-EternalLegacy)**. + +El código es libre, siéntase a gusto de editarlo. Desarrollado en Ubuntu. + +**Solo se ha probado en Linux** +**Teóricamente funciona en todos los sistemas operativos** + +--- + +## Índice + +- [Instalación manual](#instalación-manual) +- [Instalación automática (recomendada)](#instalación-automática-recomendada) +- [Formas de uso](#formas-de-uso) +- [Descargo de responsabilidades](#descargo-de-responsabilidades) +- [Preguntas frecuentes](#preguntas-frecuentes) +- [Contacto y créditos](#contacto-y-créditos) + +--- + +## Instalación manual + +### Requisitos + +- Python 3 +- Hugo +- ArDrive + +### Pasos + +1. Descargue y extraiga todos los archivos del repositorio: + `.config_app`, `main.py`, `worker.py`, `requirements.txt`, `*.AppImage` + +2. Cree un entorno virtual dentro de la carpeta extraída: + ```bash + python3 -m venv venv + +3. Active el entorno virtual + ```bash + source venv/bin/activate + +4. Instale los requerimientos + ```bash + pip install -r requirements.txt + +5. Ejecute + ```bash + python main.py + +--- + +## Instalación automática (recomendada) + +### Requisitos + +- Python 3 +- Hugo (opcional) +- ArDrive + +### Pasos + +Esta instalación es la más sencilla y práctica. Hace lo mismo que la instalación manual, pero con un solo clic: + +1. Descargue el archivo .AppImage + +2. Ejecútelo. Se instalará una carpeta llamada ArWeb en su directorio Documentos y se lanzará la aplicación automáticamente. + +**Para futuras ejecuciones, no volverá a crear carpetas, solo ejecutará la app directamente.** + +--- + +## Formas de uso + +### Requisitos iniciales + +- Una wallet Arweave (puede crear una nueva o importar una existente). + +### Flujo de trabajo + +1. Seleccione si desea: + - Editar una web ya creada + - Crear una desde cero + +2. Seleccione o importe una plantilla + *(Las plantillas están en formato `.zip` y pueden descargarse desde la web oficial)* + +3. Complete los campos del formulario: + - **Project name**: nombre del proyecto (carpeta raíz) + - **Website name**: nombre del sitio web + - **Web description**: descripción visible en motores de búsqueda + - **SEO keywords**: palabras clave separadas por comas (para posicionamiento SEO) + - **Favicon**: imagen que aparece en las pestañas del navegador + +4. La columna derecha incluye campos personalizados según la plantilla. Rellene todos los campos requeridos. + +5. Después de completar el formulario podrá: + - Ver el sitio en su navegador + - Abrir la carpeta del proyecto + - Publicar en Arweave + + +### Para editar su sitio web: + +- Presione **Edit website** +- Seleccione el nombre del proyecto en la esquina superior derecha + +Podrá: +- Abrir la ubicación local del sitio +- Ver su manifiesto +- Agregar contenido nuevo + +--- + +## Descargo de responsabilidades + +- **ArWeb no recolecta ningún dato personal.** + El código está disponible públicamente para su inspección. + +- **Las plantillas `.zip` están firmadas digitalmente.** + Si detecta una firma inválida, desconfíe del archivo o del autor. + +--- + +## Preguntas frecuentes + +### ¿Dónde se guarda mi sitio web localmente? +Todos los proyectos se almacenan en: + +``` +.config_app/profiles/default/ +``` + +`.config_app` es una carpeta oculta. + + +### ¿Qué necesito para publicar en Arweave? +Necesitarás tokens **AR**. +Con aproximadamente **$1–$2 USD** es suficiente para publicar. + + +### ¿Puedo actualizar un sitio después de subirlo? +Sí. +ArWeb utiliza un sistema de **criptografía local** para: + +- Detectar qué archivos ya han sido subidos. +- Subir solo los nuevos. +- Actualizar el manifiesto, preservando metadatos anteriores. +- Evitar gastos innecesarios. + +--- + +## Contacto y créditos + +- **Autor principal**: Zenaku +Direccion monero + ```bash + 41kbbxc2VYtHmc9jNAnRoSTqxCAfXz1XdR1WfVWQNKsMGFL8MWcsxTQSoNUmiDNPnNNh1FkKKSjZn2uAXHTP8Jhv1GeGfwr + + +- **Colaborador**: SimplifiedPrivacy +Direccion monero + ```bash + xx + +Apoya este proyecto y la libertad digital con una donación + + +¡Gracias por usar **ArWeb**! \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a89c8b7 --- /dev/null +++ b/readme.md @@ -0,0 +1,190 @@ +--- +title: "ArWeb" +description: "Eternal legacy of websites. A domain-less, universal web resistant to censorship, blocks, and attacks." +image: "" +categories: + - "" + - "" +tags: + - "" +date: 2025-03-03 +authors: + - "Zenaku" +--- +**ArWeb** is a static website builder for the **Arweave** network, also known as **Eternal Legacy (arWeb-EternalLegacy)**. + +The code is open source — feel free to edit it. Developed on Ubuntu. + +**Only tested on Linux** +**Theoretically works on all operating systems** + +--- + +## Index + +- [Manual Installation](#manual-installation) +- [Automatic Installation (Recommended)](#automatic-installation-recommended) +- [Usage](#usage) +- [Disclaimer](#disclaimer) +- [Frequently Asked Questions](#frequently-asked-questions) +- [Contact and Credits](#contact-and-credits) + +--- + +## Manual Installation + +### Requirements + +- Python 3 +- Hugo +- ArDrive + +### Steps + +1. Download and extract all repository files: + `.config_app`, `main.py`, `worker.py`, `requirements.txt`, `*.AppImage` + +2. Create a virtual environment inside the extracted folder: + ```bash + python3 -m venv venv + +3. Activate the virtual environment: + ```bash + source venv/bin/activate + +4. Install the requirements: + ```bash + pip install -r requirements.txt + +5. Run: + ```bash + python main.py + +--- + +## Automatic Installation (Recommended) + +### Requirements + +- Python 3 +- Hugo (opcional) +- ArDrive + +### Steps + +This is the easiest and most practical method. It does the same as the manual installation but with a single click: + +1. Download the .AppImage file. + +2. Run it. A folder called ArWeb will be created in your Documents directory and the app will launch automatically. + +**For future executions, it will not create new folders — it will just launch the app directly.** + +--- + +## Usage + +### Initial Requirements + +- An Arweave wallet (you can create a new one or import an existing one). + + +### Workflow + +1. Choose whether to: + - Edit an existing website + - Create a new one from scratch + +2. Select or import a template + *(Templates are in .zip format and can be downloaded from the official website)* + + +3. Fill in the form fields: + - **Project name**: name of the project (root folder) + - **Website name**: name of the site + - **Web description**: visible in search engines + - **SEO keywords**: comma-separated keywords for SEO + - **Favicon**: image shown in browser tabs + +4. The right-hand column includes custom fields based on the selected template. Fill in all required fields. +5. After completing the form, you can: + - View the site in your browser + + - View the site in your browser + + - Publish to Arweave + +### To edit your website: + +- Click **Edit website** +- Select the project name in the top right corner + + +You can then: +- Open the local site folder +- View the manifest +- Add new content + +--- + +## Disclaimer + +- **ArWeb does not collect any personal data.** + The code is available publicly for review. + + +- **The layouts`.zip` are digitally signed.** + If you detect an invalid signature, be cautious of the file or the author. + +--- + +## Frequently Asked Questions + +### Where is my website stored locally? +All projects are saved in: + +``` +.config_app/profiles/default/ +``` + +`.config_app` is a hidden folder. + + + + + + + +### +What do I need to publish to Arweave? +You’ll need AR tokens. +About $1–$2 USD is enough to publish. + + +### Can I update a site after uploading? +Yes. +ArWeb uses a local **cryptography system** to: + +- Detect already uploaded files +- Upload only new files +- Update the manifest while preserving previous metadata +- Avoid unnecessary expenses + +--- + +## Contact and Credits + +- **Author main**: Zenaku +Monero address: + ```bash + 41kbbxc2VYtHmc9jNAnRoSTqxCAfXz1XdR1WfVWQNKsMGFL8MWcsxTQSoNUmiDNPnNNh1FkKKSjZn2uAXHTP8Jhv1GeGfwr + + +- **Contributor:**: SimplifiedPrivacy +Monero address: + ```bash + XXX + +Support this project and digital freedom with a donation. + +Thank you for using ArWeb! diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1ae9d3f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +cffi==1.17.1 +cryptography==44.0.3 +pycparser==2.22 +PyQt6==6.9.0 +PyQt6-Qt6==6.9.0 +PyQt6_sip==13.10.0 +tqdm==4.67.1 diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..02f7976 --- /dev/null +++ b/worker.py @@ -0,0 +1,734 @@ +# Módulos estándar +import glob +import hashlib +import json +import os +import re +import shutil +import subprocess +import time + +# Módulos de terceros +from tqdm import tqdm + +# Módulos de PyQt6 +from PyQt6.QtCore import QThread, pyqtSignal + +# Otros +import importlib.util + +class WorkerThread(QThread): + result = pyqtSignal(bool) + + def __init__(self, key, profile_selected, hugo_memory=None, template_selected=None, project_selected=None, parent=None): + super().__init__(parent) + self.key = key + self.profile_selected = profile_selected + self.hugo_memory = hugo_memory or {} + self.template_selected = template_selected + self.project_selected = project_selected + + + + def run(self): + success = False + if self.key == 'verify_wallet': + success = self.verify_wallet() + elif self.key == 'create_wallet': + success = self.create_wallet() + elif self.key == 'create_website': + success = self.create_website() + elif self.key == 'edit_website': + success = self.edit_website() + elif self.key == 'public_arweave': + success = self.public_arweave() + elif self.key == 'refresh_arweave': + success = self.refresh_arweave() + else: + print("Invalid key provided.") + self.result.emit(success) + + def create_wallet(self): + # Ruta del perfil + profile_path = os.path.join(".config_app", "profiles", self.profile_selected) + wallet_path = os.path.join(profile_path, f"wallet_{self.profile_selected}.json") + + # Verificar si el directorio del perfil existe. Si no, crearlo + if not os.path.exists(profile_path): + try: + os.makedirs(profile_path) # Crear el directorio para el perfil si no existe + print(f"El directorio del perfil {self.profile_selected} ha sido creado.") + except Exception as e: + print(f"Error al crear el directorio del perfil {self.profile_selected}: {e}") + + # Generación de la seedphrase + seed_proc = subprocess.run(["ardrive", "generate-seedphrase"], text=True, stdout=subprocess.PIPE) + seed = seed_proc.stdout.strip().replace('"', '') + + # Generar la wallet + result = subprocess.run( + ["ardrive", "generate-wallet", "-s", seed], + cwd=profile_path, + text=True, + stdout=subprocess.PIPE + ) + wallet_data = result.stdout.strip() + + # Guardar la wallet en un archivo JSON + try: + with open(wallet_path, 'w') as f: + json.dump(json.loads(wallet_data), f, indent=4) + except Exception as e: + print(f"Error al guardar la wallet en {wallet_path}: {e}") + + # Obtener el balance de la wallet + try: + subprocess.run( + ["ardrive", "get-balance", "-w", wallet_path], + capture_output=True, text=True, check=True + ) + return True + except subprocess.CalledProcessError: + return False + def verify_wallet(self): + wallet_path = os.path.join( + ".config_app", "profiles", self.profile_selected, f"wallet_{self.profile_selected}.json" + ) + + try: + result = subprocess.run( + ["ardrive", "get-balance", "-w", wallet_path], + capture_output=True, + text=True, + check=True + ) + print("Resultado del balance:") + print(result.stdout) + return True + + except subprocess.CalledProcessError as e: + print("Error al obtener el balance:") + print(e.stderr) + return False + + def create_website(self): + print(self.hugo_memory) + def create_hugo_structure(): + #creaar estructura hugo + time.sleep(2) + print(f"BasePage Variables:\nprofile_selected: {self.profile_selected}\nproject_selected: {self.project_selected}\ntemplate_selected: {self.template_selected}") + + general_path = os.path.join(".config_app", "profiles", self.profile_selected) + create_hugo = subprocess.run( + ["hugo", "new", "site", self.project_selected], + cwd=general_path, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + if create_hugo.returncode != 0: + print(f"Error: {create_hugo.stderr}") + return False + + print(f"Website '{self.project_selected}' created successfully!") + + project_path = os.path.join(general_path, self.project_selected) + os.makedirs(os.path.join(project_path, "config", "_default"), exist_ok=True) + os.makedirs(os.path.join(project_path, "kontent"), exist_ok=True) + for fname in ["config.yaml", "taxonomies.yaml", "outputs.yaml"]: + open(os.path.join(project_path, "config", "_default", fname), "a").close() + + for unwanted in ["content", "layouts", "static"]: + path = os.path.join(project_path, unwanted) + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + hugo_toml = os.path.join(project_path, "hugo.toml") + if os.path.isfile(hugo_toml): + os.remove(hugo_toml) + + # Copiar la carpeta del tema (ya implementado) + src_theme_path = os.path.join( + ".config_app", "templates", self.template_selected, self.template_selected + ) + dst_theme_dir = os.path.join(project_path, "themes") + dst_theme_path = os.path.join(dst_theme_dir, self.template_selected) + + if os.path.isdir(src_theme_path): + os.makedirs(dst_theme_dir, exist_ok=True) + shutil.copytree(src_theme_path, dst_theme_path, dirs_exist_ok=True) + print(f"Template '{self.template_selected}' copiado a 'themes/'.") + else: + print(f"Error: template no encontrado en {src_theme_path}") + return False + + # Copiar la carpeta 'kontent' del template a profile/ + copy_kontent_path = os.path.join( + ".config_app", "templates", self.template_selected, "kontent" + ) + paste_kontent_path = os.path.join(general_path, self.project_selected, "kontent") + + if os.path.isdir(copy_kontent_path): + shutil.copytree(copy_kontent_path, paste_kontent_path, dirs_exist_ok=True) + print(f"Karpeta 'kontent' copiada a {paste_kontent_path}.") + else: + print(f"Error: carpeta 'kontent' no encontrada en {copy_kontent_path}") + + return True + + + time.sleep(1) + def call_create_markdown(): + template_dir = os.path.join(".config_app","templates",self.template_selected) + for file_path in glob.glob(f"{template_dir}/*.py"): + try: + name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + #Ir al main.py de layout (plantilla) y esperar envio de sesultado, se le envia + #hugo memory + + if hasattr(module, 'create_markdown'): + print(f"Execute create_markdown from: {file_path}") + result = module.create_markdown( + self.profile_selected, + self.hugo_memory, + self.template_selected, + self.project_selected + ) + + if result is True: + print("Markdown creado correctamente. Añadiendo contenido y compilando...") + + base_path = os.path.join(".config_app","profiles",self.profile_selected, self.project_selected) + config_dir = os.path.join(base_path, "config", "_default") + os.makedirs(config_dir, exist_ok=True) + + config_path = os.path.join(config_dir, "config.yaml") + config_text = ( + f'title: "{self.hugo_memory.get("line_website_name", "Generic name")}"\n' + f'contentDir: "kontent"\n' + f'theme: "{self.template_selected}"\n' + f'relativeURLs: true\n' + f'params:\n' + f' author: "{self.profile_selected}"\n' + f' description: "{self.hugo_memory.get("plain_website_description", "No description")}"\n' + f' seoKeys: "{self.hugo_memory.get("plain_website_seo", "No keywords")}"\n' + f'markup:\n' + f' goldmark:\n' + f' renderer:\n' + f' unsafe: true\n' + ) + with open(config_path, "w", encoding="utf-8") as f: + f.write(config_text) + print(f"Config created in {config_path}") + + outputs_path = os.path.join(config_dir, "outputs.yaml") + outputs_text = ( + f'home:\n' + f' - HTML\n' + f' - RSS\n' + f'section:\n' + f' - HTML\n' + f'page:\n' + f' - HTML\n' + f'taxonomy:\n' + f' - HTML\n' + f'term:\n' + f' - HTML\n' + ) + with open(outputs_path, "w", encoding="utf-8") as f: + f.write(outputs_text) + print(f"Config created in {outputs_path}") + + taxonomies_path = os.path.join(config_dir, "taxonomies.yaml") + + # Obtener la lista desde hugo_memory, o usar lista vacía si no existe + taxonomies_list = self.hugo_memory.get("taxonomies", []) + + # Generar el texto YAML dinámicamente + taxonomies_text = "" + for taxonomy in taxonomies_list: + taxonomies_text += f'{taxonomy}: "{taxonomy}"\n' + + # Guardar el archivo YAML + with open(taxonomies_path, "w", encoding="utf-8") as f: + f.write(taxonomies_text) + + print(f"Config created in {taxonomies_path}") + print(f"Ejecutando Hugo en: {base_path}") + print("Comando: hugo") + + build = subprocess.run( + ["hugo"], + cwd=base_path, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + print("Salida estándar:") + print(build.stdout) + print("Salida de error:") + print(build.stderr) + + if build.returncode != 0: + print(f"Error hugo:\n{build.stderr}") + return False + + + # 🔐 Crear y guardar estructura con hashes + print("New Structure!!") + structure = {} + public_path = os.path.join(base_path, "public") + for root, dirs, files in os.walk(public_path, topdown=False): + rel_path = os.path.relpath(root, public_path) + pointer = structure + if rel_path != ".": + for part in rel_path.split(os.sep): + pointer = pointer.setdefault(part, {}) + file_data = {} + for file in sorted(files): + full_path = os.path.join(root, file) + with open(full_path, "rb") as f: + file_hash = hashlib.md5(f.read()).hexdigest() + pointer[file] = file_hash + file_data[file] = file_hash + for d in sorted(dirs): + sub_pointer = structure + for part in os.path.join(rel_path, d).split(os.sep): + sub_pointer = sub_pointer.get(part, {}) + if "__hash__" in sub_pointer: + file_data[d] = {"__hash__": sub_pointer["__hash__"]} + pointer["__hash__"] = hashlib.md5(json.dumps(file_data, sort_keys=True).encode()).hexdigest() + + structure_path = os.path.join(base_path, "structure.json") + with open(structure_path, "w", encoding="utf-8") as f: + json.dump(structure, f, indent=4, sort_keys=True) + print(f"Save structure in {structure_path}") + + return True + + return False + except Exception as e: + print(f"Error{file_path} → {e}") + return False + print("No found create_markdown") + return False + + + # Flujo principal + if create_hugo_structure(): + return call_create_markdown() + return False + + def edit_website(self): + print(self.template_selected) + #obtener self.template_selected + template_dir = os.path.join(".config_app","templates",self.template_selected) + base_path = os.path.join(".config_app", "profiles", self.profile_selected, self.project_selected) + + for file_path in glob.glob(f"{template_dir}/*.py"): + try: + name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, 'create_markdown'): + result = module.create_markdown( + self.profile_selected, + self.hugo_memory, + self.template_selected, + self.project_selected + ) + + if result is True: + build = subprocess.run( + ["hugo"], + cwd=base_path, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + print(build.stdout) + print(build.stderr) + return result + except Exception as e: + print(f"Error {file_path} → {e}") + return False + + return False + + def public_arweave(self): + print("DEBUG2, public first:", self.profile_selected, self.project_selected) + base_path = os.path.join(".config_app","profiles",self.profile_selected) + wallet_path = os.path.join(base_path, f"wallet_{self.profile_selected}.json") + wallet_path2 = os.path.abspath(wallet_path) + project_path = os.path.join(base_path, self.project_selected) + public_path = os.path.join(project_path, "public") + manifest_path = os.path.join(project_path, "manifest.json") + drive_file_path = os.path.join(project_path, f"drive_{self.project_selected}.json") + time.sleep(5) + print("Wait ensambling") + + #si no existe ejecutar este codigo normalmente + create_drive = [ + "ardrive", "create-drive", + "--wallet-file", wallet_path, + "--drive-name", self.project_selected + ] + result = subprocess.run(create_drive, capture_output=True, text=True) + if result.returncode != 0: + print("Error creating drive:", result.stderr) + return False + + project_data = json.loads(result.stdout) + with open(drive_file_path, "w", encoding="utf-8") as f: + json.dump(project_data, f, indent=4) + + drive_id = next( + (d["entityId"] for d in project_data.get("created", []) if d.get("type") == "folder"), + None + ) + if not drive_id: + print("No drive_id found") + return False + + print("Drive ID:", drive_id) + for _ in tqdm(range(5), desc="Stop", ncols=80): + time.sleep(2) + + upload_website = ( + f"for i in *; do ardrive upload-file --local-path \"$i\" " + f"--parent-folder-id {drive_id} -w '{wallet_path2}'; done" + ) + print(upload_website) + + subprocess.run(upload_website, shell=True, cwd=public_path, text=True) + + for _ in tqdm(range(5), desc="Make Manifest", ncols=80): + time.sleep(1) + + upload_manifest = ( + f"ardrive create-manifest -f '{drive_id}' -w '{wallet_path2}' > ../manifest.json" + ) + subprocess.run(upload_manifest, shell=True, cwd=public_path, text=True) + if os.path.isfile(manifest_path): + print("Manifest created successfully") + for _ in tqdm(range(10), desc="Wait, distrubution on arweave system", ncols=80): + time.sleep(1) + return True + else: + print("Manifest file not created") + return False + + def refresh_arweave(self): + print("DEBUG-Refresh-Arweave, public first:", self.profile_selected, self.project_selected) + + print(self.template_selected) + profile_path = os.path.join(".config_app","profiles",self.profile_selected) + def change_manifest(profile_path):#firts step + print(profile_path) + try: + manifest_path = os.path.join(profile_path, self.project_selected, "manifest.json") + with open(manifest_path, 'r', encoding='utf-8') as f: + data = json.load(f) + paths_data = data["manifest"]["paths"] + + new_path = os.path.join(os.path.dirname(manifest_path), "ma.json") + with open(new_path, 'w', encoding='utf-8') as f: + json.dump({"paths": paths_data}, f, separators=(',', ':')) + os.remove(manifest_path) + print("manifest.json ---> ma.json.") + except Exception as e: + print(f"Error processing manifest: {e}") + + def generate_structure(profile_path): + public_path = os.path.join(profile_path) + print(f"Buscando en: {public_path}") + print(self.project_selected) + + if not os.path.exists(public_path): + print("El directorio 'public' no existe.") + return {} + + structure = {} + + for root, dirs, files in os.walk(public_path, topdown=False): + print(f"Explorando: {root}") + rel_path = os.path.relpath(root, public_path) + pointer = structure + if rel_path != ".": + for part in rel_path.split(os.sep): + pointer = pointer.setdefault(part, {}) + + file_data = {} + for file in sorted(files): + print(f" - Archivo encontrado: {file}") + full_path = os.path.join(root, file) + with open(full_path, "rb") as f: + data = f.read() + file_hash = hashlib.md5(data).hexdigest() + pointer[file] = file_hash + file_data[file] = file_hash + + for d in sorted(dirs): + sub_path = os.path.join(rel_path, d) + sub_pointer = structure + for part in sub_path.split(os.sep): + sub_pointer = sub_pointer.get(part, {}) + if "__hash__" in sub_pointer: + file_data[d] = {"__hash__": sub_pointer["__hash__"]} + + folder_hash = hashlib.md5(json.dumps(file_data, sort_keys=True).encode()).hexdigest() + pointer["__hash__"] = folder_hash + + print("Nueva radiografía generada:") + print(json.dumps(structure, indent=2, ensure_ascii=False)) + return structure + + def compare_hash(profile_path): + print("Verify structure") + public_path = os.path.join(profile_path,self.project_selected, "public") + swarp_path = os.path.join(profile_path,self.project_selected, "swarp") + radiografia_path = os.path.join(profile_path,self.project_selected, "structure.json") + + if os.path.exists(radiografia_path): + with open(radiografia_path, "r") as f: + old_structure = json.load(f) + else: + print("Empty,fail old structure") + return + + new_structure = generate_structure(public_path) + + os.makedirs(swarp_path, exist_ok=True) + + for root, dirs, files in os.walk(public_path, topdown=False): + rel_path = os.path.relpath(root, public_path) + old_pointer = old_structure + new_pointer = new_structure + + if rel_path != ".": + for part in rel_path.split(os.sep): + old_pointer = old_pointer.get(part, {}) + new_pointer = new_pointer.get(part, {}) + + for file in files: + if file == "__hash__": + continue + new_file_hash = new_pointer.get(file) + old_file_hash = old_pointer.get(file) + + if new_file_hash and old_file_hash == new_file_hash: + src = os.path.join(root, file) + dest_dir = os.path.join(swarp_path, rel_path) + os.makedirs(dest_dir, exist_ok=True) + shutil.move(src, os.path.join(dest_dir, file)) + print(f"No changes, No upload: {rel_path}/{file}") + + if not os.listdir(root): + os.rmdir(root) + + os.remove(radiografia_path) + def new_radiography(profile_path): + print("Generate new structure.json") + public_path = os.path.join(profile_path, self.project_selected, "public") + radiografia_path = os.path.join(profile_path, self.project_selected, "structure.json") + + structure = generate_structure(public_path) + + with open(radiografia_path, "w", encoding="utf-8") as f: + json.dump(structure, f, indent=4, sort_keys=True) + #bien, listo para lo que sigue + def upload_public(profile_path): + print("Upload new files") + + project_file = os.path.join(profile_path,self.project_selected, f"drive_{self.project_selected}.json") + wallet_path = os.path.join(profile_path, f"wallet_{self.profile_selected}.json") + wallet_path2 = os.path.abspath(wallet_path) + + public_path = os.path.join(profile_path, self.project_selected, "public") + project_path = os.path.join(profile_path, self.project_selected) + + + print('en esta direccion buscarmeos el json drive') + print(project_file) + time.sleep(30) + with open(project_file, "r") as f: + project_data = json.load(f) + + drive_id = next( + (d["entityId"] for d in project_data.get("created", []) if d.get("type") == "folder"), + None + ) + + if not drive_id: + print("No valid drive_id") + return + + for _ in tqdm(range(10), desc="Wait...", ncols=80): + time.sleep(1) + + upload_cmd = ( + f"for i in *; do ardrive upload-file --local-path \"$i\" " + f"--parent-folder-id '{drive_id}' -w '{wallet_path2}'; done > ../ni.json" + ) + + subprocess.run(upload_cmd, shell=True, cwd=public_path, text=True) + print("New files upload ---> ni.json") + + for _ in tqdm(range(10), desc="Formating ni.json", ncols=80): + time.sleep(1) + ni_path = os.path.join(project_path, "ni.json") + + with open(ni_path, 'r', encoding='utf-8') as f: + contenido = f.read() + + patron = r'"created"\s*:\s*(\[[^\]]*\](?:\s*,\s*\[[^\]]*\])*)' + coincidencias = re.findall(patron, contenido, re.DOTALL) + + resultado_paths = {} + + for coincidencia in coincidencias: + try: + lista = json.loads(coincidencia) + for item in lista: + if item.get("type") == "file" and "sourceUri" in item and "dataTxId" in item: + # Limpiar el sourceUri: quitar todo antes de "public/" + uri = item["sourceUri"] + uri = re.sub(r'^.*?public/', '', uri) # Elimina todo antes de "public/" + resultado_paths[uri] = {"id": item["dataTxId"]} # Reemplazar dataTxId por id + except json.JSONDecodeError: + continue + + with open(ni_path, 'w', encoding='utf-8') as out: + json.dump({"paths": resultado_paths}, out, indent=4, ensure_ascii=False) + + print(f"ni.json correcty formatted") + ##voy aca + def combine_folders(profile_path): + public_path = os.path.join(profile_path,self.project_selected, "public") + swarp_path = os.path.join(profile_path,self.project_selected, "swarp") + + if not os.path.exists(swarp_path): + print("No swarp") + return + + for item in os.listdir(swarp_path): + s_item = os.path.join(swarp_path, item) + p_item = os.path.join(public_path, item) + + if os.path.isdir(s_item): + shutil.copytree(s_item, p_item, dirs_exist_ok=True) + else: + shutil.copy2(s_item, p_item) + + shutil.rmtree(swarp_path) + def new_manifest(profile_path): + print("ma.json + ni.json = manifest.json...") + + ma_path = os.path.join(profile_path,self.project_selected, "ma.json") + ni_path = os.path.join(profile_path,self.project_selected, "ni.json") + mani_path = os.path.join(profile_path,self.project_selected, "manifest.json") + + try: + # Leer los archivos 'ma.json' (old) y 'ni.json' (new) + with open(ma_path, 'r', encoding='utf-8') as f: + ma_data = json.load(f) + + with open(ni_path, 'r', encoding='utf-8') as f: + ni_data = json.load(f) + + ma_paths = ma_data.get("paths", {}) + ni_paths = ni_data.get("paths", {}) + + for key in ni_paths: + if key in ma_paths: + del ma_paths[key] + + ma_paths.update(ni_paths) + + new_manifest_data = { + "manifest": "arweave/paths", + "version": "0.1.0", + "index": { + "path": "index.html" + }, + "paths": ma_paths + } + + # Guardar el nuevo manifiesto combinado en 'mani.json' + with open(mani_path, 'w', encoding='utf-8') as f: + json.dump(new_manifest_data, f, indent=4) + + print("Manifest ready for upload.") + + except Exception as e: + print(f"Error 404 {e}") + + def upload_manifest(profile_path): + print("Upload manifest") + + project_file = os.path.join(profile_path, self.project_selected, f"drive_{self.project_selected}.json") + wallet_path = os.path.join(profile_path, f"wallet_{self.profile_selected}.json") + wallet_path2 = os.path.abspath(wallet_path) + project_path = os.path.join(profile_path, self.project_selected) + + with open(project_file, "r") as f: + project_data = json.load(f) + + drive_id = next( + (d["entityId"] for d in project_data.get("created", []) if d.get("type") == "folder"), + None + ) + + if not drive_id: + print("No found drive_id") + return + + upload_cmd = ( + f"ardrive upload-file --content-type 'application/x.arweave-manifest+json' " + f"--local-path manifest.json --parent-folder-id '{drive_id}' -w '{wallet_path2}' > fest.json" + ) + + subprocess.run(upload_cmd, shell=True, cwd=project_path, text=True) + print("Manifest correctly uploaded") + + mani_path = os.path.join(profile_path,self.project_selected, "manifest.json") + fest_path = os.path.join(profile_path,self.project_selected, "fest.json") + final_manifest_path = os.path.join(profile_path,self.project_selected, "manifest.json") + + with open(mani_path, "r", encoding="utf-8") as f: + mani_data = json.load(f) + + with open(fest_path, "r", encoding="utf-8") as f: + fest_data = json.load(f) + + combined_data = { + "manifest": mani_data, + **fest_data # Esto "expande" created, tips, fees directamente + } + + with open(final_manifest_path, "w", encoding="utf-8") as f: + json.dump(combined_data, f, indent=4) + + def delete(profile_path): + project_path = os.path.join(profile_path, self.project_selected) + + archivos = ["ma.json", "ni.json", "fest.json"] + for archivo in archivos: + ruta = os.path.join(project_path, archivo) + if os.path.exists(ruta): + os.remove(ruta) + else: + print("Error") + try: + # Call the change_manifest function + change_manifest(profile_path) + compare_hash(profile_path) + upload_public(profile_path) + combine_folders(profile_path) + new_manifest(profile_path) + new_radiography(profile_path) + upload_manifest(profile_path) + delete(profile_path) + + return True + except Exception as e: + print(f"Error: {e}") + return False