diff --git a/README.md b/README.md index 2f95606..f43085b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # Veil -Veil ist ein auf WebKit basierender Simpler Browser, in GTK \ No newline at end of file +Veil ist ein auf WebKit basierender Simpler Browser, in GTK + +============================ +DIES IST GERADE NUR EIN HOBBYPROJEKT VON MIR, +UND DAHER NOCH NICHT PRODUCTION READY!!!! +============================ +(trozdem freue ich mich über jede pull/push, ein ganzer browser ist nämlich ein bisschen viel arbeit. (heul :,( )) + + + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Build ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +$ ./build.sh clean && ./build.sh build + +nach einem erfolgreichem build landet die bin im build/linux ordner und wird in den veil ordner kopiert, die bin muss ich gleichen ordner wie die data/ directory liegen. + + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Code ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +ja, ich weiß er ist schlecht, und ja ich weiß, er könnte besser sein. bei verbesserungsvorschlägen bitte statt meckern: +1. Verbessern +2. Push +Dankii :) \ No newline at end of file diff --git a/Veil/Makefile b/Veil/Makefile new file mode 100644 index 0000000..060612e --- /dev/null +++ b/Veil/Makefile @@ -0,0 +1,43 @@ +CC = gcc +PKG_CFLAGS = $(shell pkg-config --cflags gtk+-3.0 webkit2gtk-4.1) +PKG_LIBS = $(shell pkg-config --libs gtk+-3.0 webkit2gtk-4.1) + +# === PERFORMANCE-OPTIMIZED BUILD FLAGS === +# -O3: Maximum optimization level +# -march=native: Optimize for current CPU architecture +# -mtune=native: Tune for current CPU +# -flto: Link-time optimization for cross-file inlining +# -ffast-math: Faster floating point (safe for browser UI) +# -fomit-frame-pointer: Free up a register +# -pipe: Faster compilation + +CFLAGS = $(PKG_CFLAGS) -O3 -march=native -mtune=native -flto \ + -ffast-math -fomit-frame-pointer -pipe \ + -Wall -Wextra -Wno-unused-parameter + +LDFLAGS = $(PKG_LIBS) -flto -Wl,-O1 -Wl,--as-needed + +TARGET = veil +SRC = src/veil.c + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +# Debug build (slower but easier to debug) +debug: CFLAGS = $(PKG_CFLAGS) -O0 -g -Wall -Wextra +debug: LDFLAGS = $(PKG_LIBS) +debug: $(TARGET) + +# Release build with stripped binary +release: $(TARGET) + strip --strip-all $(TARGET) + +clean: + rm -f $(TARGET) + +install: $(TARGET) + install -Dm755 $(TARGET) /usr/local/bin/$(TARGET) + +.PHONY: all clean install debug release diff --git a/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto b/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto new file mode 100644 index 0000000..8270005 Binary files /dev/null and b/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto differ diff --git a/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto~lock~ b/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto~lock~ new file mode 100644 index 0000000..e7af586 Binary files /dev/null and b/Veil/Projektdateien (EXTERN)/Weil Icon.afphoto~lock~ differ diff --git a/Veil/build.sh b/Veil/build.sh new file mode 100755 index 0000000..0fc5e5d --- /dev/null +++ b/Veil/build.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# +# Veil Browser Build Script +# Linux build only +# + +set -e + +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$PROJECT_DIR/build" +SRC="$PROJECT_DIR/src/veil.c" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +print_status() { echo -e "${GREEN}[*]${NC} $1"; } +print_warn() { echo -e "${YELLOW}[!]${NC} $1"; } +print_error() { echo -e "${RED}[x]${NC} $1"; } + +show_help() { + echo "Veil Browser Build Script" + echo "" + echo "Usage: $0 [target]" + echo "" + echo "Targets:" + echo " build Build for Linux (default)" + echo " clean Remove build artifacts" + echo " help Show this help" + echo "" +} + +build_linux() { + print_status "Building Veil for Linux..." + + # Check dependencies + if ! pkg-config --exists gtk+-3.0 webkit2gtk-4.1 2>/dev/null; then + print_error "Missing dependencies. Install: gtk3 webkit2gtk" + exit 1 + fi + + CFLAGS=$(pkg-config --cflags gtk+-3.0 webkit2gtk-4.1) + LIBS=$(pkg-config --libs gtk+-3.0 webkit2gtk-4.1) + + mkdir -p "$BUILD_DIR/linux" + + gcc $CFLAGS \ + -O3 -march=native -mtune=native -flto -ffast-math \ + -fomit-frame-pointer -pipe \ + -Wall -Wextra -Wno-unused-parameter \ + -o "$BUILD_DIR/linux/veil" "$SRC" \ + $LIBS -flto -Wl,-O1 -Wl,--as-needed + + # Copy to project root + cp "$BUILD_DIR/linux/veil" "$PROJECT_DIR/veil" + + print_status "Build complete: $BUILD_DIR/linux/veil" + print_status "Also copied to: $PROJECT_DIR/veil" +} + +clean() { + print_status "Cleaning build artifacts..." + rm -rf "$BUILD_DIR" + rm -f "$PROJECT_DIR/veil" + print_status "Clean complete." +} + +# Main +cd "$PROJECT_DIR" + +case "${1:-build}" in + build|linux) + build_linux + ;; + clean) + clean + ;; + help|--help|-h) + show_help + ;; + *) + print_error "Unknown target: $1" + show_help + exit 1 + ;; +esac diff --git a/Veil/build/linux/veil b/Veil/build/linux/veil new file mode 100755 index 0000000..47a2aff Binary files /dev/null and b/Veil/build/linux/veil differ diff --git a/Veil/data/icons/hicolor/128x128/apps/veil.png b/Veil/data/icons/hicolor/128x128/apps/veil.png new file mode 100644 index 0000000..8256e80 Binary files /dev/null and b/Veil/data/icons/hicolor/128x128/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/16x16/apps/veil.png b/Veil/data/icons/hicolor/16x16/apps/veil.png new file mode 100644 index 0000000..03d80c6 Binary files /dev/null and b/Veil/data/icons/hicolor/16x16/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/24x24/apps/veil.png b/Veil/data/icons/hicolor/24x24/apps/veil.png new file mode 100644 index 0000000..20d7ca1 Binary files /dev/null and b/Veil/data/icons/hicolor/24x24/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/256x256/apps/veil.png b/Veil/data/icons/hicolor/256x256/apps/veil.png new file mode 100644 index 0000000..0d0db88 Binary files /dev/null and b/Veil/data/icons/hicolor/256x256/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/32x32/apps/veil.png b/Veil/data/icons/hicolor/32x32/apps/veil.png new file mode 100644 index 0000000..c66c719 Binary files /dev/null and b/Veil/data/icons/hicolor/32x32/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/48x48/apps/veil.png b/Veil/data/icons/hicolor/48x48/apps/veil.png new file mode 100644 index 0000000..3dd464e Binary files /dev/null and b/Veil/data/icons/hicolor/48x48/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/512x512/apps/veil.png b/Veil/data/icons/hicolor/512x512/apps/veil.png new file mode 100644 index 0000000..4a69507 Binary files /dev/null and b/Veil/data/icons/hicolor/512x512/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/64x64/apps/veil.png b/Veil/data/icons/hicolor/64x64/apps/veil.png new file mode 100644 index 0000000..68fa244 Binary files /dev/null and b/Veil/data/icons/hicolor/64x64/apps/veil.png differ diff --git a/Veil/data/icons/hicolor/scalable/apps/veil.svg b/Veil/data/icons/hicolor/scalable/apps/veil.svg new file mode 100644 index 0000000..d7e6944 --- /dev/null +++ b/Veil/data/icons/hicolor/scalable/apps/veil.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Veil/data/meson.build b/Veil/data/meson.build new file mode 100644 index 0000000..24f91bb --- /dev/null +++ b/Veil/data/meson.build @@ -0,0 +1,14 @@ +# Desktop file installation +install_data('veil.desktop', + install_dir: join_paths(get_option('datadir'), 'applications') +) + +# Icon installation (PNG at various sizes) +icon_sizes = ['16x16', '24x24', '32x32', '48x48', '64x64', '128x128', '256x256', '512x512'] + +foreach size : icon_sizes + install_data( + join_paths('icons/hicolor', size, 'apps/veil.png'), + install_dir: join_paths(get_option('datadir'), 'icons/hicolor', size, 'apps') + ) +endforeach diff --git a/Veil/data/veil.desktop b/Veil/data/veil.desktop new file mode 100644 index 0000000..442a386 --- /dev/null +++ b/Veil/data/veil.desktop @@ -0,0 +1,10 @@ +#!/usr/bin/env xdg-open +[Desktop Entry] +Name=Veil +Comment=Minimalistischer WebKit-Browser +Exec=veil +Icon=veil +Terminal=false +Type=Application +Categories=Network;WebBrowser; +Keywords=browser;web;internet; diff --git a/Veil/meson.build b/Veil/meson.build new file mode 100644 index 0000000..4048ea1 --- /dev/null +++ b/Veil/meson.build @@ -0,0 +1,36 @@ +project('veil', 'c', + version: '0.1.0', + default_options: [ + 'warning_level=2', + 'c_std=c11', + 'optimization=3', + 'b_lto=true', + 'strip=true' + ] +) + +gtk3_dep = dependency('gtk+-3.0', version: '>= 3.24') +webkit2_dep = dependency('webkit2gtk-4.1') + +# Performance-optimized compiler flags +add_project_arguments( + '-ffast-math', + '-fomit-frame-pointer', + '-pipe', + language: 'c' +) + +# Native CPU optimization (comment out for portable builds) +add_project_arguments( + '-march=native', + '-mtune=native', + language: 'c' +) + +executable('veil', + 'src/veil.c', + dependencies: [gtk3_dep, webkit2_dep], + install: true +) + +subdir('data') diff --git a/Veil/src/pages/settings.h b/Veil/src/pages/settings.h new file mode 100644 index 0000000..59f7312 --- /dev/null +++ b/Veil/src/pages/settings.h @@ -0,0 +1,189 @@ +/* + * Veil Browser - Settings Page (about:settings) + */ + +#ifndef VEIL_PAGE_SETTINGS_H +#define VEIL_PAGE_SETTINGS_H + +static const char *SETTINGS_HTML = +"" +"Settings" +"" +"" +"
" +"
" +"

Allgemein

" +"
" +" " +" " +"
" +"
" +" " +" " +"
" +"
" +" " +" " +"
" +"
" +" " +" " +" " +"
" +"
" +"
" +"

Anpassung

" +"
" +" " +" " +"
" +"
" +" " +" " +"
" +"
" +" " +" " +"
" +"
" +"

Farben

" +"
" +" " +" " +" " +"
" +"
" +" " +" " +" " +"
" +"
" +" " +" " +" " +"
" +"
" +" " +" " +" " +"
" +"
" +" " +" " +" " +"
" +" " +"
" +"
" +"

Info

" +"
" +"

Tastenkürzel

" +"

Ctrl+T Neuer Tab

" +"

Ctrl+W Tab schließen

" +"

Ctrl+L URL-Leiste fokussieren

" +"

Ctrl+R / F5 Neu laden

" +"

F11 Vollbild umschalten

" +"
" +"
" +"

Über Veil

" +"

Ein Privater WebKit-Browser, ohne unnötigen Müll :)

" +"

Gebaut mit GTK3 und WebKit2GTK

" +"

Entwickelt von Packed https://papp-box.de/

" +"
" +"

Veil Browser v1.0

