Browse Source

refactor to separated entities, init abstractions

PHP-GTK3
yggverse 5 months ago
parent
commit
90acc3ac2d
  1. 34
      README.md
  2. 227
      config.json
  3. 41
      src/Abstract/Entity/Button.php
  4. 64
      src/Abstract/Entity/Entry.php
  5. 18
      src/Abstract/Entity/Window/Tab/Address/Navbar/Button.php
  6. 18
      src/Abstract/Entity/Window/Tab/Address/Navbar/Entry.php
  7. 18
      src/Abstract/Entity/Window/Tab/History/Navbar/Button.php
  8. 18
      src/Abstract/Entity/Window/Tab/History/Navbar/Entry.php
  9. 217
      src/Entity/App.php
  10. 397
      src/Entity/Tab/History.php
  11. 1035
      src/Entity/Tab/Page.php
  12. 53
      src/Entity/Window.php
  13. 45
      src/Entity/Window/Header.php
  14. 88
      src/Entity/Window/Tab.php
  15. 228
      src/Entity/Window/Tab/Address.php
  16. 45
      src/Entity/Window/Tab/Address/Content.php
  17. 231
      src/Entity/Window/Tab/Address/Content/Gemtext.php
  18. 54
      src/Entity/Window/Tab/Address/Content/Plain.php
  19. 104
      src/Entity/Window/Tab/Address/Navbar.php
  20. 55
      src/Entity/Window/Tab/Address/Navbar/Base.php
  21. 17
      src/Entity/Window/Tab/Address/Navbar/Go.php
  22. 76
      src/Entity/Window/Tab/Address/Navbar/History.php
  23. 17
      src/Entity/Window/Tab/Address/Navbar/History/Back.php
  24. 17
      src/Entity/Window/Tab/Address/Navbar/History/Forward.php
  25. 31
      src/Entity/Window/Tab/Address/Navbar/Request.php
  26. 51
      src/Entity/Window/Tab/Address/Statusbar.php
  27. 35
      src/Entity/Window/Tab/Address/Title.php
  28. 53
      src/Entity/Window/Tab/History.php
  29. 220
      src/Entity/Window/Tab/History/Content.php
  30. 99
      src/Entity/Window/Tab/History/Navbar.php
  31. 30
      src/Entity/Window/Tab/History/Navbar/Delete.php
  32. 29
      src/Entity/Window/Tab/History/Navbar/Filter.php
  33. 31
      src/Entity/Window/Tab/History/Navbar/Open.php
  34. 28
      src/Entity/Window/Tab/History/Navbar/Search.php
  35. 35
      src/Entity/Window/Tab/History/Title.php
  36. 45
      src/Model/Config.php
  37. 41
      src/Model/Database.php
  38. 30
      src/Model/File.php
  39. 33
      src/Model/Memory.php
  40. 31
      src/Model/Page.php
  41. 3
      src/Page/Nothing.gmi
  42. 5
      src/Page/Oops.gmi
  43. 8
      src/Page/Protocol.gmi
  44. 8
      src/Page/Redirect.gmi
  45. 3
      src/Page/Welcome.gmi
  46. 1
      src/Theme/Default.css
  47. 29
      src/Yoda.php

34
README.md

@ -1,28 +1,24 @@ @@ -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
* [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

227
config.json

@ -1,227 +0,0 @@ @@ -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
}
}
}
}

41
src/Abstract/Entity/Button.php

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity;
abstract class Button
{
public \GtkButton $gtk;
protected bool $_sensitive = false;
protected string $_label = 'Button';
public function __construct()
{
$this->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;
}

64
src/Abstract/Entity/Entry.php

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity;
abstract class Entry
{
public \GtkEntry $gtk;
private int $_length = 1024;
private string $_placeholder = '';
private string $_value = '';
public function __construct()
{
$this->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;
}

18
src/Abstract/Entity/Window/Tab/Address/Navbar/Button.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar;
abstract class Button extends \Yggverse\Yoda\Abstract\Entity\Button
{
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar
) {
parent::__construct();
$this->navbar = $navbar;
}
}

18
src/Abstract/Entity/Window/Tab/Address/Navbar/Entry.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar;
abstract class Entry extends \Yggverse\Yoda\Abstract\Entity\Entry
{
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar
) {
parent::__construct();
$this->navbar = $navbar;
}
}

