add todos
This commit is contained in:
@@ -12,9 +12,10 @@ find_package(PkgConfig REQUIRED)
|
|||||||
pkg_check_modules(GTKMM REQUIRED gtkmm-4.0)
|
pkg_check_modules(GTKMM REQUIRED gtkmm-4.0)
|
||||||
pkg_check_modules(LAYERSHELL REQUIRED gtk4-layer-shell-0)
|
pkg_check_modules(LAYERSHELL REQUIRED gtk4-layer-shell-0)
|
||||||
pkg_check_modules(WEBKIT REQUIRED webkitgtk-6.0)
|
pkg_check_modules(WEBKIT REQUIRED webkitgtk-6.0)
|
||||||
|
pkg_check_modules(SQLITE3 REQUIRED sqlite3)
|
||||||
|
|
||||||
include_directories(${GTKMM_INCLUDE_DIRS} ${LAYERSHELL_INCLUDE_DIRS} ${WEBKIT_INCLUDE_DIRS})
|
include_directories(${GTKMM_INCLUDE_DIRS} ${LAYERSHELL_INCLUDE_DIRS} ${WEBKIT_INCLUDE_DIRS} ${SQLITE3_INCLUDE_DIRS})
|
||||||
link_directories(${GTKMM_LIBRARY_DIRS} ${LAYERSHELL_LIBRARY_DIRS} ${WEBKIT_LIBRARY_DIRS})
|
link_directories(${GTKMM_LIBRARY_DIRS} ${LAYERSHELL_LIBRARY_DIRS} ${WEBKIT_LIBRARY_DIRS} ${SQLITE3_LIBRARY_DIRS})
|
||||||
|
|
||||||
add_library(bar_lib)
|
add_library(bar_lib)
|
||||||
target_sources(bar_lib
|
target_sources(bar_lib
|
||||||
@@ -29,6 +30,7 @@ target_sources(bar_lib
|
|||||||
src/widgets/webWidget.cpp
|
src/widgets/webWidget.cpp
|
||||||
|
|
||||||
src/services/todo.cpp
|
src/services/todo.cpp
|
||||||
|
src/services/sqliteTodoAdapter.cpp
|
||||||
src/services/hyprland.cpp
|
src/services/hyprland.cpp
|
||||||
src/services/tray.cpp
|
src/services/tray.cpp
|
||||||
src/services/notifications.cpp
|
src/services/notifications.cpp
|
||||||
@@ -45,7 +47,7 @@ include_directories(bar_lib PRIVATE
|
|||||||
|
|
||||||
add_executable(bar main.cpp)
|
add_executable(bar main.cpp)
|
||||||
|
|
||||||
target_link_libraries(bar bar_lib ${GTKMM_LIBRARIES} ${LAYERSHELL_LIBRARIES} ${WEBKIT_LIBRARIES})
|
target_link_libraries(bar bar_lib ${GTKMM_LIBRARIES} ${LAYERSHELL_LIBRARIES} ${WEBKIT_LIBRARIES} ${SQLITE3_LIBRARIES})
|
||||||
|
|
||||||
# Copy `resources/bar.css` into the build directory when it changes
|
# Copy `resources/bar.css` into the build directory when it changes
|
||||||
set(RES_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/bar.css")
|
set(RES_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/bar.css")
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ public:
|
|||||||
~TodoEntry() override;
|
~TodoEntry() override;
|
||||||
|
|
||||||
int get_id() const { return id; }
|
int get_id() const { return id; }
|
||||||
|
std::string get_text() const { return text; }
|
||||||
private:
|
private:
|
||||||
int id;
|
int id;
|
||||||
|
std::string text;
|
||||||
sigc::signal<void(int)> signal_dismissed;
|
sigc::signal<void(int)> signal_dismissed;
|
||||||
|
|
||||||
void on_dismiss_clicked();
|
void on_dismiss_clicked();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "components/todoEntry.hpp"
|
#include "components/todoEntry.hpp"
|
||||||
|
#include "services/todoAdapter.hpp"
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class TodoService {
|
class TodoService {
|
||||||
public:
|
public:
|
||||||
@@ -12,11 +13,15 @@ class TodoService {
|
|||||||
std::map<int, TodoEntry *> getTodos();
|
std::map<int, TodoEntry *> getTodos();
|
||||||
void init();
|
void init();
|
||||||
void removeTodo(int id);
|
void removeTodo(int id);
|
||||||
TodoEntry *addTodo(std::string text, bool emitSignal = true);
|
TodoEntry *addTodo(std::string text, bool emitSignal = true, bool persist = true);
|
||||||
void updateTodo(int id, std::string text);
|
void updateTodo(int id, std::string text);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void load();
|
||||||
|
|
||||||
int nextId = 1;
|
int nextId = 1;
|
||||||
std::map<int, TodoEntry *> todos;
|
std::map<int, TodoEntry *> todos;
|
||||||
sigc::signal<void()> refreshSignal;
|
sigc::signal<void()> refreshSignal;
|
||||||
|
|
||||||
|
std::unique_ptr<ITodoAdapter> adapter;
|
||||||
};
|
};
|
||||||
21
include/services/todoAdapter.hpp
Normal file
21
include/services/todoAdapter.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct TodoRecord {
|
||||||
|
int id;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ITodoAdapter {
|
||||||
|
public:
|
||||||
|
virtual ~ITodoAdapter() = default;
|
||||||
|
|
||||||
|
virtual bool init() = 0;
|
||||||
|
virtual std::vector<TodoRecord> listTodos() = 0;
|
||||||
|
|
||||||
|
virtual int addTodo(const std::string &text) = 0;
|
||||||
|
virtual bool removeTodo(int id) = 0;
|
||||||
|
virtual bool updateTodo(int id, const std::string &text) = 0;
|
||||||
|
};
|
||||||
@@ -66,14 +66,6 @@ window {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.minimized {
|
|
||||||
background-color: rgba(50, 50, 50, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.restored {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
@@ -96,6 +88,9 @@ popover {
|
|||||||
background-color: rgb(30, 30, 30);
|
background-color: rgb(30, 30, 30);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-family: "IBMPlexSans-Regular", sans-serif;
|
font-family: "IBMPlexSans-Regular", sans-serif;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip {
|
tooltip {
|
||||||
@@ -111,9 +106,7 @@ tooltip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.todo-popover-container {
|
.todo-popover-container {
|
||||||
padding: 10px;
|
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-input-area {
|
.todo-input-area {
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ Popover::Popover(std::string icon, std::string name) {
|
|||||||
popover = new Gtk::Popover();
|
popover = new Gtk::Popover();
|
||||||
popover->set_parent(*this);
|
popover->set_parent(*this);
|
||||||
popover->set_autohide(true);
|
popover->set_autohide(true);
|
||||||
|
|
||||||
popover->signal_closed().connect([this]() {
|
|
||||||
this->add_css_class("minimized");
|
|
||||||
this->remove_css_class("restored");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Popover::~Popover() {
|
Popover::~Popover() {
|
||||||
@@ -28,8 +23,6 @@ void Popover::on_toggle_window() {
|
|||||||
if (popover->get_visible()) {
|
if (popover->get_visible()) {
|
||||||
popover->popdown();
|
popover->popdown();
|
||||||
} else {
|
} else {
|
||||||
this->remove_css_class("minimized");
|
|
||||||
this->add_css_class("restored");
|
|
||||||
popover->popup();
|
popover->popup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
TodoEntry::TodoEntry(int id, std::string text, sigc::signal<void(int)> signal_dismissed)
|
TodoEntry::TodoEntry(int id, std::string text, sigc::signal<void(int)> signal_dismissed)
|
||||||
: Gtk::Box(Gtk::Orientation::HORIZONTAL) {
|
: Gtk::Box(Gtk::Orientation::HORIZONTAL) {
|
||||||
this->id = id;
|
this->id = id;
|
||||||
|
this->text = text;
|
||||||
this->signal_dismissed = signal_dismissed;
|
this->signal_dismissed = signal_dismissed;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
165
src/services/sqliteTodoAdapter.cpp
Normal file
165
src/services/sqliteTodoAdapter.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#include "services/todoAdapter.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string getDbPath() {
|
||||||
|
const char *homeDir = getenv("HOME");
|
||||||
|
if (!homeDir) {
|
||||||
|
return "todos.db";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string path = std::string(homeDir) + "/.config/bar";
|
||||||
|
if (!std::filesystem::exists(path)) {
|
||||||
|
std::filesystem::create_directories(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path + "/todos.db";
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqliteTodoAdapter final : public ITodoAdapter {
|
||||||
|
public:
|
||||||
|
~SqliteTodoAdapter() override {
|
||||||
|
if (db_) {
|
||||||
|
sqlite3_close(db_);
|
||||||
|
db_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init() override {
|
||||||
|
std::string dbPath = getDbPath();
|
||||||
|
std::cout << "Opening database at: " << dbPath << std::endl;
|
||||||
|
|
||||||
|
int rc = sqlite3_open(dbPath.c_str(), &db_);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "Can't open database: " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "CREATE TABLE IF NOT EXISTS todos ("
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
"text TEXT NOT NULL);";
|
||||||
|
char *zErrMsg = nullptr;
|
||||||
|
rc = sqlite3_exec(db_, sql, nullptr, nullptr, &zErrMsg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "SQL error (create table): " << (zErrMsg ? zErrMsg : "") << std::endl;
|
||||||
|
sqlite3_free(zErrMsg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TodoRecord> listTodos() override {
|
||||||
|
std::vector<TodoRecord> results;
|
||||||
|
if (!db_) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt *stmt = nullptr;
|
||||||
|
const char *sql = "SELECT id, text FROM todos";
|
||||||
|
int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "Failed to fetch data: " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||||
|
int id = sqlite3_column_int(stmt, 0);
|
||||||
|
const unsigned char *txt = sqlite3_column_text(stmt, 1);
|
||||||
|
results.push_back({id, txt ? reinterpret_cast<const char *>(txt) : ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
int addTodo(const std::string &text) override {
|
||||||
|
if (!db_) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt *stmt = nullptr;
|
||||||
|
const char *sql = "INSERT INTO todos (text) VALUES (?1);";
|
||||||
|
int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "SQL prepare error (insert): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, text.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
std::cerr << "SQL step error (insert): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return static_cast<int>(sqlite3_last_insert_rowid(db_));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removeTodo(int id) override {
|
||||||
|
if (!db_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt *stmt = nullptr;
|
||||||
|
const char *sql = "DELETE FROM todos WHERE id = ?1;";
|
||||||
|
int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "SQL prepare error (delete): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_int(stmt, 1, id);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
std::cerr << "SQL step error (delete): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool updateTodo(int id, const std::string &text) override {
|
||||||
|
if (!db_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt *stmt = nullptr;
|
||||||
|
const char *sql = "UPDATE todos SET text = ?1 WHERE id = ?2;";
|
||||||
|
int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
std::cerr << "SQL prepare error (update): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, text.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_int(stmt, 2, id);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
std::cerr << "SQL step error (update): " << sqlite3_errmsg(db_) << std::endl;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
sqlite3 *db_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Factory kept local to this TU for now; TodoService uses it.
|
||||||
|
std::unique_ptr<ITodoAdapter> makeSqliteTodoAdapter() {
|
||||||
|
return std::make_unique<SqliteTodoAdapter>();
|
||||||
|
}
|
||||||
@@ -4,27 +4,34 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
std::unique_ptr<ITodoAdapter> makeSqliteTodoAdapter();
|
||||||
|
|
||||||
TodoService::TodoService(sigc::signal<void()> refreshSignal) {
|
TodoService::TodoService(sigc::signal<void()> refreshSignal) {
|
||||||
this->refreshSignal = refreshSignal;
|
this->refreshSignal = refreshSignal;
|
||||||
|
this->adapter = makeSqliteTodoAdapter();
|
||||||
this->init();
|
this->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
TodoService::~TodoService() {}
|
TodoService::~TodoService() {
|
||||||
|
this->adapter.reset();
|
||||||
|
}
|
||||||
|
|
||||||
std::map<int, TodoEntry *> TodoService::getTodos() {
|
std::map<int, TodoEntry *> TodoService::getTodos() {
|
||||||
return this->todos;
|
return this->todos;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TodoService::init() {
|
void TodoService::init() {
|
||||||
std::vector<std::string> items = {
|
if (!this->adapter) {
|
||||||
"Buy groceries",
|
std::cerr << "Todo adapter not set" << std::endl;
|
||||||
"Finish the report",
|
return;
|
||||||
"Call Alice",
|
|
||||||
"Schedule dentist appointment"};
|
|
||||||
|
|
||||||
for (auto item : items) {
|
|
||||||
this->addTodo(item, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this->adapter->init()) {
|
||||||
|
std::cerr << "Todo adapter init failed" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TodoService::removeTodo(int id) {
|
void TodoService::removeTodo(int id) {
|
||||||
@@ -33,24 +40,70 @@ void TodoService::removeTodo(int id) {
|
|||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
todos.erase(id);
|
if (this->adapter) {
|
||||||
|
this->adapter->removeTodo(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
todos.erase(id);
|
||||||
this->refreshSignal.emit();
|
this->refreshSignal.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
TodoEntry *TodoService::addTodo(std::string text, bool emitSignal) {
|
TodoEntry *TodoService::addTodo(std::string text, bool emitSignal, bool persist) {
|
||||||
|
int id = nextId;
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
if (!this->adapter) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
int newId = this->adapter->addTodo(text);
|
||||||
|
if (newId < 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
id = newId;
|
||||||
|
}
|
||||||
|
|
||||||
auto signal = sigc::signal<void(int)>();
|
auto signal = sigc::signal<void(int)>();
|
||||||
signal.connect(sigc::mem_fun(*this, &TodoService::removeTodo));
|
signal.connect(sigc::mem_fun(*this, &TodoService::removeTodo));
|
||||||
TodoEntry *todo = Gtk::make_managed<TodoEntry>(nextId, text, signal);
|
TodoEntry *todo = Gtk::make_managed<TodoEntry>(id, text, signal);
|
||||||
|
|
||||||
todos[nextId] = todo;
|
todos[id] = todo;
|
||||||
|
|
||||||
nextId++;
|
if (id >= nextId) {
|
||||||
|
nextId = id + 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (emitSignal) {
|
if (emitSignal) {
|
||||||
this->refreshSignal.emit();
|
this->refreshSignal.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return todo;
|
return todo;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TodoService::updateTodo(int id, std::string text) {}
|
void TodoService::updateTodo(int id, std::string text) {}
|
||||||
|
|
||||||
|
void TodoService::load() {
|
||||||
|
if (!this->adapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Loading todos from database..." << std::endl;
|
||||||
|
auto rows = this->adapter->listTodos();
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (const auto &row : rows) {
|
||||||
|
count++;
|
||||||
|
int id = row.id;
|
||||||
|
std::string text = row.text;
|
||||||
|
|
||||||
|
auto signal = sigc::signal<void(int)>();
|
||||||
|
signal.connect(sigc::mem_fun(*this, &TodoService::removeTodo));
|
||||||
|
TodoEntry *todo = Gtk::make_managed<TodoEntry>(id, text, signal);
|
||||||
|
|
||||||
|
todos[id] = todo;
|
||||||
|
if (id >= nextId) {
|
||||||
|
nextId = id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cout << "Loaded " << count << " todos." << std::endl;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ TodoPopover::TodoPopover(std::string icon, std::string title) : Popover(icon, ti
|
|||||||
std::string text = entry->get_text();
|
std::string text = entry->get_text();
|
||||||
if (!text.empty()) {
|
if (!text.empty()) {
|
||||||
this->todoService->addTodo(text);
|
this->todoService->addTodo(text);
|
||||||
|
|
||||||
|
entry->set_text("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user