feat: media_async + media_index modules, media_files table, tests

- media_async: thread-per-task async engine with primitives (sha256, sign, copy, uuid, file_size)
- media_index: media_files DB table with register_async for indexing chunks
- chat_msg: async media submission via register_async, simplified chat_msg_submit fields
- voice_recorder/attachment_sender: write single file (no chunk splitting on caller side)
- inputbar/messagelist: updated to new simplified submit interface
- test_media_async: 4-thread stress test (200K ops / 2 sec, 0 errors)
- test_media_index: 9 cases covering commit edge cases + async flow
- build: Makefile.am, CMakeLists.txt, utun_sources.cmake updated
This commit is contained in:
evgeny committed 2026-07-29 14:15:10 +03:00
1 parent ce5a84da24
commit 65dbfa6813
43 files changed
+1983 -391

No files matched your search

@@ -9,7 +9,7 @@ data class Channel(val id: String, val name: String, val lastMsgAt: Long = 0, va
data class Message(val id: Long, val author: String, val text: String, val ts: Long,
val isOutgoing: Boolean = false, val contentType: String = "text/plain",
val filePath: String = "", val waveform: List<Float> = emptyList(),
val voiceDurationMs: Int = 0)
val voiceDurationMs: Int = 0, val fileSize: Long = 0)
class ChatRepository {
private val myNodeId: Long = NativeLib.getMyNodeId()
@@ -53,6 +53,9 @@ class ChatRepository {
val contentType = obj.optString("contentType", "text/plain")
val filePath = obj.optString("filePath", "")
var displayText = text
var fileSize = 0L
/* Parse base64 waveform from voice message text (format: "wf=<100 chars A-Za-z0-9+/>;dur=1234;") */
var waveform = emptyList<Float>()
var voiceDurationMs = 0
@@ -88,16 +91,30 @@ class ChatRepository {
} catch (_: Exception) {}
}
/* Parse file attachment: "filename.ext|file_size|block_size|num_blocks|sigs" */
if (contentType == "application/octet-stream") {
try {
val pipeIdx = text.indexOf('|')
if (pipeIdx > 0) {
displayText = text.substring(0, pipeIdx)
val metaParts = text.substring(pipeIdx + 1).split("|")
if (metaParts.isNotEmpty())
fileSize = metaParts[0].toLongOrNull() ?: 0L
}
} catch (_: Exception) {}
}
list.add(Message(
id = obj.getLong("id"),
author = obj.getString("author"),
text = text,
text = displayText,
ts = obj.getLong("ts"),
isOutgoing = obj.optBoolean("isOutgoing", false),
contentType = contentType,
filePath = filePath,
waveform = waveform,
voiceDurationMs = voiceDurationMs
voiceDurationMs = voiceDurationMs,
fileSize = fileSize
))
}
} catch (e: Exception) { Log.w("utun-gui", "getMessages error", e) }
@@ -36,6 +36,9 @@ class ConfigProvider(private val context: Context) {
private val logUdpIp = stringPreferencesKey("log_udp_ip")
private val logUdpPort = intPreferencesKey("log_udp_port")
private val serversJson = stringPreferencesKey("servers")
private val storageAutoload = intPreferencesKey("storage_autoload")
private val storageAutoloadMaxsizeMb = intPreferencesKey("storage_autoload_maxsize_mb")
private val storageMaxsizeGb = intPreferencesKey("storage_maxsize_gb")
private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
private val gson = Gson()
@@ -130,6 +133,9 @@ class ConfigProvider(private val context: Context) {
key == "node.listen_port" -> context.dataStore.data.first()[nodeListenPort] ?: 12345
key == "control.port" -> context.dataStore.data.first()[controlPort] ?: 9999
key == "log_udp.port" -> context.dataStore.data.first()[logUdpPort] ?: 9999
key == "storage_autoload" -> context.dataStore.data.first()[storageAutoload] ?: 1
key == "storage_autoload_maxsize_mb" -> context.dataStore.data.first()[storageAutoloadMaxsizeMb] ?: 10
key == "storage_maxsize_gb" -> context.dataStore.data.first()[storageMaxsizeGb] ?: 1
else -> 0
}
}
@@ -149,6 +155,9 @@ class ConfigProvider(private val context: Context) {
"debug.categories" -> prefs[debugCategories] = value
"log_udp.ip" -> prefs[logUdpIp] = value
"log_udp.port" -> prefs[logUdpPort] = value.toIntOrNull() ?: 0
"storage_autoload" -> prefs[storageAutoload] = value.toIntOrNull() ?: 1
"storage_autoload_maxsize_mb" -> prefs[storageAutoloadMaxsizeMb] = value.toIntOrNull() ?: 10
"storage_maxsize_gb" -> prefs[storageMaxsizeGb] = value.toIntOrNull() ?: 1
}
}
}
@@ -62,7 +62,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
when {
isVoice -> VoiceBubble(message, textColor, isActive, playbackState, onPlayVoice, onSeekVoice)
isMedia -> Text("\uD83D\uDCC4 ${message.text}", fontSize = 15.sp, color = textColor)
isMedia -> FileBubble(message, textColor)
else -> {
Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f))
Spacer(Modifier.height(2.dp))
@@ -167,6 +167,26 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
}
}
@Composable
private fun FileBubble(message: Message, textColor: Color) {
Column {
Text(message.text, fontSize = 15.sp, color = textColor, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(2.dp))
Text(formatFileSize(message.fileSize), fontSize = 12.sp, color = textColor.copy(alpha = 0.5f))
}
}
private fun formatFileSize(bytes: Long): String {
if (bytes <= 0) return ""
if (bytes < 1024) return "${bytes}B"
val kb = bytes / 1024.0
if (kb < 1024.0) return if (kb < 10.0) "%.1fKB".format(kb) else "%.0fKB".format(kb)
val mb = kb / 1024.0
if (mb < 1024.0) return if (mb < 10.0) "%.1fMB".format(mb) else "%.0fMB".format(mb)
val gb = mb / 1024.0
return "%.1fGB".format(gb)
}
private fun formatDurationMs(ms: Int): String {
val secs = ms / 1000
return "%d:%02d".format(secs / 60, secs % 60)
@@ -42,7 +42,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
var dirty by remember { mutableStateOf(false) }
var tabIndex by remember { mutableStateOf(0) }
val tabs = listOf("Settings", "Audio")
val tabs = listOf("Settings", "Audio", "Storage")
val handleBack = {
if (dirty) {
@@ -73,6 +73,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
when (tabIndex) {
0 -> GeneralSettingsTab(vm, dirty, onDirty = { dirty = it }, onExit)
1 -> AudioSettingsTab(vm)
2 -> StorageSettingsTab()
}
}
}
@@ -414,6 +415,52 @@ private fun AudioSettingsTab(vm: ChatViewModel) {
}
}
/* ── Storage Settings Tab ── */
@Composable
private fun StorageSettingsTab() {
val provider = ChatApplication.instance.configProvider
val scope = rememberCoroutineScope()
var autoload by remember { mutableStateOf(true) }
var autoloadMaxSizeMb by remember { mutableStateOf("10") }
var maxStorageGb by remember { mutableStateOf("1") }
LaunchedEffect(Unit) {
autoload = provider.getInt("storage_autoload") != 0
autoloadMaxSizeMb = provider.getInt("storage_autoload_maxsize_mb").toString()
maxStorageGb = provider.getInt("storage_maxsize_gb").toString()
}
Column(
modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("Media Storage", style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Autoload media", modifier = Modifier.weight(1f))
Switch(checked = autoload, onCheckedChange = {
autoload = it
scope.launch { provider.setValue("storage_autoload", if (it) "1" else "0") }
})
}
HorizontalDivider(Modifier.padding(vertical = 4.dp))
OutlinedTextField(autoloadMaxSizeMb,
{ v -> autoloadMaxSizeMb = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..500) scope.launch { provider.setValue("storage_autoload_maxsize_mb", v) } },
label = { Text("Autoload max size (MB)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true, modifier = Modifier.padding(top = 4.dp))
OutlinedTextField(maxStorageGb,
{ v -> maxStorageGb = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..100) scope.launch { provider.setValue("storage_maxsize_gb", v) } },
label = { Text("Max storage size (GB)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true)
}
}
/* ── Labeled Slider ── */
@Composable
@@ -489,8 +489,8 @@ int utun_bridge_voice_get_compressor(void) {
}
void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
float rise_rate_per_500ms, float target_level_db) {
voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_500ms, target_level_db);
float rise_rate_per_sec, float target_level_db) {
voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db);
}
float utun_bridge_voice_get_peak_level(void) {
@@ -86,7 +86,7 @@ int utun_bridge_voice_get_preset(void);
void utun_bridge_voice_set_compressor(int enabled);
int utun_bridge_voice_get_compressor(void);
void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
float rise_rate_per_500ms, float target_level_db);
float rise_rate_per_sec, float target_level_db);
float utun_bridge_voice_get_peak_level(void);
/* ── Attachment ── */
@@ -12,18 +12,6 @@
#include <unistd.h>
#include <libgen.h>
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
#define MEDIA_BLOCK_TARGET 30
static uint32_t media_calc_block_size(uint64_t file_size) {
if (file_size == 0) return 0;
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
return (uint32_t)target;
}
int attachment_send(const char* channel_id, const char* src_file_path, const char* db_path) {
if (!channel_id || !src_file_path || !db_path) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: invalid args");
@@ -33,27 +21,15 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: no uasync"); return -1; }
FILE* src = fopen(src_file_path, "rb");
if (!src) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: cannot open %s", src_file_path); return -1; }
fseek(src, 0, SEEK_END);
uint64_t file_size = (uint64_t)ftell(src);
fseek(src, 0, SEEK_SET);
if (file_size == 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "attachment_send: empty file %s", src_file_path); fclose(src); return -1; }
/* extract basename and ext from path */
/* extract basename and ext */
char path_copy[1024];
snprintf(path_copy, sizeof(path_copy), "%s", src_file_path);
char* fname = basename(path_copy);
char* dot = strrchr(fname, '.');
char ext[32] = "";
if (dot) {
snprintf(ext, sizeof(ext), "%s", dot + 1);
*dot = '\0';
}
if (dot) { snprintf(ext, sizeof(ext), "%s", dot + 1); *dot = '\0'; }
/* generate dt/suffix */
/* generate dt/suffix for unique filename */
time_t t = time(NULL);
struct tm tm_buf; localtime_r(&t, &tm_buf);
char dt[32]; snprintf(dt, sizeof(dt), "%04d%02d%02d-%02d%02d%02d",
@@ -62,10 +38,7 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
int rnd = rand() & 0xFFFF;
char suffix[8]; snprintf(suffix, sizeof(suffix), "%04x", rnd);
uint32_t block_size = media_calc_block_size(file_size);
int num_blocks = (int)((file_size + block_size - 1) / block_size);
/* ensure media dir */
/* ensure media dir exists */
char media_dir[1024];
snprintf(media_dir, sizeof(media_dir), "%s/media/%s", db_path, channel_id);
{
@@ -77,46 +50,26 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
mkdir(tmp, 0755);
}
/* write blocks */
for (int n = 0; n < num_blocks; n++) {
char block_path[1280];
snprintf(block_path, sizeof(block_path), "%s/%s_%d_%s_%s.%s",
media_dir, dt, n, fname, suffix, ext[0] ? ext : "bin");
FILE* dst = fopen(block_path, "wb");
if (!dst) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: cannot create block %s", block_path);
continue;
}
uint8_t buf[65536];
uint64_t remaining = block_size;
while (remaining > 0) {
size_t to_read = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf);
size_t rd = fread(buf, 1, to_read, src);
if (rd == 0) break;
fwrite(buf, 1, rd, dst);
remaining -= rd;
}
fclose(dst);
}
fclose(src);
/* single file path — no splitting */
char media_file[1280];
snprintf(media_file, sizeof(media_file), "%s/%s_%s_%s.%s",
media_dir, dt, fname, suffix, ext[0] ? ext : "bin");
/* post chat_msg_submit */
/* post chat_msg_submit — media_copy=1 so media_index copies src to dest */
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + 1);
if (!req) return -1;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id);
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt);
snprintf(req->media_basename, sizeof(req->media_basename), "%s", fname);
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix);
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext[0] ? ext : "bin");
req->media_num_blocks = (uint32_t)num_blocks;
snprintf(req->media_src, sizeof(req->media_src), "%s", src_file_path);
snprintf(req->media_dest, sizeof(req->media_dest), "%s", media_file);
req->media_copy = 1;
req->data = (uint8_t*)(req + 1);
req->data_len = 0;
req->timestamp = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "attachment_send: ch=%s file=%s blocks=%d size=%llu",
channel_id, src_file_path, num_blocks, (unsigned long long)file_size);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "attachment_send: ch=%s src=%s dst=%s",
channel_id, src_file_path, media_file);
uasync_post(ua, chat_core_submit_trampoline, req);
return 0;
@@ -58,6 +58,8 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR)
${SRC_DIR}/proxy
${SRC_DIR}/lwip_tcp
${SRC_DIR}/BBR
${SRC_DIR}/media_delivery
${SRC_DIR}/media_async
${TOOLS_DIR}
${CONFIG_DIR}
PARENT_SCOPE
@@ -17,9 +17,6 @@
#include <unistd.h>
#include <errno.h>
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
#define MEDIA_BLOCK_TARGET 30
#define OPUS_MAGIC 0x5355504F
#define FRAME_MS 20
#define MAX_PACKET 4000
@@ -85,9 +82,9 @@ int voice_recorder_init(const char* db_path) {
cfg.block_duration_ms = 20;
cfg.lookback_ms = 200;
cfg.lookahead_ms = 100;
cfg.max_gain_db = 30.0f;
cfg.rise_rate_per_500ms = 2.0f;
cfg.target_level = 0.25f;
cfg.max_gain_db = 25.0f;
cfg.rise_rate_per_sec = 10.0f;
cfg.target_level = 1.0f;
audio_compressor_configure(g_rec->compressor, &cfg);
}
@@ -199,14 +196,6 @@ int voice_recorder_feed(const int16_t* samples, int count) {
return 0;
}
static uint32_t media_calc_block_size(uint64_t file_size) {
if (file_size == 0) return 0;
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
return (uint32_t)target;
}
static void voice_cleanup_locked(struct voice_recorder* rec) {
u_free(rec->pcm_buffer); rec->pcm_buffer = NULL;
rec->pcm_count = 0; rec->pcm_cap = 0;
@@ -364,37 +353,8 @@ int voice_recorder_stop(int* out_duration_ms) {
return -1;
}
/* get file size and calculate blocks */
FILE* fsize = fopen(temp_path, "rb"); uint64_t file_size = 0;
if (fsize) { fseek(fsize, 0, SEEK_END); file_size = (uint64_t)ftell(fsize); fclose(fsize); }
uint32_t block_size = media_calc_block_size(file_size);
int num_blocks = (int)((file_size + block_size - 1) / block_size);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: file_size=%llu block_size=%u num_blocks=%d",
(unsigned long long)file_size, block_size, num_blocks);
/* split file into blocks */
FILE* src = fopen(temp_path, "rb");
if (!src) { unlink(temp_path); voice_cleanup_locked(g_rec); pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; }
for (int n = 0; n < num_blocks; n++) {
char block_path[1280];
snprintf(block_path, sizeof(block_path), "%s/%s_%d_%s_%s.opus",
media_dir, dt, n, basename, suffix);
FILE* dst = fopen(block_path, "wb");
if (!dst) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: cannot create block %s", block_path); continue; }
uint8_t buf[65536];
uint64_t remaining = block_size;
while (remaining > 0) {
size_t rd = fread(buf, 1, remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf), src);
if (rd == 0) break;
fwrite(buf, 1, rd, dst);
remaining -= rd;
}
fclose(dst);
}
fclose(src);
unlink(temp_path);
/* file is already at temp_path in media dir – no splitting needed */
/* media_index_register_async will handle block size calculation, signing, and DB */
/* build compact waveform: 100 bytes base64 (A-Za-z0-9+/), log scale */
static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -421,18 +381,16 @@ int voice_recorder_stop(int* out_duration_ms) {
snprintf(req->channel_id, sizeof(req->channel_id), "%s", g_rec->channel_id);
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt);
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename);
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix);
snprintf(req->media_ext, sizeof(req->media_ext), "opus");
req->media_num_blocks = (uint32_t)num_blocks;
snprintf(req->media_src, sizeof(req->media_src), "%s", temp_path);
snprintf(req->media_dest, sizeof(req->media_dest), "%s", temp_path);
req->media_copy = 0;
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)total_data_len;
memcpy(req->data, wf_data, total_data_len);
req->timestamp = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: posting media ch=%s dt=%s stem=%s blocks=%d",
req->channel_id, dt, basename, num_blocks);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: posting media ch=%s path=%s",
req->channel_id, temp_path);
uasync_post(ua, chat_core_submit_trampoline, req);
@@ -505,7 +463,7 @@ int voice_recorder_is_compressor_enabled(void) {
}
void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
float rise_rate_per_500ms, float target_level_db) {
float rise_rate_per_sec, float target_level_db) {
pthread_mutex_lock(&g_init_mtx);
if (g_rec && g_rec->compressor) {
pthread_mutex_lock(&g_rec->mtx);
@@ -516,11 +474,11 @@ void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int
cfg.lookback_ms = lookback_ms;
cfg.lookahead_ms = lookahead_ms;
cfg.max_gain_db = (float)max_gain_db;
cfg.rise_rate_per_500ms = rise_rate_per_500ms;
cfg.rise_rate_per_sec = rise_rate_per_sec;
cfg.target_level = powf(10.0f, target_level_db / 20.0f);
audio_compressor_configure(g_rec->compressor, &cfg);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder compressor config: gain=%ddB lookback=%d lookahead=%d rise=%.1f target=%.0fdBFS",
max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_500ms, target_level_db);
max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db);
pthread_mutex_unlock(&g_rec->mtx);
}
pthread_mutex_unlock(&g_init_mtx);
@@ -24,7 +24,7 @@ int voice_recorder_get_preset(void);
void voice_recorder_set_compressor_enabled(int enabled);
int voice_recorder_is_compressor_enabled(void);
void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
float rise_rate_per_500ms, float target_level_db);
float rise_rate_per_sec, float target_level_db);
float voice_recorder_get_peak_level(void);
#ifdef __cplusplus
+1
View File
@@ -119,6 +119,7 @@ add_executable(chatgui
src/voiceplayback.cpp
src/media_blocks.cpp
src/audiodevicesettingspage.cpp
src/storagesettingspage.cpp
transport/utun_node.cpp
transport/node_config.cpp
transport/config_updater.cpp
+2
View File
@@ -67,6 +67,8 @@ target_include_directories(utun PUBLIC
${SRC_DIR}
${SRC_DIR}/transport_layer
${SRC_DIR}/routing_layer
${SRC_DIR}/media_delivery
${SRC_DIR}/media_async
${LIB_DIR}
${SRC_DIR}/uip
${CMAKE_SOURCE_DIR}/db
+17 -48
View File
@@ -158,11 +158,8 @@ void AudioDeviceSettingsPage::loadFromDb() {
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1);
m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1);
m_savedCompressorEnabled = m_db->getUiStateInt("compressor_enabled", 0);
m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 30);
m_savedCompressorLookbackMs = m_db->getUiStateInt("compressor_lookback_ms", 200);
m_savedCompressorLookaheadMs = m_db->getUiStateInt("compressor_lookahead_ms", 100);
m_savedCompressorRiseRateTenths = m_db->getUiStateInt("compressor_rise_rate_tenths", 20);
m_savedCompressorTargetLevelDb = m_db->getUiStateInt("compressor_target_level_db", -12);
m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 25);
m_savedCompressorRiseRate = m_db->getUiStateInt("compressor_rise_rate", 10);
}
/* Select saved indices */
@@ -181,10 +178,7 @@ void AudioDeviceSettingsPage::loadFromDb() {
m_compressorEnabledCb->setChecked(m_savedCompressorEnabled != 0);
m_compressorMaxGainSlider->setValue(m_savedCompressorMaxGainDb);
m_compressorLookbackSlider->setValue(m_savedCompressorLookbackMs);
m_compressorLookaheadSlider->setValue(m_savedCompressorLookaheadMs);
m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRateTenths);
m_compressorTargetLevelSlider->setValue(m_savedCompressorTargetLevelDb);
m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRate);
updateCompressorLabels();
}
@@ -211,30 +205,20 @@ void AudioDeviceSettingsPage::applyAndSave() {
int compEnabled = m_compressorEnabledCb->isChecked() ? 1 : 0;
int compMaxGain = m_compressorMaxGainSlider->value();
int compLookback = m_compressorLookbackSlider->value();
int compLookahead = m_compressorLookaheadSlider->value();
int compRiseRateTenths = m_compressorRiseRateSlider->value();
int compTargetLevel = m_compressorTargetLevelSlider->value();
int compRiseRate = m_compressorRiseRateSlider->value();
saveKey("compressor_enabled", compEnabled);
saveKey("compressor_max_gain_db", compMaxGain);
saveKey("compressor_lookback_ms", compLookback);
saveKey("compressor_lookahead_ms", compLookahead);
saveKey("compressor_rise_rate_tenths", compRiseRateTenths);
saveKey("compressor_target_level_db", compTargetLevel);
saveKey("compressor_rise_rate", compRiseRate);
m_savedOutputIdx = outIdx;
m_savedInputIdx = inIdx;
m_savedCodecPreset = codecPreset;
m_savedCompressorEnabled = compEnabled;
m_savedCompressorMaxGainDb = compMaxGain;
m_savedCompressorLookbackMs = compLookback;
m_savedCompressorLookaheadMs = compLookahead;
m_savedCompressorRiseRateTenths = compRiseRateTenths;
m_savedCompressorTargetLevelDb = compTargetLevel;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB lookback=%d lookahead=%d rise=%.1fx target=%ddB",
outIdx, inIdx, codecPreset, compEnabled, compMaxGain, compLookback, compLookahead,
(float)compRiseRateTenths / 10.0f, compTargetLevel);
m_savedCompressorRiseRate = compRiseRate;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB rise=%.1fx",
outIdx, inIdx, codecPreset, compEnabled, compMaxGain, (float)compRiseRate);
if (m_recorder) {
m_recorder->setCompressorEnabled(compEnabled != 0);
@@ -243,10 +227,10 @@ void AudioDeviceSettingsPage::applyAndSave() {
compCfg.sample_rate = 48000;
compCfg.channels = 1;
compCfg.max_gain_db = (float)compMaxGain;
compCfg.lookback_ms = compLookback;
compCfg.lookahead_ms = compLookahead;
compCfg.rise_rate_per_500ms = (float)compRiseRateTenths / 10.0f;
compCfg.target_level = powf(10.0f, (float)compTargetLevel / 20.0f);
compCfg.lookback_ms = 200;
compCfg.lookahead_ms = 100;
compCfg.rise_rate_per_sec = (float)compRiseRate;
compCfg.target_level = 1.0f;
audio_compressor_configure(m_recorder->compressor(), &compCfg);
}
}
@@ -342,37 +326,22 @@ void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) {
};
makeSliderRow("Max gain:", m_compressorMaxGainSlider, m_compressorMaxGainLabel,
0, 30, 1);
makeSliderRow("Lookback time:", m_compressorLookbackSlider, m_compressorLookbackLabel,
50, 500, 10);
makeSliderRow("Lookahead time:", m_compressorLookaheadSlider, m_compressorLookaheadLabel,
20, 500, 10);
makeSliderRow("Rise rate (x/500ms):", m_compressorRiseRateSlider, m_compressorRiseRateLabel,
11, 100, 1);
makeSliderRow("Target level:", m_compressorTargetLevelSlider, m_compressorTargetLevelLabel,
-40, 0, 1);
0, 50, 1);
makeSliderRow("Rise rate (x/sec):", m_compressorRiseRateSlider, m_compressorRiseRateLabel,
2, 10, 1);
auto connectSlider = [this](QSlider* slider) {
connect(slider, &QSlider::valueChanged, this, &AudioDeviceSettingsPage::updateCompressorLabels);
};
connectSlider(m_compressorMaxGainSlider);
connectSlider(m_compressorLookbackSlider);
connectSlider(m_compressorLookaheadSlider);
connectSlider(m_compressorRiseRateSlider);
connectSlider(m_compressorTargetLevelSlider);
}
void AudioDeviceSettingsPage::updateCompressorLabels() {
if (m_compressorMaxGainLabel)
m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + " dB");
if (m_compressorLookbackLabel)
m_compressorLookbackLabel->setText(QString::number(m_compressorLookbackSlider->value()) + " ms");
if (m_compressorLookaheadLabel)
m_compressorLookaheadLabel->setText(QString::number(m_compressorLookaheadSlider->value()) + " ms");
if (m_compressorRiseRateLabel) {
float v = (float)m_compressorRiseRateSlider->value() / 10.0f;
m_compressorRiseRateLabel->setText(QString::number(v, 'f', 1) + "x");
int v = m_compressorRiseRateSlider->value();
m_compressorRiseRateLabel->setText(QString::number(v) + "x");
}
if (m_compressorTargetLevelLabel)
m_compressorTargetLevelLabel->setText(QString::number(m_compressorTargetLevelSlider->value()) + " dBFS");
}
+2 -11
View File
@@ -45,19 +45,10 @@ private:
QCheckBox* m_compressorEnabledCb;
QSlider* m_compressorMaxGainSlider;
QSlider* m_compressorLookbackSlider;
QSlider* m_compressorLookaheadSlider;
QSlider* m_compressorRiseRateSlider;
QSlider* m_compressorTargetLevelSlider;
QLabel* m_compressorMaxGainLabel;
QLabel* m_compressorLookbackLabel;
QLabel* m_compressorLookaheadLabel;
QLabel* m_compressorRiseRateLabel;
QLabel* m_compressorTargetLevelLabel;
int m_savedCompressorEnabled = 0;
int m_savedCompressorMaxGainDb = 30;
int m_savedCompressorLookbackMs = 200;
int m_savedCompressorLookaheadMs = 100;
int m_savedCompressorRiseRateTenths = 20;
int m_savedCompressorTargetLevelDb = -12;
int m_savedCompressorMaxGainDb = 25;
int m_savedCompressorRiseRate = 10;
};
+15 -20
View File
@@ -233,21 +233,15 @@ void InputBar::onPttReleased() {
qPrintable(mediaPath), QDir(mediaPath).exists(), qPrintable(m_channelIdForRecord));
QString fileName = VoiceEncoder::generateFileName();
QString tempPath = mediaPath + "/" + fileName;
QString mediaFile = mediaPath + "/" + fileName;
float durationSec = 0;
int frames = VoiceEncoder::encodeToFile(pcm, pcmSize, 48000, 1, tempPath, durationSec);
int frames = VoiceEncoder::encodeToFile(pcm, pcmSize, 48000, 1, mediaFile, durationSec);
if (frames <= 0) {
QFile::remove(tempPath);
QFile::remove(mediaFile);
return;
}
MediaBlockInfo blk = media_split_to_blocks(tempPath, mediaPath, true);
if (blk.numBlocks <= 0) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar: block split failed");
return;
}
m_voiceFilePath = QString("%1/%2_%3_%4.%5").arg(mediaPath, blk.dt, blk.basename, blk.suffix, blk.ext);
m_voiceFilePath = mediaFile;
const auto& wf = m_recorder->waveformLevels();
QString wfStr = "wf=";
@@ -267,10 +261,9 @@ void InputBar::onPttReleased() {
wfStr += b64[idx];
}
wfStr += QString(";dur=%1;").arg(durationMs);
GUI_INFO("InputBar: voice message ready: dt=%s stem=%s blocks=%d dur=%.1fs",
qPrintable(blk.dt), qPrintable(blk.basename), blk.numBlocks, durationSec);
emit sendVoiceMessage(blk.dt, blk.basename, blk.suffix, blk.ext,
blk.numBlocks, blk.blockSize, durationMs, wfStr);
GUI_INFO("InputBar: voice message ready: file=%s dur=%.1fs",
qPrintable(mediaFile), durationSec);
emit sendVoiceMessage(mediaFile, mediaFile, durationMs, wfStr);
}
void InputBar::onAttachClicked() {
@@ -285,13 +278,15 @@ void InputBar::onAttachClicked() {
VoiceEncoder::ensureMediaDir(mediaPath);
QDir().mkpath(mediaPath);
MediaBlockInfo blk = media_split_to_blocks(srcPath, mediaPath, false);
if (blk.numBlocks <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar::onAttachClicked: split failed"); return; }
QDateTime now = QDateTime::currentDateTime();
QString ts = now.toString("yyyyMMdd-HHmmss");
int rnd = rand() & 0xFFFF;
QString destPath = QString("%1/%2_%3_%4.%5")
.arg(mediaPath, ts).arg(fi.completeBaseName()).arg(rnd, 4, 16, QChar('0')).arg(fi.suffix().isEmpty() ? "bin" : fi.suffix());
GUI_INFO("InputBar::onAttachClicked: file=%s dt=%s blocks=%d",
qPrintable(fi.fileName()), qPrintable(blk.dt), blk.numBlocks);
emit sendFileAttachment(blk.dt, blk.basename, blk.suffix, blk.ext,
blk.numBlocks, blk.blockSize, fi.fileName());
GUI_INFO("InputBar::onAttachClicked: src=%s dest=%s",
qPrintable(srcPath), qPrintable(destPath));
emit sendFileAttachment(srcPath, destPath, fi.fileName());
}
void InputBar::resizeEvent(QResizeEvent* event) {
+2 -6
View File
@@ -19,13 +19,9 @@ public:
signals:
void sendMessage(const QString &text);
void sendVoiceMessage(const QString& mediaDt, const QString& mediaBasename,
const QString& mediaSuffix, const QString& mediaExt,
int numBlocks, qint64 blockSize,
void sendVoiceMessage(const QString& mediaSrc, const QString& mediaDest,
int durationMs, const QString& waveformStr);
void sendFileAttachment(const QString& mediaDt, const QString& mediaBasename,
const QString& mediaSuffix, const QString& mediaExt,
int numBlocks, qint64 blockSize,
void sendFileAttachment(const QString& mediaSrc, const QString& mediaDest,
const QString& displayName);
protected:
+7 -5
View File
@@ -217,11 +217,13 @@ void MainWindow::setupSoundFromConfig() {
m_recorder->setCompressorEnabled(compEnabled != 0);
if (m_recorder->compressor()) {
audio_compressor_config_t compCfg = {0};
compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 30);
compCfg.lookback_ms = m_db->getUiStateInt("compressor_lookback_ms", 200);
compCfg.lookahead_ms = m_db->getUiStateInt("compressor_lookahead_ms", 100);
compCfg.rise_rate_per_500ms = (float)m_db->getUiStateInt("compressor_rise_rate_tenths", 20) / 10.0f;
compCfg.target_level = powf(10.0f, (float)m_db->getUiStateInt("compressor_target_level_db", -12) / 20.0f);
compCfg.sample_rate = 48000;
compCfg.channels = 1;
compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 25);
compCfg.lookback_ms = 200;
compCfg.lookahead_ms = 100;
compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10);
compCfg.target_level = 1.0f;
audio_compressor_configure(m_recorder->compressor(), &compCfg);
}
}
+79
View File
@@ -185,6 +185,16 @@ static int measureTextHeight(const QFont &font, const QString &text, int width)
return r.height();
}
static QString formatFileSize(qint64 bytes) {
if (bytes < 1024) return QString("%1B").arg(bytes);
double kb = bytes / 1024.0;
if (kb < 1024.0) return QString("%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0);
double mb = kb / 1024.0;
if (mb < 1024.0) return QString("%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0);
double gb = mb / 1024.0;
return QString("%1GB").arg(gb, 0, 'f', 1);
}
static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) {
int maxW = 0;
int pos = 0;
@@ -259,6 +269,35 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
return L;
}
/* File attachment detection */
L.isFile = (contentType == "application/octet-stream");
if (L.isFile) {
QFontMetrics fm(option.font);
int th = fm.height();
int ih = 24; /* icon height */
L.bubbleWidth = 230;
int bubbleH = kPadTop + ih + 2 + th + kPadBot;
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap;
int contentEdge = L.isOutgoing
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth) : contentEdge;
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH);
L.textRect = QRect(bx + kPadH + ih + 8, kPadTop, L.bubbleWidth - 2*kPadH - ih - 8, th);
L.statusRect = QRect(bx + kPadH + ih + 8 - 48, kPadTop + th + 2, L.bubbleWidth - 2*kPadH - ih - 8 + 48 - 40, th);
if (!L.narrow) {
int avatarY = L.bubbleRect.bottom() - kAvatar;
if (!L.isOutgoing)
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
else
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar);
}
return L;
}
L.emojiCount = detectEmojiOnly(text);
L.emojiOnly = (L.emojiCount >= 1);
@@ -758,6 +797,46 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
return;
}
if (L.isFile) {
drawBubble(painter, L, option);
if (!L.narrow) drawAvatar(painter, L, index);
/* File icon */
QFont iconFont = option.font;
iconFont.setPointSize(iconFont.pointSize() + 6);
painter->setFont(iconFont);
painter->setPen(option.palette.text().color());
QRect iconRect(L.bubbleRect.left() + kPadH, kPadTop, 24, 24);
painter->drawText(iconRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x93\x84"));
QFont fileFont = option.font;
fileFont.setBold(true);
painter->setFont(fileFont);
painter->setPen(option.palette.text().color());
QString displayName = index.data(MsgFileDisplayNameRole).toString();
painter->drawText(L.textRect, Qt::AlignLeft | Qt::AlignVCenter, displayName);
QFont sizeFont = option.font;
sizeFont.setPointSize(sizeFont.pointSize() - 2);
painter->setFont(sizeFont);
painter->setPen(QColor(0x80, 0x80, 0x80));
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
QString sizeStr = formatFileSize(fsz);
int nameH = QFontMetrics(fileFont).height();
QRect sizeRect(L.textRect.left(), L.textRect.top() + nameH, L.textRect.width(), QFontMetrics(sizeFont).height());
painter->drawText(sizeRect, Qt::AlignLeft | Qt::AlignVCenter, sizeStr);
/* Timestamp */
QFont tsFont = option.font;
tsFont.setPointSize(tsFont.pointSize() - 1);
painter->setFont(tsFont);
painter->drawText(QRect(L.textRect.right() - 40, L.textRect.top() + nameH, 40, QFontMetrics(sizeFont).height()),
Qt::AlignRight | Qt::AlignVCenter, index.data(MsgTimeRole).toString());
painter->restore();
return;
}
auto *cv = qobject_cast<const ChatView *>(option.widget);
bool singleTextSel = false;
bool multiRowSel = false;
+2
View File
@@ -26,6 +26,7 @@ enum MessageDataRole {
MsgVoiceNumBlocksRole = Qt::UserRole + 21,
MsgVoiceSigsRole = Qt::UserRole + 22,
MsgFileDisplayNameRole = Qt::UserRole + 23,
MsgFileSizeRole = Qt::UserRole + 24,
};
class MessageDelegate : public QStyledItemDelegate {
@@ -61,6 +62,7 @@ private:
QRect emojiArea;
bool isOutgoing = false;
bool isVoice = false;
bool isFile = false;
QRect voicePlayBtnRect;
QRect voiceWaveformRect;
QRect voiceDurationRect;
+20 -19
View File
@@ -153,8 +153,17 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
}
}
if (ct == "application/octet-stream") {
QString fname = QString::fromUtf8(data);
QString raw = QString::fromUtf8(data);
int pipePos = raw.indexOf('|');
QString fname = (pipePos > 0) ? raw.left(pipePos) : raw;
item->setData(fname, MsgFileDisplayNameRole);
if (pipePos > 0) {
QString meta = raw.mid(pipePos + 1);
QStringList parts = meta.split('|');
if (parts.size() >= 1)
item->setData(parts[0].toULongLong(), MsgFileSizeRole);
}
}
}
@@ -246,10 +255,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString& dt, const QString& basename,
const QString& suffix, const QString& ext,
int numBlocks, qint64 blockSize,
int durationMs, const QString& wfStr) {
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString& mediaSrc, const QString& mediaDest,
int durationMs, const QString& wfStr) {
if (m_currentChannelId.isEmpty()) return;
QByteArray textData = wfStr.toUtf8();
@@ -261,11 +268,9 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
req->media_num_blocks = (uint32_t)numBlocks;
snprintf(req->media_src, sizeof(req->media_src), "%s", mediaSrc.toUtf8().constData());
snprintf(req->media_dest, sizeof(req->media_dest), "%s", mediaDest.toUtf8().constData());
req->media_copy = 0;
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)textData.size();
memcpy(req->data, textData.constData(), textData.size());
@@ -273,10 +278,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& dt, const QString& basename,
const QString& suffix, const QString& ext,
int numBlocks, qint64 blockSize,
const QString& displayName) {
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& mediaSrc, const QString& mediaDest,
const QString& displayName) {
if (m_currentChannelId.isEmpty()) return;
QByteArray textData = displayName.toUtf8();
@@ -288,11 +291,9 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
req->media_num_blocks = (uint32_t)numBlocks;
snprintf(req->media_src, sizeof(req->media_src), "%s", mediaSrc.toUtf8().constData());
snprintf(req->media_dest, sizeof(req->media_dest), "%s", mediaDest.toUtf8().constData());
req->media_copy = (mediaSrc != mediaDest) ? 1 : 0;
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)textData.size();
memcpy(req->data, textData.constData(), textData.size());
+13 -5
View File
@@ -6,6 +6,7 @@
#include "nodespage.h"
#include "soundsettingspage.h"
#include "audiodevicesettingspage.h"
#include "storagesettingspage.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
#include "../db/db_manager.h"
@@ -45,6 +46,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
"QListWidget::item { padding: 10px 14px; }"
"QListWidget::item:selected { background: #3390EC; color: white; }");
m_categoryList->addItem("Profile");
m_categoryList->addItem("Storage");
m_categoryList->addItem("Network");
m_categoryList->addItem("Database");
m_categoryList->addItem("Status");
@@ -84,6 +86,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
profLayout->addRow(sortLabel, m_sortCombo);
m_pages->addWidget(profilePage);
// Storage page
m_storagePage = new StorageSettingsPage(db, m_pages);
m_pages->addWidget(m_storagePage);
// Network page
m_networkPage = new NetworkSettingsPage(m_pages);
m_pages->addWidget(m_networkPage);
@@ -166,11 +172,12 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) {
void SettingsDialog::onCategoryChanged(int row) {
m_pages->setCurrentIndex(row);
if (row == 2) m_databasePage->refreshStatus();
if (row == 3) m_statusPage->refreshStatus();
if (row == 4) m_nodesPage->refreshNodes();
if (row == 5) m_soundPage->loadFromDb();
if (row == 6) m_audioDevicePage->loadFromDb();
if (row == 1) m_storagePage->loadFromDb();
if (row == 3) m_databasePage->refreshStatus();
if (row == 4) m_statusPage->refreshStatus();
if (row == 5) m_nodesPage->refreshNodes();
if (row == 6) m_soundPage->loadFromDb();
if (row == 7) m_audioDevicePage->loadFromDb();
}
void SettingsDialog::onSave() {
@@ -227,5 +234,6 @@ void SettingsDialog::onSave() {
m_soundPage->applyAndSave();
m_audioDevicePage->applyAndSave();
m_storagePage->applyAndSave();
accept();
}
+2
View File
@@ -15,6 +15,7 @@ class StatusPage;
class NodesPage;
class SoundSettingsPage;
class AudioDeviceSettingsPage;
class StorageSettingsPage;
class DbManager;
class UtunNode;
class AudioRecorder;
@@ -45,4 +46,5 @@ private:
NodesPage* m_nodesPage;
SoundSettingsPage* m_soundPage;
AudioDeviceSettingsPage* m_audioDevicePage;
StorageSettingsPage* m_storagePage;
};
+90
View File
@@ -0,0 +1,90 @@
#include "storagesettingspage.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
extern "C" {
#include "chat/chat_core.h"
}
#include <QVBoxLayout>
#include <QFormLayout>
#include <QCheckBox>
#include <QSpinBox>
#include <QLabel>
#include <QFrame>
static void saveUiStateInt(const char* key, int val) {
QByteArray v = QByteArray::number(val);
size_t klen = strlen(key), vlen = (size_t)v.size(), total = klen + 1 + vlen + 1;
if (total > 256) return;
void* arg = u_malloc(total);
memcpy(arg, key, klen + 1);
memcpy((char*)arg + klen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
StorageSettingsPage::StorageSettingsPage(DbManager* db, QWidget* parent)
: QWidget(parent)
, m_db(db)
{
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 12, 0, 0);
layout->setSpacing(14);
auto* title = new QLabel("Media Storage", this);
title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title);
auto* form = new QFormLayout();
form->setSpacing(10);
form->setContentsMargins(0, 8, 0, 0);
m_autoloadCheck = new QCheckBox("Autoload media", this);
m_autoloadCheck->setStyleSheet("font-size: 13px;");
form->addRow(m_autoloadCheck);
m_autoloadMaxSize = new QSpinBox(this);
m_autoloadMaxSize->setRange(1, 500);
m_autoloadMaxSize->setSuffix(" MB");
m_autoloadMaxSize->setFixedWidth(110);
m_autoloadMaxSize->setStyleSheet(
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 5px 8px; font-size: 13px; background: palette(base); }");
form->addRow("Autoload max size:", m_autoloadMaxSize);
m_maxStorageSize = new QSpinBox(this);
m_maxStorageSize->setRange(1, 100);
m_maxStorageSize->setSuffix(" GB");
m_maxStorageSize->setFixedWidth(110);
m_maxStorageSize->setStyleSheet(
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 5px 8px; font-size: 13px; background: palette(base); }");
form->addRow("Max storage size:", m_maxStorageSize);
layout->addLayout(form);
auto* sep = new QFrame(this);
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet("QFrame { color: palette(mid); }");
layout->addWidget(sep);
layout->addStretch();
}
void StorageSettingsPage::loadFromDb() {
if (!m_db) return;
bool autoload = m_db->getUiStateInt("storage_autoload", 1);
m_autoloadCheck->setChecked(autoload);
int maxSizeMb = m_db->getUiStateInt("storage_autoload_maxsize_mb", 10);
m_autoloadMaxSize->setValue(maxSizeMb);
int maxGb = m_db->getUiStateInt("storage_maxsize_gb", 1);
m_maxStorageSize->setValue(maxGb);
}
void StorageSettingsPage::applyAndSave() {
saveUiStateInt("storage_autoload", m_autoloadCheck->isChecked() ? 1 : 0);
saveUiStateInt("storage_autoload_maxsize_mb", m_autoloadMaxSize->value());
saveUiStateInt("storage_maxsize_gb", m_maxStorageSize->value());
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QWidget>
class DbManager;
class QCheckBox;
class QSpinBox;
class StorageSettingsPage : public QWidget {
Q_OBJECT
public:
explicit StorageSettingsPage(DbManager* db, QWidget* parent = nullptr);
void loadFromDb();
void applyAndSave();
private:
DbManager* m_db = nullptr;
QCheckBox* m_autoloadCheck;
QSpinBox* m_autoloadMaxSize;
QSpinBox* m_maxStorageSize;
};