18
src/Abstract/Entity/Window/Tab/History/Navbar/Button.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar;
abstract class Button extends \Yggverse\Yoda\Abstract\Entity\Button
{
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar $navbar;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\History\Navbar $navbar
) {
parent::__construct();
$this->navbar = $navbar;
}
}

18
src/Abstract/Entity/Window/Tab/History/Navbar/Entry.php

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar;
abstract class Entry extends \Yggverse\Yoda\Abstract\Entity\Entry
{
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar $navbar;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\History\Navbar $navbar
) {
parent::__construct();
$this->navbar = $navbar;
}
}

217
src/Entity/App.php

@ -1,217 +0,0 @@ @@ -1,217 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity;
class App
{
public \Yggverse\Yoda\Model\Config $config;
public \Yggverse\Yoda\Model\Database $database;
public \Yggverse\Yoda\Entity\Tab\History $history;
public \GtkWindow $window;
public \GtkHeaderBar $header;
public \GtkNotebook $tabs;
public function __construct()
{
// Init config
$this->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
);
}
}

397
src/Entity/Tab/History.php

@ -1,397 +0,0 @@ @@ -1,397 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Tab;
class History
{
public \Yggverse\Yoda\Entity\App $app;
public \GtkBox $box,
$header,
$body;
public \GtkButton $open,
$delete,
$search;
public \GtkEntry $filter;
public \GtkListStore $list;
public \GtkTreeView $treeview;
public \GtkScrolledWindow $container;
public object $config;
public function __construct(
\Yggverse\Yoda\Entity\App $app
) {
// Init app
$this->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;
}
}

1035
src/Entity/Tab/Page.php

File diff suppressed because it is too large Load Diff

53
src/Entity/Window.php

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity;
use \Yggverse\Yoda\Entity\Window\Header;
use \Yggverse\Yoda\Entity\Window\Tab;
class Window
{
public \GtkWindow $gtk;
public \Yggverse\Yoda\Model\Database $database;
public \Yggverse\Yoda\Entity\Window\Header $header;
public \Yggverse\Yoda\Entity\Window\Tab $tab;
// Defaults
private int $_width = 640;
private int $_height = 480;
public function __construct(
\Yggverse\Yoda\Model\Database $database
) {
$this->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();
}
}

45
src/Entity/Window/Header.php

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window;
class Header
{
public \GtkHeaderBar $gtk;
public \Yggverse\Yoda\Entity\Window $window;
// Defaults
private bool $_actions = true;
private string $_title = 'Yoda';
public function __construct(
\Yggverse\Yoda\Entity\Window $window
) {
$this->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
)
);
}
}

88
src/Entity/Window/Tab.php

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window;
use \Yggverse\Yoda\Entity\Window\Tab\Address;
use \Yggverse\Yoda\Entity\Window\Tab\History;
class Tab
{
public \GtkNotebook $gtk;
public \Yggverse\Yoda\Entity\Window $window;
// Defaults
private bool $_reorderable = true;
private bool $_scrollable = true;
public function __construct(
\Yggverse\Yoda\Entity\Window $window
) {
$this->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()
);
}
}

228
src/Entity/Window/Tab/Address.php

@ -0,0 +1,228 @@ @@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Title;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Content;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Statusbar;
class Address
{
public \GtkBox $gtk;
public \Yggverse\Yoda\Entity\Window\Tab $tab;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Title $title;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Content $content;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Statusbar $statusbar;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab $tab
) {
$this->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();
}
}

