Izrada projekta QwebX-v1.0 web browser-a

Struktura projekta QwebX

qwebx/
├── package.json
├── main.js
├── preload.js
├── icon.png
├── index.html
├── about.html
├── node_modules/
└── build scripts (kasnije)

package.json

{
  "name": "QwebX",
  "version": "1.0.0",
  "description": "The fastest Serbian web browser.!",
  "main": "main.js",
  "homepage": "https://abel.rs/qweb-munjeviti-srpski-browser-nove-generacije/",
  "author": {
    "name": "Aleksandar Maričić",
    "email": "sale.maricic@gmail.com"
  },
  "scripts": {
    "start": "electron .",
    "build-deb": "electron-builder --linux deb",
    "build-appimage": "electron-builder --linux AppImage",
    "build-win": "electron-builder --win portable",
    "build-mac": "electron-builder --mac zip",
    "make-ico": "convert build/icon.png -resize 256x256 build/icon.ico",
    "make-icns": "convert build/icon.png -resize 1024x1024 build/icon_1024.png && png2icns build/icon.icns build/icon_1024.png",
    "icons": "npm run make-ico && npm run make-icns"
  },
  "license": "MIT",
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.13.3",
    "png-to-ico": "^2.1.8"
  },
  "build": {
    "appId": "com.AleksandarMaricic.QwebX",
    "productName": "QwebX",
    "linux": {
      "target": ["deb", "AppImage"],
      "category": "Network",
      "icon": "build/icon.png",
      "executableName": "QwebX",
      "maintainer": "Aleksandar Maričić <sale.maricic@gmail.com>"
    },
    "win": {
      "target": ["portable"],
      "icon": "build/icon.ico"
    },
    "mac": {
      "target": ["zip"],
      "icon": "build/icon.icns",
      "category": "public.app-category.productivity"
    },
    "files": [
      "index.html",
      "main.js",
      "preload.js",
      "about.html",
      "icon.png",
      "build/icon.png",
      "build/icon.ico",
      "build/icon.icns",
      "build/icon_1024.png"
    ]
  }
}

main.js

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1400,
    height: 900,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true,
      webviewTag: true,
      webSecurity: false,
      allowRunningInsecureContent: true
    },
    title: "QwebX Browser",
    icon: path.join(__dirname, 'icon.png'),
    show: false,
    backgroundColor: '#ffffff'
  });

  mainWindow.loadFile('index.html');
  mainWindow.once('ready-to-show', () => mainWindow.show());

  // Omogući učitavanje lokalnih fajlova
  mainWindow.webContents.session.protocol.registerFileProtocol('file', (request, callback) => {
    const url = request.url.substr(8); // Ukloni 'file://'
    try {
      return callback(decodeURIComponent(url));
    } catch (error) {
      console.error('Error loading file:', error);
    }
  });

  // DevTools shortcut
  mainWindow.webContents.on('before-input-event', (event, input) => {
    if (input.control && input.shift && input.key.toLowerCase() === 'i') {
      mainWindow.webContents.openDevTools({ mode: 'detach' });
    }
  });
}

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

