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