45
src/Entity/Window/Tab/Address/Content.php

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Content\Gemtext;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Content\Plain;
class Content
{
public \GtkScrolledWindow $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address $address;
public Gemtext | Plain $data;
private int $_margin = 8;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address $address
) {
$this->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();
}
}

231
src/Entity/Window/Tab/Address/Content/Gemtext.php

@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Content;
use \Yggverse\Gemtext\Document;
use \Yggverse\Net\Address;
class Gemtext
{
public \GtkLabel $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Content $content;
// Defaults
private string $_value = '';
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address\Content $content
) {
$this->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(
'<tt>%s</tt>',
htmlspecialchars(
$entity->getAlt()
)
);
}
else
{
// @TODO multiline
}
break;
case $entity instanceof \Yggverse\Gemtext\Entity\Header:
switch ($entity->getLevel())
{
case 1: // #
$line[] = sprintf(
'<span size="xx-large">%s</span>',
htmlspecialchars(
$entity->getText()
)
);
// Find and return document title by first # tag
if (empty($title))
{
$title = $entity->getText();
}
break;
case 2: // ##
$line[] = sprintf(
'<span size="x-large">%s</span>',
htmlspecialchars(
$entity->getText()
)
);
break;
case 3: // ###
$line[] = sprintf(
'<span size="large">%s</span>',
htmlspecialchars(
$entity->getText()
)
);
break;
default:
throw new \Exception;
}
break;
case $entity instanceof \Yggverse\Gemtext\Entity\Link:
$line[] = sprintf(
'<a href="%s" title="%s">%s</a>',
$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(
'<i>%s</i>',
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();
}
}

54
src/Entity/Window/Tab/Address/Content/Plain.php

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Content;
class Plain
{
public \GtkLabel $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Content $content;
// Defaults
private string $_value = '';
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address\Content $content
) {
$this->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
);
}
}

104
src/Entity/Window/Tab/Address/Navbar.php

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Base;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Go;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Request;
class Navbar
{
public \GtkBox $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address $address;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Base $base;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Go $go;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History $history;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\Request $request;
// Defaults
private int $_margin = 8;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address $address
) {
$this->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
);
}
}

55
src/Entity/Window/Tab/Address/Navbar/Base.php

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar;
class Base extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar\Button
{
protected string $_label = 'Base';
protected function _onCLick(
\GtkButton $entity
): void
{
$address = new \Yggverse\Net\Address(
$this->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())
);
}
}

17
src/Entity/Window/Tab/Address/Navbar/Go.php

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar;
class Go extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar\Button
{
protected string $_label = 'Go';
protected function _onCLick(
\GtkButton $entity
): void
{
$this->navbar->address->update();
}
}

76
src/Entity/Window/Tab/Address/Navbar/History.php

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History\Back;
use \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History\Forward;
class History
{
public \GtkButtonBox $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History\Back $back;
public \Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History\Forward $forward;
private \Yggverse\Yoda\Model\History $_history;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address\Navbar $navbar
) {
$this->_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()
);
}
}

17
src/Entity/Window/Tab/Address/Navbar/History/Back.php

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History;
class Back extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar\Button
{
protected string $_label = 'Back';
protected function _onCLick(
\GtkButton $entity
): void
{
// @TODO
}
}

17
src/Entity/Window/Tab/Address/Navbar/History/Forward.php

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar\History;
class Forward extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar\Button
{
protected string $_label = 'Forward';
protected function _onCLick(
\GtkButton $entity
): void
{
// @TODO
}
}

31
src/Entity/Window/Tab/Address/Navbar/Request.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address\Navbar;
class Request extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\Address\Navbar\Entry
{
private string $_placeholder = 'URL or search term...';
protected function _onActivate(
\GtkEntry $entry
): void
{
$this->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()
)
);
}
}

