Претрага Ибарских Новости – Софтвер за архивску претрагу

🎯 О програму

Претрага Ибарских Новости је напредни софтверски пакет специјално дизајниран за ефикасну претрагу и преглед дигиталне архиве Ибарских Новости. Овај програм представља моћно решење за све који истражују или користе историјски садржај овог значајног листа.

✨ Кључне функционалности

🔍 Паметна претрага

  • Двослојна претрага – претрага HTML индекса и PDF садржаја
  • Кључне речи – прецизна локација жељених појмова у архиви
  • Текстуална анализа – напредно поклапање шаблона у документима

📄 PDF менаџмент

  • Аутоматско преузимање – директно преузимање PDF издања
  • Кеширање – локално складиштење за бржи приступ
  • Напредни прегледач – интегрисани PDF прегледач са увећањем

🖼️ Селекција садржаја

  • Селекција слика – прецизно издвајање иловације и фотографија
  • Custom курсор – маркирање центром крстића за пиксел-савршену прецизност
  • Екстракција области – чување исечака слика у различним форматима

📊 Генерисање извештаја

  • HTML извештаји – аутоматско генерисање резултата претраге
  • Структуирани приказ – организована листа пронађених докумената
  • Директни линкови – брз приступ оригиналним PDF фајловима

🛠️ Техничке карактеристике

Архитектура

  • Модерни GUI – PyQt6 базиран кориснички интерфејс
  • Мулти-тхреадинг – независно извршавање дуготрајних операција
  • Модуларни дизајн – лако одржавање и проширивост

Перформансе

  • Оптимизована претрага – брза обрада великих архива
  • Ефикасно меморијско управљање – кеширање и брисање привремених фајлова
  • Progress tracking – реално праћење напретка операција

Компатибилност

  • Крос-платформ – ради на Windows и Linux системима
  • Unicode подршка – пуна подршка за ћирилични текст
  • Стандардни формати – PDF, PNG, JPEG, HTML

🎨 Кориснички интерфејс

Интуитивни дизајн

  • Splitter панели – прилагодљиви распоред елемената
  • Контекстни менији – паметне опције засељене на радни контекст
  • Visual feedback – јасне индикације тренутног стања

Продуктивност

  • Пречице – брз приступ честим операцијама
  • Zoom контроле – динамично увећање/умањење докумената
  • Fit-to-width – аутоматско подешавање приказа

🔒 Поузданост

Стабилност

  • Error handling – елегантно руковање грешкама
  • Resource management – аутоматско чишћење ресурса
  • Session management – заштита података током сесије

Безбедност

  • Локална обрада – сви подаци се обрађују локално
  • Нема серверске комуникације – осим преузимања PDF фајлова
  • Приватност – никакви подаци не напуштају рачунар корисника

🌟 Предности

За истраживаче

  • Временска уштеда – смањује рутинске задатке са сатима на минуте
  • Прецизност – тачна локација релевантног садржаја
  • Комплетност – исцрпна анализа целокупне архиве

За установе

  • Дигитализација – подршка дигиталним архивама
  • Приступачност – олакшан приступ историјском наслеђу
  • Одрживост – очување и ревитализација културне баштине

📦 Технолошки стек

  • Python 3 – основни програмски језик
  • PyQt6 – напредни GUI фрејмворк
  • Qt WebEngine – рендеровање PDF докумената
  • Standard Library – стабилне и проверене компоненте

🚀 Инсталација и покретање

Програм је спреман за употребу без сложених инсталационих процедура. Једноставно покретање извршног фајла омогућава непосредно коришћење свих функционалности.


Претрага Ибарских Новости представља врхунац софтверског инжењерства за архивску претрагу, комбинујући снагу модерних технологија са једноставноћом коришћења. Овај алат не само да убрзава истраживачки рад, већ и отвара нове могућности за проучавање и чување културног наслеђа.

Развијено са пажњом за детаље и посвећеношћу изврсности.

Programski kod za ibarske.py

#ibarske.py
#instalacija potrebnih zavisnosti: pip install PyQt6 PyQt6-WebEngine


import os
import sys
import webbrowser
import tempfile
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                            QHBoxLayout, QLineEdit, QListWidget, QLabel,
                            QPushButton, QProgressBar, QSplitter, QMessageBox,
                            QListWidgetItem, QGridLayout, QMenu, QFileDialog, QDialog)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QBuffer, QIODevice, QTimer, QPoint, QRect
from PyQt6.QtGui import QAction, QGuiApplication, QCursor, QPixmap, QPainter, QPen, QColor
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt6.QtCore import QUrl
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage

class AboutDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("О програму")
        self.setModal(True)
        self.resize(500, 400)

        layout = QVBoxLayout(self)

        # Naslov
        title_label = QLabel("Претрага Ибарских Новости")
        title_label.setStyleSheet("font-size: 18pt; font-weight: bold; color: #2c3e50; margin: 10px;")
        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(title_label)

        # Logo ili ikonica
        logo_label = QLabel("📰 Ибарске Новости 📰")
        logo_label.setStyleSheet("font-size: 24pt; margin: 10px;")
        logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(logo_label)

        # Opis programa
        description_label = QLabel(
            "Овај програм служи за претрагу и преглед архиве Ибарских Новости.\n\n"
            "Омогућава:\n"
            "• Претрагу HTML фајлова по кључним речима\n"
            "• Преузимање и приказ PDF издања\n"
            "• Претрагу текста унутар PDF докумената\n"
            "• Селекцију и чување делова слике из PDF-ова\n"
            "• Генерисање HTML извештаја о резултатима претраге"
        )
        description_label.setStyleSheet("font-size: 10pt; margin: 10px; line-height: 1.4;")
        description_label.setWordWrap(True)
        layout.addWidget(description_label)

        # Informacije o autoru i licenci
        info_text = (
            "⚙️ <b>Техничке карактеристике:</b><br>"
            "• Направљено са PyQt6 и Python-ом<br>"
            "• Користи Qt WebEngine за приказ PDF-ова<br>"
            "• Подржава српски ћирилични текст<br><br>"

            "👨‍💻 <b>Аутор:</b> Александар Маричић<br>"
            "📅 <b>Година развоја:</b> 2025<br>"
            "📜 <b>Лиценца:</b> MIT License<br><br>"

            "🔗 <b>Изворни код:</b> Отвореног типа<br>"
            "🐛 <b>Пријаве грешака:</b> kroz GitHub issues"
        )

        info_label = QLabel(info_text)
        info_label.setStyleSheet("font-size: 9pt; margin: 10px; line-height: 1.4; background-color: #f8f9fa; padding: 10px; border-radius: 5px;")
        info_label.setWordWrap(True)
        layout.addWidget(info_label)

        # MIT License info
        mit_text = (
            "MIT License омогућава слободно коришћење, модификацију и дистрибуцију програма.\n\n"
            "Програм се доставља 'as is' без гаранције од стране аутора."
        )
        mit_label = QLabel(mit_text)
        mit_label.setStyleSheet("font-size: 8pt; margin: 10px; color: #666; font-style: italic; line-height: 1.3;")
        mit_label.setWordWrap(True)
        layout.addWidget(mit_label)

        # Dugme za zatvaranje
        close_button = QPushButton("Затвори")
        close_button.clicked.connect(self.accept)
        close_button.setStyleSheet("""
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
        """)
        layout.addWidget(close_button, alignment=Qt.AlignmentFlag.AlignCenter)

