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
});

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>