51
src/Entity/Window/Tab/Address/Statusbar.php

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address;
class Statusbar
{
public \GtkLabel $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address $address;
// Defaults
private int $_margin = 8;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address $address
) {
$this->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
);
}
}

35
src/Entity/Window/Tab/Address/Title.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\Address;
class Title
{
public \GtkLabel $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\Address $address;
// Defaults
private int $_ellipsize = 3;
private int $_length = 12;
private string $_value = 'New address';
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\Address $address,
) {
$this->address = $address;
$this->gtk = new \GtkLabel(
$this->_value
);
$this->gtk->set_width_chars(
$this->_length
);
$this->gtk->set_ellipsize(
$this->_ellipsize
);
}
}

53
src/Entity/Window/Tab/History.php

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab;
use \Yggverse\Yoda\Entity\Window\Tab\History\Title;
use \Yggverse\Yoda\Entity\Window\Tab\History\Navbar;
use \Yggverse\Yoda\Entity\Window\Tab\History\Content;
class History
{
public \GtkBox $gtk;
public \Yggverse\Yoda\Entity\Window\Tab $tab;
public \Yggverse\Yoda\Entity\Window\Tab\History\Title $title;
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar $navbar;
public \Yggverse\Yoda\Entity\Window\Tab\History\Content $content;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab $tab
) {
$this->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
);
}
}

220
src/Entity/Window/Tab/History/Content.php

@ -0,0 +1,220 @@ @@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History;
use \Yggverse\Yoda\Entity\Window\Tab\Address;
class Content
{
public \GtkScrolledWindow $gtk;
public \GtkTreeView $treeview;
public \GtkListStore $list;
public \Yggverse\Yoda\Entity\Window\Tab\History $history;
// Defaults
private string $_time = 'Time';
private string $_title = 'Title';
private string $_url = 'URL';
private string $_format = 'c';
private int $_margin = 8;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\History $history
) {
$this->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;
}
}

99
src/Entity/Window/Tab/History/Navbar.php

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History;
use \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Delete;
use \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Filter;
use \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Open;
use \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Search;
class Navbar
{
public \GtkBox $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\History $history;
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Delete $delete;
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Filter $filter;
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Open $open;
public \Yggverse\Yoda\Entity\Window\Tab\History\Navbar\Search $search;
// Defaults
private int $_margin = 8;
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\History $history
) {
$this->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
);
}
}

30
src/Entity/Window/Tab/History/Navbar/Delete.php

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History\Navbar;
class Delete extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar\Button
{
protected string $_label = 'Delete';
protected function _onCLick(
\GtkButton $entity
): void
{
if ($id = $this->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()
);
}
}
}

29
src/Entity/Window/Tab/History/Navbar/Filter.php

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History\Navbar;
class Filter extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar\Entry
{
private string $_placeholder = 'Search in history...';
protected function _onActivate(
\GtkEntry $entry
): void
{
$this->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()
);
}
}

31
src/Entity/Window/Tab/History/Navbar/Open.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History\Navbar;
use \Yggverse\Yoda\Entity\Window\Tab\Address;
class Open extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar\Button
{
protected string $_label = 'Open';
protected function _onCLick(
\GtkButton $entity
): void
{
$address = new Address(
$this->navbar->history->tab
);
$this->navbar->history->tab->append( // @TODO
$address
);
$address->navbar->request->gtk->set_text(
$this->navbar->history->content->getSelectedUrl()
);
$address->update();
}
}

28
src/Entity/Window/Tab/History/Navbar/Search.php

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History\Navbar;
class Search extends \Yggverse\Yoda\Abstract\Entity\Window\Tab\History\Navbar\Button
{
protected bool $_sensitive = true;
protected string $_label = 'Search';
protected function _onCLick(
\GtkButton $entity
): void
{
$this->gtk->set_sensitive(
false
);
$this->navbar->history->content->search(
$this->navbar->filter->gtk->get_text()
);
$this->gtk->set_sensitive(
true
);
}
}

