update sounds ang transtions
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-22 17:16:36 +07:00
parent d70a4e45af
commit 523f98b74c
28 changed files with 2449 additions and 138 deletions

View File

@@ -5,12 +5,21 @@
#include "util/id_util.hpp"
#include "util/string_util.hpp"
#include "util/string_util.hpp"
#include <sqlite3.h>
#include <algorithm>
#include <cctype>
#include <vector>
namespace lm {
namespace {
constexpr const char* kSoundSelect =
"SELECT id, name, description, file_name, duration_ms, enabled, volume, is_system, created_at, updated_at "
"FROM sounds";
nlohmann::json rowToJson(sqlite3_stmt* stmt)
{
auto textOrNull = [&](int col) -> nlohmann::json {
@@ -26,8 +35,44 @@ nlohmann::json rowToJson(sqlite3_stmt* stmt)
? nlohmann::json(nullptr)
: nlohmann::json(sqlite3_column_int(stmt, 4))},
{"enabled", sqlite3_column_int(stmt, 5) != 0},
{"created_at", textOrNull(6)},
{"updated_at", textOrNull(7)}};
{"volume", sqlite3_column_int(stmt, 6)},
{"is_system", sqlite3_column_int(stmt, 7) != 0},
{"created_at", textOrNull(8)},
{"updated_at", textOrNull(9)}};
}
int clampVolume(int v)
{
if (v < 0)
return 0;
if (v > 100)
return 100;
return v;
}
bool isReservedSystemName(const std::string& name)
{
const std::string lower = StringUtil::toLower(StringUtil::trimCopy(name));
return lower == "beep" || lower == "horn" || lower == "chime";
}
bool findNameConflict(sqlite3* db, const std::string& name, const std::string& except_id)
{
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db,
except_id.empty()
? "SELECT id FROM sounds WHERE lower(name) = lower(?1) LIMIT 1"
: "SELECT id FROM sounds WHERE lower(name) = lower(?1) AND id != ?2 LIMIT 1",
-1,
&stmt,
nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
if (!except_id.empty())
sqlite3_bind_text(stmt, 2, except_id.c_str(), -1, SQLITE_TRANSIENT);
const bool conflict = sqlite3_step(stmt) == SQLITE_ROW;
sqlite3_finalize(stmt);
return conflict;
}
} // namespace
@@ -38,13 +83,9 @@ nlohmann::json SoundStore::list() const
{
std::lock_guard<std::mutex> lock(mu_);
nlohmann::json sounds = nlohmann::json::array();
const std::string query = std::string(kSoundSelect) + " ORDER BY is_system DESC, name";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
"FROM sounds ORDER BY name",
-1,
&stmt,
nullptr) != SQLITE_OK)
if (sqlite3_prepare_v2(db_.handle(), query.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
return sounds;
while (sqlite3_step(stmt) == SQLITE_ROW)
sounds.push_back(rowToJson(stmt));
@@ -55,13 +96,9 @@ nlohmann::json SoundStore::list() const
std::optional<nlohmann::json> SoundStore::find(const std::string& id) const
{
std::lock_guard<std::mutex> lock(mu_);
const std::string query = std::string(kSoundSelect) + " WHERE id = ?1";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
"FROM sounds WHERE id = ?1",
-1,
&stmt,
nullptr) != SQLITE_OK)
if (sqlite3_prepare_v2(db_.handle(), query.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
return std::nullopt;
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
std::optional<nlohmann::json> out;
@@ -84,17 +121,31 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
err = "name is required";
return std::nullopt;
}
if (isReservedSystemName(name))
{
err = "name reserved for built-in system sounds";
return std::nullopt;
}
std::lock_guard<std::mutex> lock(mu_);
if (findNameConflict(db_.handle(), name, ""))
{
err = "sound name already exists";
return std::nullopt;
}
const std::string id = payload.value("id", IdUtil::newId());
const std::string now = IdUtil::nowIso8601();
const std::string description = payload.value("description", "");
const bool enabled = payload.value("enabled", true);
const int volume = clampVolume(payload.value("volume", 100));
const bool is_system = payload.value("is_system", false);
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, created_at, updated_at) "
"VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6)",
"INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, volume, "
"is_system, created_at, updated_at) "
"VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6,?7,?8)",
-1,
&stmt,
nullptr) != SQLITE_OK)
@@ -106,8 +157,10 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, enabled ? 1 : 0);
sqlite3_bind_text(stmt, 5, now.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 5, volume);
sqlite3_bind_int(stmt, 6, is_system ? 1 : 0);
sqlite3_bind_text(stmt, 7, now.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 8, now.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE)
{
@@ -123,6 +176,8 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
{"file_name", nullptr},
{"duration_ms", nullptr},
{"enabled", enabled},
{"volume", volume},
{"is_system", is_system},
{"created_at", now},
{"updated_at", now}};
}
@@ -136,18 +191,42 @@ bool SoundStore::update(const std::string& id, const nlohmann::json& payload, st
return false;
}
const bool is_system = existing->value("is_system", false);
nlohmann::json merged = *existing;
for (const char* key : {"name", "description", "enabled", "duration_ms"})
for (const char* key : {"description", "enabled", "duration_ms", "volume"})
{
if (payload.contains(key))
merged[key] = payload[key];
}
if (!is_system && payload.contains("name"))
{
const std::string new_name = StringUtil::trimCopy(payload["name"].get<std::string>());
if (new_name.empty())
{
err = "name is required";
return false;
}
if (isReservedSystemName(new_name))
{
err = "name reserved for built-in system sounds";
return false;
}
if (findNameConflict(db_.handle(), new_name, id))
{
err = "sound name already exists";
return false;
}
merged["name"] = new_name;
}
if (merged.contains("volume"))
merged["volume"] = clampVolume(merged["volume"].get<int>());
const std::string now = IdUtil::nowIso8601();
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, updated_at=?6 WHERE id=?1",
"UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, volume=?6, "
"updated_at=?7 WHERE id=?1",
-1,
&stmt,
nullptr) != SQLITE_OK)
@@ -163,7 +242,8 @@ bool SoundStore::update(const std::string& id, const nlohmann::json& payload, st
sqlite3_bind_int(stmt, 5, merged["duration_ms"].get<int>());
else
sqlite3_bind_null(stmt, 5);
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 6, clampVolume(merged.value("volume", 100)));
sqlite3_bind_text(stmt, 7, now.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
if (!ok)
@@ -180,6 +260,11 @@ bool SoundStore::remove(const std::string& id, std::string& err)
err = "sound not found";
return false;
}
if (existing->value("is_system", false))
{
err = "system sounds cannot be deleted";
return false;
}
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
@@ -217,11 +302,17 @@ bool SoundStore::saveFile(const std::string& id,
const std::string& bytes,
std::string& err)
{
if (!find(id))
auto existing = find(id);
if (!existing)
{
err = "sound not found";
return false;
}
if (existing->value("is_system", false))
{
err = "system sounds cannot be replaced";
return false;
}
std::error_code ec;
std::filesystem::create_directories(db_.soundsDir(), ec);
@@ -254,4 +345,57 @@ bool SoundStore::saveFile(const std::string& id,
return ok;
}
bool SoundStore::ensureSystemDefaults(std::string& err)
{
{
std::vector<std::string> dup_ids;
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id FROM sounds WHERE is_system = 0 AND lower(name) IN ('beep', 'horn', 'chime')",
-1,
&stmt,
nullptr) == SQLITE_OK)
{
while (sqlite3_step(stmt) == SQLITE_ROW)
{
if (sqlite3_column_type(stmt, 0) != SQLITE_NULL)
dup_ids.push_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)));
}
sqlite3_finalize(stmt);
}
for (const auto& dup_id : dup_ids)
{
sqlite3_stmt* del = nullptr;
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sounds WHERE id = ?1", -1, &del, nullptr) == SQLITE_OK)
{
sqlite3_bind_text(del, 1, dup_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_step(del);
sqlite3_finalize(del);
}
}
}
struct DefaultSound
{
const char* id;
const char* name;
};
static const DefaultSound kDefaults[] = {
{"sys_beep", "Beep"},
{"sys_horn", "Horn"},
{"sys_chime", "Chime"},
};
for (const auto& d : kDefaults)
{
if (find(d.id))
continue;
nlohmann::json payload = {{"id", d.id}, {"name", d.name}, {"is_system", true}, {"volume", 100}};
if (!create(payload, err))
return false;
}
return true;
}
} // namespace lm