diff --git a/README.md b/README.md
index 29a11bd0..105fd880 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,24 @@
# Yoda is [PHP-GTK](https://github.com/scorninpc/php-gtk3) Browser for [Gemini Protocol](https://geminiprotocol.net)
-At this moment project under development!
+At this moment project in development!
-## Protocols
+## Install
-* [x] Gemini
-* [x] Nex
+1. Build latest [PHP-GTK3](https://github.com/scorninpc/php-gtk3) from sources or get [Appimage](https://github.com/scorninpc/php-gtk3/releases)
+2. `apt install git composer`
+3. `git clone https://github.com/YGGverse/Yoda.git`
+4. `cd Yoda`
+5. `composer update`
-## Features
+## Launch
-* [x] Custom DNS resolver with memory cache (useful for alt networks like [Yggdrasil](https://github.com/yggdrasil-network/yggdrasil-go))
-* [x] Flexible settings in `config.json`, then UI
-* [x] Native GTK environment, no custom colors until you change it by `css`
-* [x] Multi-tabs
-* [x] Navigation history
-* [ ] Bookmarks
-* [ ] Certificate features
-* [ ] Local snaps to make resources accessible even offline
-* [ ] `Gemfeed` reader
-* [ ] Search engine integrations, probably [Yo!](https://github.com/YGGverse/Yo/tree/gemini) Search by default
-* [ ] Machine translations (e.g. [Lingva API](https://github.com/thedaviddelta/lingva-translate))
+``` bash
+/path/to/php-gtk3 src/Yoda.php
+```
## Components
-* [gemini-php](https://github.com/YGGverse/gemini-php) - PHP 8 library for Gemini protocol
-* [gemtext-php](https://github.com/YGGverse/gemtext-php) - PHP 8 library for Gemtext operations
-* [net-php](https://github.com/YGGverse/net-php) - PHP 8 library for DNS resolver and address parser
\ No newline at end of file
+* [gemini-php](https://github.com/YGGverse/gemini-php) - Gemini protocol connections
+* [gemtext-php](https://github.com/YGGverse/gemtext-php) - Gemtext operations
+* [net-php](https://github.com/YGGverse/net-php) - DNS resolver and network address features
+* [nex-php](https://github.com/YGGverse/nex-php) - NEX protocol connections
\ No newline at end of file
diff --git a/config.json b/config.json
deleted file mode 100644
index 4e671a2a..00000000
--- a/config.json
+++ /dev/null
@@ -1,227 +0,0 @@
-{
- "name":"Yoda",
- "theme":"Default",
- "database":
- {
- "name":"database.sqlite",
- "username":null,
- "password":null
- },
- "header":
- {
- "enabled":true,
- "button":
- {
- "close":true
- }
- },
- "width":640,
- "height":480,
- "tab":
- {
- "page":
- {
- "title":
- {
- "default":"New page",
- "width":
- {
- "chars":32
- },
- "ellipsize":
- {
- "mode":3
- },
- "postfix":
- {
- "hostname":true
- }
- },
- "redirect":
- {
- "follow":
- {
- "enabled":true,
- "max":5,
- "code":
- [
- 30,
- 31
- ]
- }
- },
- "resolver":
- {
- "enabled":true,
- "request":
- {
- "timeout":1,
- "host":
- [
- "1.1.1.1",
- "8.8.8.8"
- ],
- "record":
- [
- "A",
- "AAAA"
- ]
- },
- "result":
- {
- "shuffle":false,
- "cache":
- {
- "timeout":3600
- }
- }
- },
- "history":
- {
- "memory":
- {
- "enabled":true
- },
- "database":
- {
- "enabled":true,
- "mode":
- {
- "renew":true
- }
- }
- },
- "progressbar":
- {
- "visible":true
- },
- "header":
- {
- "margin":8,
- "button":
- {
- "home":
- {
- "visible":true,
- "label":"Home",
- "url":"yoda://welcome"
- },
- "back":
- {
- "visible":true,
- "label":"Back"
- },
- "forward":
- {
- "visible":true,
- "label":"Forward"
- },
- "base":
- {
- "visible":true,
- "label":"Base"
- },
- "go":
- {
- "visible":true,
- "label":"Go"
- }
- },
- "entry":
- {
- "request":
- {
- "placeholder":"URL or any search term...",
- "length":
- {
- "max":1024
- },
- "autocomplete":
- {
- "enabled":true,
- "inline":
- {
- "completion":true,
- "selection":true
- },
- "key":
- {
- "length":1
- },
- "ignore":
- {
- "keycode":
- [
- 111,
- 116
- ]
- },
- "result":
- {
- "limit":15
- }
- }
- }
- }
- },
- "body":
- {
- "margin":8
- },
- "footer":
- {
- "margin":8,
- "status":
- {
- "open":
- {
- "complete":"{REQUEST_BASE_URL} | {TIME_C} | {RESPONSE_META} | {RESPONSE_LENGTH} bytes | {RESPONSE_SECONDS} seconds"
- }
- }
- }
- },
- "history":
- {
- "enabled":true,
- "label":"History",
- "clean":
- {
- "timeout":null
- },
- "time":
- {
- "format":"c"
- },
- "header":
- {
- "margin":8,
- "button":
- {
- "open":
- {
- "visible":true,
- "label":"Open"
- },
- "delete":
- {
- "visible":true,
- "label":"Delete"
- },
- "search":
- {
- "visible":true,
- "label":"Search"
- }
- },
- "filter":
- {
- "placeholder":"Search in history..."
- }
- },
- "body":
- {
- "margin":8
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Abstract/Entity/Button.php b/src/Abstract/Entity/Button.php
new file mode 100644
index 00000000..04e27133
--- /dev/null
+++ b/src/Abstract/Entity/Button.php
@@ -0,0 +1,41 @@
+gtk = new \GtkButton;
+
+ $this->gtk->set_sensitive(
+ $this->_sensitive
+ );
+
+ $this->gtk->set_label(
+ $this->_label
+ );
+
+ $this->gtk->connect(
+ 'clicked',
+ function(
+ \GtkButton $entity
+ ) {
+ $this->_onClick(
+ $entity
+ );
+ }
+ );
+ }
+
+ abstract protected function _onClick(
+ \GtkButton $entity
+ ): void;
+}
\ No newline at end of file
diff --git a/src/Abstract/Entity/Entry.php b/src/Abstract/Entity/Entry.php
new file mode 100644
index 00000000..b3e3bd8d
--- /dev/null
+++ b/src/Abstract/Entity/Entry.php
@@ -0,0 +1,64 @@
+gtk = new \GtkEntry;
+
+ $this->gtk->set_placeholder_text(
+ $this->_placeholder
+ );
+
+ $this->gtk->set_max_length(
+ $this->_length
+ );
+
+ $this->gtk->set_text(
+ $this->_value
+ );
+
+ $this->gtk->connect(
+ 'activate',
+ function(
+ \GtkEntry $entry
+ ) {
+ $this->_onActivate(
+ $entry
+ );
+ }
+ );
+
+ $this->gtk->connect(
+ 'key-release-event',
+ function (
+ \GtkEntry $entry,
+ \GdkEvent $event
+ ) {
+ $this->_onKeyRelease(
+ $entry,
+ $event
+ );
+ }
+ );
+ }
+
+ abstract protected function _onActivate(
+ \GtkEntry $entry
+ ): void;
+
+ abstract protected function _onKeyRelease(
+ \GtkEntry $entry,
+ \GdkEvent $event
+ ): void;
+}
\ No newline at end of file
diff --git a/src/Abstract/Entity/Window/Tab/Address/Navbar/Button.php b/src/Abstract/Entity/Window/Tab/Address/Navbar/Button.php
new file mode 100644
index 00000000..89e8e3ed
--- /dev/null
+++ b/src/Abstract/Entity/Window/Tab/Address/Navbar/Button.php
@@ -0,0 +1,18 @@
+navbar = $navbar;
+ }
+}
diff --git a/src/Abstract/Entity/Window/Tab/Address/Navbar/Entry.php b/src/Abstract/Entity/Window/Tab/Address/Navbar/Entry.php
new file mode 100644
index 00000000..ccf2321e
--- /dev/null
+++ b/src/Abstract/Entity/Window/Tab/Address/Navbar/Entry.php
@@ -0,0 +1,18 @@
+navbar = $navbar;
+ }
+}
diff --git a/src/Abstract/Entity/Window/Tab/History/Navbar/Button.php b/src/Abstract/Entity/Window/Tab/History/Navbar/Button.php
new file mode 100644
index 00000000..31f0d508
--- /dev/null
+++ b/src/Abstract/Entity/Window/Tab/History/Navbar/Button.php
@@ -0,0 +1,18 @@
+navbar = $navbar;
+ }
+}
diff --git a/src/Abstract/Entity/Window/Tab/History/Navbar/Entry.php b/src/Abstract/Entity/Window/Tab/History/Navbar/Entry.php
new file mode 100644
index 00000000..4a31ad12
--- /dev/null
+++ b/src/Abstract/Entity/Window/Tab/History/Navbar/Entry.php
@@ -0,0 +1,18 @@
+navbar = $navbar;
+ }
+}
diff --git a/src/Entity/App.php b/src/Entity/App.php
deleted file mode 100644
index 652107db..00000000
--- a/src/Entity/App.php
+++ /dev/null
@@ -1,217 +0,0 @@
-config = new \Yggverse\Yoda\Model\Config;
-
- // Init database
- $this->database = new \Yggverse\Yoda\Model\Database(
- $this->config->database->name,
- $this->config->database->username,
- $this->config->database->password
- );
-
- // Init theme
- $css = new \GtkCssProvider();
-
- $css->load_from_data(
- \Yggverse\Yoda\Model\File::getTheme(
- $this->config->theme
- )
- );
-
- $style = new \GtkStyleContext();
-
- $style->add_provider_for_screen(
- $css,
- 600
- );
-
- // Init window
- $this->window = new \GtkWindow;
-
- $this->window->set_size_request(
- $this->config->width,
- $this->config->height
- );
-
- if ($this->config->header->enabled)
- {
- $this->header = new \GtkHeaderBar;
-
- $this->header->set_title(
- $this->config->name
- );
-
- $this->header->set_show_close_button(
- $this->config->header->button->close
- );
-
- $this->window->set_titlebar(
- $this->header
- );
- }
-
- // Init tabs
- $this->tabs = new \GtkNotebook;
-
- $this->tabs->set_scrollable(
- true
- );
-
- // + button
- $this->tabs->append_page(
- new \GtkLabel,
- new \GtkLabel(
- '+'
- )
- );
-
- // History features
- if ($this->config->tab->history->enabled)
- {
- $this->history = new \Yggverse\Yoda\Entity\Tab\History(
- $this
- );
-
- $this->tabs->append_page(
- $this->history->box,
- new \GtkLabel(
- $this->config->tab->history->label
- )
- );
-
- $this->tabs->set_tab_reorderable(
- $this->history->box,
- true
- );
- }
-
- // Append blank page
- $page = $this->blankPage();
-
- $page->open(
- $this->config->tab->page->header->button->home->url // @TODO
- );
-
- // Render
- $this->window->add(
- $this->tabs
- );
-
- $this->window->show_all();
-
- // Init event listener
- $this->tabs->connect(
- 'switch-page',
- function (
- \GtkNotebook $tabs,
- \GtkWidget $child,
- int $position
- ) {
- // Update window title on tab change
- $this->setTitle(
- $tabs->get_tab_label($child)->get_text()
- );
-
- // Add new tab event
- if ('+' == $tabs->get_tab_label($child)->get_text())
- {
- \Gtk::timeout_add(
- 0,
- function()
- {
- $this->blankPage();
-
- return false;
- }
- );
- }
- }
- );
-
- $this->window->connect(
- 'destroy',
- function()
- {
- \Gtk::main_quit();
- }
- );
- }
-
- public function blankPage(): \Yggverse\Yoda\Entity\Tab\Page
- {
- $page = new \Yggverse\Yoda\Entity\Tab\Page(
- $this
- );
-
- $this->tabs->append_page(
- $page->box,
- new \GtkLabel(
- $this->config->tab->page->title->default
- )
- );
-
- $this->tabs->set_tab_reorderable(
- $page->box,
- true
- );
-
- $this->tabs->show_all();
-
- $this->tabs->set_current_page(
- $this->tabs->page_num(
- $page->box
- )
- );
-
- return $page;
- }
-
- public function setTitle(
- ?string $value = null
- ): void
- {
- if ($value)
- {
- if ($value == 'Welcome to Yoda!')
- {
- $title = $value;
- }
-
- else
- {
- $title = sprintf(
- '%s - %s',
- $value,
- $this->config->name
- );
- }
- }
-
- else
- {
- $title = $this->config->name;
- }
-
- $this->header->set_title(
- $title
- );
- }
-}
\ No newline at end of file
diff --git a/src/Entity/Tab/History.php b/src/Entity/Tab/History.php
deleted file mode 100644
index 486ed063..00000000
--- a/src/Entity/Tab/History.php
+++ /dev/null
@@ -1,397 +0,0 @@
-app = $app;
-
- // Init config namespace
- $this->config = $app->config->tab->history;
-
- // Cleanup expired history
- if ($this->config->clean->timeout)
- {
- $this->app->database->cleanHistory(
- $this->config->clean->timeout
- );
- }
-
- // Compose header
- $this->header = new \GtkBox(
- \GtkOrientation::HORIZONTAL
- );
-
- $this->header->set_margin_top(
- $this->config->header->margin
- );
-
- $this->header->set_margin_bottom(
- $this->config->header->margin
- );
-
- $this->header->set_margin_start(
- $this->config->header->margin
- );
-
- $this->header->set_margin_end(
- $this->config->header->margin
- );
-
- $this->header->set_spacing(
- $this->config->header->margin
- );
-
- // Open button
- $this->open = \GtkButton::new_with_label(
- $this->config->header->button->open->label
- );
-
- $this->open->set_sensitive(
- false
- );
-
- if ($this->config->header->button->open->visible)
- {
- $this->header->add(
- $this->open
- );
- }
-
- // Delete button
- $this->delete = \GtkButton::new_with_label(
- $this->config->header->button->delete->label
- );
-
- $this->delete->set_sensitive(
- false
- );
-
- if ($this->config->header->button->delete->visible)
- {
- $this->header->add(
- $this->delete
- );
- }
-
- // Filter field
- $this->filter = new \GtkEntry;
-
- $this->filter->set_placeholder_text(
- $this->config->header->filter->placeholder
- );
-
- $this->header->pack_start(
- $this->filter,
- true,
- true,
- 0
- );
-
- // Search button
- $this->search = \GtkButton::new_with_label(
- $this->config->header->button->search->label
- );
-
- if ($this->config->header->button->search->visible)
- {
- $this->header->add(
- $this->search
- );
- }
-
- // Build history list
- $this->treeview = new \GtkTreeView();
-
- $this->treeview->append_column(
- new \GtkTreeViewColumn(
- 'Time',
- new \GtkCellRendererText(),
- 'text',
- 1
- )
- );
-
- $this->treeview->append_column(
- new \GtkTreeViewColumn(
- 'Title',
- new \GtkCellRendererText(),
- 'text',
- 2
- )
- );
-
- $this->treeview->append_column(
- new \GtkTreeViewColumn(
- 'URL',
- new \GtkCellRendererText(),
- 'text',
- 3
- )
- );
-
- // Init list storage
- $this->list = new \GtkListStore(
- \GObject::TYPE_INT,
- \GObject::TYPE_STRING,
- \GObject::TYPE_STRING,
- \GObject::TYPE_STRING
- );
-
- $this->treeview->set_model(
- $this->list
- );
-
- /* @TODO row-activated
- $this->treeview->get_selection()->set_mode(
- \GtkSelectionMode::MULTIPLE
- );
- */
-
- // Compose body
- $this->body = new \GtkBox(
- \GtkOrientation::VERTICAL
- );
-
- $this->container = new \GtkScrolledWindow();
-
- $this->container->add(
- $this->treeview
- );
-
- $this->body->set_margin_start(
- $this->config->body->margin
- );
-
- $this->body->pack_start(
- $this->container,
- true,
- true,
- 0
- );
-
- // Compose page
- $this->box = new \GtkBox(
- \GtkOrientation::VERTICAL
- );
-
- $this->box->add(
- $this->header
- );
-
- $this->box->pack_start(
- $this->body,
- true,
- true,
- 0
- );
-
- // Refresh history
- $this->refresh();
-
- // Activate events
- $this->treeview->connect(
- 'row-activated',
- function ($tree)
- {
- if ($url = $this->getSelectedColumn(3, $tree))
- {
- $page = $this->app->blankPage();
-
- $page->open(
- $url
- );
- }
- }
- );
-
- $this->treeview->connect(
- 'cursor-changed',
- function ($tree)
- {
- $url = $this->getSelectedColumn(
- 3, $tree
- );
-
- $this->open->set_sensitive(
- (bool) $url
- );
-
- $this->delete->set_sensitive(
- (bool) $url
- );
- }
- );
-
- $this->filter->connect(
- 'activate',
- function ($entry)
- {
- $this->refresh(
- $entry->get_text()
- );
- }
- );
-
- if ($this->config->header->button->open->visible)
- {
- $this->open->connect(
- 'clicked',
- function ()
- {
- if ($url = $this->getSelectedColumn(3))
- {
- $page = $this->app->blankPage();
-
- $page->open(
- $url
- );
-
- $this->refresh();
- }
- }
- );
- }
-
- if ($this->config->header->button->delete->visible)
- {
- $this->delete->connect(
- 'clicked',
- function ()
- {
- if ($id = $this->getSelectedColumn(0))
- {
- $this->app->database->deleteHistory(
- $id
- );
-
- $this->refresh();
- }
- }
- );
- }
-
- if ($this->config->header->button->search->visible)
- {
- $this->search->connect(
- 'clicked',
- function ()
- {
- $this->refresh(
- $this->filter->get_text()
- );
- }
- );
- }
- }
-
- public function refresh(): void
- {
- // Reset previous state
- $this->list->clear();
-
- // Update buttons sensibility
- $this->open->set_sensitive(
- false
- );
-
- $this->delete->set_sensitive(
- false
- );
-
- // Build history list from database records
- foreach ($this->app->database->getHistory($this->filter->get_text()) as $record)
- {
- $this->list->append(
- [
- $record->id,
- date(
- $this->config->time->format,
- $record->time
- ),
- $record->title,
- $record->url
- ]
- );
- }
-
- // Update tree
- $this->treeview->show_all();
- }
-
- public function add(
- string $url,
- ?string $title = null,
- bool $renew = false // delete previous records with same URL
- ): ?int
- {
- if ($renew)
- {
- foreach ($this->app->database->getHistory($url) as $record)
- {
- $this->app->database->deleteHistory(
- $record->id
- );
- }
- }
-
- $id = $this->app->database->addHistory(
- $url,
- $title
- );
-
- $this->refresh();
-
- return $id;
- }
-
- public function getSelectedColumn(
- int $column,
- \GtkTreeView $treeview = null
- ): null|int|string
- {
- if (is_null($treeview))
- {
- $treeview = $this->treeview;
- }
-
- list(
- $list,
- $row
- ) = $treeview->get_selection()->get_selected();
-
- if ($list && $row)
- {
- if ($value = $list->get_value($row, $column))
- {
- return $value;
- }
- }
-
- return null;
- }
-}
\ No newline at end of file
diff --git a/src/Entity/Tab/Page.php b/src/Entity/Tab/Page.php
deleted file mode 100644
index e6a06f2e..00000000
--- a/src/Entity/Tab/Page.php
+++ /dev/null
@@ -1,1035 +0,0 @@
-app = $app;
-
- // Init config namespace
- $this->config = $app->config->tab->page;
-
- // Init DNS memory
- $this->dns = new \Yggverse\Yoda\Model\Memory;
-
- // Init history
- $this->history = new \Yggverse\Yoda\Model\History;
-
- // Compose header
- $this->header = new \GtkBox(
- \GtkOrientation::HORIZONTAL
- );
-
- $this->header->set_margin_top(
- $this->config->header->margin
- );
-
- $this->header->set_margin_bottom(
- $this->config->header->margin
- );
-
- $this->header->set_margin_start(
- $this->config->header->margin
- );
-
- $this->header->set_margin_end(
- $this->config->header->margin
- );
-
- $this->header->set_spacing(
- $this->config->header->margin
- );
-
- // Init home button
- $this->home = \GtkButton::new_with_label(
- $this->config->header->button->home->label
- );
-
- if ($this->config->header->button->home->visible)
- {
- $this->home->connect(
- 'clicked',
- function ($entry)
- {
- $this->history->reset();
-
- $this->open(
- $this->config->header->button->home->url
- );
- }
- );
-
- $this->header->add(
- $this->home
- );
- }
-
- // Init back button
- $this->back = \GtkButton::new_with_label(
- $this->config->header->button->back->label
- );
-
- // Init forward button
- $this->forward = \GtkButton::new_with_label(
- $this->config->header->button->forward->label
- );
-
- // Group back/forward buttons
- if ($this->config->header->button->back->visible || $this->config->header->button->forward->visible)
- {
- $buttonGroup = new \GtkButtonBox(
- \GtkOrientation::HORIZONTAL
- );
-
- $buttonGroup->set_layout(
- \GtkButtonBoxStyle::EXPAND
- );
-
- if ($this->config->header->button->back->visible)
- {
- $this->back->set_sensitive(
- false
- );
-
- $this->back->connect(
- 'clicked',
- function ($entry)
- {
- $this->open(
- $this->history->goBack(),
- false
- );
- }
- );
-
- $buttonGroup->add(
- $this->back
- );
- }
-
- if ($this->config->header->button->forward->visible)
- {
- $this->forward->set_sensitive(
- false
- );
-
- $this->forward->connect(
- 'clicked',
- function ($entry)
- {
- $this->open(
- $this->history->goForward(),
- false
- );
- }
- );
-
- $buttonGroup->add(
- $this->forward
- );
- }
-
- $this->header->add(
- $buttonGroup
- );
- }
-
- // Init base button
- $this->base = \GtkButton::new_with_label(
- $this->config->header->button->base->label
- );
-
- $this->base->set_sensitive(
- false
- );
-
- if ($this->config->header->button->base->visible)
- {
- $this->base->connect(
- 'clicked',
- function (\GtkButton $button)
- {
- $base = new \Yggverse\Net\Address(
- $this->request->get_text()
- );
-
- $this->open(
- $base->get(
- true, // scheme
- true, // user
- true, // pass
- true, // host
- true, // port
- false, // path
- false, // query
- false // fragment
- )
- );
- }
- );
-
- $this->header->add(
- $this->base
- );
- }
-
- // Request field
- $this->request = new \GtkEntry;
-
- $this->request->set_placeholder_text(
- $this->config->header->entry->request->placeholder
- );
-
- $this->request->set_max_length(
- $this->config->header->entry->request->length->max
- );
-
- $this->header->pack_start(
- $this->request,
- true,
- true,
- 0
- );
-
- $this->request->connect(
- 'activate',
- function ($entry)
- {
- $this->open(
- $entry->get_text()
- );
- }
- );
-
- // Init autocomplete
- if ($this->config->header->entry->request->autocomplete->enabled)
- {
- $this->completion = new \GtkEntryCompletion();
-
- $this->completion->set_inline_completion(
- $this->config->header->entry->request->autocomplete->inline->completion
- );
-
- $this->completion->set_inline_selection(
- $this->config->header->entry->request->autocomplete->inline->selection
- );
-
- $this->completion->set_minimum_key_length(
- $this->config->header->entry->request->autocomplete->key->length
- );
-
- $this->completion->set_text_column(
- 0
- );
-
- $this->suggestion = new \GtkListStore(
- \GObject::TYPE_STRING
- );
-
- $this->completion->set_model(
- $this->suggestion
- );
-
- $this->request->connect(
- 'key-release-event',
- function ($entry, $event)
- {
- if (
- mb_strlen($entry->get_text()) >= $this->config->header->entry->request->autocomplete->key->length
- &&
- isset($event->key->keycode)
- &&
- !in_array(
- $event->key->keycode,
- $this->config->header->entry->request->autocomplete->ignore->keycode
- )
- ) {
- $this->suggestion->clear();
-
- foreach ($this->app->database->getHistory(
- $entry->get_text(), 0, $this->config->header->entry->request->autocomplete->result->limit
- ) as $suggestion)
- {
- $this->suggestion->append(
- [
- $suggestion->url
- ]
- );
- }
-
- $this->request->set_completion(
- $this->completion
- );
- }
- }
- );
- }
-
- // Go button
- $this->go = \GtkButton::new_with_label(
- $this->config->header->button->go->label
- );
-
- if ($this->config->header->button->go->visible)
- {
- $this->go->connect(
- 'clicked',
- function ($entry)
- {
- $this->open(
- $this->request->get_text()
- );
- }
- );
-
- $this->header->add(
- $this->go
- );
- }
-
- // Compose body
- $this->content = new \GtkLabel;
-
- $this->content->set_use_markup(
- true
- );
-
- $this->content->set_selectable(
- true
- );
-
- $this->content->set_line_wrap(
- true
- );
-
- $this->content->set_xalign(
- 0
- );
-
- $this->content->set_yalign(
- 0
- );
-
- // Init scrolled container
- $this->container = new \GtkScrolledWindow;
-
- $this->container->add(
- $this->content
- );
-
- $this->body = new \GtkBox(
- \GtkOrientation::VERTICAL
- );
-
- $this->body->set_margin_start(
- $this->config->body->margin
- );
-
- $this->body->pack_start(
- $this->container,
- true,
- true,
- 0
- );
-
- $this->content->connect(
- 'activate-link',
- function ($label, $href)
- {
- $address = new \Yggverse\Net\Address(
- $href
- );
-
- if ($address->isRelative())
- {
- $base = new \Yggverse\Net\Address(
- $this->request->get_text()
- );
-
- if ($absolute = $address->getAbsolute($base))
- {
- $this->open(
- $absolute
- );
- }
-
- else
- {
- throw new Exception(); // @TODO
- }
- }
-
- else
- {
- $this->open(
- $address->get()
- );
- }
- }
- );
-
- // Init progressbar
- $this->progressbar = new \GtkProgressBar();
-
- $this->progressbar->set_opacity(
- 0
- );
-
- // Compose footer
- $this->footer = new \GtkBox(
- \GtkOrientation::HORIZONTAL
- );
-
- $this->footer->set_margin_top(
- $this->config->footer->margin
- );
-
- $this->footer->set_margin_bottom(
- $this->config->footer->margin
- );
-
- $this->footer->set_margin_start(
- $this->config->footer->margin
- );
-
- $this->footer->set_margin_end(
- $this->config->footer->margin
- );
-
- $this->footer->set_spacing(
- $this->config->footer->margin
- );
-
- $this->status = new \GtkLabel;
-
- $this->status->connect(
- 'activate-link',
- function ($label, $href)
- {
- $this->open(
- $href
- );
- }
- );
-
- $this->footer->add(
- $this->status
- );
-
- // Compose page
- $this->box = new \GtkBox(
- \GtkOrientation::VERTICAL
- );
-
- $this->box->add(
- $this->header
- );
-
- $this->box->pack_start(
- $this->body,
- true,
- true,
- 0
- );
-
- $this->box->add(
- $this->progressbar
- );
-
- $this->box->add(
- $this->footer
- );
- }
-
- public function open(
- string $url,
- bool $history = true,
- int $code = 0
- ): void
- {
- // Filter URL
- $url = trim(
- $url
- );
-
- // Update history in memory pool
- if ($history && $this->config->history->memory->enabled && $url != $this->history->getCurrent())
- {
- $this->history->add(
- $url
- );
- }
-
- // Update home button sensibility on match requested
- if ($this->config->header->button->home->visible)
- {
- $this->home->set_sensitive(
- !($url == $this->config->header->button->home->url)
- );
- }
-
- // Update back button sensibility
- if ($this->config->header->button->back->visible)
- {
- $this->back->set_sensitive(
- (bool) $this->history->getBack()
- );
- }
-
- // Update forward button sensibility
- if ($this->config->header->button->forward->visible)
- {
- $this->forward->set_sensitive(
- (bool) $this->history->getForward()
- );
- }
-
- // Update base button sensibility
- if ($this->config->header->button->base->visible)
- {
- // Update address
- $base = new \Yggverse\Net\Address(
- $this->request->get_text()
- );
-
- $this->base->set_sensitive(
- !($url == $base->get(
- true, // scheme
- true, // user
- true, // pass
- true, // host
- true, // port
- false, // path
- false, // query
- false // fragment
- ))
- );
- }
-
- // Update request field by requested
- $this->request->set_text(
- $url
- );
-
- // Open current address
- switch (true)
- {
- case str_starts_with($url, 'gemini://'):
-
- $this->_openGemini(
- $url,
- $code
- );
-
- break;
-
- case str_starts_with($url, 'nex://'):
-
- $this->_openNex(
- $url
- );
-
- break;
-
- case str_starts_with($url, 'yoda://'):
-
- $this->_openYoda(
- $url
- );
-
- break;
-
- default:
-
- $this->_openYoda(
- 'yoda://protocol'
- );
- }
- }
-
- private function _openGemini(
- string $url,
- int $code = 0,
- int $redirects = 0,
- bool $history = true
- ): void
- {
- // Init progressbar
- if ($this->config->progressbar->visible)
- {
- $this->setProgress(0);
- }
-
- // Init base URL
- $origin = new \Yggverse\Net\Address(
- $url
- );
-
- // Track response time
- $start = microtime(true);
-
- // Init custom resolver
- $host = null;
-
- if ($this->config->resolver->enabled)
- {
- $address = new \Yggverse\Net\Address(
- $url
- );
-
- $name = $address->getHost();
-
- if (!$host = $this->dns->get($name))
- {
- $resolve = new \Yggverse\Net\Resolve(
- $this->config->resolver->request->record,
- $this->config->resolver->request->host,
- $this->config->resolver->request->timeout,
- $this->config->resolver->result->shuffle
- );
-
- $resolved = $resolve->address(
- $address
- );
-
- if ($resolved)
- {
- $host = $resolved->getHost();
-
- $this->dns->set(
- $name,
- $host
- );
- }
- }
- }
-
- $request = new \Yggverse\Gemini\Client\Request(
- $url,
- $host
- );
-
- $raw = $request->getResponse();
-
- $end = microtime(true);
-
- $response = new \Yggverse\Gemini\Client\Response(
- $raw
- );
-
- // Process redirect
- if (in_array($response->getCode(), $this->config->redirect->follow->code))
- {
- if ($this->config->redirect->follow->enabled)
- {
- if ($redirects > $this->config->redirect->follow->max)
- {
- $this->_openYoda(
- 'yoda://redirect'
- );
-
- return;
- }
-
- $redirect = new \Yggverse\Net\Address(
- $url
- );
-
- $redirect->setPath(
- $response->getMeta()
- );
-
- $this->open(
- $redirect->get(),
- false,
- $response->getCode(),
- $redirects + 1
- );
-
- return;
- }
-
- else
- {
- $this->_openYoda(
- 'yoda://redirect'
- );
-
- return;
- }
- }
-
- // Process error codes
- if (20 != $response->getCode()) // not found
- {
- $this->_openYoda(
- 'yoda://nothing'
- );
-
- return;
- } // @TODO other codes
-
- $this->content->set_markup(
- \Yggverse\Gemini\Gtk3\Pango::fromGemtext(
- $response->getBody()
- )
- );
-
- $body = new \Yggverse\Gemini\Gemtext\Body(
- $response->getBody()
- );
-
- // Try to detect document title
- if ($h1 = $body->getH1())
- {
- $title = reset(
- $h1
- );
- }
-
- else if ($h2 = $body->getH2())
- {
- $title = reset(
- $h2
- );
- }
-
- else if ($h3 = $body->getH3())
- {
- $title = reset(
- $h3
- );
- }
-
- else
- {
- $title = $origin->getHost();
- }
-
- $this->setTitle(
- $title
- );
-
- $this->status->set_markup(
- str_replace( // Custom macros mask from config.json
- [
- '{TIME_C}',
- '{REQUEST_BASE}',
- '{REQUEST_BASE_URL}',
- '{RESPONSE_CODE}',
- '{RESPONSE_META}',
- '{RESPONSE_LENGTH}',
- '{RESPONSE_SECONDS}'
- ],
- [
- date(
- 'c'
- ),
- $origin->getHost(),
- sprintf(
- '%s',
- $origin->get(
- true, // scheme
- true, // user
- true, // pass
- true, // host
- true, // port
- false, // path
- false, // query
- false // fragment
- ),
- $origin->getHost()
- ),
- $response->getCode(),
- ($code ? sprintf('%d:', $code) : '')
- .
- ($response->getMeta() ? $response->getMeta() : $response->getCode()),
- number_format(
- mb_strlen(
- $raw
- )
- ),
- round(
- $end - $start, 2
- )
- ],
- $this->config->footer->status->open->complete
- )
- );
-
- // Update history database
- if ($history && $this->config->history->database->enabled)
- {
- $this->app->history->add(
- $url,
- $title,
- $this->config->history->database->mode->renew
- );
- }
- }
-
- private function _openNex(
- string $url,
- bool $history = true
- ): void
- {
- // Init progressbar
- if ($this->config->progressbar->visible)
- {
- $this->setProgress(0);
- }
-
- // Init base URL
- $origin = new \Yggverse\Net\Address(
- $url
- );
-
- // Track response time
- $start = microtime(true);
-
- // @TODO custom resolver support
-
- $client = new \Yggverse\Nex\Client;
-
- $response = $client->request(
- $url
- );
-
- $end = microtime(true);
-
- $this->content->set_markup(
- $response
- );
-
- $this->setTitle(
- $origin->getHost()
- );
-
- $this->status->set_markup(
- str_replace( // Custom macros mask from config.json
- [
- '{TIME_C}',
- '{REQUEST_BASE}',
- '{REQUEST_BASE_URL}',
- '{RESPONSE_CODE}',
- '{RESPONSE_META}',
- '{RESPONSE_LENGTH}',
- '{RESPONSE_SECONDS}'
- ],
- [
- date(
- 'c'
- ),
- $origin->getHost(),
- sprintf(
- '%s',
- $origin->get(
- true, // scheme
- true, // user
- true, // pass
- true, // host
- true, // port
- false, // path
- false, // query
- false // fragment
- ),
- $origin->getHost()
- ),
- '-', // @TODO
- '-',
- number_format(
- mb_strlen(
- $response
- )
- ),
- round(
- $end - $start, 2
- )
- ],
- $this->config->footer->status->open->complete
- )
- );
-
- // Update history database
- if ($history && $this->config->history->database->enabled)
- {
- $this->app->history->add(
- $url,
- $title,
- $this->config->history->database->mode->renew
- );
- }
- }
-
- private function _openYoda(
- string $url
- ): void
- {
- // Load local page
- if (!$data = \Yggverse\Yoda\Model\Page::get(str_replace('yoda://', '', $url)))
- {
- $data = \Yggverse\Yoda\Model\Page::get('Nothing');
- }
-
- $this->content->set_markup(
- \Yggverse\Gemini\Gtk3\Pango::fromGemtext(
- $data
- )
- );
-
- // Parse gemtext
- $body = new \Yggverse\Gemini\Gemtext\Body(
- $data
- );
-
- if ($h1 = $body->getH1())
- {
- $title = reset(
- $h1
- );
-
- $this->setTitle(
- $title
- );
- }
- }
-
- public function setTitle(
- ?string $value = null
- ): void
- {
- // Append hostname postfix
- if ($this->config->title->postfix->hostname && str_starts_with($this->request->get_text(), 'gemini://'))
- {
- $address = new \Yggverse\Net\Address(
- $this->request->get_text()
- );
-
- if ($address->getHost())
- {
- $value = sprintf(
- '%s - %s',
- $value,
- $address->getHost()
- );
- }
- }
-
- // Build new tab label on title length reached
- if ($value && mb_strlen($value) > $this->config->title->width->chars)
- {
- $label = new \GtkLabel(
- $value ? $value : $this->config->title->default
- );
-
- if ($this->config->title->width->chars)
- {
- $label->set_width_chars(
- $this->config->title->width->chars
- );
- }
-
- if ($this->config->title->ellipsize->mode)
- {
- $label->set_ellipsize(
- // https://docs.gtk.org/Pango/enum.EllipsizeMode.html
- $this->config->title->ellipsize->mode
- );
- }
-
- $this->app->tabs->set_tab_label(
- $this->box,
- $label
- );
- }
-
- else
- {
- $this->app->tabs->set_tab_label_text(
- $this->box,
- $value
- );
- }
-
- // Update window title
- $this->app->setTitle(
- $value ? $value : $this->config->title->default
- );
- }
-
- public function setProgress(
- float $value
- ): void
- {
- $this->progressbar->set_fraction(
- $value
- );
-
- \Gtk::timeout_add(
- 10,
- function()
- {
- $progress = $this->progressbar->get_fraction();
-
- $progress = $progress + 0.02;
-
- $this->progressbar->set_fraction(
- $progress
- );
-
- if ($progress < 1)
- {
- $this->progressbar->set_opacity(
- 1
- );
- }
-
- else
- {
- $this->progressbar->set_opacity(
- 0
- );
-
- return false;
- }
- }
- );
- }
-}
\ No newline at end of file
diff --git a/src/Entity/Window.php b/src/Entity/Window.php
new file mode 100644
index 00000000..b1d23e5a
--- /dev/null
+++ b/src/Entity/Window.php
@@ -0,0 +1,53 @@
+database = $database;
+
+ $this->gtk = new \GtkWindow;
+
+ $this->gtk->set_size_request(
+ $this->_width,
+ $this->_height
+ );
+
+ $this->header = new Header(
+ $this
+ );
+
+ $this->gtk->set_titlebar(
+ $this->header->gtk
+ );
+
+ $this->tab = new Tab(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->tab->gtk
+ );
+
+ $this->gtk->show_all();
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Header.php b/src/Entity/Window/Header.php
new file mode 100644
index 00000000..2daf063c
--- /dev/null
+++ b/src/Entity/Window/Header.php
@@ -0,0 +1,45 @@
+window = $window;
+
+ $this->gtk = new \GtkHeaderBar;
+
+ $this->gtk->set_show_close_button(
+ $this->_actions
+ );
+
+ $this->setTitle(
+ $this->_title
+ );
+ }
+
+ public function setTitle(
+ ?string $title = null
+ ): void
+ {
+ $this->gtk->set_title(
+ is_null($title) ? $this->_title : sprintf(
+ '%s - %s',
+ $title,
+ $this->_title
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab.php b/src/Entity/Window/Tab.php
new file mode 100644
index 00000000..a028924a
--- /dev/null
+++ b/src/Entity/Window/Tab.php
@@ -0,0 +1,88 @@
+window = $window;
+
+ $this->gtk = new \GtkNotebook;
+
+ $this->gtk->set_scrollable(
+ $this->_scrollable
+ );
+
+ $this->gtk->connect(
+ 'switch-page',
+ function (
+ \GtkNotebook $entity,
+ \GtkWidget $child,
+ int $position
+ ) {
+ $this->window->header->setTitle(
+ $entity->get_tab_label(
+ $child
+ )->get_text()
+ );
+ }
+ );
+
+ $this->append( // @TODO remove
+ new History(
+ $this
+ )
+ );
+
+ $this->append( // @TODO remove
+ new Address(
+ $this
+ )
+ );
+ }
+
+ public function append(
+ Address | History $entity,
+ ?bool $reorderable = null
+ ): void
+ {
+ $this->gtk->append_page(
+ $entity->gtk,
+ $entity->title->gtk
+ );
+
+ $this->gtk->set_tab_reorderable(
+ $entity->gtk,
+ is_null($reorderable) ? $this->_reorderable : $reorderable
+ );
+
+ $this->gtk->show_all();
+
+ // Focus on appended tab
+ $this->gtk->set_current_page(
+ $this->gtk->page_num(
+ $entity->gtk
+ )
+ );
+
+ // Update application title
+ $this->window->header->setTitle(
+ $entity->title->gtk->get_text()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address.php b/src/Entity/Window/Tab/Address.php
new file mode 100644
index 00000000..28bc98a5
--- /dev/null
+++ b/src/Entity/Window/Tab/Address.php
@@ -0,0 +1,228 @@
+tab = $tab;
+
+ $this->title = new Title(
+ $this
+ );
+
+ $this->gtk = new \GtkBox(
+ \GtkOrientation::VERTICAL
+ );
+
+ $this->navbar = new Navbar(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->navbar->gtk
+ );
+
+ $this->content = new Content(
+ $this
+ );
+
+ $this->gtk->pack_start(
+ $this->content->gtk,
+ true,
+ true,
+ 0
+ );
+
+ $this->statusbar = new Statusbar(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->statusbar->gtk
+ );
+ }
+
+ public function update(): void
+ {
+ // Parse address
+ $address = new \Yggverse\Net\Address(
+ $this->navbar->request->gtk->get_text()
+ );
+
+ // Update title
+ $this->title->gtk->set_text(
+ $address->getHost()
+ );
+
+ // Update navbar elements
+ $this->navbar->base->update(
+ $address
+ );
+
+ // Remember address in the navigation memory
+ $this->navbar->history->add(
+ $address->get()
+ );
+
+ // Refresh history in database
+ $this->navbar->address->tab->window->database->refreshHistory(
+ $address->get(),
+ // @TODO title
+ );
+
+ // Update statusbar indicator
+ $this->statusbar->gtk->set_text(
+ 'Loading...'
+ );
+
+ // Detect protocol
+ switch ($address->getScheme())
+ {
+ case 'file':
+
+ // @TODO
+
+ break;
+
+ case 'nex':
+
+ // @TODO
+
+ break;
+
+ case 'gemini':
+
+ $request = new \Yggverse\Gemini\Client\Request(
+ $address->get()
+ );
+
+ $response = new \Yggverse\Gemini\Client\Response(
+ $request->getResponse()
+ );
+
+ if (20 === $response->getCode())
+ {
+ switch (true)
+ {
+ case str_contains($response->getMeta(), 'text/gemini'):
+
+ $title = null;
+
+ $this->content->data->setValue(
+ $response->getBody(),
+ $title
+ );
+
+ if ($title) // detect title by document h1
+ {
+ $this->title->gtk->set_text(
+ $title
+ );
+ }
+
+ break;
+
+ default:
+
+ $this->content->data->setValue(
+ $response->getBody()
+ );
+ }
+
+ $this->statusbar->gtk->set_text(
+ $response->getMeta()
+ );
+ }
+
+ else
+ {
+ $this->title->gtk->set_text(
+ 'Failure'
+ );
+
+ $this->content->data->setValue(
+ sprintf(
+ 'Resource not available (code %d)',
+ intval(
+ $response->getCode()
+ )
+ )
+ );
+
+ $this->statusbar->gtk->set_text(
+ 'Request failed'
+ );
+ }
+
+ break;
+
+ case null:
+
+ // Try gemini protocol
+ $address = new \Yggverse\Net\Address(
+ sprintf(
+ 'gemini://%s',
+ $this->navbar->request->gtk->get_text()
+ )
+ );
+
+ // Address correct
+ if ($address->getHost())
+ {
+ $this->navbar->request->gtk->set_text(
+ $address->get()
+ );
+
+ $this->update();
+ }
+
+ // Search request
+ else
+ {
+ // @TODO
+ }
+
+ return;
+
+ default:
+
+ $this->title->gtk->set_text(
+ 'Oops!'
+ );
+
+ $this->content->data->setValue(
+ sprintf(
+ 'Protocol not supported',
+ intval(
+ $response->getCode()
+ )
+ )
+ );
+ }
+
+ $this->tab->window->header->setTitle(
+ $this->title->gtk->get_text()
+ );
+
+ $this->gtk->show_all();
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Content.php b/src/Entity/Window/Tab/Address/Content.php
new file mode 100644
index 00000000..f789f049
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Content.php
@@ -0,0 +1,45 @@
+address = $address;
+
+ $this->gtk = new \GtkScrolledWindow;
+
+ $this->gtk->set_margin_start(
+ $this->_margin
+ );
+
+ $this->gtk->set_margin_end(
+ $this->_margin
+ );
+
+ $this->data = new Gemtext(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->data->gtk
+ );
+
+ $this->gtk->show_all();
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Content/Gemtext.php b/src/Entity/Window/Tab/Address/Content/Gemtext.php
new file mode 100644
index 00000000..e377e03d
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Content/Gemtext.php
@@ -0,0 +1,231 @@
+content = $content;
+
+ $this->gtk = new \GtkLabel;
+
+ $this->gtk->set_use_markup(
+ true
+ );
+
+ $this->gtk->set_selectable(
+ true
+ );
+
+ $this->gtk->set_line_wrap(
+ true
+ );
+
+ $this->gtk->set_xalign(
+ 0
+ );
+
+ $this->gtk->set_yalign(
+ 0
+ );
+
+ $this->setValue(
+ $this->_value
+ );
+
+ $this->gtk->connect(
+ 'activate-link',
+ function(
+ \GtkLabel $label,
+ string $href
+ ) {
+ $this->content->address->navbar->request->gtk->set_text(
+ $this->_url(
+ $href
+ )
+ );
+
+ $this->content->address->update();
+ }
+ );
+ }
+
+ public function setValue(
+ string $value,
+ string | null &$title = null
+ ): void
+ {
+ $document = new Document(
+ $value
+ );
+
+ $line = [];
+
+ foreach ($document->getEntities() as $entity)
+ {
+ switch (true)
+ {
+ case $entity instanceof \Yggverse\Gemtext\Entity\Code:
+
+ if ($entity->isInline())
+ {
+ $line[] = sprintf(
+ '%s',
+ htmlspecialchars(
+ $entity->getAlt()
+ )
+ );
+ }
+
+ else
+ {
+ // @TODO multiline
+ }
+
+ break;
+
+ case $entity instanceof \Yggverse\Gemtext\Entity\Header:
+
+ switch ($entity->getLevel())
+ {
+ case 1: // #
+
+ $line[] = sprintf(
+ '%s',
+ htmlspecialchars(
+ $entity->getText()
+ )
+ );
+
+ // Find and return document title by first # tag
+ if (empty($title))
+ {
+ $title = $entity->getText();
+ }
+
+ break;
+
+ case 2: // ##
+
+ $line[] = sprintf(
+ '%s',
+ htmlspecialchars(
+ $entity->getText()
+ )
+ );
+
+ break;
+
+ case 3: // ###
+
+ $line[] = sprintf(
+ '%s',
+ htmlspecialchars(
+ $entity->getText()
+ )
+ );
+
+ break;
+ default:
+
+ throw new \Exception;
+ }
+
+ break;
+
+ case $entity instanceof \Yggverse\Gemtext\Entity\Link:
+
+ $line[] = sprintf(
+ '%s',
+ $this->_url(
+ $entity->getAddress()
+ ),
+ htmlspecialchars(
+ $entity->getAddress()
+ ),
+ htmlspecialchars(
+ $entity->getAlt() ? $entity->getAlt()
+ : $entity->getAddress() // @TODO date
+ )
+ );
+
+ break;
+
+ case $entity instanceof \Yggverse\Gemtext\Entity\Listing:
+
+ $line[] = sprintf(
+ '* %s',
+ htmlspecialchars(
+ $entity->getItem()
+ )
+ );
+
+ break;
+
+ case $entity instanceof \Yggverse\Gemtext\Entity\Quote:
+
+ $line[] = sprintf(
+ '%s',
+ htmlspecialchars(
+ $entity->getText()
+ )
+ );
+
+ break;
+
+ case $entity instanceof \Yggverse\Gemtext\Entity\Text:
+
+ $line[] = htmlspecialchars(
+ $entity->getData()
+ );
+
+ break;
+
+ default:
+
+ throw new \Exception;
+ }
+ }
+
+ $this->gtk->set_markup(
+ implode(
+ PHP_EOL,
+ $line
+ )
+ );
+ }
+
+ private function _url(
+ string $link
+ ): ?string
+ {
+ $address = new Address(
+ $link
+ );
+
+ if ($address->isRelative())
+ {
+ $address->toAbsolute(
+ new Address(
+ $this->content->address->navbar->request->gtk->get_text()
+ )
+ );
+ }
+
+ return $address->get();
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Content/Plain.php b/src/Entity/Window/Tab/Address/Content/Plain.php
new file mode 100644
index 00000000..c2ebbbf3
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Content/Plain.php
@@ -0,0 +1,54 @@
+content = $content;
+
+ $this->gtk = new \GtkLabel(
+ $this->_value
+ );
+
+ $this->gtk->set_use_markup(
+ false
+ );
+
+ $this->gtk->set_selectable(
+ true
+ );
+
+ $this->gtk->set_line_wrap(
+ true
+ );
+
+ $this->gtk->set_xalign(
+ 0
+ );
+
+ $this->gtk->set_yalign(
+ 0
+ );
+ }
+
+ public function setValue(
+ string $value
+ ): void
+ {
+ $this->gtk->set_text(
+ $value
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Navbar.php b/src/Entity/Window/Tab/Address/Navbar.php
new file mode 100644
index 00000000..c63c363a
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Navbar.php
@@ -0,0 +1,104 @@
+address = $address;
+
+ // Init navbar area
+ $this->gtk = new \GtkBox(
+ \GtkOrientation::HORIZONTAL
+ );
+
+ $this->setMargins(
+ $this->_margin
+ );
+
+ // Append base button
+ $this->base = new Base(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->base->gtk
+ );
+
+ // Append history buttons group
+ $this->history = new History(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->history->gtk
+ );
+
+ // Append request entry, fill empty space
+ $this->request = new Request(
+ $this
+ );
+
+ $this->gtk->pack_start(
+ $this->request->gtk,
+ true,
+ true,
+ 0
+ );
+
+ // Append go button
+ $this->go = new Go(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->go->gtk
+ );
+ }
+
+ public function setMargins(
+ ?int $value
+ ): void
+ {
+ $this->gtk->set_margin_top(
+ $value ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_bottom(
+ $value ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_start(
+ $value ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_end(
+ $value ?? $this->_margin
+ );
+
+ $this->gtk->set_spacing(
+ $value ?? $this->_margin
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Navbar/Base.php b/src/Entity/Window/Tab/Address/Navbar/Base.php
new file mode 100644
index 00000000..b24b3222
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Navbar/Base.php
@@ -0,0 +1,55 @@
+navbar->request->gtk->get_text()
+ );
+
+ if ($address->getHost())
+ {
+ $this->navbar->request->gtk->set_text(
+ $address->get( // build base
+ true,
+ true,
+ true,
+ true,
+ true,
+ false,
+ false,
+ false
+ )
+ );
+
+ $this->navbar->address->update();
+ }
+
+ $this->update();
+ }
+
+ public function update(
+ ?\Yggverse\Net\Address $address = null
+ ): void
+ {
+ if (is_null($address))
+ {
+ $address = new \Yggverse\Net\Address(
+ $this->navbar->request->gtk->get_text()
+ );
+ }
+
+ $this->navbar->base->gtk->set_sensitive(
+ $address->getHost() && ($address->getPath() || $address->getQuery())
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Navbar/Go.php b/src/Entity/Window/Tab/Address/Navbar/Go.php
new file mode 100644
index 00000000..2f9993dc
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Navbar/Go.php
@@ -0,0 +1,17 @@
+navbar->address->update();
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Navbar/History.php b/src/Entity/Window/Tab/Address/Navbar/History.php
new file mode 100644
index 00000000..bd244264
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Navbar/History.php
@@ -0,0 +1,76 @@
+_history = new \Yggverse\Yoda\Model\History();
+
+ $this->navbar = $navbar;
+
+ $this->gtk = new \GtkButtonBox(
+ \GtkOrientation::HORIZONTAL
+ );
+
+ $this->gtk->set_layout(
+ \GtkButtonBoxStyle::EXPAND
+ );
+
+ $this->back = new Back(
+ $this->navbar
+ );
+
+ $this->gtk->add(
+ $this->back->gtk
+ );
+
+ $this->forward = new Forward(
+ $this->navbar
+ );
+
+ $this->gtk->add(
+ $this->forward->gtk
+ );
+ }
+
+ public function add(
+ string $url
+ ): void
+ {
+ if (empty($url))
+ {
+ throw new \Exception;
+ }
+
+ if ($url != $this->_history->getCurrent())
+ {
+ $this->_history->add(
+ $url
+ );
+ }
+
+ $this->back->gtk->set_sensitive(
+ (bool) $this->_history->getBack()
+ );
+
+ $this->forward->gtk->set_sensitive(
+ (bool) $this->_history->getForward()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Navbar/History/Back.php b/src/Entity/Window/Tab/Address/Navbar/History/Back.php
new file mode 100644
index 00000000..7e39d47b
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Navbar/History/Back.php
@@ -0,0 +1,17 @@
+navbar->address->update();
+ }
+
+ protected function _onKeyRelease(
+ \GtkEntry $entry,
+ \GdkEvent $event
+ ): void
+ {
+ $this->navbar->base->update();
+
+ $this->navbar->go->gtk->set_sensitive(
+ !empty(
+ $entry->get_text()
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Statusbar.php b/src/Entity/Window/Tab/Address/Statusbar.php
new file mode 100644
index 00000000..0c396952
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Statusbar.php
@@ -0,0 +1,51 @@
+address = $address;
+
+ $this->gtk = new \GtkLabel;
+
+ $this->gtk->set_line_wrap(
+ true
+ );
+
+ $this->gtk->set_xalign(
+ 0
+ );
+
+ $this->gtk->set_yalign(
+ 0
+ );
+
+ $this->gtk->set_margin_top(
+ $this->_margin
+ );
+
+ $this->gtk->set_margin_bottom(
+ $this->_margin
+ );
+
+ $this->gtk->set_margin_start(
+ $this->_margin
+ );
+
+ $this->gtk->set_margin_end(
+ $this->_margin
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/Address/Title.php b/src/Entity/Window/Tab/Address/Title.php
new file mode 100644
index 00000000..e7f4fd27
--- /dev/null
+++ b/src/Entity/Window/Tab/Address/Title.php
@@ -0,0 +1,35 @@
+address = $address;
+
+ $this->gtk = new \GtkLabel(
+ $this->_value
+ );
+
+ $this->gtk->set_width_chars(
+ $this->_length
+ );
+
+ $this->gtk->set_ellipsize(
+ $this->_ellipsize
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/History.php b/src/Entity/Window/Tab/History.php
new file mode 100644
index 00000000..b9c0c130
--- /dev/null
+++ b/src/Entity/Window/Tab/History.php
@@ -0,0 +1,53 @@
+tab = $tab;
+
+ $this->title = new Title(
+ $this
+ );
+
+ $this->gtk = new \GtkBox(
+ \GtkOrientation::VERTICAL
+ );
+
+ $this->content = new Content(
+ $this
+ );
+
+ $this->navbar = new Navbar(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->navbar->gtk
+ );
+
+ $this->gtk->pack_start(
+ $this->content->gtk,
+ true,
+ true,
+ 0
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/History/Content.php b/src/Entity/Window/Tab/History/Content.php
new file mode 100644
index 00000000..08fd9f12
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Content.php
@@ -0,0 +1,220 @@
+history = $history;
+
+ $this->gtk = new \GtkScrolledWindow;
+
+ $this->gtk->set_margin_start(
+ $this->_margin
+ );
+
+ $this->gtk->set_margin_end(
+ $this->_margin
+ );
+
+ $this->treeview = new \GtkTreeView;
+
+ $this->treeview->append_column(
+ new \GtkTreeViewColumn(
+ $this->_time,
+ new \GtkCellRendererText(),
+ 'text',
+ 1
+ )
+ );
+
+ $this->treeview->append_column(
+ new \GtkTreeViewColumn(
+ $this->_url,
+ new \GtkCellRendererText(),
+ 'text',
+ 2
+ )
+ );
+
+ $this->treeview->append_column(
+ new \GtkTreeViewColumn(
+ $this->_title,
+ new \GtkCellRendererText(),
+ 'text',
+ 3
+ )
+ );
+
+ $this->list = new \GtkListStore(
+ \GObject::TYPE_INT,
+ \GObject::TYPE_STRING,
+ \GObject::TYPE_STRING,
+ \GObject::TYPE_STRING
+ );
+
+ $this->treeview->set_model(
+ $this->list
+ );
+
+ $this->gtk->add(
+ $this->treeview
+ );
+
+ $this->search();
+
+ $this->treeview->connect(
+ 'row-activated',
+ function(
+ \GtkTreeView $treeview
+ ) {
+ $address = new Address(
+ $this->history->tab
+ );
+
+ $address->navbar->request->gtk->set_text(
+ $this->getSelectedUrl()
+ );
+
+ $this->history->tab->append(
+ $address
+ );
+
+ $address->update();
+ }
+ );
+
+ $this->treeview->connect(
+ 'cursor-changed',
+ function(
+ \GtkTreeView $treeview
+ ) {
+ $this->history->navbar->open->gtk->set_sensitive(
+ (bool) $this->getSelectedId()
+ );
+
+ $this->history->navbar->delete->gtk->set_sensitive(
+ (bool) $this->getSelectedId()
+ );
+ }
+ );
+ }
+
+ public function append(
+ int $id,
+ int $time,
+ string $url,
+ ?string $title
+ ): void
+ {
+ $this->list->append(
+ [
+ $id,
+ date(
+ $this->_format,
+ $time
+ ),
+ $url,
+ strval(
+ $title
+ )
+ ]
+ );
+ }
+
+ public function clear(): void
+ {
+ $this->list->clear();
+ }
+
+ public function search(
+ string $filter = ''
+ ): void
+ {
+ $this->clear();
+
+ if ($records = $this->history->tab->window->database->findHistory($filter))
+ {
+ foreach ($records as $record)
+ {
+ $this->append(
+ $record->id,
+ $record->time,
+ $record->url,
+ $record->title
+ );
+ }
+ }
+
+ else
+ {
+ $this->history->navbar->open->gtk->set_sensitive(
+ false
+ );
+
+ $this->history->navbar->delete->gtk->set_sensitive(
+ false
+ );
+ }
+ }
+
+ public function getSelectedId(): ?int
+ {
+ if ($id = $this->_getSelected(0))
+ {
+ return $id;
+ }
+
+ return null;
+ }
+
+ public function getSelectedUrl(): ?string
+ {
+ if ($url = $this->_getSelected(2))
+ {
+ return $url;
+ }
+
+ return null;
+ }
+
+ private function _getSelected(
+ int $column
+ ): null|int|string
+ {
+ list(
+ $list,
+ $row
+ ) = $this->treeview->get_selection()->get_selected();
+
+ if ($list && $row)
+ {
+ if ($value = $list->get_value($row, $column))
+ {
+ return $value;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/History/Navbar.php b/src/Entity/Window/Tab/History/Navbar.php
new file mode 100644
index 00000000..5cc91b86
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Navbar.php
@@ -0,0 +1,99 @@
+history = $history;
+
+ $this->gtk = new \GtkBox(
+ \GtkOrientation::HORIZONTAL
+ );
+
+ $this->setMargin(
+ $this->_margin
+ );
+
+ $this->open = new Open(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->open->gtk
+ );
+
+ $this->delete = new Delete(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->delete->gtk
+ );
+
+ $this->filter = new Filter(
+ $this
+ );
+
+ $this->gtk->pack_start(
+ $this->filter->gtk,
+ true,
+ true,
+ 0
+ );
+
+ $this->search = new Search(
+ $this
+ );
+
+ $this->gtk->add(
+ $this->search->gtk
+ );
+ }
+
+ public function setMargin(
+ ?int $value = null
+ ): void
+ {
+ $this->gtk->set_margin_top(
+ $margin ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_bottom(
+ $margin ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_start(
+ $margin ?? $this->_margin
+ );
+
+ $this->gtk->set_margin_end(
+ $margin ?? $this->_margin
+ );
+
+ $this->gtk->set_spacing(
+ $margin ?? $this->_margin
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/History/Navbar/Delete.php b/src/Entity/Window/Tab/History/Navbar/Delete.php
new file mode 100644
index 00000000..9ae985ef
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Navbar/Delete.php
@@ -0,0 +1,30 @@
+navbar->history->content->getSelectedId())
+ {
+ $this->navbar->history->tab->window->database->deleteHistory(
+ $id
+ );
+
+ $this->navbar->open->gtk->set_sensitive(
+ false
+ );
+
+ $this->navbar->history->content->search(
+ $this->navbar->filter->gtk->get_text()
+ );
+ }
+ }
+}
diff --git a/src/Entity/Window/Tab/History/Navbar/Filter.php b/src/Entity/Window/Tab/History/Navbar/Filter.php
new file mode 100644
index 00000000..5fb55c2f
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Navbar/Filter.php
@@ -0,0 +1,29 @@
+navbar->history->content->search(
+ $this->navbar->filter->gtk->get_text()
+ );
+ }
+
+ protected function _onKeyRelease(
+ \GtkEntry $entry,
+ \GdkEvent $event
+ ): void
+ {
+ $this->navbar->history->content->search(
+ $this->navbar->filter->gtk->get_text()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Window/Tab/History/Navbar/Open.php b/src/Entity/Window/Tab/History/Navbar/Open.php
new file mode 100644
index 00000000..c7e05d2e
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Navbar/Open.php
@@ -0,0 +1,31 @@
+navbar->history->tab
+ );
+
+ $this->navbar->history->tab->append( // @TODO
+ $address
+ );
+
+ $address->navbar->request->gtk->set_text(
+ $this->navbar->history->content->getSelectedUrl()
+ );
+
+ $address->update();
+ }
+}
diff --git a/src/Entity/Window/Tab/History/Navbar/Search.php b/src/Entity/Window/Tab/History/Navbar/Search.php
new file mode 100644
index 00000000..78eea2b2
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Navbar/Search.php
@@ -0,0 +1,28 @@
+gtk->set_sensitive(
+ false
+ );
+
+ $this->navbar->history->content->search(
+ $this->navbar->filter->gtk->get_text()
+ );
+
+ $this->gtk->set_sensitive(
+ true
+ );
+ }
+}
diff --git a/src/Entity/Window/Tab/History/Title.php b/src/Entity/Window/Tab/History/Title.php
new file mode 100644
index 00000000..2c69f62a
--- /dev/null
+++ b/src/Entity/Window/Tab/History/Title.php
@@ -0,0 +1,35 @@
+history = $history;
+
+ $this->gtk = new \GtkLabel(
+ $this->_value
+ );
+
+ $this->gtk->set_width_chars(
+ $this->_length
+ );
+
+ $this->gtk->set_ellipsize(
+ $this->_ellipsize
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Config.php b/src/Model/Config.php
deleted file mode 100644
index c3d891e2..00000000
--- a/src/Model/Config.php
+++ /dev/null
@@ -1,45 +0,0 @@
- $value)
- {
- $this->{$key} = $value; // @TODO
- }
- }
-}
\ No newline at end of file
diff --git a/src/Model/Database.php b/src/Model/Database.php
index 79ff67bf..c59ca9c8 100644
--- a/src/Model/Database.php
+++ b/src/Model/Database.php
@@ -64,8 +64,8 @@ class Database
return (int) $this->_database->lastInsertId();
}
- public function getHistory(
- string $search = '',
+ public function findHistory(
+ string $value = '',
int $start = 0,
int $limit = 1000
): array
@@ -73,7 +73,7 @@ class Database
$query = $this->_database->prepare(
sprintf(
'SELECT * FROM `history`
- WHERE `url` LIKE :search OR `title` LIKE :search
+ WHERE `url` LIKE :value OR `title` LIKE :value
ORDER BY `id` DESC
LIMIT %d,%d',
$start,
@@ -83,9 +83,9 @@ class Database
$query->execute(
[
- ':search' => sprintf(
+ ':value' => sprintf(
'%%%s%%',
- $search
+ $value
)
]
);
@@ -122,4 +122,35 @@ class Database
return $query->rowCount();
}
+
+ public function refreshHistory(
+ string $url,
+ ?string $title = null
+ ): void
+ {
+ // Find same records match URL
+ $query = $this->_database->prepare(
+ 'SELECT * FROM `history` WHERE `url` LIKE :url'
+ );
+
+ $query->execute(
+ [
+ ':url' => $url
+ ]
+ );
+
+ // Drop previous records
+ foreach ($query->fetchAll() as $record)
+ {
+ $this->deleteHistory(
+ $record->id
+ );
+ }
+
+ // Add new record
+ $this->addHistory(
+ $url,
+ $title
+ );
+ }
}
\ No newline at end of file
diff --git a/src/Model/File.php b/src/Model/File.php
deleted file mode 100644
index 2d7fdc7d..00000000
--- a/src/Model/File.php
+++ /dev/null
@@ -1,30 +0,0 @@
-_memory[$key] = $value;
- }
-
- public function get(string $key): mixed
- {
- if (isset($this->_memory[$key]))
- {
- return $this->_memory[$key];
- }
-
- return null;
- }
-
- public function flush(): void
- {
- $this->_memory = [];
- }
-}
\ No newline at end of file
diff --git a/src/Model/Page.php b/src/Model/Page.php
deleted file mode 100644
index c5dcc638..00000000
--- a/src/Model/Page.php
+++ /dev/null
@@ -1,31 +0,0 @@
- https://github.com/YGGverse/Yoda/issues Report
\ No newline at end of file
diff --git a/src/Page/Protocol.gmi b/src/Page/Protocol.gmi
deleted file mode 100644
index 0ba850d0..00000000
--- a/src/Page/Protocol.gmi
+++ /dev/null
@@ -1,8 +0,0 @@
-# Protocol issue
-
-At this moment, supported protocols:
-
-* gemini
-* yoda
-
-=> https://github.com/YGGverse/Yoda/issues Report
\ No newline at end of file
diff --git a/src/Page/Redirect.gmi b/src/Page/Redirect.gmi
deleted file mode 100644
index 3cabf1d7..00000000
--- a/src/Page/Redirect.gmi
+++ /dev/null
@@ -1,8 +0,0 @@
-# Redirect issue
-
-You see this message because page redirect could not be processed properly
-
-## Possible reasons
-
-* Max redirects reached
-* Redirects disabled by settings
\ No newline at end of file
diff --git a/src/Page/Welcome.gmi b/src/Page/Welcome.gmi
deleted file mode 100644
index 92a688fa..00000000
--- a/src/Page/Welcome.gmi
+++ /dev/null
@@ -1,3 +0,0 @@
-# Welcome to Yoda!
-
-=> https://github.com/YGGverse/Yoda
\ No newline at end of file
diff --git a/src/Theme/Default.css b/src/Theme/Default.css
deleted file mode 100644
index ed4b5e90..00000000
--- a/src/Theme/Default.css
+++ /dev/null
@@ -1 +0,0 @@
-/* Custom CSS */
\ No newline at end of file
diff --git a/src/Yoda.php b/src/Yoda.php
index d957d312..06313871 100644
--- a/src/Yoda.php
+++ b/src/Yoda.php
@@ -6,9 +6,34 @@ require_once __DIR__ .
DIRECTORY_SEPARATOR . 'vendor' .
DIRECTORY_SEPARATOR . 'autoload.php';
-// Init app
+// Init filesystem
+$filesystem = new \Yggverse\Yoda\Model\Filesystem(
+ (
+ getenv('HOME') ?? __DIR__ . DIRECTORY_SEPARATOR . '..'
+ ) . DIRECTORY_SEPARATOR . '.yoda'
+);
+
+// Init database
+$database = new \Yggverse\Yoda\Model\Database(
+ $filesystem->getAbsolute(
+ 'database.sqlite'
+ )
+);
+
+// Init GTK
\Gtk::init();
-new \Yggverse\Yoda\Entity\App;
+// Init window
+$window = new \Yggverse\Yoda\Entity\Window(
+ $database
+);
+
+$window->gtk->connect(
+ 'destroy',
+ function()
+ {
+ \Gtk::main_quit();
+ }
+);
\Gtk::main();
\ No newline at end of file