import sys import json import os from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QListWidget, QTextEdit, QComboBox, QMessageBox, QSpacerItem, QSizePolicy, QLabel, QFileDialog, QDialog, QFormLayout, QTableWidget, QTableWidgetItem ) from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtCore import Qt, QUrl, QDateTime from PyQt5.QtSvg import QSvgWidget from PyQt5.QtGui import QDesktopServices class URLManager(QWidget): def __init__(self): super().__init__() self.setWindowTitle('tabRemember - URL Manager') self.setGeometry(100, 100, 400, 600) # Set the application icon 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("URL") self.url_layout.addWidget(self.url_input) # Settings button self.settings_button = QPushButton() self.settings_button.setFixedHeight(self.url_input.sizeHint().height()) self.settings_button.setFixedWidth(self.url_input.sizeHint().height()) self.settings_button.clicked.connect(self.show_settings_dialog) self.url_layout.addWidget(self.settings_button) # Info button self.info_button = QPushButton() self.info_button.setFixedHeight(self.url_input.sizeHint().height()) self.info_button.setFixedWidth(self.url_input.sizeHint().height()) self.info_button.clicked.connect(self.show_info_dialog) self.url_layout.addWidget(self.info_button) self.layout.addLayout(self.url_layout) # Update icons based on the color mode self.update_icon_color() # Description/Thoughts input self.description_input = QTextEdit() self.description_input.setPlaceholderText("Enter Description or Thoughts") self.description_input.setMaximumHeight(100) # Limit height to approximately 5 lines self.layout.addWidget(self.description_input) # Add URL button self.add_button = QPushButton("Save") self.add_button.clicked.connect(self.add_url) self.layout.addWidget(self.add_button) # Add space between the Add URL button and URL list self.layout.addSpacerItem(QSpacerItem(0, 10, QSizePolicy.Minimum, QSizePolicy.Fixed)) # Search input self.search_layout = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search ...") self.search_input.textChanged.connect(self.filter_urls) self.search_layout.addWidget(self.search_input) self.layout.addLayout(self.search_layout) # Ensure the search input is full width self.search_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) # URL list self.url_list = QTableWidget(0, 3) self.url_list.setHorizontalHeaderLabels(['URL', 'Date', 'Actions']) self.url_list.setColumnWidth(0, 200) self.url_list.setColumnWidth(1, 100) self.layout.addWidget(self.url_list) self.setLayout(self.layout) self.urls = [] self.groups = set() # Load settings self.load_settings() # Load URLs self.load_urls() # Search engines 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 update_icon_color(self): # Heuristic to check if the application is in dark mode palette = self.palette() if palette.color(palette.Window).value() < 128: settings_icon_path = 'assets/cogwheel_dark.svg' info_icon_path = 'assets/info_dark.svg' else: settings_icon_path = 'assets/cogwheel_light.svg' info_icon_path = 'assets/info_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) self.urls.append({'url': url, 'description': description, 'date': date_added, 'group': group}) self.save_urls() self.update_url_list() self.url_input.clear() self.description_input.clear() def update_url(self): selected_item = self.url_list.currentItem() if selected_item: row = self.url_list.row(selected_item) 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, 'Delete URL', 'Are you sure you want to delete this URL?', 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().lower() 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 update_url_list(self, urls=None): if urls is None: urls = self.urls self.url_list.setRowCount(0) for url in urls: row_position = self.url_list.rowCount() self.url_list.insertRow(row_position) self.url_list.setItem(row_position, 0, QTableWidgetItem(url['url'])) self.url_list.setItem(row_position, 1, QTableWidgetItem(url['date'])) # Action buttons actions_layout = QHBoxLayout() edit_button = QPushButton("Edit") edit_button.setStyleSheet("padding: 2px;") delete_button = QPushButton("Delete") delete_button.setStyleSheet("padding: 2px;") search_button = QPushButton("Search") search_button.setStyleSheet("padding: 2px;") edit_button.clicked.connect(lambda ch, row=row_position: self.edit_url(row)) delete_button.clicked.connect(lambda ch, row=row_position: self.delete_url(row)) search_button.clicked.connect(lambda ch, row=row_position: self.search_url(row)) actions_layout.addWidget(edit_button) actions_layout.addWidget(search_button) actions_layout.addWidget(delete_button) actions_widget = QWidget() actions_widget.setLayout(actions_layout) self.url_list.setCellWidget(row_position, 2, actions_widget) def edit_url(self, row): url = self.urls[row] self.url_input.setText(url['url']) self.description_input.setPlainText(url['description']) self.urls.pop(row) def search_url(self, row): url = self.urls[row]['url'] search_engine = self.search_engines[self.search_engine] search_url = search_engine + QUrl(url).toString(QUrl.FullyEncoded) QDesktopServices.openUrl(QUrl(search_url)) def load_settings(self): settings_path = 'data/settings.json' if os.path.exists(settings_path): if os.path.getsize(settings_path) > 0: with open(settings_path, 'r') as file: settings = json.load(file) self.data_directory = settings.get('data_directory', 'data/') self.search_engine = settings.get('search_engine', 'Google') else: self.data_directory = 'data/' self.search_engine = 'Google' self.save_settings() else: self.data_directory = 'data/' self.search_engine = 'Google' self.save_settings() def save_settings(self): settings_path = 'data/settings.json' os.makedirs(os.path.dirname(settings_path), exist_ok=True) with open(settings_path, 'w') as file: settings = { 'data_directory': self.data_directory, 'search_engine': self.search_engine } json.dump(settings, file) def load_urls(self): urls_path = os.path.join(self.data_directory, 'urls.json') if os.path.exists(urls_path): with open(urls_path, 'r') as file: self.urls = json.load(file) self.update_url_list() def save_urls(self): urls_path = os.path.join(self.data_directory, 'urls.json') os.makedirs(os.path.dirname(urls_path), exist_ok=True) with open(urls_path, 'w') as file: json.dump(self.urls, file) def show_info_dialog(self): dialog = QDialog(self) dialog.setWindowTitle("About tabRemember") layout = QVBoxLayout() # SVG logo svg_widget = QSvgWidget('assets/logo.svg') svg_widget.setFixedSize(256, 256) layout.addWidget(svg_widget, alignment=Qt.AlignCenter) # Written by label written_by_label = QLabel("Written by Axel Rafn Benediktsson") layout.addWidget(written_by_label, alignment=Qt.AlignCenter) # Link label link_label = QLabel() link_label.setText('https://git.axelrafn.is/axelrafn/tabRemember') link_label.setOpenExternalLinks(True) layout.addWidget(link_label, alignment=Qt.AlignCenter) dialog.setLayout(layout) dialog.exec_() def show_settings_dialog(self): dialog = SettingsDialog(self) dialog.exec_() class SettingsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Settings") self.setFixedWidth(360) # Set the width of the window to 360 pixels self.parent = parent layout = QVBoxLayout() # Data Directory label directory_label = QLabel("Data Directory:") layout.addWidget(directory_label) # File/Directory selection layout directory_layout = QHBoxLayout() self.directory_input = QLineEdit(self.parent.data_directory) self.browse_button = QPushButton("Browse...") self.browse_button.clicked.connect(self.browse_directory) directory_layout.addWidget(self.directory_input) directory_layout.addWidget(self.browse_button) layout.addLayout(directory_layout) # Add 4 pixels of empty space layout.addSpacerItem(QSpacerItem(0, 4, QSizePolicy.Minimum, QSizePolicy.Fixed)) # Search engine selection form_layout = QFormLayout() 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.search_engine) form_layout.addRow("Search Engine:", self.search_engine_combobox) layout.addLayout(form_layout) # Save button self.save_button = QPushButton("Save") 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 save_settings(self): self.parent.data_directory = self.directory_input.text() self.parent.search_engine = self.search_engine_combobox.currentText() self.parent.save_settings() self.accept() if __name__ == '__main__': app = QApplication(sys.argv) app.setWindowIcon(QIcon('assets/logo.png')) # Set the application icon using PNG for compatibility window = URLManager() window.show() sys.exit(app.exec_())