// IPC handlers
ipcMain.handle('get-about-path', () => {
  return path.join(__dirname, 'about.html');
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');
const path = require('path');

contextBridge.exposeInMainWorld('electronAPI', {
  getAboutPagePath: () => {
    return `file://${path.join(__dirname, 'about.html')}`;
  },
  getAboutPath: () => ipcRenderer.invoke('get-about-path'),
  platform: process.platform
});

icon.png

index.html

index.html<!DOCTYPE html>
<html lang="sr">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; script-src * 'unsafe-inline' 'unsafe-eval';">
  <title>QwebX Browser</title>
  <style>
    :root {
      --bg: #111;
      --toolbar: #222;
      --accent: #1a73e8;
      --text: white;
      --input-bg: rgba(255, 255, 255, 0.15);
      --input-border: #555;
      --input-focus: #1a73e8;
    }
    body {
      margin: 0;
      font-family: system-ui;
      background: var(--bg);
      color: var(--text);
      height: 100vh;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .toolbar {
      background: var(--toolbar);
      padding: 8px;
      display: flex;
      flex-direction: column;
      gap: 8px;
      border-bottom: 1px solid #444;
      flex-shrink: 0;
    }
    .nav {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
    }
    .left-nav {
      display: flex;
      gap: 8px;
      align-items: center;
      flex: 1;
    }
    .right-nav { display: flex; gap: 8px; }

    button {
      background: #444;
      border: none;
      color: white;
      padding: 10px;
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.2s;
      font-size: 16px;
    }
    button:hover { background: #666; }
    button:disabled { opacity: 0.5; cursor: not-allowed; }

    #search-btn {
      background: var(--accent);
      width: 50px;
      height: 50px;
      border-radius: 50%;
      font-size: 22px;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 4px 10px rgba(26, 115, 232, 0.4);
    }
    #search-btn:hover {
      background: #0d47a1;
      transform: scale(1.1);
    }

    #address-bar {
      flex: 1;
      min-width: 400px;
      padding: 14px 20px;
      background: var(--input-bg);
      border: 2px solid var(--input-border);
      border-radius: 30px;
      color: white;
      font-size: 17px;
      backdrop-filter: blur(12px);
    }
    #address-bar::placeholder { color: rgba(255, 255, 255, 0.75); }
    #address-bar:focus {
      outline: none;
      border-color: var(--input-focus);
      box-shadow: 0 0 0 4px rgba(26, 115, 232, 0.3);
      background: rgba(255, 255, 255, 0.25);
    }

    .tabs, .bookmarks { display: flex; gap: 6px; overflow-x: auto; padding: 0 8px; }
    .tab {
      background: #333;
      padding: 8px 16px;
      border-radius: 8px 8px 0 0;
      cursor: pointer;
      min-width: 150px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      position: relative;
      font-size: 14px;
    }
    .tab.active { background: var(--accent); }
    .tab .close { position: absolute; right: 6px; top: 6px; font-size: 12px; opacity: 0.7; }
    .tab .close:hover { opacity: 1; color: red; }

    .bookmark {
      background: #333;
      padding: 6px 14px;
      border-radius: 20px;
      font-size: 13px;
      cursor: pointer;
    }
    .bookmark:hover { background: #555; }

    #content { flex: 1; position: relative; background: white; display: flex; flex-direction: column; }
    webview { flex: 1; width: 100%; min-height: 100vh; display: none; border: none; background: white; }
    webview.active { display: flex; }

    #status {
      position: fixed;
      bottom: 12px;
      left: 12px;
      background: rgba(0,0,0,0.8);
      color: white;
      padding: 8px 14px;
      border-radius: 8px;
      font-size: 13px;
      z-index: 9999;
    }
  </style>