class ImageCropDialog(QDialog):
    def __init__(self, pixmap, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Маркирање и исецање слике")
        self.setModal(True)
        self.resize(1000, 700)

        self.original_pixmap = pixmap
        self.display_pixmap = pixmap.scaled(self.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)

        self.selection_start = None
        self.selection_end = None
        self.selection_rect = QRect()
        self.is_selecting = False

        # Postavi custom kursor u obliku krsta sa tačkom u centru
        self.create_cross_cursor()

        layout = QVBoxLayout(self)

        # Toolbar
        toolbar_layout = QHBoxLayout()
        self.save_button = QPushButton("Сачувај слику...")
        self.save_button.clicked.connect(self.save_image)
        self.cancel_button = QPushButton("Откажи")
        self.cancel_button.clicked.connect(self.reject)

        toolbar_layout.addWidget(self.save_button)
        toolbar_layout.addWidget(self.cancel_button)
        toolbar_layout.addStretch()

        layout.addLayout(toolbar_layout)

        # Image display area
        self.image_label = QLabel()
        self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.image_label.setStyleSheet("border: 1px solid gray; background-color: white;")
        self.image_label.setMinimumSize(600, 400)
        self.image_label.setCursor(self.cross_cursor)

        # Set the pixmap
        self.update_display()

        layout.addWidget(self.image_label)

        # Status label
        self.status_label = QLabel("Изаберите област на слици левим кликом и повлачењем (маркирање се врши центром крстића)")
        layout.addWidget(self.status_label)

    def create_cross_cursor(self):
        """Kreira custom kursor u obliku krsta sa centrom"""
        pixmap = QPixmap(32, 32)
        pixmap.fill(Qt.GlobalColor.transparent)

        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # Crveni krug u centru
        painter.setPen(QPen(QColor(255, 0, 0), 2))
        painter.setBrush(QColor(255, 0, 0))
        painter.drawEllipse(14, 14, 4, 4)

        # Horizontalna linija
        painter.setPen(QPen(QColor(255, 0, 0), 1))
        painter.drawLine(0, 16, 32, 16)

        # Vertikalna linija
        painter.drawLine(16, 0, 16, 32)

        painter.end()

        # CENTAR KURSORA JE NA (27, 16) - eksperimentalno utvrđeno
        self.cross_cursor = QCursor(pixmap, 27, 16)

    def update_display(self):
        """Update the display with current pixmap and selection"""
        display_pixmap = self.display_pixmap.copy()

        if not self.selection_rect.isNull():
            painter = QPainter(display_pixmap)
            painter.setPen(QPen(QColor(255, 0, 0), 2, Qt.PenStyle.SolidLine))
            painter.drawRect(self.selection_rect)
            painter.fillRect(self.selection_rect, QColor(255, 0, 0, 30))

            # Dodaj centar selekcije
            if self.selection_start:
                painter.setPen(QPen(QColor(0, 255, 0), 2))
                painter.drawEllipse(self.selection_start, 3, 3)

            if self.selection_end:
                painter.setPen(QPen(QColor(0, 0, 255), 2))
                painter.drawEllipse(self.selection_end, 3, 3)

            painter.end()

        self.image_label.setPixmap(display_pixmap)

    def get_selected_image(self):
        """Get the cropped image based on selection"""
        if self.selection_rect.isNull() or not self.selection_rect.isValid():
            return None

        # Calculate scale factor between display and original
        scale_x = self.original_pixmap.width() / self.display_pixmap.width()
        scale_y = self.original_pixmap.height() / self.display_pixmap.height()

        # Scale selection rectangle to original image coordinates
        original_rect = QRect(
            int(self.selection_rect.x() * scale_x),
            int(self.selection_rect.y() * scale_y),
            int(self.selection_rect.width() * scale_x),
            int(self.selection_rect.height() * scale_y)
        )

        return self.original_pixmap.copy(original_rect)

    def save_image(self):
        """Save the selected image area"""
        selected_image = self.get_selected_image()
        if selected_image is None:
            QMessageBox.warning(self, "Упозорење", "Прво изаберите област на слици.")
            return

        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "Сачувај слику",
            "selected_image.png",
            "Images (*.png *.jpg *.jpeg *.bmp);;All Files (*)"
        )

        if file_path:
            if selected_image.save(file_path):
                QMessageBox.information(self, "Успех", f"Слика је успешно сачувана:\n{file_path}")
                self.accept()
            else:
                QMessageBox.warning(self, "Грешка", "Грешка при чувању слике.")

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            # Convert click position to image label coordinates
            pos = self.image_label.mapFromParent(event.pos())
            if self.image_label.rect().contains(pos):
                # Koristimo centar kursora kao početну tačku
                self.selection_start = pos
                self.selection_end = pos
                # Inicijalno pravougaonik je tačka
                self.selection_rect = QRect(self.selection_start, self.selection_end)
                self.is_selecting = True
                self.update_display()

    def mouseMoveEvent(self, event):
        if self.is_selecting and self.selection_start:
            # Convert move position to image label coordinates
            pos = self.image_label.mapFromParent(event.pos())

            # Constrain to image label boundaries
            label_rect = self.image_label.rect()
            pos.setX(max(label_rect.left(), min(pos.x(), label_rect.right())))
            pos.setY(max(label_rect.top(), min(pos.y(), label_rect.bottom())))

            self.selection_end = pos

            # Kreiraj pravougaonik od centra početне tačke do centra trenutne pozicije
            self.selection_rect = QRect(self.selection_start, self.selection_end).normalized()

            self.update_display()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton and self.is_selecting:
            # Convert release position to image label coordinates
            pos = self.image_label.mapFromParent(event.pos())

            # Constrain to image label boundaries
            label_rect = self.image_label.rect()
            pos.setX(max(label_rect.left(), min(pos.x(), label_rect.right())))
            pos.setY(max(label_rect.top(), min(pos.y(), label_rect.bottom())))

            self.selection_end = pos

            # Finalizuj selekciju - pravougaonik od centra na početku do centra na kraju
            self.selection_rect = QRect(self.selection_start, self.selection_end).normalized()
            self.is_selecting = False

            # Update status
            if self.selection_rect.isValid() and self.selection_rect.width() > 2 and self.selection_rect.height() > 2:
                self.status_label.setText(f"Изабрана област: {self.selection_rect.width()} x {self.selection_rect.height()} пиксела")
            else:
                self.status_label.setText("Изаберите област на слици левим кликом и повлачењем (маркирање се врши центром крстића)")