35
src/Entity/Window/Tab/History/Title.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Entity\Window\Tab\History;
class Title
{
public \GtkLabel $gtk;
public \Yggverse\Yoda\Entity\Window\Tab\History $history;
// Defaults
private int $_ellipsize = 0;
private int $_length = 12;
private string $_value = 'History';
public function __construct(
\Yggverse\Yoda\Entity\Window\Tab\History $history
) {
$this->history = $history;
$this->gtk = new \GtkLabel(
$this->_value
);
$this->gtk->set_width_chars(
$this->_length
);
$this->gtk->set_ellipsize(
$this->_ellipsize
);
}
}

45
src/Model/Config.php

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Model;
class Config
{
public function __construct(
?string $filename = null
) {
if (empty($filename))
{
$filename = __DIR__ .
DIRECTORY_SEPARATOR . '..' .
DIRECTORY_SEPARATOR . '..' .
DIRECTORY_SEPARATOR . 'config.json';
}
if (!file_exists($filename))
{
throw new \Exception; // @TODO
}
if (!is_readable($filename))
{
throw new \Exception; // @TODO
}
if (!$data = file_get_contents($filename))
{
throw new \Exception; // @TODO
}
if (!$config = @json_decode($data))
{
throw new \Exception; // @TODO
}
foreach ($config as $key => $value)
{
$this->{$key} = $value; // @TODO
}
}
}

41
src/Model/Database.php

@ -64,8 +64,8 @@ class Database @@ -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 @@ -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 @@ -83,9 +83,9 @@ class Database
$query->execute(
[
':search' => sprintf(
':value' => sprintf(
'%%%s%%',
$search
$value
)
]
);
@ -122,4 +122,35 @@ class Database @@ -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
);
}
}

30
src/Model/File.php

@ -1,30 +0,0 @@ @@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Model;
class File
{
public static function getTheme(string $name): string
{
$filename = __DIR__ .
DIRECTORY_SEPARATOR . '..' .
DIRECTORY_SEPARATOR . 'Theme' .
DIRECTORY_SEPARATOR . $name . '.css';
if (file_exists($filename) && is_readable($filename))
{
$result = file_get_contents(
$filename
);
}
if (empty($result))
{
throw new \Exception(); // @TODO
}
return $result;
}
}

33
src/Model/Memory.php

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Model;
class Memory
{
private array $_memory = [];
public function __construct()
{}
public function set(string $key, mixed $value): void
{
$this->_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 = [];
}
}

31
src/Model/Page.php

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Model;
class Page
{
public static function get(string $name): ?string
{
$name = ucfirst(
mb_strtolower(
$name
)
);
$filename = __DIR__ .
DIRECTORY_SEPARATOR . '..' .
DIRECTORY_SEPARATOR . 'Page' .
DIRECTORY_SEPARATOR . $name . '.gmi';
if (file_exists($filename) && is_readable($filename))
{
return file_get_contents(
$filename
);
}
return null;
}
}

3
src/Page/Nothing.gmi

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
# Not found
Requested resource not available

5
src/Page/Oops.gmi

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
# Oops!
Something went wrong..
=> https://github.com/YGGverse/Yoda/issues Report

8
src/Page/Protocol.gmi

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
# Protocol issue
At this moment, supported protocols:
* gemini
* yoda
=> https://github.com/YGGverse/Yoda/issues Report

8
src/Page/Redirect.gmi

@ -1,8 +0,0 @@ @@ -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

3
src/Page/Welcome.gmi

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
# Welcome to Yoda!
=> https://github.com/YGGverse/Yoda

1
src/Theme/Default.css

@ -1 +0,0 @@ @@ -1 +0,0 @@
/* Custom CSS */

29
src/Yoda.php

@ -6,9 +6,34 @@ require_once __DIR__ . @@ -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();
Loading…
Cancel
Save