</head>
<body>
  <div class="toolbar">
    <div class="tabs" id="tabs"></div>
    <div class="nav">
      <div class="left-nav">
        <button id="back" disabled>←</button>
        <button id="forward" disabled>→</button>
        <button id="reload">↻</button>
        <button id="home">🏠</button>
        <button id="newtab">+</button>
        <button id="bookmark">⭐</button>
        <input type="text" id="address-bar" placeholder="Pretraži Brave ili unesi URL...">
        <button id="search-btn" title="Pretraži">🔍</button>
      </div>
      <div class="right-nav">
        <button id="devtools" title="DevTools">🛠️</button>
        <button id="about" title="O programu">ℹ️</button>
      </div>
    </div>
    <div class="bookmarks" id="bookmarks"></div>
  </div>

  <div id="content"></div>
  <div id="status">Spreman</div>

  <script>
    const tabsContainer = document.getElementById('tabs');
    const content = document.getElementById('content');
    const addressBar = document.getElementById('address-bar');
    const status = document.getElementById('status');
    const bookmarksBar = document.getElementById('bookmarks');

    let tabs = [];
    let activeTab = null;
    let tabIdCounter = 0;
    let bookmarks = JSON.parse(localStorage.getItem('bookmarks') || '[]');

    function resizeWebviews() {
      const height = content.clientHeight;
      tabs.forEach(t => t.webview.style.height = `${height}px`);
    }

    function updateNavButtons() {
      if (!activeTab) return;
      const wv = activeTab.webview;
      const canBack = wv.canGoBack();
      const canForward = wv.canGoForward();
      document.getElementById('back').disabled = !canBack;
      document.getElementById('forward').disabled = !canForward;
      document.getElementById('back').style.opacity = canBack ? '1' : '0.5';
      document.getElementById('forward').style.opacity = canForward ? '1' : '0.5';
    }

    function updateTabTitle(tabId, title) {
      const tab = tabs.find(t => t.id === tabId);
      if (tab) {
        const titleSpan = tab.tab.querySelector('span:first-child');
        if (titleSpan) {
          titleSpan.textContent = title || 'Nova kartica';
        }
      }
    }

    function createTab(url = 'https://abel.rs/zx/', title = 'Nova kartica') {
      // Ako je about stranica, postavi odgovarajući naslov
      if (url.includes('about.html')) {
        title = 'O QwebX Browseru';
      }

      const id = tabIdCounter++;
      const tab = document.createElement('div');
      tab.className = 'tab';
      tab.dataset.id = id;
      tab.innerHTML = `<span>${title}</span><span class="close">×</span>`;

      const webview = document.createElement('webview');
      webview.src = url;
      webview.partition = 'persist:qwebxbrowser';
      webview.allowpopups = true;
      webview.setAttribute('webpreferences', 'nativeWindowOpen=yes, contextIsolation=no');
      webview.dataset.id = id;

      // Rukovanje novim prozorima
      webview.addEventListener('new-window', (e) => {
        e.preventDefault();
        createTab(e.url);
      });

      // Pratimo promene URL-a
      webview.addEventListener('will-navigate', (e) => {
        if (activeTab && activeTab.webview === webview) {
          addressBar.value = e.url;
        }
      });

      // Ažuriramo naslov taba
      webview.addEventListener('page-title-updated', (e) => {
        updateTabTitle(id, e.title);
      });

      // Ažuriramo status učitavanja
      webview.addEventListener('did-start-loading', () => {
        status.textContent = 'Učitavam...';
        if (activeTab && activeTab.webview === webview) {
          tab.classList.add('loading');
        }
      });

      webview.addEventListener('did-stop-loading', () => {
        status.textContent = 'Spreman';
        if (activeTab && activeTab.webview === webview) {
          tab.classList.remove('loading');
          addressBar.value = webview.src;
        }
      });

      const events = ['did-start-loading', 'did-stop-loading', 'did-navigate', 'did-navigate-in-page', 'dom-ready'];
      events.forEach(ev => webview.addEventListener(ev, updateNavButtons));

      const poll = setInterval(updateNavButtons, 1000);

      tab.onclick = e => {
        if (e.target.classList.contains('close')) {
          clearInterval(poll);
          events.forEach(ev => webview.removeEventListener(ev, updateNavButtons));
          closeTab(id);
        } else {
          switchTab(id);
        }
      };

      tabsContainer.appendChild(tab);
      content.appendChild(webview);
      tabs.push({ id, tab, webview, url, poll });
      switchTab(id);
      updateNavButtons();
      resizeWebviews();

      return id;
    }

    function switchTab(id) {
      tabs.forEach(t => {
        t.tab.classList.toggle('active', t.id === id);
        t.webview.style.display = t.id === id ? 'flex' : 'none';
      });
      activeTab = tabs.find(t => t.id === id);
      if (activeTab) {
        addressBar.value = activeTab.webview.src;
        resizeWebviews();
        updateNavButtons();

        const title = activeTab.webview.getTitle();
        if (title) {
          updateTabTitle(id, title);
        }
      }
    }

    function closeTab(id) {
      const idx = tabs.findIndex(t => t.id === id);
      if (idx === -1) return;
      clearInterval(tabs[idx].poll);
      tabs[idx].tab.remove();
      tabs[idx].webview.remove();
      tabs.splice(idx, 1);
      if (activeTab?.id === id) {
        if (tabs.length > 0) {
          switchTab(tabs[Math.max(0, idx - 1)].id);
        } else {
          activeTab = null;
          addressBar.value = '';
          createTab();
        }
      }
    }

    // Navigacioni dugmići
    document.getElementById('back').onclick = () => activeTab?.webview.canGoBack() && activeTab.webview.goBack();
    document.getElementById('forward').onclick = () => activeTab?.webview.canGoForward() && activeTab.webview.goForward();
    document.getElementById('reload').onclick = () => activeTab?.webview.reload();
    document.getElementById('home').onclick = () => activeTab && (activeTab.webview.src = 'https://abel.rs/zx/');
    document.getElementById('newtab').onclick = () => createTab();
    document.getElementById('devtools').onclick = () => activeTab?.webview.openDevTools();

    // About funkcionalnost
    document.getElementById('about').onclick = async () => {
      let aboutPagePath;

      // Pokušaj da koristiš electronAPI ako postoji
      if (window.electronAPI && window.electronAPI.getAboutPagePath) {
        aboutPagePath = window.electronAPI.getAboutPagePath();
      } else {
        // Fallback za različite načine učitavanja
        aboutPagePath = 'about.html';
      }

      if (activeTab) {
        activeTab.webview.src = aboutPagePath;
      } else {
        createTab(aboutPagePath);
      }
    };

    // Navigacija
    const navigate = () => {
      let input = addressBar.value.trim();
      if (!input) return;
      if (!input.includes('.')) {
        input = `https://search.brave.com/search?q=${encodeURIComponent(input)}`;
      } else if (!input.startsWith('http')) {
        input = 'https://' + input;
      }
      if (activeTab) activeTab.webview.src = input;
    };

    document.getElementById('search-btn').onclick = navigate;
    addressBar.addEventListener('keypress', e => e.key === 'Enter' && navigate());

    // Bookmark funkcionalnost
    document.getElementById('bookmark').onclick = () => {
      if (!activeTab) return;
      const url = activeTab.webview.src;
      const title = activeTab.webview.getTitle() || 'Bookmark';

      const exists = bookmarks.find(b => b.url === url);
      if (!exists) {
        bookmarks.push({ url, title });
        localStorage.setItem('bookmarks', JSON.stringify(bookmarks));
        renderBookmarks();
        status.textContent = 'Dodat u bookmarke!';
        setTimeout(() => status.textContent = 'Spreman', 2000);
      } else {
        status.textContent = 'Već je u bookmarkima!';
        setTimeout(() => status.textContent = 'Spreman', 2000);
      }
    };

    function renderBookmarks() {
      bookmarksBar.innerHTML = '';

      const allBookmarks = bookmarks.length > 0 ? bookmarks : [
        {title: 'Brave', url: 'https://search.brave.com/'},
        {title: 'YouTube', url: 'https://youtube.com'},
        {title: 'Gmail', url: 'https://gmail.com'},
        {title: 'Abel', url: 'https://abel.rs'}
      ];

      allBookmarks.forEach(b => {
        const el = document.createElement('div');
        el.className = 'bookmark';
        el.textContent = b.title;
        el.title = b.url;
        el.onclick = () => {
          if (activeTab) {
            activeTab.webview.src = b.url;
          } else {
            createTab(b.url);
          }
        };
        bookmarksBar.appendChild(el);
      });
    }

    // Resize handler
    window.addEventListener('resize', resizeWebviews);
    window.addEventListener('load', () => {
      setTimeout(resizeWebviews, 100);
      createTab();
      renderBookmarks();
    });

    // Inicijalizacija
    createTab();
    renderBookmarks();
  </script>