class CustomWebEnginePage(QWebEnginePage):
    def __init__(self, parent=None):
        super().__init__(parent)

    def createStandardContextMenu(self):
        # Ovo sprečava pojavljivanje standardnog context menija
        return None

class CustomWebEngineView(QWebEngineView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.text_selection_mode = False
        self.image_selection_mode = False
        self.selection_start = None
        self.selection_end = None
        self.selection_rect = QRect()
        self.is_selecting = False
        self.has_image_selection = False

        # Instaliraj custom page
        self.custom_page = CustomWebEnginePage(self)
        self.setPage(self.custom_page)

    def contextMenuEvent(self, event):
        menu = QMenu(self)

        # Uklonjen "Маркирај текст" - ostaje samo "Маркирај слику"
        select_image_action = QAction('Маркирај слику', self)
        select_image_action.triggered.connect(self.enable_image_selection)
        menu.addAction(select_image_action)

        menu.exec(event.globalPos())

    def enable_image_selection(self):
        """Uključuje mod za selekciju slike - otvara screenshot u novom prozoru"""
        self.clear_selection()

        # Napravi screenshot trenutnog PDF prikaza
        screenshot = self.grab()

        # Otvori dijalog za selekciju i kropovanje
        crop_dialog = ImageCropDialog(screenshot, self)
        crop_dialog.exec()

    def clear_selection(self):
        """Briše selekciju i vraća normalan mod"""
        self.text_selection_mode = False
        self.image_selection_mode = False
        self.has_image_selection = False
        self.is_selecting = False
        self.selection_start = None
        self.selection_end = None
        self.selection_rect = QRect()
        QGuiApplication.restoreOverrideCursor()
        self.setMouseTracking(False)
        self.update()

class HTMLSearchThread(QThread):
    update_progress = pyqtSignal(int, int, str)
    search_finished = pyqtSignal(list)

    def __init__(self, directory, search_phrase):
        super().__init__()
        self.directory = directory
        self.search_phrase = search_phrase
        self.matching_files = []

    def run(self):
        try:
            # Pronađi sve .html fajlove
            html_files = [f for f in os.listdir(self.directory) if f.endswith('.html')]
            total_files = len(html_files)

            for index, filename in enumerate(html_files):
                file_path = os.path.join(self.directory, filename)
                self.update_progress.emit(index + 1, total_files, filename)

                try:
                    with open(file_path, 'r', encoding='utf-8') as file:
                        content = file.read()
                        if self.search_phrase.lower() in content.lower():
                            self.matching_files.append(filename)
                except Exception as e:
                    print(f"Greška pri čitanju fajla {file_path}: {e}")

            self.search_finished.emit(self.matching_files)

        except Exception as e:
            print(f"Greška u niti pretrage: {e}")
            self.search_finished.emit([])

class PDFDownloader:
    def __init__(self):
        self.network_manager = QNetworkAccessManager()

    def download_pdf(self, pdf_url, progress_callback, finished_callback):
        request = QNetworkRequest(QUrl(pdf_url))
        reply = self.network_manager.get(request)

        # Poveži signal za progress
        if progress_callback:
            reply.downloadProgress.connect(progress_callback)

        # Poveži signal za završetak preuzimanja
        reply.finished.connect(lambda: self._download_finished(reply, pdf_url, finished_callback))

    def _download_finished(self, reply, pdf_url, finished_callback):
        if reply.error() == QNetworkReply.NetworkError.NoError:
            pdf_data = reply.readAll()
            finished_callback(pdf_data, pdf_url, True)
        else:
            print(f"Greška pri preuzimanju PDF-a: {reply.errorString()}")
            finished_callback(None, pdf_url, False)
        reply.deleteLater()

class PDFViewerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Претрага Ибарских Новости")

        # Podesi prozor preko celog ekrana
        screen = QApplication.primaryScreen()
        screen_geometry = screen.availableGeometry()
        self.setGeometry(screen_geometry)

        # Centralni widget
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # Glavni layout
        main_layout = QHBoxLayout(central_widget)
        main_layout.setContentsMargins(5, 5, 5, 5)

        # Splitter za podelu ekrana
        splitter = QSplitter(Qt.Orientation.Horizontal)

        # Levi panel (pretraga i lista) - sada uži
        left_panel = QWidget()
        left_panel.setMaximumWidth(350)
        left_layout = QVBoxLayout(left_panel)
        left_layout.setContentsMargins(5, 5, 5, 5)

        # Horizontalni layout za HTML pretragu
        html_search_layout = QHBoxLayout()
        html_search_layout.setContentsMargins(0, 0, 0, 0)

        # HTML pretraga - uža polja
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("Кључна реч за PDF-ове...")
        self.search_input.returnPressed.connect(self.start_search)

        self.search_button = QPushButton("ENTER")
        self.search_button.clicked.connect(self.start_search)

        # Podesi širine - polje za unos je sada uža
        self.search_button.setFixedWidth(80)
        self.search_input.setFixedWidth(200)

        html_search_layout.addWidget(self.search_input)
        html_search_layout.addWidget(self.search_button)

        left_layout.addLayout(html_search_layout)

        # Lista pronađenih PDF-ova
        left_layout.addWidget(QLabel("Пронађени PDF-ови:"))
        self.pdf_list = QListWidget()
        self.pdf_list.itemClicked.connect(self.on_pdf_selected)
        left_layout.addWidget(self.pdf_list)

        # Progress bar za pretragu HTML fajlova
        self.search_progress_bar = QProgressBar()
        self.search_progress_bar.setVisible(False)
        left_layout.addWidget(QLabel("Напредак претраге:"))
        left_layout.addWidget(self.search_progress_bar)

        # Progress bar za preuzimanje PDF-ova
        self.download_progress_bar = QProgressBar()
        self.download_progress_bar.setVisible(False)
        left_layout.addWidget(QLabel("Напредак преузимања:"))
        left_layout.addWidget(self.download_progress_bar)

        # Label za status
        self.status_label = QLabel("Статус: Чека се претрага...")
        left_layout.addWidget(self.status_label)

        # Label za trenutni fajl
        self.current_file_label = QLabel("Тренутни фајл: ")
        left_layout.addWidget(self.current_file_label)

        # Label za preuzimanje
        self.download_label = QLabel("")
        left_layout.addWidget(self.download_label)

        # Desni panel (Web pregledač za PDF) - širi
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        right_layout.setContentsMargins(5, 5, 5, 5)
        right_layout.setSpacing(5)

        # Horizontalni layout za PDF pretragu - polje i dugme odmah jedno pored drugog
        pdf_search_layout = QHBoxLayout()
        pdf_search_layout.setContentsMargins(0, 0, 0, 0)
        pdf_search_layout.setSpacing(5)

        # PDF pretraga
        self.pdf_search_input = QLineEdit()
        self.pdf_search_input.setPlaceholderText("Претрага у PDF-у...")
        self.pdf_search_input.returnPressed.connect(self.search_in_pdf)

        self.pdf_search_button = QPushButton("Претражи у PDF-у")
        self.pdf_search_button.clicked.connect(self.search_in_pdf)

        self.pdf_search_button.setFixedWidth(120)

        pdf_search_layout.addWidget(self.pdf_search_input)
        pdf_search_layout.addWidget(self.pdf_search_button)

        right_layout.addLayout(pdf_search_layout)

        # Koristimo custom WebEngineView
        self.web_view = CustomWebEngineView()

        # Omogući PDF prikaz u WebEngine
        self.web_view.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
        self.web_view.settings().setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, True)

        # Podesi za bolji prikaz PDF-a
        self.web_view.settings().setAttribute(QWebEngineSettings.WebAttribute.ScrollAnimatorEnabled, True)

        right_layout.addWidget(self.web_view)

        # Dodaj panele u splitter sa većим desnim panelom
        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        splitter.setSizes([300, screen_geometry.width() - 350])

        main_layout.addWidget(splitter)

        # Inicijalizacija
        self.current_pdf_url = None
        self.current_pdf_filename = None
        self.current_search_phrase = ""
        self.search_thread = None
        self.pdf_downloader = PDFDownloader()
        self.downloaded_pdfs = {}
        self.temp_files = {}
        self.current_search_index = 0

        # Timer za automatsku pretragu nakon učitavanja PDF-a
        self.auto_search_timer = QTimer()
        self.auto_search_timer.setSingleShot(True)
        self.auto_search_timer.timeout.connect(self.auto_search_in_pdf)

        # Kreiraj meni
        self.create_menu()

        # Proveri da li postoji html direktorijum
        if not os.path.exists('./html/'):
            os.makedirs('./html/')
            self.status_label.setText("Статус: Креиран 'html' директоријум. Додајте HTML фајлове.")

    def create_menu(self):
        menubar = self.menuBar()

        file_menu = menubar.addMenu('Фајл')

        exit_action = QAction('Изађи', self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)

        # Dodatne opcije
        view_menu = menubar.addMenu('Преглед')

        generate_html_action = QAction('Генериши HTML резултате', self)
        generate_html_action.triggered.connect(self.generate_html_results)
        view_menu.addAction(generate_html_action)

        # Zoom akcije
        zoom_in_action = QAction('Увећај (+)', self)
        zoom_in_action.setShortcut('Ctrl++')
        zoom_in_action.triggered.connect(self.zoom_in)
        view_menu.addAction(zoom_in_action)

        zoom_out_action = QAction('Умањи (-)', self)
        zoom_out_action.setShortcut('Ctrl+-')
        zoom_out_action.triggered.connect(self.zoom_out)
        view_menu.addAction(zoom_out_action)

        fit_to_page_action = QAction('Fit to Width', self)
        fit_to_page_action.setShortcut('Ctrl+0')
        fit_to_page_action.triggered.connect(self.fit_to_width)
        view_menu.addAction(fit_to_page_action)

        # DODAJEMO "О ПРОГРАМУ" MENI STAVKU
        help_menu = menubar.addMenu('Помоћ')

        about_action = QAction('О програму', self)
        about_action.triggered.connect(self.show_about_dialog)
        help_menu.addAction(about_action)

    def show_about_dialog(self):
        """Prikazuje dijalog sa informacijama o programu"""
        about_dialog = AboutDialog(self)
        about_dialog.exec()

    def zoom_in(self):
        """Uvećava PDF prikaz"""
        current_zoom = self.web_view.zoomFactor()
        self.web_view.setZoomFactor(current_zoom * 1.2)

    def zoom_out(self):
        """Umanjuje PDF prikaz"""
        current_zoom = self.web_view.zoomFactor()
        self.web_view.setZoomFactor(current_zoom / 1.2)

    def fit_to_width(self):
        """Podesi PDF da se uklopi u širinu prozora"""
        self.web_view.setZoomFactor(1.0)
        self.web_view.page().runJavaScript("""
            var viewer = document.querySelector('embed') || document.querySelector('iframe');
            if (viewer) {
                viewer.style.width = '100%';
                viewer.style.height = '100%';
            }
        """)

    def start_search(self):
        search_phrase = self.search_input.text().strip()
        if not search_phrase:
            QMessageBox.warning(self, "Упозорење", "Молимо унесите кључну реч за претрагу.")
            return

        self.current_search_phrase = search_phrase
        self.current_search_index = 0

        # Očisti listu
        self.pdf_list.clear()

        # Prikaži progress bar za pretragu
        self.search_progress_bar.setVisible(True)
        self.search_progress_bar.setValue(0)
        self.download_progress_bar.setVisible(False)
        self.status_label.setText("Статус: Претрага у току...")

        # Pokreni pretragu u posebnoj niti
        self.search_thread = HTMLSearchThread('./html/', search_phrase)
        self.search_thread.update_progress.connect(self.update_progress)
        self.search_thread.search_finished.connect(self.on_search_finished)
        self.search_thread.start()

    def update_progress(self, current, total, filename):
        self.search_progress_bar.setMaximum(total)
        self.search_progress_bar.setValue(current)
        progress_percent = int((current / total) * 100) if total > 0 else 0
        self.current_file_label.setText(f"Тренутно претражујем: {filename}")
        self.status_label.setText(f"Статус: Претрага у току... ({current}/{total} - {progress_percent}%)")

    def on_search_finished(self, matching_files):
        self.search_progress_bar.setVisible(False)
        self.status_label.setText("Статус: Претрага завршена.")
        self.current_file_label.setText("Тренутни фајл: ")

        if matching_files:
            for html_file in matching_files:
                pdf_filename = html_file.replace('.html', '.pdf')
                item = QListWidgetItem(pdf_filename)
                item.setData(Qt.ItemDataRole.UserRole, html_file)
                self.pdf_list.addItem(item)

            self.status_label.setText(f"Статус: Пронађено {len(matching_files)} PDF-ова.")
        else:
            self.status_label.setText("Статус: Није пронађен ниједан PDF.")
            QMessageBox.information(self, "Резултат претраге", "Није пронађен ниједан PDF са задатом кључном речи.")

    def on_pdf_selected(self, item):
        pdf_filename = item.text()
        html_filename = item.data(Qt.ItemDataRole.UserRole)

        # Postavi ključну reč u polje za PDF pretragu
        if self.current_search_phrase:
            self.pdf_search_input.setText(self.current_search_phrase)

        # Resetuj indeks pretrage za novi PDF
        self.current_search_index = 0

        # Formiraj URL za PDF
        pdf_url = f"http://kraljevcani.rs/{pdf_filename}"
        self.current_pdf_url = pdf_url
        self.current_pdf_filename = pdf_filename

        # Proveri da li je PDF već preuzet
        if pdf_url in self.downloaded_pdfs:
            self._display_pdf(self.downloaded_pdfs[pdf_url], pdf_url)
            self.status_label.setText(f"Статус: Приказан PDF: {pdf_filename} (из кеша)")
            self.download_progress_bar.setVisible(False)
            self.download_label.setText("")
        else:
            # Preuzmi PDF sa servera
            self.status_label.setText(f"Статус: Преузимам PDF: {pdf_filename}...")
            self.download_progress_bar.setVisible(True)
            self.download_progress_bar.setValue(0)
            self.download_label.setText("0%")

            self.pdf_downloader.download_pdf(
                pdf_url,
                self._on_download_progress,
                self._on_pdf_downloaded
            )

    def _on_download_progress(self, bytes_received, bytes_total):
        if bytes_total > 0:
            progress = int((bytes_received / bytes_total) * 100)
            self.download_progress_bar.setValue(progress)
            self.download_label.setText(f"{progress}% ({self._format_bytes(bytes_received)} / {self._format_bytes(bytes_total)})")

            # Ažuriraj status ako je potrebno
            if progress < 100:
                self.status_label.setText(f"Статус: Преузимам {self.current_pdf_filename}... {progress}%")

    def _format_bytes(self, bytes):
        """Formatira bajte u čitljivu formu"""
        if bytes < 1024:
            return f"{bytes} B"
        elif bytes < 1024 * 1024:
            return f"{bytes / 1024:.1f} KB"
        else:
            return f"{bytes / (1024 * 1024):.1f} MB"

    def _on_pdf_downloaded(self, pdf_data, pdf_url, success):
        if success and pdf_data:
            self.downloaded_pdfs[pdf_url] = pdf_data
            self._display_pdf(pdf_data, pdf_url)
            pdf_filename = pdf_url.split('/')[-1]
            self.status_label.setText(f"Статус: Учитан PDF: {pdf_filename}")
            self.download_progress_bar.setVisible(False)
            self.download_label.setText("")

            # Pokreni automatsku pretragu nakon 1.5 sekunde
            if self.current_search_phrase:
                self.auto_search_timer.start(1500)
        else:
            QMessageBox.warning(self, "Грешка", f"Не могу да преузмем PDF: {pdf_url}")
            self.status_label.setText(f"Статус: Грешка при преузимању PDF-а")
            self.download_progress_bar.setVisible(False)
            self.download_label.setText("")

    def _display_pdf(self, pdf_data, pdf_url):
        """Prikazuje PDF koristeći WebEngine"""
        if pdf_data:
            try:
                # Sačuvaj PDF u privremeni fajl
                with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
                    temp_file.write(pdf_data.data())
                    temp_path = temp_file.name

                # Sačuvaj putanju privremenog fajla za kasnije brisanje
                self.temp_files[pdf_url] = temp_path

                # Učitaj PDF direktno iz fajla
                file_url = QUrl.fromLocalFile(temp_path)
                self.web_view.load(file_url)

                # Podesi fit to width nakon učitavanja
                self.web_view.loadFinished.connect(lambda: self.fit_to_width())

                print(f"PDF uspešno učitán u WebEngine: {temp_path}")

            except Exception as e:
                print(f"Greška pri prikazu PDF-a: {e}")
                QMessageBox.warning(self, "Грешка", f"Грешка при приказу PDF-а: {str(e)}")

    def auto_search_in_pdf(self):
        """Automatska pretraga nakon učitavanja PDF-a"""
        if self.current_search_phrase and self.pdf_search_input.text().strip():
            self.current_search_index = 0
            self.search_in_pdf()

    def search_in_pdf(self):
        search_phrase = self.pdf_search_input.text().strip()
        if not search_phrase:
            QMessageBox.warning(self, "Упозорење", "Молимо унесите реч за претрагу у PDF-у.")
            return

        # WebEngine ima ugrađenu pretragu sa opcijama
        find_flags = QWebEnginePage.FindFlag(0)
        if self.current_search_index > 0:
            find_flags = QWebEnginePage.FindFlag.FindWrapsAroundDocument

        # Pokreni pretragu
        self.web_view.findText(search_phrase, find_flags, self._on_find_text_finished)

        self.status_label.setText(f"Статус: Претрага за '{search_phrase}'...")

    def _on_find_text_finished(self, result):
        """Poziva se kada se završi pretraga"""
        try:
            if hasattr(result, 'numberOfMatches'):
                number_of_matches = result.numberOfMatches
            elif hasattr(result, 'matchCount'):
                number_of_matches = result.matchCount
            else:
                number_of_matches = 0

            if number_of_matches == 0:
                self.status_label.setText("Статус: Није пронађена реч у PDF-у.")
                self.current_search_index = 0
            else:
                self.current_search_index += 1
                if self.current_search_index > number_of_matches:
                    self.current_search_index = 1

                self.status_label.setText(f"Статус: Пронађено {number_of_matches} поклапања. Тренутно: {self.current_search_index}")
        except Exception as e:
            print(f"Greška u _on_find_text_finished: {e}")
            self.status_label.setText("Статус: Грешка при претрази PDF-а")
            self.current_search_index = 0

    def generate_html_results(self):
        """Generiše HTML fajl sa rezultatima u istom folderu gde je program.py"""
        search_phrase = self.search_input.text().strip()
        if not search_phrase:
            QMessageBox.warning(self, "Упозорење", "Молимо прво извршите претрагу.")
            return

        matching_files = []
        for i in range(self.pdf_list.count()):
            item = self.pdf_list.item(i)
            pdf_file = item.text()
            matching_files.append(pdf_file)

        # Koristi trenutni direktorijum gde se pokreće program (ne ./html/)
        current_dir = os.path.dirname(os.path.abspath(__file__))
        output_file = os.path.join(current_dir, 'results.html')

        try:
            with open(output_file, 'w', encoding='utf-8') as file:
                file.write('<!DOCTYPE html>\n<html lang="sr">\n<head>\n')
                file.write('<meta charset="UTF-8">\n<title>Rezultati pretrage HTML fajlova</title>\n')
                file.write('</head>\n<body>\n')
                file.write('<h1>Ибарске Новости</h1>\n')
                file.write(f'<p>Задата реченица: <strong>{search_phrase}</strong></p>\n')

                if matching_files:
                    file.write('<ul>\n')
                    for pdf_file in matching_files:
                        file.write(f'<li><a href="http://kraljevcani.rs/{pdf_file}" target="_blank">{pdf_file}</a></li>\n')
                    file.write('</ul>\n')
                else:
                    file.write('<p>Нема фајлова који садрже задату реченицу.</p>\n')
                file.write('</body>\n</html>')

            webbrowser.open('file://' + os.path.realpath(output_file))
            self.status_label.setText(f"Статус: HTML резултати генерисани: {output_file}")

        except Exception as e:
            QMessageBox.warning(self, "Грешка", f"Грешка при генерисању HTML фајла: {str(e)}")

    def closeEvent(self, event):
        """Čišćenje privremenih fajlova pri zatvaranju aplikacije"""
        for temp_path in self.temp_files.values():
            try:
                if os.path.exists(temp_path):
                    os.unlink(temp_path)
            except Exception as e:
                print(f"Greška pri brisanju privremenog fajla {temp_path}: {e}")
        event.accept()