" +"
" +"
" +""; + +#endif /* VEIL_PAGE_SETTINGS_H */ diff --git a/Veil/src/pages/start.h b/Veil/src/pages/start.h new file mode 100644 index 0000000..1c8b9d8 --- /dev/null +++ b/Veil/src/pages/start.h @@ -0,0 +1,398 @@ +/* + * Veil Browser - Start Page (about:start) + */ + +#ifndef VEIL_PAGE_START_H +#define VEIL_PAGE_START_H + +static const char *START_HTML = +"" +"Veil" +"" +"
" +"
" +" " +"
██╗   ██╗███████╗██╗██╗     \n"
+"██║   ██║██╔════╝██║██║     \n"
+"██║   ██║█████╗  ██║██║     \n"
+"╚██╗ ██╔╝██╔══╝  ██║██║     \n"
+" ╚████╔╝ ███████╗██║███████╗\n"
+"  ╚═══╝  ╚══════╝╚═╝╚══════╝
" +"
" +"
" +" " +" " +" " +" " +"
" +" " +"
" +"" +"" +""; + +#endif /* VEIL_PAGE_START_H */ diff --git a/Veil/src/veil.c b/Veil/src/veil.c new file mode 100644 index 0000000..aad8927 --- /dev/null +++ b/Veil/src/veil.c @@ -0,0 +1,2414 @@ +/* + * Veil Browser - Minimalistischer WebKit-Browser + * Entwickelt von Erik Hellak + */ + +#include +#include +#include +#include +#include + +#include "pages/settings.h" +#include "pages/start.h" + +typedef struct _VeilTab VeilTab; +typedef struct _VeilBrowser VeilBrowser; +typedef struct _VeilSettings VeilSettings; + +struct _VeilSettings { + char bg_color[8]; + char accent_color[8]; + char grad_start[8]; + char grad_mid[8]; + char grad_end[8]; + char download_dir[512]; /* Benutzerdefiniertes Download-Verzeichnis */ + int search_engine; /* 0=DuckDuckGo, 1=Google, 2=Brave, 3=Startpage */ + int show_bookmarks; /* 0=versteckt, 1=sichtbar */ + int show_quicklinks; /* 0=versteckt, 1=sichtbar */ + int use_bg_image; /* 0=nein, 1=ja */ + int bg_opacity; /* 0-100 (Deckkraft) */ +}; + +static const VeilSettings DEFAULT_SETTINGS = { + "#0a0a0f", + "#4a9eff", + "#00d4ff", + "#0066ff", + "#cc00ff", + "", /* Leer = System-Standard-Download-Verzeichnis */ + 0, + 1, /* Lesezeichen standardmäßig sichtbar */ + 1, /* Quick-Links standardmäßig sichtbar */ + 0, /* Kein Hintergrundbild standardmäßig */ + 100 /* Volle Deckkraft standardmäßig */ +}; + +struct _VeilTab { + VeilBrowser *browser; + WebKitWebView *webview; + GtkWidget *tab_box; + GtkWidget *tab_label; + GtkWidget *close_button; +}; + +typedef struct _VeilBookmark { + char *name; + char *url; /* NULL für Ordner */ + gboolean is_folder; + GList *children; /* Liste von VeilBookmark-Kindern für Ordner */ +} VeilBookmark; + +typedef struct _VeilDownload { + char *filename; + char *path; /* Vollständiger Dateipfad zum Download */ + char *url; + gint64 timestamp; +} VeilDownload; + +typedef struct _VeilActiveDownload { + WebKitDownload *download; + GtkWidget *row; + GtkWidget *progress_bar; + GtkWidget *status_label; + GtkWidget *speed_label; + gint64 last_bytes; + gint64 last_time; +} VeilActiveDownload; + +struct _VeilBrowser { + GtkWidget *window; + GtkWidget *header; + GtkWidget *main_box; + GtkWidget *tab_container; + GtkWidget *nav_bar; + GtkWidget *bookmarks_bar; + GtkWidget *back_btn; + GtkWidget *forward_btn; + GtkWidget *reload_btn; + GtkWidget *url_entry; + GtkWidget *star_btn; + GtkWidget *star_popover; + GtkWidget *menu_btn; + GtkWidget *menu; + GtkWidget *progress_bar; + GtkWidget *webview_stack; + GtkWidget *gradient_line; + GList *tabs; + VeilTab *current_tab; + gboolean is_maximized; + VeilSettings settings; + GtkCssProvider *css_provider; + WebKitWebContext *web_context; + GList *bookmarks; /* Liste von VeilBookmark */ + GList *downloads; /* Liste von VeilDownload */ + GList *active_downloads; /* Liste von VeilActiveDownload */ + GtkWidget *downloads_popover; + GtkWidget *downloads_list_box; + GtkWidget *active_downloads_box; + GtkWidget *current_folder_popover; /* Aktuell geöffnetes Ordner-Popup */ +}; + +static VeilBrowser *g_browser = NULL; + +static void save_settings(VeilBrowser *b); +static void load_settings(VeilBrowser *b); +static void apply_css(VeilBrowser *b); +static void show_settings_page(VeilBrowser *b); + +static char *build_css(VeilBrowser *b) { + VeilSettings *s = &b->settings; + return g_strdup_printf( + "@define-color bg %s;\n" + "@define-color surface shade(%s, 1.1);\n" + "@define-color hover shade(%s, 1.3);\n" + "@define-color accent %s;\n" + "@define-color text #e0e0e8;\n" + "@define-color text_dim #606070;\n" + "@define-color red #f85149;\n" + "\n" + "* { font-family: 'JetBrains Mono', 'Fira Code', monospace; }\n" + "window, .background { background: @bg; }\n" + "\n" + "decoration, decoration:backdrop {\n" + " border-radius: 8px;\n" + " border: 2px solid;\n" + " border-image: linear-gradient(135deg, %s, %s, %s) 1;\n" + " background: @bg;\n" + " box-shadow: 0 0 15px alpha(%s, 0.3);\n" + "}\n" + "\n" + "headerbar, headerbar:backdrop {\n" + " background: @bg;\n" + " border: none; box-shadow: none;\n" + " min-height: 28px; padding: 0 4px;\n" + "}\n" + "\n" + ".tab-box { background: transparent; border-radius: 4px 4px 0 0; padding: 2px 8px; }\n" + ".tab-box:hover { background: @hover; }\n" + ".tab-box.active { background: @surface; }\n" + ".tab-box label { color: @text_dim; font-size: 12px; }\n" + ".tab-box.active label { color: @text; }\n" + ".tab-close { min-width: 14px; min-height: 14px; padding: 0; margin-left: 6px; border-radius: 2px; opacity: 0; }\n" + ".tab-box:hover .tab-close { opacity: 0.5; }\n" + ".tab-close:hover { background: @red; opacity: 1; }\n" + "\n" + ".header-btn { min-width: 16px; min-height: 16px; padding: 2px; margin: 0 1px; border-radius: 3px; opacity: 0.5; }\n" + ".header-btn:hover { background: @hover; opacity: 1; }\n" + ".header-btn.close:hover { background: @red; }\n" + "\n" + ".brand { font-size: 12px; font-weight: bold; color: %s; text-shadow: 0 0 10px %s, 0 0 20px %s; }\n" + "\n" + ".nav-bar { background: @surface; padding: 3px 4px; }\n" + ".nav-btn { min-width: 22px; min-height: 22px; padding: 0; border-radius: 3px; opacity: 0.6; }\n" + ".nav-btn:hover { background: @hover; opacity: 1; }\n" + ".nav-btn:disabled { opacity: 0.2; }\n" + ".star-btn { min-width: 32px; min-height: 28px; padding: 0 8px; border-radius: 4px; font-size: 18px; opacity: 0.7; }\n" + ".star-btn:hover { background: @hover; opacity: 1; color: @accent; }\n" + "\n" + ".url-box { background: @bg; border: 1px solid shade(@bg, 1.5); border-radius: 4px; padding: 0 6px; min-height: 26px; }\n" + ".url-box:focus-within { border-color: @accent; }\n" + ".url-entry { background: transparent; border: none; box-shadow: none; color: @text; font-size: 13px; }\n" + "\n" + ".gradient-line { min-height: 2px; background: linear-gradient(90deg, %s, %s, %s); }\n" + ".progress { min-height: 2px; background: transparent; }\n" + ".progress progress { background: linear-gradient(90deg, %s, %s, %s); }\n" + "\n" + ".bookmarks-bar { background: @surface; padding: 4px 0; border-bottom: 1px solid shade(@bg, 1.3); }\n" + ".bookmark-btn { padding: 4px 10px; font-size: 12px; color: @text_dim; border-radius: 4px; }\n" + ".bookmark-btn:hover { color: @text; background: @hover; }\n" + "\n" + ".veil-menu { background: @surface; border: 2px solid; border-image: linear-gradient(135deg, %s, %s, %s) 1; padding: 4px; box-shadow: 0 4px 20px alpha(%s, 0.3); }\n" + ".veil-menu menuitem { padding: 6px 12px; color: @text; font-size: 13px; }\n" + ".veil-menu menuitem:hover { background: @hover; }\n" + ".veil-popover, .veil-popover.background { background: @surface; border: 2px solid; border-image: linear-gradient(135deg, %s, %s, %s) 1; box-shadow: 0 4px 20px alpha(%s, 0.3); padding: 4px; }\n" + ".veil-popover arrow, popover.veil-popover arrow, popover arrow { border: none; background: none; min-width: 0; min-height: 0; box-shadow: none; opacity: 0; -gtk-icon-source: none; }\n" + ".veil-popover > contents { background: @surface; border: none; }\n" + ".veil-popover label { color: @text; font-size: 13px; }\n" + ".veil-popover button { color: @text; }\n" + ".veil-popover button:hover { background: @hover; }\n" + ".veil-popover entry { background: @bg; border: 1px solid shade(@bg, 1.5); border-radius: 4px; padding: 6px 8px; color: @text; }\n" + ".veil-popover entry:focus { border-color: @accent; }\n" + ".veil-popover combobox { background: @bg; border: 1px solid shade(@bg, 1.5); border-radius: 4px; }\n" + ".veil-popover progressbar { min-height: 6px; }\n" + ".veil-popover progressbar trough { background: shade(@bg, 1.2); border-radius: 3px; min-height: 6px; }\n" + ".veil-popover progressbar progress { background: linear-gradient(90deg, %s, %s, %s); border-radius: 3px; min-height: 6px; }\n" + ".bookmark-folder { padding: 4px 10px; font-size: 12px; color: @text_dim; border-radius: 4px; }\n" + ".bookmark-folder:hover { color: @text; background: @hover; }\n" + ".dim-label { color: @text_dim; font-size: 11px; }\n" + ".suggested-action { background: @accent; color: @bg; }\n" + ".suggested-action:hover { background: shade(@accent, 1.1); }\n" + ".destructive-action { background: @red; color: white; }\n" + ".destructive-action:hover { background: shade(@red, 1.1); }\n" + "\n" + "button { color: @accent; }\n" + "button:hover { background: @hover; }\n" + "entry selection { background: alpha(@accent, 0.3); }\n" + "scale trough highlight { background: @accent; }\n" + "switch:checked slider { background: @accent; }\n" + "checkbutton check:checked, radiobutton radio:checked { background: @accent; border-color: @accent; }\n", + s->bg_color, s->bg_color, s->bg_color, s->accent_color, + s->grad_start, s->grad_mid, s->grad_end, s->grad_start, + s->accent_color, s->accent_color, s->accent_color, /* brand glow */ + s->grad_start, s->grad_mid, s->grad_end, + s->grad_start, s->grad_mid, s->grad_end, + s->grad_start, s->grad_mid, s->grad_end, s->grad_start, /* Menü-Gradient */ + s->grad_start, s->grad_mid, s->grad_end, s->grad_start, /* Popover-Gradient */ + s->grad_start, s->grad_mid, s->grad_end /* Fortschrittsbalken-Gradient */ + ); +} + +static void apply_css(VeilBrowser *b) { + char *css = build_css(b); + gtk_css_provider_load_from_data(b->css_provider, css, -1, NULL); + g_free(css); +} + +static char *get_config_path(void) { + const char *home = g_get_home_dir(); + return g_build_filename(home, ".config", "veil", "settings.conf", NULL); +} + +static void save_settings(VeilBrowser *b) { + char *path = get_config_path(); + char *dir = g_path_get_dirname(path); + g_mkdir_with_parents(dir, 0755); + g_free(dir); + + FILE *f = fopen(path, "w"); + if (f) { + fprintf(f, "bg=%s\n", b->settings.bg_color); + fprintf(f, "accent=%s\n", b->settings.accent_color); + fprintf(f, "grad_start=%s\n", b->settings.grad_start); + fprintf(f, "grad_mid=%s\n", b->settings.grad_mid); + fprintf(f, "grad_end=%s\n", b->settings.grad_end); + fprintf(f, "download_dir=%s\n", b->settings.download_dir); + fprintf(f, "search_engine=%d\n", b->settings.search_engine); + fprintf(f, "show_bookmarks=%d\n", b->settings.show_bookmarks); + fprintf(f, "show_quicklinks=%d\n", b->settings.show_quicklinks); + fprintf(f, "use_bg_image=%d\n", b->settings.use_bg_image); + fprintf(f, "bg_opacity=%d\n", b->settings.bg_opacity); + fclose(f); + } + g_free(path); +} + +static void load_settings(VeilBrowser *b) { + b->settings = DEFAULT_SETTINGS; + + char *path = get_config_path(); + FILE *f = fopen(path, "r"); + if (f) { + char line[600]; /* Larger buffer for download_dir path */ + while (fgets(line, sizeof(line), f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = 0; + char *val = eq + 1; + val[strcspn(val, "\n")] = 0; + if (strcmp(line, "bg") == 0) strncpy(b->settings.bg_color, val, 7); + else if (strcmp(line, "accent") == 0) strncpy(b->settings.accent_color, val, 7); + else if (strcmp(line, "grad_start") == 0) strncpy(b->settings.grad_start, val, 7); + else if (strcmp(line, "grad_mid") == 0) strncpy(b->settings.grad_mid, val, 7); + else if (strcmp(line, "grad_end") == 0) strncpy(b->settings.grad_end, val, 7); + else if (strcmp(line, "download_dir") == 0) strncpy(b->settings.download_dir, val, 511); + else if (strcmp(line, "search_engine") == 0) b->settings.search_engine = atoi(val); + else if (strcmp(line, "show_bookmarks") == 0) b->settings.show_bookmarks = atoi(val); + else if (strcmp(line, "show_quicklinks") == 0) b->settings.show_quicklinks = atoi(val); + else if (strcmp(line, "use_bg_image") == 0) b->settings.use_bg_image = atoi(val); + else if (strcmp(line, "bg_opacity") == 0) b->settings.bg_opacity = atoi(val); + } + fclose(f); + } + g_free(path); +} + +/* === Lesezeichen-Verwaltung === */ +static char *get_bookmarks_path(void) { + const char *home = g_get_home_dir(); + return g_build_filename(home, ".config", "veil", "bookmarks.conf", NULL); +} + +static void save_bookmark_item(FILE *f, VeilBookmark *bm, int depth) { + for (int i = 0; i < depth; i++) fprintf(f, ">"); + if (bm->is_folder) { + fprintf(f, "F|%s\n", bm->name); + for (GList *c = bm->children; c; c = c->next) { + save_bookmark_item(f, c->data, depth + 1); + } + } else { + fprintf(f, "B|%s|%s\n", bm->name, bm->url ? bm->url : ""); + } +} + +static void save_bookmarks(VeilBrowser *b) { + char *path = get_bookmarks_path(); + char *dir = g_path_get_dirname(path); + g_mkdir_with_parents(dir, 0755); + g_free(dir); + + FILE *f = fopen(path, "w"); + if (f) { + for (GList *l = b->bookmarks; l; l = l->next) { + save_bookmark_item(f, l->data, 0); + } + fclose(f); + } + g_free(path); +} + +static void free_bookmark(VeilBookmark *bm) { + if (!bm) return; + g_free(bm->name); + g_free(bm->url); + for (GList *c = bm->children; c; c = c->next) { + free_bookmark(c->data); + } + g_list_free(bm->children); + g_free(bm); +} + +static void load_bookmarks(VeilBrowser *b) { + /* Existierende Lesezeichen leeren */ + for (GList *l = b->bookmarks; l; l = l->next) { + free_bookmark(l->data); + } + g_list_free(b->bookmarks); + b->bookmarks = NULL; + + char *path = get_bookmarks_path(); + FILE *f = fopen(path, "r"); + if (f) { + char line[512]; + VeilBookmark *folder_stack[16] = {NULL}; + int stack_depth = 0; + + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\n")] = 0; + + /* Verschachtelungstiefe zählen (Anzahl der '>' am Anfang) */ + int depth = 0; + char *p = line; + while (*p == '>') { depth++; p++; } + + stack_depth = depth; + + char *type_sep = strchr(p, '|'); + if (!type_sep) continue; + + char type = *p; + p = type_sep + 1; + + VeilBookmark *bm = g_new0(VeilBookmark, 1); + + if (type == 'F') { + bm->is_folder = TRUE; + bm->name = g_strdup(p); + bm->url = NULL; + bm->children = NULL; + } else { + /* Format: B|name|url */ + char *url_sep = strchr(p, '|'); + if (url_sep) { + *url_sep = 0; + bm->name = g_strdup(p); + bm->url = g_strdup(url_sep + 1); + } else { + bm->name = g_strdup(p); + bm->url = g_strdup(""); + } + bm->is_folder = FALSE; + } + + if (depth > 0 && stack_depth <= 15 && folder_stack[depth - 1]) { + folder_stack[depth - 1]->children = g_list_append( + folder_stack[depth - 1]->children, bm); + } else { + b->bookmarks = g_list_append(b->bookmarks, bm); + } + + if (bm->is_folder && depth < 15) { + folder_stack[depth] = bm; + } + } + fclose(f); + } + g_free(path); +} + +/* === Download-Verwaltung === */ +static char *get_downloads_path(void) { + const char *home = g_get_home_dir(); + return g_build_filename(home, ".config", "veil", "downloads.conf", NULL); +} + +static void rebuild_downloads_list(VeilBrowser *b); + +static void save_downloads(VeilBrowser *b) { + char *path = get_downloads_path(); + char *dir = g_path_get_dirname(path); + g_mkdir_with_parents(dir, 0755); + g_free(dir); + + FILE *f = fopen(path, "w"); + if (f) { + for (GList *l = b->downloads; l; l = l->next) { + VeilDownload *dl = l->data; + fprintf(f, "%s|%s|%s|%ld\n", dl->filename, dl->path, dl->url, (long)dl->timestamp); + } + fclose(f); + } + g_free(path); +} + +static void load_downloads(VeilBrowser *b) { + /* Existierende Downloads freigeben */ + for (GList *l = b->downloads; l; l = l->next) { + VeilDownload *dl = l->data; + g_free(dl->filename); + g_free(dl->path); + g_free(dl->url); + g_free(dl); + } + g_list_free(b->downloads); + b->downloads = NULL; + + char *path = get_downloads_path(); + FILE *f = fopen(path, "r"); + if (f) { + char line[1024]; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\n")] = 0; + char *sep1 = strchr(line, '|'); + if (!sep1) continue; + *sep1 = 0; + char *sep2 = strchr(sep1 + 1, '|'); + if (!sep2) continue; + *sep2 = 0; + char *sep3 = strchr(sep2 + 1, '|'); + if (!sep3) continue; + *sep3 = 0; + + VeilDownload *dl = g_new0(VeilDownload, 1); + dl->filename = g_strdup(line); + dl->path = g_strdup(sep1 + 1); + dl->url = g_strdup(sep2 + 1); + dl->timestamp = atol(sep3 + 1); + b->downloads = g_list_prepend(b->downloads, dl); + } + fclose(f); + } + g_free(path); +} + +static void rebuild_downloads_popover(VeilBrowser *b); + +static void on_clear_downloads(GtkButton *btn, VeilBrowser *b) { + (void)btn; + for (GList *l = b->downloads; l; l = l->next) { + VeilDownload *dl = l->data; + g_free(dl->filename); + g_free(dl->path); + g_free(dl->url); + g_free(dl); + } + g_list_free(b->downloads); + b->downloads = NULL; + save_downloads(b); + rebuild_downloads_popover(b); +} + +static void on_open_file(GtkButton *btn, gpointer user_data) { + (void)btn; + const char *path = (const char *)user_data; + if (!path) return; + + char *uri = g_strdup_printf("file://%s", path); + + GError *error = NULL; + gtk_show_uri_on_window(NULL, uri, GDK_CURRENT_TIME, &error); + if (error) { + g_warning("Could not open file: %s", error->message); + g_error_free(error); + } + + g_free(uri); +} + +static void on_open_folder(GtkButton *btn, gpointer user_data) { + (void)btn; + const char *path = (const char *)user_data; + if (!path) return; + + char *dir = g_path_get_dirname(path); + + /* Use xdg-open for better compatibility with file managers */ + char *cmd = g_strdup_printf("xdg-open \"%s\"", dir); + GError *error = NULL; + if (!g_spawn_command_line_async(cmd, &error)) { + g_warning("Could not open folder: %s", error->message); + g_error_free(error); + } + g_free(cmd); + g_free(dir); +} + +static void on_remove_download(GtkButton *btn, VeilBrowser *b) { + VeilDownload *dl = g_object_get_data(G_OBJECT(btn), "download"); + if (!dl) return; + + b->downloads = g_list_remove(b->downloads, dl); + g_free(dl->filename); + g_free(dl->path); + g_free(dl->url); + g_free(dl); + + save_downloads(b); + rebuild_downloads_popover(b); +} + +static void on_cancel_download(GtkButton *btn, VeilBrowser *b) { + VeilActiveDownload *ad = g_object_get_data(G_OBJECT(btn), "active_download"); + if (!ad || !ad->download) return; + + webkit_download_cancel(ad->download); + + b->active_downloads = g_list_remove(b->active_downloads, ad); + g_free(ad); + + rebuild_downloads_popover(b); +} + +static char *format_size(guint64 bytes) { + if (bytes < 1024) { + return g_strdup_printf("%lu B", (unsigned long)bytes); + } else if (bytes < 1024 * 1024) { + return g_strdup_printf("%.1f KB", bytes / 1024.0); + } else if (bytes < 1024 * 1024 * 1024) { + return g_strdup_printf("%.1f MB", bytes / (1024.0 * 1024.0)); + } else { + return g_strdup_printf("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } +} + +static char *format_speed(guint64 bytes_per_sec) { + if (bytes_per_sec < 1024) { + return g_strdup_printf("%lu B/s", (unsigned long)bytes_per_sec); + } else if (bytes_per_sec < 1024 * 1024) { + return g_strdup_printf("%.1f KB/s", bytes_per_sec / 1024.0); + } else { + return g_strdup_printf("%.1f MB/s", bytes_per_sec / (1024.0 * 1024.0)); + } +} + +static void rebuild_downloads_popover(VeilBrowser *b) { + if (!b->downloads_list_box || !b->active_downloads_box) return; + + /* Clear active downloads box */ + GList *children = gtk_container_get_children(GTK_CONTAINER(b->active_downloads_box)); + for (GList *l = children; l; l = l->next) { + gtk_widget_destroy(GTK_WIDGET(l->data)); + } + g_list_free(children); + + /* Clear completed downloads box */ + children = gtk_container_get_children(GTK_CONTAINER(b->downloads_list_box)); + for (GList *l = children; l; l = l->next) { + gtk_widget_destroy(GTK_WIDGET(l->data)); + } + g_list_free(children); + + /* Rebuild active downloads */ + if (b->active_downloads) { + for (GList *l = b->active_downloads; l; l = l->next) { + VeilActiveDownload *ad = l->data; + + GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_widget_set_margin_start(row, 4); + gtk_widget_set_margin_end(row, 4); + gtk_widget_set_margin_top(row, 6); + gtk_widget_set_margin_bottom(row, 6); + + /* Top line: filename and cancel button */ + GtkWidget *top_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + + const char *dest = webkit_download_get_destination(ad->download); + char *filename = dest ? g_path_get_basename(dest) : g_strdup("Downloading..."); + + GtkWidget *name_label = gtk_label_new(filename); + gtk_label_set_ellipsize(GTK_LABEL(name_label), PANGO_ELLIPSIZE_MIDDLE); + gtk_label_set_max_width_chars(GTK_LABEL(name_label), 28); + gtk_widget_set_halign(name_label, GTK_ALIGN_START); + gtk_widget_set_hexpand(name_label, TRUE); + gtk_box_pack_start(GTK_BOX(top_box), name_label, TRUE, TRUE, 0); + g_free(filename); + + GtkWidget *cancel_btn = gtk_button_new_from_icon_name("process-stop-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(cancel_btn), "flat"); + gtk_widget_set_tooltip_text(cancel_btn, "Abbrechen"); + g_object_set_data(G_OBJECT(cancel_btn), "active_download", ad); + g_signal_connect(cancel_btn, "clicked", G_CALLBACK(on_cancel_download), b); + gtk_box_pack_end(GTK_BOX(top_box), cancel_btn, FALSE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(row), top_box, FALSE, FALSE, 0); + + /* Progress bar */ + ad->progress_bar = gtk_progress_bar_new(); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ad->progress_bar), + webkit_download_get_estimated_progress(ad->download)); + gtk_box_pack_start(GTK_BOX(row), ad->progress_bar, FALSE, FALSE, 0); + + /* Status line */ + GtkWidget *status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + + guint64 received = webkit_download_get_received_data_length(ad->download); + WebKitURIResponse *response = webkit_download_get_response(ad->download); + guint64 total = response ? webkit_uri_response_get_content_length(response) : 0; + + char *received_str = format_size(received); + char *total_str = total > 0 ? format_size(total) : g_strdup("?"); + char *status_text = g_strdup_printf("%s / %s", received_str, total_str); + + ad->status_label = gtk_label_new(status_text); + gtk_style_context_add_class(gtk_widget_get_style_context(ad->status_label), "dim-label"); + gtk_widget_set_halign(ad->status_label, GTK_ALIGN_START); + gtk_box_pack_start(GTK_BOX(status_box), ad->status_label, FALSE, FALSE, 0); + + g_free(received_str); + g_free(total_str); + g_free(status_text); + + ad->speed_label = gtk_label_new(""); + gtk_style_context_add_class(gtk_widget_get_style_context(ad->speed_label), "dim-label"); + gtk_widget_set_halign(ad->speed_label, GTK_ALIGN_END); + gtk_widget_set_hexpand(ad->speed_label, TRUE); + gtk_box_pack_end(GTK_BOX(status_box), ad->speed_label, FALSE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(row), status_box, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(b->active_downloads_box), row); + gtk_widget_show_all(row); + } + } + + /* Abgeschlossene Downloads anzeigen */ + if (!b->downloads) { + if (!b->active_downloads) { + GtkWidget *empty_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_top(empty_box, 20); + gtk_widget_set_margin_bottom(empty_box, 20); + + GtkWidget *icon = gtk_image_new_from_icon_name("folder-download-symbolic", GTK_ICON_SIZE_DND); + gtk_style_context_add_class(gtk_widget_get_style_context(icon), "dim-label"); + gtk_box_pack_start(GTK_BOX(empty_box), icon, FALSE, FALSE, 0); + + GtkWidget *empty_label = gtk_label_new("Keine Downloads"); + gtk_style_context_add_class(gtk_widget_get_style_context(empty_label), "dim-label"); + gtk_box_pack_start(GTK_BOX(empty_box), empty_label, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(b->downloads_list_box), empty_box); + gtk_widget_show_all(empty_box); + } + return; + } + + /* Abgeschlossene Downloads zur Liste hinzufügen */ + for (GList *l = b->downloads; l; l = l->next) { + VeilDownload *dl = l->data; + + GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(row, 4); + gtk_widget_set_margin_end(row, 4); + gtk_widget_set_margin_top(row, 4); + gtk_widget_set_margin_bottom(row, 4); + + /* File icon */ + GtkWidget *icon = gtk_image_new_from_icon_name("text-x-generic-symbolic", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(row), icon, FALSE, FALSE, 0); + + /* Filename - clickable to open */ + GtkWidget *name_btn = gtk_button_new_with_label(dl->filename); + gtk_style_context_add_class(gtk_widget_get_style_context(name_btn), "flat"); + gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(name_btn))), PANGO_ELLIPSIZE_MIDDLE); + gtk_label_set_max_width_chars(GTK_LABEL(gtk_bin_get_child(GTK_BIN(name_btn))), 22); + gtk_widget_set_halign(name_btn, GTK_ALIGN_START); + gtk_widget_set_hexpand(name_btn, TRUE); + gtk_widget_set_tooltip_text(name_btn, dl->path); + g_object_set_data_full(G_OBJECT(name_btn), "path", g_strdup(dl->path), g_free); + g_signal_connect(name_btn, "clicked", G_CALLBACK(on_open_file), + g_object_get_data(G_OBJECT(name_btn), "path")); + gtk_box_pack_start(GTK_BOX(row), name_btn, TRUE, TRUE, 0); + + /* Open folder button */ + GtkWidget *folder_btn = gtk_button_new_from_icon_name("folder-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(folder_btn), "flat"); + gtk_widget_set_tooltip_text(folder_btn, "Im Ordner zeigen"); + g_object_set_data_full(G_OBJECT(folder_btn), "path", g_strdup(dl->path), g_free); + g_signal_connect(folder_btn, "clicked", G_CALLBACK(on_open_folder), + g_object_get_data(G_OBJECT(folder_btn), "path")); + gtk_box_pack_start(GTK_BOX(row), folder_btn, FALSE, FALSE, 0); + + /* Remove button */ + GtkWidget *remove_btn = gtk_button_new_from_icon_name("list-remove-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(remove_btn), "flat"); + gtk_widget_set_tooltip_text(remove_btn, "Aus Liste entfernen"); + g_object_set_data(G_OBJECT(remove_btn), "download", dl); + g_signal_connect(remove_btn, "clicked", G_CALLBACK(on_remove_download), b); + gtk_box_pack_start(GTK_BOX(row), remove_btn, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(b->downloads_list_box), row); + gtk_widget_show_all(row); + } +} + + +static void on_bookmark_click(GtkButton *btn, VeilBrowser *b); +static void rebuild_bookmarks_bar(VeilBrowser *b); + +static VeilBookmark *find_bookmark_by_url(VeilBrowser *b, const char *url) { + if (!url) return NULL; + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *bm = l->data; + if (bm->url && strcmp(bm->url, url) == 0) return bm; + /* Also search in folder children */ + if (bm->is_folder) { + for (GList *c = bm->children; c; c = c->next) { + VeilBookmark *child = c->data; + if (child->url && strcmp(child->url, url) == 0) return child; + } + } + } + return NULL; +} + +static void update_star_button(VeilBrowser *b) { + if (!b->current_tab || !b->star_btn) return; + const char *uri = webkit_web_view_get_uri(b->current_tab->webview); + if (!uri || g_str_has_prefix(uri, "veil:") || g_str_has_prefix(uri, "about:")) { + gtk_button_set_label(GTK_BUTTON(b->star_btn), "☆"); + return; + } + VeilBookmark *bm = find_bookmark_by_url(b, uri); + gtk_button_set_label(GTK_BUTTON(b->star_btn), bm ? "★" : "☆"); +} + +/* Globale Variablen für Lesezeichen-Popover */ +static GtkWidget *g_star_name_entry = NULL; +static GtkWidget *g_star_url_entry = NULL; +static GtkWidget *g_star_folder_combo = NULL; +static VeilBookmark *g_star_editing_bookmark = NULL; + +static void on_star_popover_save(GtkButton *btn, VeilBrowser *b) { + (void)btn; + if (!g_star_name_entry || !g_star_url_entry) return; + + const char *name = gtk_entry_get_text(GTK_ENTRY(g_star_name_entry)); + const char *url = gtk_entry_get_text(GTK_ENTRY(g_star_url_entry)); + + if (!name || !*name || !url || !*url) return; + + /* Ausgewählten Ordner ermitteln */ + VeilBookmark *target_folder = NULL; + if (g_star_folder_combo) { + gint active = gtk_combo_box_get_active(GTK_COMBO_BOX(g_star_folder_combo)); + if (active > 0) { + /* Ordner bei Index (active - 1) finden, da 0 "Keine" ist */ + int idx = 0; + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *bm = l->data; + if (bm->is_folder) { + if (idx == active - 1) { + target_folder = bm; + break; + } + idx++; + } + } + } + } + + if (g_star_editing_bookmark) { + /* Bestehendes Lesezeichen aktualisieren */ + g_free(g_star_editing_bookmark->name); + g_free(g_star_editing_bookmark->url); + g_star_editing_bookmark->name = g_strdup(name); + g_star_editing_bookmark->url = g_strdup(url); + } else { + /* Neues Lesezeichen erstellen */ + VeilBookmark *bm = g_new0(VeilBookmark, 1); + bm->name = g_strdup(name); + bm->url = g_strdup(url); + bm->is_folder = FALSE; + bm->children = NULL; + + if (target_folder) { + target_folder->children = g_list_append(target_folder->children, bm); + } else { + b->bookmarks = g_list_append(b->bookmarks, bm); + } + } + + save_bookmarks(b); + rebuild_bookmarks_bar(b); + update_star_button(b); + + if (b->star_popover) { + gtk_popover_popdown(GTK_POPOVER(b->star_popover)); + } +} + +static void on_star_popover_remove(GtkButton *btn, VeilBrowser *b) { + (void)btn; + if (!g_star_editing_bookmark) return; + + /* Von oberster Ebene entfernen */ + b->bookmarks = g_list_remove(b->bookmarks, g_star_editing_bookmark); + + /* Auch in Ordnern prüfen */ + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *folder = l->data; + if (folder->is_folder) { + folder->children = g_list_remove(folder->children, g_star_editing_bookmark); + } + } + + free_bookmark(g_star_editing_bookmark); + g_star_editing_bookmark = NULL; + + save_bookmarks(b); + rebuild_bookmarks_bar(b); + update_star_button(b); + + if (b->star_popover) { + gtk_popover_popdown(GTK_POPOVER(b->star_popover)); + } +} + +static void create_star_popover(VeilBrowser *b) { + if (b->star_popover) { + gtk_widget_destroy(b->star_popover); + } + + b->star_popover = gtk_popover_new(b->star_btn); + gtk_style_context_add_class(gtk_widget_get_style_context(b->star_popover), "veil-popover"); + gtk_widget_set_size_request(b->star_popover, 280, -1); + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(box), 12); + + /* Titel */ + const char *uri = b->current_tab ? webkit_web_view_get_uri(b->current_tab->webview) : NULL; + g_star_editing_bookmark = uri ? find_bookmark_by_url(b, uri) : NULL; + + GtkWidget *title = gtk_label_new(g_star_editing_bookmark ? "Lesezeichen bearbeiten" : "Lesezeichen hinzufügen"); + PangoAttrList *attrs = pango_attr_list_new(); + pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + gtk_label_set_attributes(GTK_LABEL(title), attrs); + pango_attr_list_unref(attrs); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0); + + /* Namens-Feld */ + GtkWidget *name_label = gtk_label_new("Name"); + gtk_widget_set_halign(name_label, GTK_ALIGN_START); + gtk_box_pack_start(GTK_BOX(box), name_label, FALSE, FALSE, 0); + + g_star_name_entry = gtk_entry_new(); + const char *page_title = b->current_tab ? webkit_web_view_get_title(b->current_tab->webview) : ""; + gtk_entry_set_text(GTK_ENTRY(g_star_name_entry), + g_star_editing_bookmark ? g_star_editing_bookmark->name : (page_title ? page_title : "")); + gtk_box_pack_start(GTK_BOX(box), g_star_name_entry, FALSE, FALSE, 0); + + /* URL-Feld */ + GtkWidget *url_label = gtk_label_new("URL"); + gtk_widget_set_halign(url_label, GTK_ALIGN_START); + gtk_box_pack_start(GTK_BOX(box), url_label, FALSE, FALSE, 0); + + g_star_url_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(g_star_url_entry), + g_star_editing_bookmark ? g_star_editing_bookmark->url : (uri ? uri : "")); + gtk_box_pack_start(GTK_BOX(box), g_star_url_entry, FALSE, FALSE, 0); + + /* Ordner-Auswahl (nur für neue Lesezeichen) */ + if (!g_star_editing_bookmark) { + GtkWidget *folder_label = gtk_label_new("Ordner"); + gtk_widget_set_halign(folder_label, GTK_ALIGN_START); + gtk_box_pack_start(GTK_BOX(box), folder_label, FALSE, FALSE, 0); + + g_star_folder_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(g_star_folder_combo), "Keine (Lesezeichenleiste)"); + + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *bm = l->data; + if (bm->is_folder) { + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(g_star_folder_combo), bm->name); + } + } + gtk_combo_box_set_active(GTK_COMBO_BOX(g_star_folder_combo), 0); + gtk_box_pack_start(GTK_BOX(box), g_star_folder_combo, FALSE, FALSE, 0); + } else { + g_star_folder_combo = NULL; + } + + /* Schaltflächen */ + GtkWidget *btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_top(btn_box, 8); + + if (g_star_editing_bookmark) { + GtkWidget *remove_btn = gtk_button_new_with_label("Entfernen"); + gtk_style_context_add_class(gtk_widget_get_style_context(remove_btn), "destructive-action"); + g_signal_connect(remove_btn, "clicked", G_CALLBACK(on_star_popover_remove), b); + gtk_box_pack_start(GTK_BOX(btn_box), remove_btn, TRUE, TRUE, 0); + } + + GtkWidget *save_btn = gtk_button_new_with_label(g_star_editing_bookmark ? "Speichern" : "Hinzufügen"); + gtk_style_context_add_class(gtk_widget_get_style_context(save_btn), "suggested-action"); + g_signal_connect(save_btn, "clicked", G_CALLBACK(on_star_popover_save), b); + gtk_box_pack_end(GTK_BOX(btn_box), save_btn, TRUE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(box), btn_box, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(b->star_popover), box); + gtk_widget_show_all(box); +} + +static void on_star_click(GtkButton *btn, VeilBrowser *b) { + (void)btn; + if (!b->current_tab) return; + const char *uri = webkit_web_view_get_uri(b->current_tab->webview); + if (!uri || g_str_has_prefix(uri, "veil:") || g_str_has_prefix(uri, "about:")) return; + + create_star_popover(b); + gtk_popover_popup(GTK_POPOVER(b->star_popover)); +} + +/* Forward-Deklarationen für Seitenfunktionen */ +static void show_start_page(VeilBrowser *b, VeilTab *tab); +static void veil_tab_load_url(VeilTab *tab, const char *url); +static char *get_bg_image_path(void); + +/* Alle Tabs mit der Startseite aktualisieren */ +static void refresh_start_page_tabs(VeilBrowser *b) { + for (GList *l = b->tabs; l; l = l->next) { + VeilTab *tab = l->data; + const char *uri = webkit_web_view_get_uri(tab->webview); + if (uri && (g_str_has_prefix(uri, "veil:start") || g_str_has_prefix(uri, "veil://start"))) { + show_start_page(b, tab); + } + } +} + +static void on_settings_message(WebKitUserContentManager *mgr, WebKitJavascriptResult *res, VeilBrowser *b) { + (void)mgr; + JSCValue *val = webkit_javascript_result_get_js_value(res); + char *json = jsc_value_to_string(val); + + /* Einfaches JSON parsen: {"key":"bg","value":"#123456"} */ + char *key_start = strstr(json, "\"key\":\""); + char *val_start = strstr(json, "\"value\":\""); + + if (key_start && val_start) { + key_start += 7; + val_start += 9; + + char key[16] = {0}; + char *end; + + end = strchr(key_start, '"'); + if (end && (size_t)(end - key_start) < sizeof(key)) + strncpy(key, key_start, end - key_start); + + /* Hintergrundbild speziell behandeln - enthält große Base64-Daten */ + if (strcmp(key, "bg_image") == 0) { + char *value_end = strrchr(val_start, '"'); + if (value_end && value_end > val_start) { + size_t len = value_end - val_start; + gsize decoded_len; + guchar *decoded = g_base64_decode_inplace(g_strndup(val_start, len), &decoded_len); + if (decoded && decoded_len > 0) { + char *path = get_bg_image_path(); + char *dir = g_path_get_dirname(path); + g_mkdir_with_parents(dir, 0755); + g_free(dir); + g_file_set_contents(path, (char*)decoded, decoded_len, NULL); + g_free(path); + b->settings.use_bg_image = 1; + save_settings(b); + refresh_start_page_tabs(b); /* Startseite mit neuem Bild aktualisieren */ + } + g_free(decoded); + } + } else if (strcmp(key, "download_dir") == 0) { + /* Download-Verzeichnis speziell behandeln - benötigt größeren Puffer für Pfad */ + char *value_end = strchr(val_start, '"'); + if (value_end && value_end > val_start) { + size_t len = value_end - val_start; + if (len < sizeof(b->settings.download_dir)) { + memset(b->settings.download_dir, 0, sizeof(b->settings.download_dir)); + strncpy(b->settings.download_dir, val_start, len); + save_settings(b); + } + } + } else { + char value[16] = {0}; + end = strchr(val_start, '"'); + if (end && (size_t)(end - val_start) < sizeof(value)) + strncpy(value, val_start, end - val_start); + + if (strcmp(key, "bg") == 0) strncpy(b->settings.bg_color, value, 7); + else if (strcmp(key, "accent") == 0) strncpy(b->settings.accent_color, value, 7); + else if (strcmp(key, "grad_start") == 0) strncpy(b->settings.grad_start, value, 7); + else if (strcmp(key, "grad_mid") == 0) strncpy(b->settings.grad_mid, value, 7); + else if (strcmp(key, "grad_end") == 0) strncpy(b->settings.grad_end, value, 7); + else if (strcmp(key, "search_engine") == 0) b->settings.search_engine = atoi(value); + else if (strcmp(key, "show_bookmarks") == 0) { + b->settings.show_bookmarks = atoi(value); + if (b->bookmarks_bar) { + GtkWidget *event_box = g_object_get_data(G_OBJECT(b->bookmarks_bar), "event_box"); + if (event_box) { + gtk_widget_set_visible(event_box, b->settings.show_bookmarks); + } + } + } + else if (strcmp(key, "show_quicklinks") == 0) { + b->settings.show_quicklinks = atoi(value); + } + else if (strcmp(key, "use_bg_image") == 0) { + b->settings.use_bg_image = atoi(value); + refresh_start_page_tabs(b); /* Startseite aktualisieren */ + } + else if (strcmp(key, "bg_opacity") == 0) { + b->settings.bg_opacity = atoi(value); + refresh_start_page_tabs(b); /* Startseite aktualisieren */ + } + + apply_css(b); + save_settings(b); + } + } + + g_free(json); +} + +static void on_navigate_message(WebKitUserContentManager *mgr, WebKitJavascriptResult *res, VeilBrowser *b) { + (void)mgr; + JSCValue *val = webkit_javascript_result_get_js_value(res); + char *query = jsc_value_to_string(val); + + if (b->current_tab && query && *query) { + veil_tab_load_url(b->current_tab, query); + } + + g_free(query); +} + +static void show_settings_page(VeilBrowser *b) { + if (!b->current_tab) return; + + VeilSettings *s = &b->settings; + char *html = g_strdup_printf(SETTINGS_HTML, + /* :root variables */ + s->bg_color, s->accent_color, s->grad_start, s->grad_mid, s->grad_end, + /* Search engine options (selected attribute) */ + s->search_engine == 0 ? "selected" : "", + s->search_engine == 1 ? "selected" : "", + s->search_engine == 2 ? "selected" : "", + s->search_engine == 3 ? "selected" : "", + /* Bookmarks bar toggle */ + s->show_bookmarks ? "selected" : "", + s->show_bookmarks ? "" : "selected", + /* Quick links toggle */ + s->show_quicklinks ? "selected" : "", + s->show_quicklinks ? "" : "selected", + /* Download directory */ + s->download_dir, + /* Background image toggle */ + s->use_bg_image ? "selected" : "", + s->use_bg_image ? "" : "selected", + /* Background opacity slider */ + s->bg_opacity, s->bg_opacity, + /* bg input */ + s->bg_color, DEFAULT_SETTINGS.bg_color, + /* accent input */ + s->accent_color, DEFAULT_SETTINGS.accent_color, + /* grad_start input */ + s->grad_start, DEFAULT_SETTINGS.grad_start, + /* grad_mid input */ + s->grad_mid, DEFAULT_SETTINGS.grad_mid, + /* grad_end input */ + s->grad_end, DEFAULT_SETTINGS.grad_end, + /* resetAll defaults */ + DEFAULT_SETTINGS.bg_color, DEFAULT_SETTINGS.accent_color, + DEFAULT_SETTINGS.grad_start, DEFAULT_SETTINGS.grad_mid, DEFAULT_SETTINGS.grad_end + ); + + webkit_web_view_load_html(b->current_tab->webview, html, "veil://settings/"); + gtk_entry_set_text(GTK_ENTRY(b->url_entry), "veil:settings"); + g_free(html); +} + +static char *get_bg_image_path(void) { + const char *home = g_get_home_dir(); + return g_build_filename(home, ".config", "veil", "background.png", NULL); +} + +static char *get_bg_image_data_url(void) { + char *path = get_bg_image_path(); + if (!g_file_test(path, G_FILE_TEST_EXISTS)) { + g_free(path); + return g_strdup(""); + } + + gchar *contents; + gsize length; + if (g_file_get_contents(path, &contents, &length, NULL)) { + /* Detect MIME type from magic bytes */ + const char *mime_type = "image/png"; /* default */ + if (length >= 2) { + guchar *data = (guchar*)contents; + if (data[0] == 0xFF && data[1] == 0xD8) { + mime_type = "image/jpeg"; + } else if (length >= 4 && data[0] == 0x89 && data[1] == 0x50 && + data[2] == 0x4E && data[3] == 0x47) { + mime_type = "image/png"; + } else if (length >= 4 && data[0] == 'G' && data[1] == 'I' && + data[2] == 'F' && data[3] == '8') { + mime_type = "image/gif"; + } else if (length >= 4 && data[0] == 'R' && data[1] == 'I' && + data[2] == 'F' && data[3] == 'F') { + mime_type = "image/webp"; + } + } + gchar *base64 = g_base64_encode((guchar*)contents, length); + gchar *data_url = g_strdup_printf("data:%s;base64,%s", mime_type, base64); + g_free(contents); + g_free(base64); + g_free(path); + return data_url; + } + + g_free(path); + return g_strdup(""); +} + +static char *get_icon_data_url(void) { + /* Mehrere Pfade versuchen - 128x128 bevorzugen für bessere Qualität */ + const char *relative_paths[] = { + "data/icons/hicolor/128x128/apps/veil.png", + "data/icons/hicolor/256x256/apps/veil.png", + NULL + }; + const char *absolute_paths[] = { + "/usr/share/icons/hicolor/128x128/apps/veil.png", + "/usr/share/icons/hicolor/256x256/apps/veil.png", + "/usr/local/share/icons/hicolor/128x128/apps/veil.png", + "/usr/local/share/icons/hicolor/256x256/apps/veil.png", + NULL + }; + + /* Zuerst relative Pfade versuchen (Entwicklung) */ + char *cwd = g_get_current_dir(); + for (int i = 0; relative_paths[i]; i++) { + char *full_path = g_build_filename(cwd, relative_paths[i], NULL); + if (g_file_test(full_path, G_FILE_TEST_EXISTS)) { + gchar *contents; + gsize length; + if (g_file_get_contents(full_path, &contents, &length, NULL)) { + gchar *base64 = g_base64_encode((guchar*)contents, length); + gchar *data_url = g_strdup_printf("data:image/png;base64,%s", base64); + g_free(contents); + g_free(base64); + g_free(full_path); + g_free(cwd); + return data_url; + } + } + g_free(full_path); + } + g_free(cwd); + + /* Absolute Pfade versuchen (installiert) */ + for (int i = 0; absolute_paths[i]; i++) { + if (g_file_test(absolute_paths[i], G_FILE_TEST_EXISTS)) { + gchar *contents; + gsize length; + if (g_file_get_contents(absolute_paths[i], &contents, &length, NULL)) { + gchar *base64 = g_base64_encode((guchar*)contents, length); + gchar *data_url = g_strdup_printf("data:image/png;base64,%s", base64); + g_free(contents); + g_free(base64); + return data_url; + } + } + } + + return g_strdup(""); +} + +static void show_start_page(VeilBrowser *b, VeilTab *tab) { + if (!tab) tab = b->current_tab; + if (!tab) return; + + VeilSettings *s = &b->settings; + char *icon_path = get_icon_data_url(); + + /* Hintergrundbild-Stil erstellen, wenn aktiviert */ + char *bg_style = g_strdup(""); + if (s->use_bg_image) { + char *bg_data = get_bg_image_data_url(); + if (bg_data && *bg_data) { + g_free(bg_style); + /* Linear-Gradient-Overlay für Deckkraft-Effekt verwenden */ + float opacity = (100 - s->bg_opacity) / 100.0f; + bg_style = g_strdup_printf( + "background-image: linear-gradient(rgba(10,10,15,%.2f), rgba(10,10,15,%.2f)), url('%s'); " + "background-size: cover, cover; background-position: center, center;", + opacity, opacity, bg_data); + } + g_free(bg_data); + } + + char *html = g_strdup_printf(START_HTML, + s->bg_color, s->accent_color, s->grad_start, s->grad_mid, s->grad_end, + bg_style, /* Hintergrundbild-Stil */ + s->show_quicklinks ? "block" : "none", /* Quick-Links-Sichtbarkeit */ + icon_path /* Logo-Bild */ + ); + + webkit_web_view_load_html(tab->webview, html, "veil://start/"); + if (tab == b->current_tab) { + gtk_entry_set_text(GTK_ENTRY(b->url_entry), "veil:start"); + } + g_free(bg_style); + g_free(icon_path); + g_free(html); +} + +static VeilTab *veil_tab_new(VeilBrowser *browser, const char *url); +static void veil_browser_new_tab(VeilBrowser *browser, const char *url); +static void veil_browser_close_tab(VeilBrowser *browser, VeilTab *tab); +static void veil_browser_switch_to_tab(VeilBrowser *browser, VeilTab *tab); + +static void on_minimize(GtkButton *btn, VeilBrowser *b) { (void)btn; gtk_window_iconify(GTK_WINDOW(b->window)); } +static void on_maximize(GtkButton *btn, VeilBrowser *b) { + (void)btn; + if (b->is_maximized) gtk_window_unmaximize(GTK_WINDOW(b->window)); + else gtk_window_maximize(GTK_WINDOW(b->window)); +} +static void on_close_window(GtkButton *btn, VeilBrowser *b) { (void)btn; (void)b; gtk_main_quit(); } +static gboolean on_window_state(GtkWidget *w, GdkEventWindowState *e, VeilBrowser *b) { + (void)w; b->is_maximized = (e->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) != 0; + return FALSE; +} + +static void on_load_changed(WebKitWebView *wv, WebKitLoadEvent ev, VeilTab *tab) { + (void)wv; VeilBrowser *b = tab->browser; + gtk_widget_set_sensitive(b->back_btn, webkit_web_view_can_go_back(tab->webview)); + gtk_widget_set_sensitive(b->forward_btn, webkit_web_view_can_go_forward(tab->webview)); + if (ev == WEBKIT_LOAD_FINISHED) gtk_widget_hide(b->progress_bar); +} + +static void on_title_changed(WebKitWebView *wv, GParamSpec *p, VeilTab *tab) { + (void)p; + const char *t = webkit_web_view_get_title(wv); + const char *uri = webkit_web_view_get_uri(wv); + if (uri && (g_str_has_prefix(uri, "veil:settings") || g_str_has_prefix(uri, "veil://settings"))) + gtk_label_set_text(GTK_LABEL(tab->tab_label), "Settings"); + else if (uri && (g_str_has_prefix(uri, "veil:start") || g_str_has_prefix(uri, "veil://start"))) + gtk_label_set_text(GTK_LABEL(tab->tab_label), "Start"); + else + gtk_label_set_text(GTK_LABEL(tab->tab_label), (t && *t) ? t : "New Tab"); +} + +static void on_uri_changed(WebKitWebView *wv, GParamSpec *p, VeilTab *tab) { + (void)p; + if (tab != tab->browser->current_tab) return; + const char *uri = webkit_web_view_get_uri(wv); + if (uri) gtk_entry_set_text(GTK_ENTRY(tab->browser->url_entry), uri); + update_star_button(tab->browser); +} + +static void on_progress(WebKitWebView *wv, GParamSpec *p, VeilTab *tab) { + (void)p; + if (tab != tab->browser->current_tab) return; + double prog = webkit_web_view_get_estimated_load_progress(wv); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(tab->browser->progress_bar), prog); + if (prog < 1.0) gtk_widget_show(tab->browser->progress_bar); + else gtk_widget_hide(tab->browser->progress_bar); +} + +static void on_close_tab(GtkButton *btn, VeilTab *tab) { (void)btn; veil_browser_close_tab(tab->browser, tab); } + +/* target="_blank"-Links behandeln - in neuem Tab öffnen */ +static GtkWidget *on_create_new_window(WebKitWebView *wv, WebKitNavigationAction *nav, VeilTab *tab) { + (void)wv; + WebKitURIRequest *req = webkit_navigation_action_get_request(nav); + const char *uri = webkit_uri_request_get_uri(req); + if (uri) { + veil_browser_new_tab(tab->browser, uri); + } + return NULL; /* NULL zurückgeben, um zu verhindern, dass WebKit ein eigenes Fenster erstellt */ +} + +/* Navigations-Policy-Entscheidungen behandeln */ +static gboolean on_decide_policy(WebKitWebView *wv, WebKitPolicyDecision *decision, + WebKitPolicyDecisionType type, VeilTab *tab) { + (void)wv; + (void)tab; + + if (type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION || + type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { + WebKitNavigationPolicyDecision *nav = WEBKIT_NAVIGATION_POLICY_DECISION(decision); + WebKitNavigationAction *action = webkit_navigation_policy_decision_get_navigation_action(nav); + WebKitURIRequest *req = webkit_navigation_action_get_request(action); + const char *uri = webkit_uri_request_get_uri(req); + + /* Alle http/https/file/about-Navigationen erlauben */ + if (uri && (g_str_has_prefix(uri, "http://") || + g_str_has_prefix(uri, "https://") || + g_str_has_prefix(uri, "file://") || + g_str_has_prefix(uri, "about:"))) { + webkit_policy_decision_use(decision); + return TRUE; + } + } + + /* WebKit andere Entscheidungen überlassen */ + return FALSE; +} + +static gboolean on_tab_click(GtkWidget *w, GdkEventButton *e, VeilTab *tab) { + (void)w; + if (e->button == 1) { veil_browser_switch_to_tab(tab->browser, tab); return FALSE; } + if (e->button == 2) { veil_browser_close_tab(tab->browser, tab); return TRUE; } + return FALSE; +} + +static void on_url_activate(GtkEntry *e, VeilBrowser *b) { + if (b->current_tab) veil_tab_load_url(b->current_tab, gtk_entry_get_text(e)); +} + +/* Gesamten Text automatisch markieren, wenn URL-Leiste angeklickt wird */ +static gboolean on_url_focus(GtkWidget *w, GdkEventFocus *e, VeilBrowser *b) { + (void)e; (void)b; + gtk_editable_select_region(GTK_EDITABLE(w), 0, -1); + return FALSE; +} + +static void on_back(GtkButton *btn, VeilBrowser *b) { (void)btn; if (b->current_tab) webkit_web_view_go_back(b->current_tab->webview); } +static void on_forward(GtkButton *btn, VeilBrowser *b) { (void)btn; if (b->current_tab) webkit_web_view_go_forward(b->current_tab->webview); } +static void on_reload(GtkButton *btn, VeilBrowser *b) { + (void)btn; + if (!b->current_tab) return; + const char *uri = gtk_entry_get_text(GTK_ENTRY(b->url_entry)); + /* Interne veil:-Seiten durch Neu-Rendern aktualisieren */ + if (uri && g_str_has_prefix(uri, "veil:")) { + veil_tab_load_url(b->current_tab, uri); + } else { + webkit_web_view_reload(b->current_tab->webview); + } +} +static void on_new_tab(GtkButton *btn, VeilBrowser *b) { (void)btn; veil_browser_new_tab(b, "veil:start"); } + +static void on_bookmark_click(GtkButton *btn, VeilBrowser *b) { + const char *url = g_object_get_data(G_OBJECT(btn), "url"); + if (url && b->current_tab) { + veil_tab_load_url(b->current_tab, url); + } +} + +/* Lesezeichen-Kontextmenü */ +static VeilBookmark *g_context_bookmark = NULL; +static VeilBookmark *g_context_parent_folder = NULL; + +static void on_bookmark_edit(GtkMenuItem *item, VeilBrowser *b) { + (void)item; + if (!g_context_bookmark) return; + + GtkWidget *dialog = gtk_dialog_new_with_buttons( + g_context_bookmark->is_folder ? "Ordner bearbeiten" : "Lesezeichen bearbeiten", + GTK_WINDOW(b->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Abbrechen", GTK_RESPONSE_CANCEL, "Speichern", GTK_RESPONSE_OK, NULL); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_container_set_border_width(GTK_CONTAINER(content), 12); + + GtkWidget *name_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(name_entry), g_context_bookmark->name); + gtk_box_pack_start(GTK_BOX(content), gtk_label_new("Name:"), FALSE, FALSE, 4); + gtk_box_pack_start(GTK_BOX(content), name_entry, FALSE, FALSE, 4); + + GtkWidget *url_entry = NULL; + if (!g_context_bookmark->is_folder) { + url_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(url_entry), g_context_bookmark->url ? g_context_bookmark->url : ""); + gtk_box_pack_start(GTK_BOX(content), gtk_label_new("URL:"), FALSE, FALSE, 4); + gtk_box_pack_start(GTK_BOX(content), url_entry, FALSE, FALSE, 4); + } + + gtk_widget_show_all(dialog); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { + g_free(g_context_bookmark->name); + g_context_bookmark->name = g_strdup(gtk_entry_get_text(GTK_ENTRY(name_entry))); + if (url_entry) { + g_free(g_context_bookmark->url); + g_context_bookmark->url = g_strdup(gtk_entry_get_text(GTK_ENTRY(url_entry))); + } + save_bookmarks(b); + rebuild_bookmarks_bar(b); + } + gtk_widget_destroy(dialog); +} + +static void on_bookmark_delete(GtkMenuItem *item, VeilBrowser *b) { + (void)item; + if (!g_context_bookmark) return; + + if (g_context_parent_folder) { + g_context_parent_folder->children = g_list_remove(g_context_parent_folder->children, g_context_bookmark); + } else { + b->bookmarks = g_list_remove(b->bookmarks, g_context_bookmark); + } + + free_bookmark(g_context_bookmark); + g_context_bookmark = NULL; + g_context_parent_folder = NULL; + + save_bookmarks(b); + rebuild_bookmarks_bar(b); + update_star_button(b); +} + +static void on_bookmark_move_to_folder(GtkMenuItem *item, VeilBrowser *b) { + VeilBookmark *target_folder = g_object_get_data(G_OBJECT(item), "folder"); + if (!g_context_bookmark) return; + + if (g_context_parent_folder) { + g_context_parent_folder->children = g_list_remove(g_context_parent_folder->children, g_context_bookmark); + } else { + b->bookmarks = g_list_remove(b->bookmarks, g_context_bookmark); + } + + if (target_folder) { + target_folder->children = g_list_append(target_folder->children, g_context_bookmark); + } else { + b->bookmarks = g_list_append(b->bookmarks, g_context_bookmark); + } + + save_bookmarks(b); + rebuild_bookmarks_bar(b); +} + +static VeilBookmark *find_parent_folder(VeilBrowser *b, VeilBookmark *child) { + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *folder = l->data; + if (folder->is_folder) { + for (GList *c = folder->children; c; c = c->next) { + if (c->data == child) return folder; + } + } + } + return NULL; +} + +static gboolean on_bookmark_button_press(GtkWidget *w, GdkEventButton *e, VeilBrowser *b) { + if (e->button == 3) { /* Rechtsklick */ + g_context_bookmark = g_object_get_data(G_OBJECT(w), "bookmark"); + g_context_parent_folder = find_parent_folder(b, g_context_bookmark); + + GtkWidget *menu = gtk_menu_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(menu), "veil-menu"); + + /* Bearbeiten */ + GtkWidget *edit_item = gtk_menu_item_new_with_label("Bearbeiten"); + g_signal_connect(edit_item, "activate", G_CALLBACK(on_bookmark_edit), b); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), edit_item); + + /* Untermenü "Verschieben nach" (nur für Lesezeichen) */ + if (!g_context_bookmark->is_folder) { + gboolean has_folders = FALSE; + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *folder = l->data; + if (folder->is_folder) { has_folders = TRUE; break; } + } + + if (has_folders || g_context_parent_folder) { + GtkWidget *move_item = gtk_menu_item_new_with_label("Verschieben nach"); + GtkWidget *move_menu = gtk_menu_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(move_menu), "veil-menu"); + + if (g_context_parent_folder) { + GtkWidget *root_item = gtk_menu_item_new_with_label("Lesezeichenleiste"); + g_object_set_data(G_OBJECT(root_item), "folder", NULL); + g_signal_connect(root_item, "activate", G_CALLBACK(on_bookmark_move_to_folder), b); + gtk_menu_shell_append(GTK_MENU_SHELL(move_menu), root_item); + } + + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *folder = l->data; + if (folder->is_folder && folder != g_context_parent_folder) { + GtkWidget *folder_item = gtk_menu_item_new_with_label(folder->name); + g_object_set_data(G_OBJECT(folder_item), "folder", folder); + g_signal_connect(folder_item, "activate", G_CALLBACK(on_bookmark_move_to_folder), b); + gtk_menu_shell_append(GTK_MENU_SHELL(move_menu), folder_item); + } + } + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(move_item), move_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), move_item); + } + } + + /* Trennlinie */ + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + + /* Löschen */ + GtkWidget *delete_item = gtk_menu_item_new_with_label("Löschen"); + g_signal_connect(delete_item, "activate", G_CALLBACK(on_bookmark_delete), b); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), delete_item); + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)e); + return TRUE; + } + return FALSE; +} + +static void on_folder_child_click(GtkButton *btn, VeilBrowser *b) { + const char *url = g_object_get_data(G_OBJECT(btn), "url"); + if (url && b->current_tab) { + veil_tab_load_url(b->current_tab, url); + } + /* Popover schließen und Verfolgung aufheben */ + GtkWidget *popover = g_object_get_data(G_OBJECT(btn), "popover"); + if (popover) { + gtk_popover_popdown(GTK_POPOVER(popover)); + if (b->current_folder_popover == popover) { + b->current_folder_popover = NULL; + } + } +} + +static void on_folder_click(GtkButton *btn, VeilBrowser *b) { + GtkWidget *popover = g_object_get_data(G_OBJECT(btn), "popover"); + if (!popover || !GTK_IS_POPOVER(popover)) return; + + /* Zuvor geöffnetes Ordner-Popover schließen */ + if (b->current_folder_popover && b->current_folder_popover != popover) { + gtk_popover_popdown(GTK_POPOVER(b->current_folder_popover)); + } + + /* Dieses Ordner-Popover öffnen und verfolgen */ + gtk_popover_popup(GTK_POPOVER(popover)); + b->current_folder_popover = popover; +} + +static void create_folder_popover(GtkWidget *btn, VeilBookmark *folder, VeilBrowser *b) { + GtkWidget *popover = gtk_popover_new(btn); + gtk_style_context_add_class(gtk_widget_get_style_context(popover), "veil-popover"); + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_set_border_width(GTK_CONTAINER(box), 4); + + if (!folder->children) { + GtkWidget *empty = gtk_label_new("(leer)"); + gtk_style_context_add_class(gtk_widget_get_style_context(empty), "dim-label"); + gtk_widget_set_margin_start(empty, 12); + gtk_widget_set_margin_end(empty, 12); + gtk_widget_set_margin_top(empty, 8); + gtk_widget_set_margin_bottom(empty, 8); + gtk_box_pack_start(GTK_BOX(box), empty, FALSE, FALSE, 0); + } else { + for (GList *c = folder->children; c; c = c->next) { + VeilBookmark *child = c->data; + + GtkWidget *item = gtk_button_new_with_label(child->name); + gtk_style_context_add_class(gtk_widget_get_style_context(item), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(item), "bookmark-btn"); + gtk_widget_set_halign(item, GTK_ALIGN_START); + + if (!child->is_folder && child->url) { + g_object_set_data(G_OBJECT(item), "url", child->url); + g_object_set_data(G_OBJECT(item), "popover", popover); + g_signal_connect(item, "clicked", G_CALLBACK(on_folder_child_click), b); + } + gtk_box_pack_start(GTK_BOX(box), item, FALSE, FALSE, 0); + } + } + + gtk_container_add(GTK_CONTAINER(popover), box); + gtk_widget_show_all(box); + + g_object_set_data(G_OBJECT(btn), "popover", popover); + g_signal_connect(btn, "clicked", G_CALLBACK(on_folder_click), b); +} + +static void on_add_folder(GtkMenuItem *item, VeilBrowser *b) { + (void)item; + + GtkWidget *dialog = gtk_dialog_new_with_buttons("Neuer Ordner", + GTK_WINDOW(b->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Abbrechen", GTK_RESPONSE_CANCEL, "Erstellen", GTK_RESPONSE_OK, NULL); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_container_set_border_width(GTK_CONTAINER(content), 12); + + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "Ordnername"); + gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 4); + + gtk_widget_show_all(dialog); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { + const char *name = gtk_entry_get_text(GTK_ENTRY(entry)); + if (name && *name) { + VeilBookmark *folder = g_new0(VeilBookmark, 1); + folder->name = g_strdup(name); + folder->is_folder = TRUE; + folder->url = NULL; + folder->children = NULL; + b->bookmarks = g_list_append(b->bookmarks, folder); + save_bookmarks(b); + rebuild_bookmarks_bar(b); + } + } + gtk_widget_destroy(dialog); +} + + +/* Drag & Drop für Lesezeichen */ +static VeilBookmark *g_drag_bookmark = NULL; + +static void on_bookmark_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer data) { + (void)context; + (void)data; + g_drag_bookmark = g_object_get_data(G_OBJECT(widget), "bookmark"); +} + +static gboolean on_folder_drag_drop(GtkWidget *widget, GdkDragContext *context, + gint x, gint y, guint time, VeilBrowser *b) { + (void)x; (void)y; + VeilBookmark *folder = g_object_get_data(G_OBJECT(widget), "bookmark"); + + if (g_drag_bookmark && folder && folder->is_folder && g_drag_bookmark != folder) { + /* Aus übergeordnetem Ordner entfernen, falls vorhanden, andernfalls von oberster Ebene */ + VeilBookmark *parent = find_parent_folder(b, g_drag_bookmark); + if (parent) { + parent->children = g_list_remove(parent->children, g_drag_bookmark); + } else { + b->bookmarks = g_list_remove(b->bookmarks, g_drag_bookmark); + } + /* Zu Zielordner hinzufügen */ + folder->children = g_list_append(folder->children, g_drag_bookmark); + save_bookmarks(b); + rebuild_bookmarks_bar(b); + } + + gtk_drag_finish(context, TRUE, FALSE, time); + g_drag_bookmark = NULL; + return TRUE; +} + +static gboolean on_bookmarks_bar_button_press(GtkWidget *w, GdkEventButton *e, VeilBrowser *b) { + (void)w; + if (e->button == 3) { /* Rechtsklick auf leeren Bereich */ + GtkWidget *menu = gtk_menu_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(menu), "veil-menu"); + + GtkWidget *new_folder = gtk_menu_item_new_with_label("Neuer Ordner"); + g_signal_connect(new_folder, "activate", G_CALLBACK(on_add_folder), b); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), new_folder); + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)e); + return TRUE; + } + return FALSE; +} + +static void rebuild_bookmarks_bar(VeilBrowser *b) { + if (!b->bookmarks_bar) return; + + /* Ordner-Popover-Verfolgung löschen (Popovers werden mit Kindern zerstört) */ + b->current_folder_popover = NULL; + + /* Alle Kinder entfernen */ + GList *children = gtk_container_get_children(GTK_CONTAINER(b->bookmarks_bar)); + for (GList *l = children; l; l = l->next) { + gtk_widget_destroy(GTK_WIDGET(l->data)); + } + g_list_free(children); + + /* Drag-Zieltypen */ + static GtkTargetEntry target_entries[] = { + {"VEIL_BOOKMARK", GTK_TARGET_SAME_APP, 0} + }; + + /* Lesezeichen und Ordner hinzufügen */ + for (GList *l = b->bookmarks; l; l = l->next) { + VeilBookmark *bm = l->data; + + if (bm->is_folder) { + GtkWidget *btn = gtk_button_new_with_label(bm->name); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), "bookmark-folder"); + gtk_button_set_image(GTK_BUTTON(btn), + gtk_image_new_from_icon_name("folder-symbolic", GTK_ICON_SIZE_MENU)); + gtk_button_set_always_show_image(GTK_BUTTON(btn), TRUE); + g_object_set_data(G_OBJECT(btn), "bookmark", bm); + g_signal_connect(btn, "button-press-event", G_CALLBACK(on_bookmark_button_press), b); + create_folder_popover(btn, bm, b); + + /* Als Drop-Ziel aktivieren */ + gtk_drag_dest_set(btn, GTK_DEST_DEFAULT_ALL, target_entries, 1, GDK_ACTION_MOVE); + g_signal_connect(btn, "drag-drop", G_CALLBACK(on_folder_drag_drop), b); + + gtk_box_pack_start(GTK_BOX(b->bookmarks_bar), btn, FALSE, FALSE, 0); + gtk_widget_show(btn); + } else { + GtkWidget *btn = gtk_button_new_with_label(bm->name); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), "bookmark-btn"); + g_object_set_data(G_OBJECT(btn), "url", bm->url); + g_object_set_data(G_OBJECT(btn), "bookmark", bm); + g_signal_connect(btn, "clicked", G_CALLBACK(on_bookmark_click), b); + g_signal_connect(btn, "button-press-event", G_CALLBACK(on_bookmark_button_press), b); + + /* Als Drag-Quelle aktivieren */ + gtk_drag_source_set(btn, GDK_BUTTON1_MASK, target_entries, 1, GDK_ACTION_MOVE); + g_signal_connect(btn, "drag-begin", G_CALLBACK(on_bookmark_drag_begin), NULL); + + gtk_box_pack_start(GTK_BOX(b->bookmarks_bar), btn, FALSE, FALSE, 0); + gtk_widget_show(btn); + } + } +} + +static void on_menu_settings(GtkMenuItem *item, VeilBrowser *b) { + (void)item; + show_settings_page(b); +} + +static void on_menu_new_tab(GtkMenuItem *item, VeilBrowser *b) { + (void)item; + veil_browser_new_tab(b, "veil:start"); +} + +static gboolean on_key(GtkWidget *w, GdkEventKey *e, VeilBrowser *b) { + (void)w; + if (e->state & GDK_CONTROL_MASK) { + switch (e->keyval) { + case GDK_KEY_t: veil_browser_new_tab(b, "veil:start"); return TRUE; + case GDK_KEY_w: if (b->current_tab) veil_browser_close_tab(b, b->current_tab); return TRUE; + case GDK_KEY_l: + gtk_widget_grab_focus(b->url_entry); + gtk_editable_select_region(GTK_EDITABLE(b->url_entry), 0, -1); + return TRUE; + case GDK_KEY_r: on_reload(NULL, b); return TRUE; + case GDK_KEY_comma: show_settings_page(b); return TRUE; + } + } + if (e->keyval == GDK_KEY_F5) { on_reload(NULL, b); return TRUE; } + if (e->keyval == GDK_KEY_F11) { + if (b->is_maximized) gtk_window_unmaximize(GTK_WINDOW(b->window)); + else gtk_window_maximize(GTK_WINDOW(b->window)); + return TRUE; + } + return FALSE; +} + +static WebKitWebView *create_webview_with_settings(VeilBrowser *b) { + WebKitWebView *wv = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(b->web_context)); + + WebKitSettings *s = webkit_web_view_get_settings(wv); + /* Kerneinstellungen */ + webkit_settings_set_enable_javascript(s, TRUE); + webkit_settings_set_javascript_can_access_clipboard(s, TRUE); + webkit_settings_set_user_agent(s, "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"); + + /* === PERFORMANCE-OPTIMIERUNGEN === */ + + /* Hardware-Beschleunigung - IMMER für beste Performance (DMABUF in main() deaktiviert) */ + webkit_settings_set_hardware_acceleration_policy(s, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); + webkit_settings_set_enable_webgl(s, TRUE); + + /* Sanftes Scrollen deaktiviert für schnellere Reaktion */ + webkit_settings_set_enable_smooth_scrolling(s, FALSE); + + /* Media-Optimierungen */ + webkit_settings_set_enable_media_stream(s, TRUE); + webkit_settings_set_enable_mediasource(s, TRUE); + webkit_settings_set_media_playback_requires_user_gesture(s, FALSE); + + /* JavaScript-JIT-Optimierung */ + webkit_settings_set_enable_javascript_markup(s, TRUE); + + /* Seiten-Cache für schnellere Vor-/Zurück-Navigation */ + webkit_settings_set_enable_page_cache(s, TRUE); + + /* Konsolennachrichten nicht ausgeben (Debugging kann in Produktion verlangsamen) */ + webkit_settings_set_enable_write_console_messages_to_stdout(s, FALSE); + + /* Entwickler-Extras für Performance deaktiviert */ + g_object_set(s, "enable-developer-extras", FALSE, NULL); + + /* Nachrichten-Handler für interne Seiten registrieren */ + WebKitUserContentManager *mgr = webkit_web_view_get_user_content_manager(wv); + g_signal_connect(mgr, "script-message-received::settings", G_CALLBACK(on_settings_message), b); + webkit_user_content_manager_register_script_message_handler(mgr, "settings"); + g_signal_connect(mgr, "script-message-received::navigate", G_CALLBACK(on_navigate_message), b); + webkit_user_content_manager_register_script_message_handler(mgr, "navigate"); + + /* Farbschema auf dunkel setzen für native dunkle Formularelemente ohne Seitenstile zu beeinträchtigen */ + const char *dark_scheme_css = ":root { color-scheme: dark; }"; + + WebKitUserStyleSheet *style = webkit_user_style_sheet_new( + dark_scheme_css, + WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, + WEBKIT_USER_STYLE_LEVEL_USER, + NULL, NULL); + webkit_user_content_manager_add_style_sheet(mgr, style); + + return wv; +} + +static VeilTab *veil_tab_new(VeilBrowser *browser, const char *url) { + VeilTab *tab = g_new0(VeilTab, 1); + tab->browser = browser; + + tab->webview = create_webview_with_settings(browser); + gtk_widget_set_size_request(GTK_WIDGET(tab->webview), 100, 100); + gtk_widget_set_vexpand(GTK_WIDGET(tab->webview), TRUE); + gtk_widget_set_hexpand(GTK_WIDGET(tab->webview), TRUE); + + g_signal_connect(tab->webview, "load-changed", G_CALLBACK(on_load_changed), tab); + g_signal_connect(tab->webview, "notify::title", G_CALLBACK(on_title_changed), tab); + g_signal_connect(tab->webview, "notify::uri", G_CALLBACK(on_uri_changed), tab); + g_signal_connect(tab->webview, "notify::estimated-load-progress", G_CALLBACK(on_progress), tab); + g_signal_connect(tab->webview, "create", G_CALLBACK(on_create_new_window), tab); + g_signal_connect(tab->webview, "decide-policy", G_CALLBACK(on_decide_policy), tab); + + GtkWidget *ebox = gtk_event_box_new(); + gtk_widget_set_events(ebox, GDK_BUTTON_PRESS_MASK); + g_signal_connect(ebox, "button-press-event", G_CALLBACK(on_tab_click), tab); + + tab->tab_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_style_context_add_class(gtk_widget_get_style_context(tab->tab_box), "tab-box"); + gtk_container_add(GTK_CONTAINER(ebox), tab->tab_box); + + tab->tab_label = gtk_label_new("New Tab"); + gtk_label_set_ellipsize(GTK_LABEL(tab->tab_label), PANGO_ELLIPSIZE_END); + gtk_label_set_width_chars(GTK_LABEL(tab->tab_label), 12); + gtk_label_set_max_width_chars(GTK_LABEL(tab->tab_label), 16); + + tab->close_button = gtk_button_new_from_icon_name("window-close-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(tab->close_button), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(tab->close_button), "tab-close"); + g_signal_connect(tab->close_button, "clicked", G_CALLBACK(on_close_tab), tab); + + gtk_box_pack_start(GTK_BOX(tab->tab_box), tab->tab_label, FALSE, FALSE, 0); + gtk_box_pack_end(GTK_BOX(tab->tab_box), tab->close_button, FALSE, FALSE, 0); + + g_object_set_data(G_OBJECT(tab->tab_box), "ebox", ebox); + + gtk_widget_show_all(ebox); + gtk_widget_show_all(GTK_WIDGET(tab->webview)); + + if (url && strcmp(url, "about:blank") != 0) veil_tab_load_url(tab, url); + return tab; +} + +static void veil_tab_load_url(VeilTab *tab, const char *url) { + if (!tab || !url || !*url) return; + + /* Interne veil:-Seiten behandeln */ + if (strcmp(url, "veil:settings") == 0 || strcmp(url, "veil://settings/") == 0) { + show_settings_page(tab->browser); + return; + } + if (strcmp(url, "veil:start") == 0 || strcmp(url, "veil://start/") == 0 || + strcmp(url, "about:blank") == 0) { + show_start_page(tab->browser, tab); + return; + } + + char *final; + if (g_str_has_prefix(url, "http://") || g_str_has_prefix(url, "https://") || + g_str_has_prefix(url, "veil:") || g_str_has_prefix(url, "file://")) { + final = g_strdup(url); + } else if (strchr(url, '.') && !strchr(url, ' ')) { + final = g_strdup_printf("https://%s", url); + } else { + char *enc = g_uri_escape_string(url, NULL, TRUE); + const char *search_urls[] = { + "https://duckduckgo.com/?q=%s", + "https://www.google.com/search?q=%s", + "https://search.brave.com/search?q=%s", + "https://www.startpage.com/do/search?q=%s" + }; + int se = tab->browser->settings.search_engine; + if (se < 0 || se > 3) se = 0; + final = g_strdup_printf(search_urls[se], enc); + g_free(enc); + } + webkit_web_view_load_uri(tab->webview, final); + g_free(final); +} + +static void veil_browser_new_tab(VeilBrowser *b, const char *url) { + VeilTab *tab = veil_tab_new(b, url); + b->tabs = g_list_append(b->tabs, tab); + + GtkWidget *ebox = g_object_get_data(G_OBJECT(tab->tab_box), "ebox"); + gtk_box_pack_start(GTK_BOX(b->tab_container), ebox, FALSE, FALSE, 0); + + char *name = g_strdup_printf("t%p", (void*)tab); + gtk_stack_add_named(GTK_STACK(b->webview_stack), GTK_WIDGET(tab->webview), name); + g_free(name); + + veil_browser_switch_to_tab(b, tab); +} + +static void veil_browser_switch_to_tab(VeilBrowser *b, VeilTab *tab) { + if (b->current_tab) + gtk_style_context_remove_class(gtk_widget_get_style_context(b->current_tab->tab_box), "active"); + + b->current_tab = tab; + gtk_style_context_add_class(gtk_widget_get_style_context(tab->tab_box), "active"); + + char *name = g_strdup_printf("t%p", (void*)tab); + gtk_stack_set_visible_child_name(GTK_STACK(b->webview_stack), name); + g_free(name); + + const char *uri = webkit_web_view_get_uri(tab->webview); + gtk_entry_set_text(GTK_ENTRY(b->url_entry), uri ? uri : ""); + + gtk_widget_set_sensitive(b->back_btn, webkit_web_view_can_go_back(tab->webview)); + gtk_widget_set_sensitive(b->forward_btn, webkit_web_view_can_go_forward(tab->webview)); + update_star_button(b); + gtk_widget_grab_focus(GTK_WIDGET(tab->webview)); +} + +static void veil_browser_close_tab(VeilBrowser *b, VeilTab *tab) { + if (g_list_length(b->tabs) <= 1) { gtk_main_quit(); return; } + + GList *node = g_list_find(b->tabs, tab); + GList *next = node->next ? node->next : node->prev; + + b->tabs = g_list_remove(b->tabs, tab); + + GtkWidget *ebox = g_object_get_data(G_OBJECT(tab->tab_box), "ebox"); + gtk_container_remove(GTK_CONTAINER(b->tab_container), ebox); + gtk_container_remove(GTK_CONTAINER(b->webview_stack), GTK_WIDGET(tab->webview)); + + if (tab == b->current_tab && next) { + b->current_tab = NULL; + veil_browser_switch_to_tab(b, next->data); + } + g_free(tab); +} + +static GtkWidget *make_btn(const char *icon, const char *cls, GCallback cb, gpointer data) { + GtkWidget *btn = gtk_button_new_from_icon_name(icon, GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(btn), cls); + if (cb) g_signal_connect(btn, "clicked", cb, data); + return btn; +} + +/* === Download-Handler === */ +static VeilActiveDownload *find_active_download(VeilBrowser *b, WebKitDownload *download) { + for (GList *l = b->active_downloads; l; l = l->next) { + VeilActiveDownload *ad = l->data; + if (ad->download == download) return ad; + } + return NULL; +} + +static void on_download_progress(WebKitDownload *download, GParamSpec *pspec, VeilBrowser *b) { + (void)pspec; + VeilActiveDownload *ad = find_active_download(b, download); + if (!ad || !ad->progress_bar) return; + + double progress = webkit_download_get_estimated_progress(download); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ad->progress_bar), progress); + + /* Status aktualisieren */ + if (ad->status_label) { + guint64 received = webkit_download_get_received_data_length(download); + WebKitURIResponse *response = webkit_download_get_response(download); + guint64 total = response ? webkit_uri_response_get_content_length(response) : 0; + + char *received_str = format_size(received); + char *total_str = total > 0 ? format_size(total) : g_strdup("?"); + char *status_text = g_strdup_printf("%s / %s", received_str, total_str); + gtk_label_set_text(GTK_LABEL(ad->status_label), status_text); + g_free(received_str); + g_free(total_str); + g_free(status_text); + } + + /* Geschwindigkeit berechnen und aktualisieren */ + if (ad->speed_label) { + gint64 now = g_get_monotonic_time(); + guint64 received = webkit_download_get_received_data_length(download); + + if (ad->last_time > 0 && now - ad->last_time > 500000) { /* Alle 500ms aktualisieren */ + guint64 bytes_diff = received - ad->last_bytes; + gdouble time_diff = (now - ad->last_time) / 1000000.0; + guint64 speed = (guint64)(bytes_diff / time_diff); + + char *speed_str = format_speed(speed); + gtk_label_set_text(GTK_LABEL(ad->speed_label), speed_str); + g_free(speed_str); + + ad->last_bytes = received; + ad->last_time = now; + } else if (ad->last_time == 0) { + ad->last_bytes = received; + ad->last_time = now; + } + } +} + +static void on_download_finished(WebKitDownload *download, VeilBrowser *b) { + const char *dest = webkit_download_get_destination(download); + + /* Von aktiven Downloads entfernen */ + VeilActiveDownload *ad = find_active_download(b, download); + if (ad) { + b->active_downloads = g_list_remove(b->active_downloads, ad); + g_free(ad); + } + + if (!dest) { + rebuild_downloads_popover(b); + return; + } + + /* Zu abgeschlossenen Downloads hinzufügen */ + WebKitURIRequest *req = webkit_download_get_request(download); + const char *url = webkit_uri_request_get_uri(req); + + VeilDownload *dl = g_new0(VeilDownload, 1); + dl->path = g_strdup(dest); + dl->filename = g_path_get_basename(dest); + dl->url = g_strdup(url ? url : ""); + dl->timestamp = g_get_real_time() / 1000000; + + b->downloads = g_list_prepend(b->downloads, dl); + save_downloads(b); + rebuild_downloads_popover(b); +} + +static void on_download_failed(WebKitDownload *download, GError *error, VeilBrowser *b) { + (void)error; + /* Von aktiven Downloads entfernen */ + VeilActiveDownload *ad = find_active_download(b, download); + if (ad) { + b->active_downloads = g_list_remove(b->active_downloads, ad); + g_free(ad); + } + rebuild_downloads_popover(b); +} + +static gboolean on_download_decide_destination(WebKitDownload *download, gchar *suggested_filename, VeilBrowser *b) { + const char *download_dir; + + /* Benutzerdefiniertes Download-Verzeichnis verwenden, falls gesetzt, andernfalls System-Standard */ + if (b->settings.download_dir[0] != '\0' && + g_file_test(b->settings.download_dir, G_FILE_TEST_IS_DIR)) { + download_dir = b->settings.download_dir; + } else { + download_dir = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD); + if (!download_dir) download_dir = g_get_home_dir(); + } + + char *dest_path = g_build_filename(download_dir, suggested_filename, NULL); + + /* Existierende Dateien durch Hinzufügen eines Zahlensuffixes behandeln */ + int counter = 1; + while (g_file_test(dest_path, G_FILE_TEST_EXISTS)) { + g_free(dest_path); + char *base = g_path_get_basename(suggested_filename); + char *ext = strrchr(base, '.'); + if (ext) { + *ext = 0; + ext++; + dest_path = g_build_filename(download_dir, + g_strdup_printf("%s (%d).%s", base, counter, ext), NULL); + } else { + dest_path = g_build_filename(download_dir, + g_strdup_printf("%s (%d)", base, counter), NULL); + } + g_free(base); + counter++; + } + + char *dest_uri = g_strdup_printf("file://%s", dest_path); + webkit_download_set_destination(download, dest_uri); + g_free(dest_uri); + g_free(dest_path); + + /* Popover aktualisieren, um Dateinamen anzuzeigen */ + rebuild_downloads_popover(b); + + return TRUE; /* Wir haben das Ziel behandelt */ +} + +static void on_download_started(WebKitWebContext *context, WebKitDownload *download, VeilBrowser *b) { + (void)context; + + /* Aktiven Download-Tracker erstellen */ + VeilActiveDownload *ad = g_new0(VeilActiveDownload, 1); + ad->download = download; + ad->progress_bar = NULL; + ad->status_label = NULL; + ad->speed_label = NULL; + ad->last_bytes = 0; + ad->last_time = 0; + + b->active_downloads = g_list_prepend(b->active_downloads, ad); + + /* Signale verbinden */ + g_signal_connect(download, "decide-destination", G_CALLBACK(on_download_decide_destination), b); + g_signal_connect(download, "notify::estimated-progress", G_CALLBACK(on_download_progress), b); + g_signal_connect(download, "finished", G_CALLBACK(on_download_finished), b); + g_signal_connect(download, "failed", G_CALLBACK(on_download_failed), b); + + /* Downloads-Popover öffnen und neu aufbauen */ + rebuild_downloads_popover(b); + if (b->downloads_popover) { + gtk_popover_popup(GTK_POPOVER(b->downloads_popover)); + } +} + + +static VeilBrowser *veil_browser_new(void) { + VeilBrowser *b = g_new0(VeilBrowser, 1); + g_browser = b; + + load_settings(b); + + /* Web-Kontext mit Festplatten-Cache für Performance erstellen */ + char *cache_dir = g_build_filename(g_get_user_cache_dir(), "veil", NULL); + char *data_dir = g_build_filename(g_get_user_data_dir(), "veil", NULL); + + WebKitWebsiteDataManager *data_mgr = webkit_website_data_manager_new( + "base-cache-directory", cache_dir, + "base-data-directory", data_dir, + "is-ephemeral", FALSE, /* Persistenter Cache */ + NULL); + + b->web_context = webkit_web_context_new_with_website_data_manager(data_mgr); + + /* Download-Handler verbinden */ + g_signal_connect(b->web_context, "download-started", G_CALLBACK(on_download_started), b); + + /* === WEB-KONTEXT PERFORMANCE-OPTIMIERUNGEN === */ + + /* Maximaler Cache für Webbrowser-Nutzungsmuster */ + webkit_web_context_set_cache_model(b->web_context, WEBKIT_CACHE_MODEL_WEB_BROWSER); + + /* Favicon-Datenbank für schnelleres Icon-Laden aktivieren */ + char *favicon_db = g_build_filename(data_dir, "favicons", NULL); + webkit_web_context_set_favicon_database_directory(b->web_context, favicon_db); + g_free(favicon_db); + + /* Hinweis: Multi-Prozess-Modell ist jetzt Standard in WebKit2GTK 2.40+ */ + /* Hinweis: DNS-Prefetching ist automatisch in modernem WebKit */ + + g_free(cache_dir); + g_free(data_dir); + g_object_unref(data_mgr); + + b->css_provider = gtk_css_provider_new(); + gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), + GTK_STYLE_PROVIDER(b->css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + apply_css(b); + + b->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size(GTK_WINDOW(b->window), 1100, 700); + + /* Lokalen Icon-Pfad für Entwicklung hinzufügen, dann Icon setzen */ + char *cwd = g_get_current_dir(); + char *local_icons = g_build_filename(cwd, "data", "icons", NULL); + gtk_icon_theme_prepend_search_path(gtk_icon_theme_get_default(), local_icons); + g_free(local_icons); + g_free(cwd); + gtk_window_set_icon_name(GTK_WINDOW(b->window), "veil"); + g_signal_connect(b->window, "destroy", G_CALLBACK(gtk_main_quit), NULL); + g_signal_connect(b->window, "key-press-event", G_CALLBACK(on_key), b); + g_signal_connect(b->window, "window-state-event", G_CALLBACK(on_window_state), b); + + b->header = gtk_header_bar_new(); + gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(b->header), FALSE); + gtk_header_bar_set_decoration_layout(GTK_HEADER_BAR(b->header), ":"); + gtk_window_set_titlebar(GTK_WINDOW(b->window), b->header); + + /* VEIL-Branding im Header (links von den Tabs) */ + GtkWidget *brand = gtk_label_new("VEIL"); + gtk_style_context_add_class(gtk_widget_get_style_context(brand), "brand"); + gtk_widget_set_margin_start(brand, 10); + gtk_widget_set_margin_end(brand, 10); + gtk_header_bar_pack_start(GTK_HEADER_BAR(b->header), brand); + + b->tab_container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_header_bar_pack_start(GTK_HEADER_BAR(b->header), b->tab_container); + + GtkWidget *add_btn = make_btn("list-add-symbolic", "header-btn", G_CALLBACK(on_new_tab), b); + gtk_header_bar_pack_start(GTK_HEADER_BAR(b->header), add_btn); + + GtkWidget *wbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(wbox), make_btn("window-minimize-symbolic", "header-btn", G_CALLBACK(on_minimize), b), FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(wbox), make_btn("window-maximize-symbolic", "header-btn", G_CALLBACK(on_maximize), b), FALSE, FALSE, 0); + GtkWidget *close_btn = make_btn("window-close-symbolic", "header-btn", G_CALLBACK(on_close_window), b); + gtk_style_context_add_class(gtk_widget_get_style_context(close_btn), "close"); + gtk_box_pack_start(GTK_BOX(wbox), close_btn, FALSE, FALSE, 0); + gtk_header_bar_pack_end(GTK_HEADER_BAR(b->header), wbox); + + b->main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(b->window), b->main_box); + + b->nav_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); + gtk_style_context_add_class(gtk_widget_get_style_context(b->nav_bar), "nav-bar"); + + b->back_btn = make_btn("go-previous-symbolic", "nav-btn", G_CALLBACK(on_back), b); + b->forward_btn = make_btn("go-next-symbolic", "nav-btn", G_CALLBACK(on_forward), b); + b->reload_btn = make_btn("view-refresh-symbolic", "nav-btn", G_CALLBACK(on_reload), b); + + gtk_box_pack_start(GTK_BOX(b->nav_bar), b->back_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(b->nav_bar), b->forward_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(b->nav_bar), b->reload_btn, FALSE, FALSE, 0); + + GtkWidget *url_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_style_context_add_class(gtk_widget_get_style_context(url_box), "url-box"); + gtk_widget_set_hexpand(url_box, TRUE); + gtk_widget_set_margin_start(url_box, 4); + gtk_widget_set_margin_end(url_box, 4); + + b->url_entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(b->url_entry), "Search or enter URL"); + gtk_style_context_add_class(gtk_widget_get_style_context(b->url_entry), "url-entry"); + gtk_widget_set_hexpand(b->url_entry, TRUE); + g_signal_connect(b->url_entry, "activate", G_CALLBACK(on_url_activate), b); + g_signal_connect(b->url_entry, "focus-in-event", G_CALLBACK(on_url_focus), b); + gtk_box_pack_start(GTK_BOX(url_box), b->url_entry, TRUE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(b->nav_bar), url_box, TRUE, TRUE, 0); + + /* Menü-Button mit Dropdown-Menü */ + b->menu_btn = gtk_menu_button_new(); + gtk_button_set_label(GTK_BUTTON(b->menu_btn), "☰"); + gtk_style_context_add_class(gtk_widget_get_style_context(b->menu_btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(b->menu_btn), "nav-btn"); + + b->menu = gtk_menu_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(b->menu), "veil-menu"); + + GtkWidget *new_tab_item = gtk_menu_item_new_with_label("New Tab"); + g_signal_connect(new_tab_item, "activate", G_CALLBACK(on_menu_new_tab), b); + gtk_menu_shell_append(GTK_MENU_SHELL(b->menu), new_tab_item); + + GtkWidget *settings_item = gtk_menu_item_new_with_label("Settings"); + g_signal_connect(settings_item, "activate", G_CALLBACK(on_menu_settings), b); + gtk_menu_shell_append(GTK_MENU_SHELL(b->menu), settings_item); + + gtk_widget_show_all(b->menu); + gtk_menu_button_set_popup(GTK_MENU_BUTTON(b->menu_btn), b->menu); + gtk_box_pack_end(GTK_BOX(b->nav_bar), b->menu_btn, FALSE, FALSE, 0); + + /* Downloads-Button mit Popover */ + GtkWidget *downloads_btn = gtk_menu_button_new(); + gtk_button_set_image(GTK_BUTTON(downloads_btn), + gtk_image_new_from_icon_name("folder-download-symbolic", GTK_ICON_SIZE_MENU)); + gtk_style_context_add_class(gtk_widget_get_style_context(downloads_btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(downloads_btn), "nav-btn"); + gtk_widget_set_tooltip_text(downloads_btn, "Downloads"); + + b->downloads_popover = gtk_popover_new(downloads_btn); + gtk_style_context_add_class(gtk_widget_get_style_context(b->downloads_popover), "veil-popover"); + gtk_widget_set_size_request(b->downloads_popover, 340, -1); + + GtkWidget *popover_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_set_border_width(GTK_CONTAINER(popover_box), 10); + + /* Header mit Titel und Löschen-Button */ + GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + + GtkWidget *header_icon = gtk_image_new_from_icon_name("folder-download-symbolic", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(header_box), header_icon, FALSE, FALSE, 0); + + GtkWidget *title_label = gtk_label_new("Downloads"); + gtk_widget_set_halign(title_label, GTK_ALIGN_START); + gtk_widget_set_hexpand(title_label, TRUE); + PangoAttrList *attrs = pango_attr_list_new(); + pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + gtk_label_set_attributes(GTK_LABEL(title_label), attrs); + pango_attr_list_unref(attrs); + gtk_box_pack_start(GTK_BOX(header_box), title_label, TRUE, TRUE, 0); + + GtkWidget *clear_btn = gtk_button_new_from_icon_name("edit-clear-all-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class(gtk_widget_get_style_context(clear_btn), "flat"); + gtk_widget_set_tooltip_text(clear_btn, "Verlauf leeren"); + g_signal_connect(clear_btn, "clicked", G_CALLBACK(on_clear_downloads), b); + gtk_box_pack_end(GTK_BOX(header_box), clear_btn, FALSE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(popover_box), header_box, FALSE, FALSE, 0); + + /* Trennlinie */ + GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_margin_top(sep, 8); + gtk_widget_set_margin_bottom(sep, 4); + gtk_box_pack_start(GTK_BOX(popover_box), sep, FALSE, FALSE, 0); + + /* Aktive Downloads-Bereich */ + b->active_downloads_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_pack_start(GTK_BOX(popover_box), b->active_downloads_box, FALSE, FALSE, 0); + + /* Scrollbare Liste abgeschlossener Downloads */ + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(scroll), 280); + gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scroll), TRUE); + + b->downloads_list_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_container_add(GTK_CONTAINER(scroll), b->downloads_list_box); + gtk_box_pack_start(GTK_BOX(popover_box), scroll, TRUE, TRUE, 0); + + gtk_container_add(GTK_CONTAINER(b->downloads_popover), popover_box); + gtk_widget_show_all(popover_box); + + gtk_menu_button_set_popover(GTK_MENU_BUTTON(downloads_btn), b->downloads_popover); + gtk_box_pack_end(GTK_BOX(b->nav_bar), downloads_btn, FALSE, FALSE, 0); + + /* Stern-Button für Lesezeichen */ + b->star_btn = gtk_button_new_with_label("☆"); + gtk_style_context_add_class(gtk_widget_get_style_context(b->star_btn), "flat"); + gtk_style_context_add_class(gtk_widget_get_style_context(b->star_btn), "star-btn"); + g_signal_connect(b->star_btn, "clicked", G_CALLBACK(on_star_click), b); + gtk_box_pack_end(GTK_BOX(b->nav_bar), b->star_btn, FALSE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(b->main_box), b->nav_bar, FALSE, FALSE, 0); + + b->progress_bar = gtk_progress_bar_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(b->progress_bar), "progress"); + gtk_widget_set_no_show_all(b->progress_bar, TRUE); + gtk_box_pack_start(GTK_BOX(b->main_box), b->progress_bar, FALSE, FALSE, 0); + + b->gradient_line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_style_context_add_class(gtk_widget_get_style_context(b->gradient_line), "gradient-line"); + gtk_box_pack_start(GTK_BOX(b->main_box), b->gradient_line, FALSE, FALSE, 0); + + /* Lesezeichenleiste (in Event-Box für Rechtsklick eingebettet) */ + GtkWidget *bookmarks_event_box = gtk_event_box_new(); + gtk_widget_set_events(bookmarks_event_box, GDK_BUTTON_PRESS_MASK); + g_signal_connect(bookmarks_event_box, "button-press-event", G_CALLBACK(on_bookmarks_bar_button_press), b); + + b->bookmarks_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_style_context_add_class(gtk_widget_get_style_context(b->bookmarks_bar), "bookmarks-bar"); + gtk_widget_set_margin_start(b->bookmarks_bar, 8); + gtk_widget_set_margin_end(b->bookmarks_bar, 8); + + gtk_container_add(GTK_CONTAINER(bookmarks_event_box), b->bookmarks_bar); + + /* Lesezeichen laden und anzeigen */ + load_bookmarks(b); + rebuild_bookmarks_bar(b); + + /* Download-Verlauf laden */ + load_downloads(b); + rebuild_downloads_popover(b); + + gtk_box_pack_start(GTK_BOX(b->main_box), bookmarks_event_box, FALSE, FALSE, 0); + gtk_widget_set_visible(bookmarks_event_box, b->settings.show_bookmarks); + g_object_set_data(G_OBJECT(b->bookmarks_bar), "event_box", bookmarks_event_box); + + b->webview_stack = gtk_stack_new(); + gtk_widget_set_vexpand(b->webview_stack, TRUE); + gtk_widget_set_hexpand(b->webview_stack, TRUE); + gtk_box_pack_start(GTK_BOX(b->main_box), b->webview_stack, TRUE, TRUE, 0); + + gtk_widget_show_all(b->window); + gtk_widget_hide(b->progress_bar); + + veil_browser_new_tab(b, "veil:start"); + + return b; +} + +int main(int argc, char *argv[]) { + /* WebKit2GTK Hardware-Beschleunigung hat Probleme mit nativem Wayland. + * X11/XWayland für Stabilität erzwingen. */ + setenv("GDK_BACKEND", "x11", 1); + + /* GBM-Puffer-Zuweisungsfehler bei einigen GPU/Treiber-Kombinationen beheben */ + setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", 1); + + gtk_init(&argc, &argv); + veil_browser_new(); + gtk_main(); + return 0; +} diff --git a/Veil/veil b/Veil/veil new file mode 100755 index 0000000..d8d71a4 Binary files /dev/null and b/Veil/veil differ