</body>
</html>

about.html

<!DOCTYPE html>
<html lang="sr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>O Qweb Browseru</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            background: #f5f5f5;
            color: #333;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #1a73e8;
            border-bottom: 2px solid #1a73e8;
            padding-bottom: 10px;
        }
        h2 {
            color: #444;
            margin-top: 25px;
        }
        .author {
            background: #e8f0fe;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
        }
        .license {
            background: #f1f3f4;
            padding: 15px;
            border-radius: 8px;
            font-family: monospace;
            font-size: 14px;
            white-space: pre-wrap;
        }
        .features {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 15px;
            margin: 20px 0;
        }
        .feature {
            background: #e8f5e8;
            padding: 15px;
            border-radius: 8px;
        }
        a {
            color: #1a73e8;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🌐 Qweb Browser</h1>
        
        <div class="author">
            <h2>Autor</h2>
            <p><strong>Aleksandar Maričić</strong></p>
            <p>Email: sale.maricic@gmail.com</p>
            <p>Website: <a href="https://abel.rs">abel.rs</a></p>
        </div>

        <h2>Istorijat izrade</h2>
        <p>Qweb Browser je nastao kao projekat za kreiranje modernog web browsera koristeći Electron framework. Browser je dizajniran da bude brz, jednostavan za korišćenje i prilagođen srpskim korisnicima.</p>

        <h2>Glavne karakteristike</h2>
        <div class="features">
            <div class="feature">
                <h3>🚀 Brzina</h3>
                <p>Optimizovan za brzo učitavanje stranica</p>
            </div>
            <div class="feature">
                <h3>📑 Tabovi</h3>
                <p>Podrška za više tabova istovremeno</p>
            </div>
            <div class="feature">
                <h3>⭐ Bookmarkovi</h3>
                <p>Jednostavno upravljanje omiljenim sajtovima</p>
            </div>
            <div class="feature">
                <h3>🔍 Brave Search</h3>
                <p>Integrisan Brave search kao podrazumevana pretraga</p>
            </div>
        </div>

        <h2>Uputstvo za korišćenje</h2>
        <ul>
            <li><strong>Navigacija:</strong> Koristi ← → dugmad za kretanje kroz istoriju</li>
            <li><strong>Novi tab:</strong> Klikni na + dugme za otvaranje novog taba</li>
            <li><strong>Bookmarkovi:</strong> Klikni na ⭐ da sačuvaš trenutnu stranicu</li>
            <li><strong>Pretraga:</strong> Unesi termin u address bar ili direktno URL</li>
            <li><strong>DevTools:</strong> F12 ili 🛠️ dugme za developer alate</li>
            <li><strong>O programu:</strong> ℹ️ dugme za informacije o browseru</li>
        </ul>

        <h2>MIT Licenca</h2>
        <div class="license">
Copyright 2024 Aleksandar Maričić

Dozvoljeno je besplatno korišćenje, kopiranje, modifikacija, spajanje, objavljivanje, distribucija, sublicenciranje i/ili prodaja kopija Softvera, uz uslov da se gornja obaveštenja o autorskim pravima i ovaj uslov dozvolle uključe u sve kopije ili značajne delove Softvera.

SOFTVER SE DOSTAVLJA "KAKAV JESTE", BEZ GARANCIJA BILO KOJE VRSTE, IZRIČITIH ILI PODRAZUMEvanih, UKLJUČUJUĆI, ALI NE OGRANIČAVAJUĆI SE NA GARANCIJE O PRODAJLJI, POGODNOSTI ZA ODREĐENU NAMENU I NEKRŠENjEM PRAVA. U NIKAKVOM SLUČAJU AUTORI ILI NOSIOCI AUTORSKIH PRAVA NEĆE BITI ODGOVORNI ZA BILO KAKAV ZAHTEV, ŠTETU ILI DRUGU ODGOVORNOST, BILO U OKVIRU UGOVORA, DELIKTA ILI NA DRUGI NAČIN, KOJA PROISTIČE IZ SOFTVERA ILI KORIŠĆENJA SOFTVERA ILI DRUGIH RADNJI U VEZI SA SOFTVERA.
        </div>

        <p style="text-align: center; margin-top: 30px; color: #666;">
            Hvala što koristite Qweb Browser! 🎉
        </p>
    </div>
</body>
</html>

By Abel

Leave a Reply

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