def main():
    app = QApplication(sys.argv)

    window = PDFViewerApp()
    window.showMaximized()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()

#MitLicense.txt
MIT License

Copyright (c) 2025 Александар Маричић

Дозвољава се, бесплатно, било којој особи која добије примерак
овог софтвера и придружених документационих фајлова (у даљем тексту
"Софтвер"), да користи Софтвер без икаквих ограничења, укључујући, али не
ограничавајући се на права да користи, копира, мења, обједињује, објављује,
дистрибуира, сублиценцира и/или продаје примерак Софтвера, и да дозволи
лицама којим је Софтвер обезбеђен да то учине, под следећим условима:

Горенаведена обавештења о ауторским правима и ово дозволе морају бити укључени
у све копије или значајне делове Софтвера.

СОФТВЕР СЕ ОБЕЗБЕЂУЈЕ "КАКАВ ЈЕСТЕ", БЕЗ ИКАКВЕ ГАРАНЦИЈЕ, ИЗРИЧИТЕ ИЛИ
ПОДРАЗУМЕВАНЕ, УКЉУЧУЈУЋИ, АЛИ НЕ ОГРАНИЧАВАЈУЋИ СЕ НА ГАРАНЦИЈЕ ТРГОВИНСКЕ
ВРЕДНОСТИ, ПОДЕСНОСТИ ЗА ОДРЕЂЕНУ НАМЕНУ И НЕПОВРЕЂИВАЊА ПРАВА. НИ У КОМ
СЛУЧАЈУ АУТОРИ ИЛИ НОСИОЦИ АУТОРСКИХ ПРАВА НЕЋЕ БИТИ ОДГОВОРНИ ЗА БИЛО КАКВУ
ТУЖБУ, ШТЕТУ ИЛИ ДРУГЕ ОДГОВОРНОСТИ, БИЛО У ОСНОВУ УГОВОРНЕ РАДЊЕ, ДЕЛИКТА ИЛИ
НА ДРУГИ НАЧИН, КОЈА ПРОИЗИЛАЗИ ИЗ СОФТВЕРА ИЛИ КОРИШЋЕЊА ИЛИ ДРУГИХ РАДЊИ У
ВЕЗИ СА СОФТВЕРОМ.

By Abel

Leave a Reply

Your email address will not be published. Required fields are marked *