aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile18
-rw-r--r--header.cc210
-rw-r--r--header.h31
-rw-r--r--main.cc45
-rw-r--r--mods.cc208
-rw-r--r--mods.h53
-rw-r--r--readerview.cc98
-rw-r--r--readerview.h32
-rw-r--r--readme.md19
-rw-r--r--sword.cc145
-rw-r--r--sword.h29
11 files changed, 888 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7778cce
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+CC=g++
+LIBS=gtkmm-3.0
+CFLAGS=-c -Wall
+LDFLAGS=-pthread -lbible
+SOURCES=header.cc main.cc mods.cc readerview.cc sword.cc
+OBJECTS=$(SOURCES:.cc=.o)
+EXECUTABLE=biblereader
+
+all: $(SOURCES) $(EXECUTABLE)
+
+$(EXECUTABLE): $(OBJECTS)
+ $(CC) $(LDFLAGS) $(OBJECTS) -o $@ `pkg-config $(LIBS) --libs`
+
+.cc.o:
+ $(CC) $(CFLAGS) $< -o $@ `pkg-config $(LIBS) --cflags`
+
+clean:
+ rm -rf *.o $(EXECUTABLE)
diff --git a/header.cc b/header.cc
new file mode 100644
index 0000000..c40e44e
--- /dev/null
+++ b/header.cc
@@ -0,0 +1,210 @@
+#include "header.h"
+#include "readerview.h"
+#include "mods.h"
+#include <gtk/gtk.h>
+
+Header::Header(ReaderView *reader, Gtk::Window *window)
+: back(), forward(), book(), menu(), bookMenu(), menuMenu()
+{
+ this->mods = new Mods(this, window);
+ this->reader = reader;
+ this->window = window;
+ set_show_close_button(true);
+ set_title ("Bible Reader");
+ set_has_subtitle(false);
+ //this->override_background_color(*(new Gdk::RGBA("#1d1f21")));
+ //this->override_color(*(new Gdk::RGBA("#c5c8c6")));
+
+ back.set_image_from_icon_name("go-previous-symbolic");
+ forward.set_image_from_icon_name("go-next-symbolic");
+ menu.set_image_from_icon_name("open-menu-symbolic");
+
+ back.signal_clicked().connect([reader, this]() {
+ reader->setChapter(reader->getChapter() - 1);
+ updateButtons();
+ });
+
+ forward.signal_clicked().connect([reader, this]() {
+ reader->setChapter(reader->getChapter() + 1);
+ updateButtons();
+ });
+
+ pack_start(back);
+ pack_start(book);
+ pack_start(forward);
+ pack_end(menu);
+
+ book.set_popover(bookMenu);
+ menu.set_popover(menuMenu);
+
+ updateButtons();
+ updateMenus();
+}
+
+Header::~Header() {}
+
+void Header::updateButtons() {
+ back.set_sensitive(reader->getChapter() > 1);
+ forward.set_sensitive(reader->getChapter() < reader->getChapterMax());
+ if(reader->getBook() == "") {
+ book.set_label("");
+ book.set_sensitive(false);
+ } else {
+ book.set_label(reader->getBook() + " " + std::to_string(reader->getChapter()));
+ book.set_sensitive(true);
+ }
+}
+
+void Header::updateMenus() {
+ // Wipe them out and start over
+ if(bookMenu.get_children().size() != 0) {
+ delete bookMenu.get_children()[0];
+ }
+ if(menuMenu.get_children().size() != 0) {
+ delete menuMenu.get_children()[0];
+ }
+
+ // Populate bookMenu
+ Gtk::HBox *outer = Gtk::manage(new Gtk::HBox);
+ bookMenu.add(*outer);
+
+ //TODO: Disgusting code duplication. Fix with strategy pattern? Or loop?
+
+ Gtk::ScrolledWindow *swl = Gtk::manage(new Gtk::ScrolledWindow);
+ Gtk::VBox *boxl = Gtk::manage(new Gtk::VBox);
+ outer->add(*swl);
+ swl->add(*boxl);
+ swl->set_propagate_natural_width(true);
+ swl->set_min_content_height(300);
+ for(std::string bookName : reader->getAllBooks()) {
+ Gtk::Button *item = Gtk::manage(new Gtk::Button(bookName));
+ item->set_relief(Gtk::ReliefStyle::RELIEF_NONE);
+ item->signal_clicked().connect([bookName, this]() {
+ bookMenu.popdown();
+ reader->setChapter(1);
+ reader->setBook(bookName);
+ updateButtons();
+ updateMenus();
+ showText();
+ });
+ boxl->add(*item);
+ }
+
+ Gtk::ScrolledWindow *swr = Gtk::manage(new Gtk::ScrolledWindow);
+ Gtk::VBox *boxr = Gtk::manage(new Gtk::VBox);
+ outer->add(*swr);
+ swr->add(*boxr);
+ swr->set_propagate_natural_width(true);
+ swr->set_min_content_height(300);
+ for(int chapter = 1; chapter <= reader->getChapterMax(); chapter++) {
+ Gtk::Button *item = Gtk::manage(new Gtk::Button(std::to_string(chapter)));
+ item->set_relief(Gtk::ReliefStyle::RELIEF_NONE);
+ item->signal_clicked().connect([chapter, this]() {
+ bookMenu.popdown();
+ reader->setChapter(chapter);
+ updateButtons();
+ showText();
+ });
+ boxr->add(*item);
+ }
+
+ bool *hasAllocated = new bool(false);
+ outer->signal_size_allocate().connect([this, swl, swr, hasAllocated](Gdk::Rectangle rec) {
+ if(! *hasAllocated) {
+ // First figure out the books
+ auto allBooks = reader->getAllBooks();
+ int position = std::find(allBooks.begin(), allBooks.end(), reader->getBookFull()) - allBooks.begin();
+ swl->get_vadjustment()->set_value(swl->get_vadjustment()->get_upper() * position / allBooks.size());
+ swr->get_vadjustment()->set_value(swr->get_vadjustment()->get_upper() * (reader->getChapter() - 1) / reader->getChapterMax());
+ }
+ *hasAllocated = true;
+ });
+
+ bookMenu.show_all_children();
+ book.signal_pressed().connect([this, hasAllocated, swl, swr]() {
+ *hasAllocated = false;
+ });
+
+ // Populate menuMenu
+ Gtk::VBox *outerBox = Gtk::manage(new Gtk::VBox);
+ menuMenu.add(*outerBox);
+
+ Gtk::VBox *versionBox = Gtk::manage(new Gtk::VBox);
+ Gtk::Label *scaleLabel = Gtk::manage(new Gtk::Label);
+ scaleLabel->set_text("Text Size");
+ versionBox->add(*scaleLabel);
+ Gtk::Scale *scale = Gtk::manage(new Gtk::Scale);
+ scale->set_range(8000, 20000);
+ scale->set_value(libbible::settingsReadInt("fontsize"));
+ scale->set_draw_value(false);
+ scale->signal_value_changed().connect([scale, this]() {
+ libbible::settingsWriteInt("fontsize", (int) scale->get_value());
+ this->reader->refresh();
+ });
+ versionBox->add(*scale);
+
+ vector<string> versions = reader->getAllVersions();
+ if(versions.size() > 8) {
+ Gtk::ScrolledWindow *sw = Gtk::manage(new Gtk::ScrolledWindow);
+ sw->set_propagate_natural_width(true);
+ sw->set_min_content_height(300);
+ sw->add(*versionBox);
+ outerBox->add(*sw);
+ } else {
+ outerBox->add(*versionBox);
+ }
+ for(string version : versions) {
+ Gtk::Button *item = Gtk::manage(new Gtk::Button(version));
+ item->set_relief(Gtk::ReliefStyle::RELIEF_NONE);
+ item->signal_clicked().connect([version, this]() {
+ menuMenu.popdown();
+ this->reader->setVersion(version);
+ updateButtons();
+ updateMenus();
+ showText();
+ });
+ Gtk::Button *delVersion = Gtk::manage(new Gtk::Button);
+ delVersion->set_image_from_icon_name("list-remove-symbolic");
+ delVersion->set_relief(Gtk::ReliefStyle::RELIEF_NONE);
+ delVersion->signal_clicked().connect([version, this]() {
+ menuMenu.popdown();
+ std::vector<std::string> toDel {version};
+ this->mods->uninstallMods(toDel);
+ reader->modsUpdated();
+ updateButtons();
+ updateMenus();
+ });
+ item->set_halign(Gtk::ALIGN_FILL);
+ delVersion->set_halign(Gtk::ALIGN_END);
+ Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox);
+ hbox->add(*item);
+ hbox->add(*delVersion);
+ versionBox->add(*hbox);
+ item->set_sensitive(version != reader->getVersion());
+ }
+ Gtk::Button *add = Gtk::manage(new Gtk::Button);
+ add->set_image_from_icon_name("list-add-symbolic");
+ add->signal_clicked().connect([this]() {
+ menuMenu.popdown();
+ showMods();
+ updateMenus();
+ updateButtons();
+ });
+ versionBox->add(*add);
+
+ menuMenu.show_all_children();
+}
+
+void Header::showMods() {
+ window->remove();
+ window->add(*this->mods);
+ //this->mods->displayMain();
+ window->show_all_children();
+}
+
+void Header::showText() {
+ window->remove();
+ window->add(*this->reader);
+ //this->reader->setChapter(1);
+ window->show_all_children();
+}
diff --git a/header.h b/header.h
new file mode 100644
index 0000000..fd88fe5
--- /dev/null
+++ b/header.h
@@ -0,0 +1,31 @@
+#pragma once
+#include <gtkmm.h>
+
+class Mods;
+class ReaderView;
+
+class Header : public Gtk::HeaderBar
+{
+public:
+ Header(ReaderView *reader, Gtk::Window *window);
+ virtual ~Header();
+
+ void updateButtons(void);
+ void updateMenus(void);
+ ReaderView *reader;
+ void showMods(void);
+ void showText(void);
+
+protected:
+ //Buttons
+ Gtk::Button back;
+ Gtk::Button forward;
+ Gtk::MenuButton book;
+ Gtk::MenuButton menu;
+ //Menus
+ Gtk::PopoverMenu bookMenu;
+ Gtk::PopoverMenu menuMenu;
+
+ Mods *mods;
+ Gtk::Window *window;
+};
diff --git a/main.cc b/main.cc
new file mode 100644
index 0000000..ad62814
--- /dev/null
+++ b/main.cc
@@ -0,0 +1,45 @@
+/* main.cpp
+ *
+ * Copyright (C) 2018 Nathan Vance
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "readerview.h"
+#include "header.h"
+#include <gtkmm.h>
+
+int main(int argc,
+ char *argv[])
+{
+ auto app = Gtk::Application::create(argc, argv, "org.homelinuxserver.vance.biblereader");
+
+ Gtk::Window window;
+ window.set_default_size(550, 400);
+ //auto theme = Gtk::IconTheme::get_default();
+ //theme->append_search_path(std::string(DATADIR) + "/icons/hicolor/scalable/apps");
+ // No clue what the size *should* be. The 100 is in magical units :/
+ //window.set_icon(theme->load_icon("org.homelinuxserver.vance.biblereader-symbolic", 100));
+
+ ReaderView reader;
+
+ window.add(reader);
+
+ Header *header = new Header(&reader, &window);
+ window.set_titlebar(*header);
+
+ window.show_all_children();
+
+ return app->run(window);
+}
diff --git a/mods.cc b/mods.cc
new file mode 100644
index 0000000..e1228aa
--- /dev/null
+++ b/mods.cc
@@ -0,0 +1,208 @@
+#include "mods.h"
+#include "header.h"
+#include "readerview.h"
+#include <iostream>
+#include <thread>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+// We want: https://developer.gnome.org/gtkmm-tutorial/stable/sec-multithread-example.html.en
+void Mods::update(unsigned long totalBytes, unsigned long completedBytes, std::string mess)
+{
+ std::lock_guard<std::mutex> lock(progressMutex);
+ if (!totalBytes) {
+ //printf("Received a 0 totalBytes\n");
+ dispatcher.emit();
+ return;
+ }
+ fracDone = (completedBytes / totalBytes);
+ message = mess;
+ //printf("Progress: %f\n", fracDone);
+ dispatcher.emit();
+}
+
+void Mods::getStatus(double *fractionDone, std::string *mess, bool *isComplete) const {
+ std::lock_guard<std::mutex> lock(progressMutex);
+ *fractionDone = fracDone;
+ *mess = message;
+ *isComplete = complete;
+}
+
+void Mods::onNotify() {
+ double fractionDone;
+ std::string mess;
+ bool isComplete;
+ getStatus(&fractionDone, &mess, &isComplete);
+ if(isComplete) {
+ if(worker && worker->joinable()) {
+ worker->join();
+ header->reader->modsUpdated();
+ header->updateButtons();
+ header->updateMenus();
+ endProgress();
+ }
+ delete worker;
+ worker = nullptr;
+ } else {
+ showProgress(mess);
+ if(fractionDone >= 0 && fractionDone <= 1) {
+ progressBar.set_fraction(fractionDone);
+ } else {
+ progressBar.pulse();
+ }
+ }
+}
+
+void Mods::showProgress(std::string message) {
+ progressDialog.set_message(message);
+ progressDialog.show();
+ progressDialog.show_all_children();
+}
+
+void Mods::endProgress() {
+ progressDialog.hide();
+ return;
+}
+
+Mods::Mods(Header *header, Gtk::Window *window) : modsAvailable(), progressDialog("", false, Gtk::MessageType::MESSAGE_INFO, Gtk::ButtonsType::BUTTONS_CANCEL), progressBar(), dispatcher(), progressMutex(), worker(nullptr) {
+ this->header = header;
+ this->window = window;
+ dispatcher.connect(sigc::mem_fun(*this, &Mods::onNotify));
+ progressDialog.signal_response().connect([this](int response) {
+ libbible::terminateDownload();
+ });
+ progressDialog.get_message_area()->add(progressBar);
+ progressBar.show();
+ displayMain();
+}
+
+Mods::~Mods() {}
+
+void Mods::displayMain() {
+ remove();
+ auto *scroll = Gtk::manage(new Gtk::ScrolledWindow);
+ add(*scroll);
+ auto *vbox = Gtk::manage(new Gtk::VBox);
+ scroll->add(*vbox);
+ auto *label = Gtk::manage(new Gtk::Label);
+ label->set_text("Install new mods");
+ vbox->pack_start(*label, false, false);
+ auto *hbox = Gtk::manage(new Gtk::HBox);
+ vbox->pack_start(*hbox, false, false);
+ auto *network = Gtk::manage(new Gtk::VBox);
+ hbox->add(*network);
+ label = Gtk::manage(new Gtk::Label);
+ label->set_text("Download over the network");
+ network->pack_start(*label, false, false);
+ auto *button = Gtk::manage(new Gtk::Button("Download"));
+ network->pack_start(*button, false, false);
+ button->signal_clicked().connect([this]() {
+ updateInstallable();
+ displayDownload();
+ });
+ auto *local = Gtk::manage(new Gtk::VBox);
+ hbox->add(*local);
+ label = Gtk::manage(new Gtk::Label);
+ label->set_text("Install local .zip file");
+ local->pack_start(*label, false, false);
+ Gtk::FileChooserButton *add = Gtk::manage(new Gtk::FileChooserButton("Install SWORD modules", Gtk::FILE_CHOOSER_ACTION_OPEN));
+ local->pack_start(*add, false, false);
+ auto filter = Gtk::FileFilter::create();
+ filter->add_mime_type("application/zip");
+ add->set_filter(filter);
+ add->signal_file_set().connect([this, add]() {
+ installMods(add->get_filenames());
+ auto newMods = this->header->reader->modsUpdated();
+ if(! newMods.empty()) {
+ this->header->reader->setVersion(newMods[0]);
+ }
+ this->header->updateMenus();
+ this->header->updateButtons();
+ this->header->showText();
+ });
+ window->show_all_children();
+}
+
+void Mods::displayDownload() {
+ remove();
+ auto *scroll = Gtk::manage(new Gtk::ScrolledWindow);
+ add(*scroll);
+ auto *vbox = Gtk::manage(new Gtk::VBox);
+ //vbox->set_homogeneous(false);
+ //vbox->set_hexpand(false);
+ scroll->add(*vbox);
+ auto *button = Gtk::manage(new Gtk::Button("Back"));
+ vbox->pack_start(*button, false, false);
+ button->signal_clicked().connect([this]() {
+ displayMain();
+ });
+ auto *label = Gtk::manage(new Gtk::Label);
+ label->set_text("Language Selection:");
+ vbox->pack_start(*label, false, false);
+ auto *langMenuButton = Gtk::manage(new Gtk::MenuButton);
+ // Perhaps default to English? Or if translations for the app are added, default to the global language setting?
+ langMenuButton->set_label("Select Language");
+ vbox->pack_start(*langMenuButton, false, false);
+ label = Gtk::manage(new Gtk::Label);
+ label->set_text("Mods Available:");
+ vbox->pack_start(*label, false, false);
+ auto *modsSelection = Gtk::manage(new Gtk::VBox);
+ modsSelection->set_vexpand(true);
+ vbox->pack_end(*modsSelection);
+ auto *langMenu = Gtk::manage(new Gtk::PopoverMenu);
+ auto *sw = Gtk::manage(new Gtk::ScrolledWindow);
+ langMenu->add(*sw);
+ auto *langBox = Gtk::manage(new Gtk::VBox);
+ sw->add(*langBox);
+ sw->set_propagate_natural_width(true);
+ sw->set_min_content_height(300);
+ for(auto item : modsAvailable) {
+ string language = item.first;
+ auto *langButton = Gtk::manage(new Gtk::Button(language));
+ langButton->set_relief(Gtk::ReliefStyle::RELIEF_NONE);
+ langButton->signal_clicked().connect([language, langMenuButton, langMenu, modsSelection, this]() {
+ langMenu->popdown();
+ langMenuButton->set_label(language);
+ for(auto child : modsSelection->get_children()) {
+ modsSelection->remove(*child);
+ }
+ for(string name : modsAvailable[language]) {
+ auto *installButton = Gtk::manage(new Gtk::Button(name));
+ installButton->signal_clicked().connect([language, name, this]() {
+ worker = new std::thread([language, name, this] {
+ complete = false;
+ libbible::installModFromInternet(language, name);
+ complete = true;
+ dispatcher.emit();
+ header->showText();
+ });
+ });
+ modsSelection->pack_start(*installButton, false, false);
+ }
+ modsSelection->show_all_children();
+ });
+ langBox->add(*langButton);
+ }
+ langMenuButton->set_popover(*langMenu);
+ langMenu->show_all_children();
+ window->show_all_children();
+}
+
+void Mods::installMods(std::vector<std::string> filenames) {
+ for(auto filename : filenames) {
+ libbible::installModFromZip(filename);
+ }
+}
+
+void Mods::uninstallMods(std::vector<std::string> modnames) {
+ for(auto mod : modnames) {
+ libbible::uninstallMod(mod);
+ }
+}
+
+void Mods::updateInstallable() {
+ if(! modsAvailable.empty()) {
+ return;
+ }
+ modsAvailable = libbible::downloadModsAvailable(this);
+}
diff --git a/mods.h b/mods.h
new file mode 100644
index 0000000..7373961
--- /dev/null
+++ b/mods.h
@@ -0,0 +1,53 @@
+#pragma once
+#include <glib.h>
+#include <thread>
+#include <mutex>
+#include <vector>
+#include <string>
+#include <gtkmm.h>
+#include <libbible.h>
+
+using namespace std;
+
+class Header;
+
+/*
+ * Credit goes to the Xiphos project for this part of the code:
+ * https://github.com/crosswire/xiphos/
+ */
+
+class Mods : public Gtk::Frame, public libbible::Status
+{
+public:
+ Mods(Header *header, Gtk::Window *window);
+ virtual ~Mods();
+
+ void installMods(std::vector<std::string> filenames);
+ void uninstallMods(std::vector<std::string> modnames);
+ void updateInstallable();
+ void displayMain();
+ void displayDownload();
+
+ // This is for Status. Huzzah for multiple inheritance!
+ virtual void update(unsigned long totalBytes, unsigned long completedBytes, std::string message);
+
+protected:
+ Header *header;
+ Gtk::Window *window;
+ std::map<std::string, std::vector<std::string>> modsAvailable;
+
+ Gtk::MessageDialog progressDialog;
+ Gtk::ProgressBar progressBar;
+ Glib::Dispatcher dispatcher;
+ mutable std::mutex progressMutex;
+ double fracDone;
+ std::string message;
+ std::thread *worker;
+ bool complete;
+ void getStatus(double *fractionDone, std::string *mess, bool *isComplete) const;
+ void onNotify();
+
+ void showProgress(std::string message);
+ void endProgress();
+
+};
diff --git a/readerview.cc b/readerview.cc
new file mode 100644
index 0000000..aef003d
--- /dev/null
+++ b/readerview.cc
@@ -0,0 +1,98 @@
+#include "readerview.h"
+#include "sword.h"
+#include <libbible.h>
+#include <gdkmm/rgba.h>
+
+ReaderView::ReaderView()
+: text()
+{
+ this->sword = new Sword();
+ this->conf = new struct config;
+ text.set_editable(false);
+ text.set_cursor_visible(false);
+ text.set_wrap_mode(Gtk::WrapMode::WRAP_WORD_CHAR);
+ //text.override_background_color(*(new Gdk::RGBA("#1d1f21")));
+ //text.override_color(*(new Gdk::RGBA("#c5c8c6")));
+ auto scroll = new Gtk::ScrolledWindow();
+ scroll->add(text);
+ add(*scroll);
+ // Open the passage we had last time
+ string book = libbible::settingsRead("book");
+ int chapter = libbible::settingsReadInt("chapter");
+ if(book.empty() || chapter == 0) {
+ book = "Genesis";
+ chapter = 1;
+ }
+ sword->getConfig(book, chapter, conf);
+ refresh();
+}
+
+ReaderView::~ReaderView() {}
+
+// Returns all new mods
+std::vector<std::string> ReaderView::modsUpdated() {
+ auto oldMods = getAllVersions();
+ std::sort(oldMods.begin(), oldMods.end());
+ sword = new Sword();
+ auto newMods = getAllVersions();
+ std::sort(newMods.begin(), newMods.end());
+ std::vector<std::string> justNew;
+ std::set_difference(newMods.begin(), newMods.end(), oldMods.begin(), oldMods.end(), std::back_inserter(justNew));
+ refresh();
+ return justNew;
+}
+
+void ReaderView::refresh() {
+ auto textBuffer = text.get_buffer();
+ libbible::settingsWrite("book", conf->book);
+ libbible::settingsWriteInt("chapter", conf->chapter);
+ sword->getConfig(conf->book, conf->chapter, conf);
+ // Get passage back
+ string passage = conf->book + " " + std::to_string(conf->chapter);
+ textBuffer->set_text(""); // Clear contents
+ //auto iter = textBuffer->get_iter_at_offset(0);
+ sword->fillBuffer(passage, textBuffer);
+}
+
+void ReaderView::setChapter(int chapter) {
+ conf->chapter = chapter;
+ refresh();
+}
+
+int ReaderView::getChapter() {
+ return conf->chapter;
+}
+
+int ReaderView::getChapterMax() {
+ return conf->maxChapter;
+}
+
+void ReaderView::setBook(std::string book) {
+ conf->book = book;
+ refresh();
+}
+
+std::string ReaderView::getBook() {
+ return conf->book;
+}
+
+std::string ReaderView::getBookFull() {
+ return conf->bookFull;
+}
+
+std::vector<std::string> ReaderView::getAllBooks() {
+ return sword->getBooks();
+}
+
+void ReaderView::setVersion(std::string version) {
+ sword->setModule(version);
+ refresh();
+}
+
+std::string ReaderView::getVersion() {
+ return conf->version;
+}
+
+std::vector<std::string> ReaderView::getAllVersions() {
+ return sword->getModules();
+}
diff --git a/readerview.h b/readerview.h
new file mode 100644
index 0000000..0ea631e
--- /dev/null
+++ b/readerview.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <gtkmm.h>
+
+class Sword;
+
+class ReaderView : public Gtk::Frame
+{
+public:
+ ReaderView();
+ virtual ~ReaderView();
+
+ void setChapter(int chapter);
+ int getChapter(void);
+ int getChapterMax(void);
+ void setBook(std::string book);
+ std::string getBook(void);
+ std::string getBookFull(void);
+ std::vector<std::string> getAllBooks(void);
+
+ void setVersion(std::string version);
+ std::string getVersion(void);
+ std::vector<std::string> getAllVersions(void);
+
+ void refresh(void);
+
+ std::vector<std::string> modsUpdated(void);
+
+protected:
+ Sword *sword;
+ Gtk::TextView text;
+ struct config *conf;
+};
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..dd68d62
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,19 @@
+# Biblereader
+
+Biblereader is a GTK frontend for libbible.
+
+Dependencies:
+
+ * gtkmm
+ * [libbible](https://vance.fish/git/libbible)
+
+## Compiling and Installing
+
+```console
+make
+sudo cp biblereader /usr/bin/
+```
+
+## Modules
+
+biblereader can install modules from the internet or from local .zip files. A collection of modules can be found [here](http://crosswire.org/sword/modules/ModDisp.jsp?modType=Bibles).
diff --git a/sword.cc b/sword.cc
new file mode 100644
index 0000000..0bf20e2
--- /dev/null
+++ b/sword.cc
@@ -0,0 +1,145 @@
+#include "sword.h"
+#include <libbible.h>
+#include <sstream>
+#include <wctype.h>
+
+using namespace std;
+
+bool isNoMods;
+
+Sword::Sword() {
+ auto mods = libbible::getModules();
+ isNoMods = mods.empty();
+ defaultMod = libbible::settingsRead("module");
+ if(mods.find(defaultMod) == mods.end()) {
+ defaultMod = string();
+ if(! mods.empty()) {
+ defaultMod = mods.begin()->first;
+ }
+ }
+}
+
+Sword::~Sword() {}
+
+vector<string> Sword::getModules() {
+ vector<string> mods;
+ auto modsInstalled = libbible::getModules();
+ for(auto pair : modsInstalled) {
+ mods.push_back(pair.first);
+ }
+ return mods;
+}
+
+void Sword::setModule(string version) {
+ defaultMod = version;
+}
+
+void Sword::fillBuffer(string ref, Glib::RefPtr<Gtk::TextBuffer> buf) {
+ buf->set_text(""); // Clear contents
+
+ if(isNoMods) {
+ auto iter = buf->get_iter_at_offset(0);
+ iter = buf->insert_markup(iter, "<big><b>No modules installed.</b></big>\n");
+ iter = buf->insert_markup(iter, "Please download some modules at:\n");
+ iter = buf->insert_markup(iter, "\thttp://crosswire.org/sword/modules/ModDisp.jsp?modType=Bibles\n");
+ iter = buf->insert_markup(iter, "Then install them using the menu in the upper right corner, or use the built-in installer to download and install modules.");
+ return;
+ }
+
+ auto texts = libbible::getText(libbible::passage{.modName = defaultMod, .reference = ref});
+
+ auto iter = buf->get_iter_at_offset(0);
+
+ auto verseSize = buf->create_tag();
+ verseSize->property_size() = libbible::settingsReadInt("fontsize");
+ if(verseSize->property_size() == 0) {
+ verseSize->property_size() = 12000;
+ }
+
+ auto verseScale = buf->create_tag();
+ verseScale->property_scale() = 0.8;
+ auto verseOffset = buf->create_tag();
+ verseOffset->property_rise() = 3000;
+ auto indent = buf->create_tag();
+ indent->property_left_margin() = 40;
+ auto redletter = buf->create_tag();
+ redletter->property_foreground_gdk() = Gdk::Color("red");
+
+ int verse = 0;
+ string indentString = " ";
+ bool isNewline = true;
+ for(auto tex : texts) {
+ std::vector<Glib::RefPtr<Gtk::TextBuffer::Tag>> tags = {verseSize};
+ bool isParagraph = false;
+ bool isIndent = false;
+ bool isDivineName = false;
+ for(string modifier : tex.modifiers) {
+ if(modifier == "paragraph") {
+ isParagraph = true;
+ } else if (modifier == "line indent0") {
+ isIndent = true;
+ } else if (modifier == "divineName") {
+ isDivineName = true;
+ } else if (modifier == "wordsOfJesus") {
+ tags.push_back(redletter);
+ }
+ }
+ if(isIndent) {
+ isParagraph = false;
+ if(isNewline) {
+ tags.push_back(indent);
+ }
+ }
+ if(isParagraph) {
+ iter = buf->insert_with_tags(iter, indentString, tags);
+ }
+ if(tex.verse != verse) {
+ std::vector<Glib::RefPtr<Gtk::TextBuffer::Tag>> verseTags(tags.begin(), tags.end());
+ verseTags.push_back(verseScale);
+ verseTags.push_back(verseOffset);
+ iter = buf->insert_with_tags(iter, " " + std::to_string(tex.verse), verseTags);
+ }
+ verse = tex.verse;
+ if(isDivineName) {
+ // There's no small caps support. Sigh. We do fake small caps instead.
+ // Because i lazy, first letter is normal caps and rest small caps, always.
+ transform(tex.text.begin(), tex.text.end(), tex.text.begin(), ::toupper);
+ iter = buf->insert_with_tags(iter, tex.text.substr(0, 1), tags);
+ auto tag = buf->create_tag();
+ tag->property_scale() = 0.75;
+ tags.push_back(tag);
+ iter = buf->insert_with_tags(iter, tex.text.substr(1), tags);
+ } else {
+ iter = buf->insert_with_tags(iter, tex.text, tags);
+ }
+ if(tex.text.back() == '\n') {
+ isNewline = true;
+ } else {
+ isNewline = false;
+ }
+ }
+}
+
+void Sword::getConfig(string book, int chapter, struct config *conf) {
+ if(isNoMods) {
+ conf->chapter = 0;
+ conf->book = "";
+ conf->bookFull = "";
+ conf->maxChapter = 0;
+ conf->version = "";
+ } else {
+ auto passages = libbible::getPassages(defaultMod, book);
+ conf->chapter = chapter;
+ conf->book = passages[0].bookShort;
+ conf->bookFull = passages[0].book;
+ conf->maxChapter = passages.back().chapterStart;
+ conf->version = defaultMod;
+ }
+}
+
+vector<string> Sword::getBooks() {
+ if(isNoMods) {
+ return vector<string>();
+ }
+ return libbible::getModules()[defaultMod];
+}
diff --git a/sword.h b/sword.h
new file mode 100644
index 0000000..aa52c13
--- /dev/null
+++ b/sword.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <gtkmm.h>
+#include <vector>
+
+using namespace::std;
+
+class Sword {
+public:
+ Sword();
+ virtual ~Sword();
+
+ std::vector<std::string> getModules(void);
+ std::vector<std::string> getBooks(void);
+ void setModule(std::string modName);
+ void fillBuffer(std::string ref, Glib::RefPtr<Gtk::TextBuffer> buf);
+ void getConfig(std::string book, int chapter, struct config *conf);
+
+protected:
+ std::string defaultMod;
+
+};
+
+struct config {
+ int chapter;
+ std::string book;
+ std::string bookFull;
+ int maxChapter;
+ std::string version;
+};