import sys import json import os from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTextEdit, QComboBox, QMessageBox, QSpacerItem, QSizePolicy, QLabel, QFileDialog, QDialog, QFormLayout, QTableWidget, QTableWidgetItem, QHeaderView ) from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt, QDateTime from PyQt5.QtSvg import QSvgWidget class URLManager(QWidget): def __init__(self): super().__init__() self.settings = self.load_settings() self.translations = {} self.load_translations(f'lang/translations_{self.settings["language"].lower()[:2]}.json') self.setWindowTitle(self.translations['title']) self.setGeometry(100, 100, 600, 800) self.setWindowIcon(QIcon('assets/logo.png')) self.layout = QVBoxLayout() # URL input and settings button layout self.url_layout = QHBoxLayout() self.url_input = QLineEdit() self.url_input.setPlaceholderText(self.translations['url_placeholder']) self.url_layout.addWidget(self.url_input) self.settings_button = QPushButton() self.settings_button.setFixedSize(24, 24) self.settings_button.clicked.connect(self.show_settings_dialog) self.url_layout.addWidget(self.settings_button) self.info_button = QPushButton() self.info_button.setFixedSize(24, 24) self.info_button.clicked.connect(self.show_info_dialog) self.url_layout.addWidget(self.info_button) self.layout.addLayout(self.url_layout) self.update_icon_color() self.description_input = QTextEdit() self.description_input.setPlaceholderText(self.translations['description_placeholder']) self.description_input.setMaximumHeight(100) self.layout.addWidget(self.description_input) self.group_layout = QHBoxLayout() self.group_combobox = QComboBox() self.group_combobox.addItem(self.translations['all_categories']) self.group_combobox.addItem(self.translations['default_category']) self.group_combobox.currentTextChanged.connect(self.filter_urls_by_category) self.group_layout.addWidget(self.group_combobox) self.category_input = QLineEdit() self.category_input.setPlaceholderText(self.translations['category_name']) self.category_input.setFixedWidth(3 * 100) # Fixed width for the text input box, assuming "Save" button width is 100 self.group_layout.addWidget(self.category_input) self.save_category_button = QPushButton(self.translations['save_category_button']) self.save_category_button.setFixedWidth(100) # Fixed width for the "Save" button self.save_category_button.clicked.connect(self.save_category) self.group_layout.addWidget(self.save_category_button) self.layout.addLayout(self.group_layout) self.add_button = QPushButton(self.translations['save_url_button']) self.add_button.clicked.connect(self.add_url) self.layout.addWidget(self.add_button) self.layout.addSpacerItem(QSpacerItem(0, 10, QSizePolicy.Minimum, QSizePolicy.Fixed)) self.search_layout = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText(self.translations['search_placeholder']) self.search_input.textChanged.connect(self.filter_urls) self.search_layout.addWidget(self.search_input) self.layout.addLayout(self.search_layout) self.search_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.url_list = QTableWidget() self.url_list.setColumnCount(3) self.url_list.setHorizontalHeaderLabels(['URL', self.translations['column_date'], self.translations['column_actions']]) self.url_list.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.url_list.setColumnWidth(1, 150) self.url_list.setColumnWidth(2, 100) self.layout.addWidget(self.url_list) self.setLayout(self.layout) self.urls = [] self.groups = set() self.load_categories() self.load_urls() self.search_engines = { "Bing": "https://www.bing.com/search?q=", "Brave": "https://search.brave.com/search?q=", "DuckDuckGo": "https://duckduckgo.com/?q=", "Ecosia": "https://www.ecosia.org/search?q=", "Google": "https://www.google.com/search?q=", "Startpage": "https://www.startpage.com/do/search?q=", "Swisscows": "https://swisscows.com/web?query=" } def load_translations(self, file_path): if os.path.exists(file_path): with open(file_path, 'r') as file: data = json.load(file) self.translations = data['translations'] def update_icon_color(self): palette = self.palette() if palette.color(palette.Window).value() < 128: settings_icon_path = 'assets/cogwheel_dark.svg' info_icon_path = 'assets/info_dark.svg' self.action_icons = { "edit": 'assets/edit_dark.svg', "delete": 'assets/delete_dark.svg', "search": 'assets/search_dark.svg' } else: settings_icon_path = 'assets/cogwheel_light.svg' info_icon_path = 'assets/info_light.svg' self.action_icons = { "edit": 'assets/edit_light.svg', "delete": 'assets/delete_light.svg', "search": 'assets/search_light.svg' } self.settings_button.setIcon(QIcon(settings_icon_path)) self.info_button.setIcon(QIcon(info_icon_path)) def add_url(self): url = self.url_input.text() description = self.description_input.toPlainText() group = self.group_combobox.currentText() if url: date_added = QDateTime.currentDateTime().toString(Qt.ISODate) url_data = {'url': url, 'description': description, 'date': date_added, 'group': group} self.urls.append(url_data) self.save_url(url_data) self.update_url_list() self.url_input.clear() self.description_input.clear() def save_url(self, url_data): category = url_data['group'] category_filename = self.get_category_filename(category) category_path = os.path.join(self.settings['data_directory'], category_filename) if os.path.exists(category_path): with open(category_path, 'r') as file: category_urls = json.load(file) else: category_urls = [] category_urls.append(url_data) with open(category_path, 'w') as file: json.dump(category_urls, file) def update_url(self): selected_item = self.url_list.currentItem() if selected_item: row = selected_item.row() new_url = self.url_input.text() new_description = self.description_input.toPlainText() self.urls[row]['url'] = new_url self.urls[row]['description'] = new_description self.save_urls() self.update_url_list() self.url_input.clear() self.description_input.clear() def delete_url(self, row): reply = QMessageBox.question(self, self.translations['delete_confirmation_title'], self.translations['delete_confirmation'], QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: del self.urls[row] self.save_urls() self.update_url_list() def filter_urls(self): search_term = self.search_input.text().strip().lower() if not search_term: self.update_url_list() return filtered_urls = [url for url in self.urls if search_term in url['url'].lower() or search_term in url['description'].lower()] self.update_url_list(filtered_urls) def filter_urls_by_category(self): selected_category = self.group_combobox.currentText() if selected_category == self.translations['all_categories']: self.update_url_list(self.urls) else: filtered_urls = [url for url in self.urls if url['group'] == selected_category] self.update_url_list(filtered_urls) def update_url_list(self, urls=None): self.url_list.setRowCount(0) urls_to_display = urls if urls else self.urls for row, url in enumerate(urls_to_display): self.url_list.insertRow(row) self.url_list.setItem(row, 0, QTableWidgetItem(url['url'])) formatted_date = self.format_date(url['date']) self.url_list.setItem(row, 1, QTableWidgetItem(formatted_date)) actions_layout = QHBoxLayout() edit_button = QPushButton() edit_button.setIcon(QIcon(self.action_icons["edit"])) edit_button.setFixedSize(18, 18) edit_button.setStyleSheet("border: none; padding: 0px;") edit_button.clicked.connect(lambda _, row=row: self.edit_url(row)) actions_layout.addWidget(edit_button) delete_button = QPushButton() delete_button.setIcon(QIcon(self.action_icons["delete"])) delete_button.setFixedSize(18, 18) delete_button.setStyleSheet("border: none; padding: 0px;") delete_button.clicked.connect(lambda _, row=row: self.delete_url(row)) actions_layout.addWidget(delete_button) actions_widget = QWidget() actions_widget.setLayout(actions_layout) self.url_list.setCellWidget(row, 2, actions_widget) vertical_header = self.url_list.verticalHeader() vertical_header.setVisible(False) def format_date(self, date_str): date = QDateTime.fromString(date_str, Qt.ISODate) if self.settings['date_format'] == 'Nerdy': return date.toString("yyyy-MM-ddTHH:mm:ss") elif self.settings['date_format'] == 'Normal': return date.toString("dd/MM/yy @ HH:mm") elif self.settings['date_format'] == 'Murica!': return date.toString("MM/dd/yyyy hh:mm AP") else: return date_str def edit_url(self, row): url = self.urls[row]['url'] description = self.urls[row]['description'] self.url_input.setText(url) self.description_input.setPlainText(description) def load_settings(self): settings_path = 'data/settings.json' if os.path.exists(settings_path): with open(settings_path, 'r') as file: settings = json.load(file) self.data_directory = settings['data_directory'] self.search_engine = settings['search_engine'] self.date_format = settings.get('date_format', 'Nerdy') self.language = settings.get('language', 'English') return settings else: default_settings = { 'data_directory': 'data/', 'search_engine': 'Google', 'date_format': 'Nerdy', 'language': 'English' } self.save_settings(default_settings) return default_settings def save_settings(self, settings=None): settings_path = 'data/settings.json' os.makedirs(os.path.dirname(settings_path), exist_ok=True) if settings is None: settings = { 'data_directory': self.data_directory, 'search_engine': self.search_engine, 'date_format': self.date_format, 'language': self.language } with open(settings_path, 'w') as file: json.dump(settings, file) def load_categories(self): self.groups = set() categories_path = os.path.join(self.settings['data_directory'], 'categories.json') if os.path.exists(categories_path): with open(categories_path, 'r') as file: self.groups = set(json.load(file)) self.update_group_combobox() self.group_combobox.setCurrentText(self.translations['default_category']) def save_categories(self): categories_path = os.path.join(self.settings['data_directory'], 'categories.json') os.makedirs(os.path.dirname(categories_path), exist_ok=True) with open(categories_path, 'w') as file: json.dump(list(self.groups), file) def update_group_combobox(self): self.group_combobox.clear() self.group_combobox.addItem(self.translations['all_categories']) self.group_combobox.addItem(self.translations['default_category']) for group in sorted(self.groups): self.group_combobox.addItem(group) def save_category(self): new_category = self.category_input.text().strip() if new_category and new_category not in self.groups: self.groups.add(new_category) self.save_categories() self.update_group_combobox() self.category_input.clear() def load_urls(self): self.urls = [] for group in self.groups: category_filename = self.get_category_filename(group) category_path = os.path.join(self.settings['data_directory'], category_filename) if os.path.exists(category_path): with open(category_path, 'r') as file: self.urls.extend(json.load(file)) self.update_url_list() def save_urls(self): for group in self.groups: group_urls = [url for url in self.urls if url['group'] == group] category_filename = self.get_category_filename(group) category_path = os.path.join(self.settings['data_directory'], category_filename) with open(category_path, 'w') as file: json.dump(group_urls, file) def get_category_filename(self, category): if ' ' in category: return category.replace(' ', '_').lower() + '.json' else: return category.lower() + '.json' def show_info_dialog(self): dialog = QDialog(self) dialog.setWindowTitle(self.translations['about_title']) layout = QVBoxLayout() svg_widget = QSvgWidget('assets/logo.svg') svg_widget.setFixedSize(256, 256) layout.addWidget(svg_widget, alignment=Qt.AlignCenter) written_by_label = QLabel(self.translations['developed_by']) layout.addWidget(written_by_label, alignment=Qt.AlignCenter) link_label = QLabel() link_label.setText(f'{self.translations["repository_link_name"]}') link_label.setOpenExternalLinks(True) layout.addWidget(link_label, alignment=Qt.AlignCenter) dialog.setLayout(layout) dialog.exec_() def show_settings_dialog(self): dialog = SettingsDialog(self) if dialog.exec_() == QDialog.Accepted: self.load_settings() self.update_url_list() class SettingsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(parent.translations['settings_title']) self.setFixedWidth(360) self.parent = parent layout = QVBoxLayout() directory_label = QLabel(parent.translations['data_directory_label']) layout.addWidget(directory_label) directory_layout = QHBoxLayout() self.directory_input = QLineEdit(self.parent.settings['data_directory']) self.browse_button = QPushButton(self.parent.translations['browse_button']) self.browse_button.clicked.connect(self.browse_directory) directory_layout.addWidget(self.directory_input) directory_layout.addWidget(self.browse_button) layout.addLayout(directory_layout) layout.addSpacerItem(QSpacerItem(0, 4, QSizePolicy.Minimum, QSizePolicy.Fixed)) form_layout = QFormLayout() # Populate Search Engine Combobox self.search_engine_combobox = QComboBox() search_engines = sorted(self.parent.search_engines.keys()) self.search_engine_combobox.addItems(search_engines) self.search_engine_combobox.setCurrentText(self.parent.settings['search_engine']) form_layout.addRow(self.parent.translations['search_engine_label'], self.search_engine_combobox) # Populate Date Format Combobox self.date_format_combobox = QComboBox() self.date_format_combobox.addItems(["Nerdy", "Normal", "Murica!"]) self.date_format_combobox.setCurrentText(self.parent.settings['date_format']) form_layout.addRow(self.parent.translations['date_format_label'], self.date_format_combobox) # Populate Language Combobox self.language_combobox = QComboBox() self.populate_language_combobox() current_language_code = self.parent.settings['language'] self.set_current_language(current_language_code) form_layout.addRow(self.parent.translations['language_label'], self.language_combobox) layout.addLayout(form_layout) self.save_button = QPushButton(self.parent.translations['save_button']) self.save_button.clicked.connect(self.save_settings) layout.addWidget(self.save_button) self.setLayout(layout) def browse_directory(self): directory = QFileDialog.getExistingDirectory(self, "Select Directory") if directory: self.directory_input.setText(directory) def populate_language_combobox(self): lang_dir = 'lang/' language_files = [f for f in os.listdir(lang_dir) if f.startswith('translations_') and f.endswith('.json')] self.language_map = {} for file_name in language_files: file_path = os.path.join(lang_dir, file_name) with open(file_path, 'r') as file: data = json.load(file) language_name = data.get('language', 'Unknown') language_code = file_name[len('translations_'):-len('.json')] display_text = f"{language_name} ({language_code})" self.language_combobox.addItem(display_text) self.language_map[language_code] = display_text def set_current_language(self, language_code): if language_code in self.language_map: display_text = self.language_map[language_code] index = self.language_combobox.findText(display_text) if index != -1: self.language_combobox.setCurrentIndex(index) def save_settings(self): selected_language = self.language_combobox.currentText() language_code = selected_language.split('(')[-1].strip(' )') # Extract language code from "(en)" format old_language = self.parent.settings['language'] self.parent.settings['data_directory'] = self.directory_input.text() self.parent.settings['search_engine'] = self.search_engine_combobox.currentText() self.parent.settings['date_format'] = self.date_format_combobox.currentText() self.parent.settings['language'] = language_code self.parent.save_settings(self.parent.settings) if old_language != language_code: QMessageBox.information(self, self.parent.translations['restart_required_title'], self.parent.translations['restart_required_message']) self.accept() if __name__ == '__main__': app = QApplication(sys.argv) window = URLManager() window.show() sys.exit(app.exec_())