diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 89fbba373..e27f579a5 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1023,8 +1023,14 @@ PRIVATE
iv/iv_delegate_impl.h
iv/iv_instance.cpp
iv/iv_instance.h
+ kotato/boxes/kotato_radio_box.cpp
+ kotato/boxes/kotato_radio_box.h
kotato/kotato_lang.cpp
kotato/kotato_lang.h
+ kotato/kotato_settings.cpp
+ kotato/kotato_settings.h
+ kotato/kotato_settings_menu.cpp
+ kotato/kotato_settings_menu.h
kotato/kotato_version.h
lang/lang_cloud_manager.cpp
lang/lang_cloud_manager.h
diff --git a/Telegram/Resources/default_kotato-settings-custom.json b/Telegram/Resources/default_kotato-settings-custom.json
new file mode 100644
index 000000000..cbbb06b3c
--- /dev/null
+++ b/Telegram/Resources/default_kotato-settings-custom.json
@@ -0,0 +1,20 @@
+// This is a list of your own settings for Kotatogram Desktop
+// You can see full list of settings in the 'kotato-settings-default.json' file
+
+{
+ // "fonts": {
+ // "main": "Open Sans",
+ // "semibold": "Open Sans Semibold",
+ // "semibold_is_bold": false,
+ // "monospaced": "Consolas"
+ // },
+ // "sticker_height": 170,
+ // "big_emoji_outline": true,
+ // "always_show_scheduled": false,
+ // "show_chat_id": false,
+ // "net_speed_boost": null,
+ // "show_phone_in_drawer": true,
+ // "scales": [],
+ // "confirm_before_calls": false,
+ // "recent_stickers_limit": 20
+}
diff --git a/Telegram/Resources/icons/settings/kotato.png b/Telegram/Resources/icons/settings/kotato.png
new file mode 100644
index 000000000..23c74dbec
Binary files /dev/null and b/Telegram/Resources/icons/settings/kotato.png differ
diff --git a/Telegram/Resources/icons/settings/kotato@2x.png b/Telegram/Resources/icons/settings/kotato@2x.png
new file mode 100644
index 000000000..04b3997dc
Binary files /dev/null and b/Telegram/Resources/icons/settings/kotato@2x.png differ
diff --git a/Telegram/Resources/icons/settings/kotato@3x.png b/Telegram/Resources/icons/settings/kotato@3x.png
new file mode 100644
index 000000000..842bd3494
Binary files /dev/null and b/Telegram/Resources/icons/settings/kotato@3x.png differ
diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json
index e77a886b4..b2fa173d9 100644
--- a/Telegram/Resources/langs/rewrites/en.json
+++ b/Telegram/Resources/langs/rewrites/en.json
@@ -26,6 +26,14 @@
"ktg_outdated_soon": "Otherwise, Kotatogram Desktop will stop updating on {date}.",
"ktg_outdated_now": "So that Kotatogram Desktop can update to newer versions.",
"ktg_mac_menu_show": "Show Kotatogram",
+ "ktg_settings_kotato": "Kotatogram Settings",
+ "ktg_settings_chats": "Chats",
+ "ktg_settings_network": "Network",
+ "ktg_settings_system": "System",
+ "ktg_settings_other": "Other",
+ "ktg_settings_filters": "Folders",
+ "ktg_settings_messages": "Messages",
+ "ktg_settings_forward": "Forward",
"ktg_in_app_update_disabled": "In-app updater is disabled.",
"dummy_last_string": ""
}
diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc
index 8a8a61953..c5e6c3b34 100644
--- a/Telegram/Resources/qrc/telegram/telegram.qrc
+++ b/Telegram/Resources/qrc/telegram/telegram.qrc
@@ -58,7 +58,8 @@
../../default_shortcuts-custom.json
- ../../../../lib/xdg/io.github.kotatogram.desktop
+ ../../default_kotato-settings-custom.json
+ ../../../../lib/xdg/io.github.kotatogram.desktop
../../langs/rewrites/en.json
diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp
index 13eb80f13..3118e7111 100644
--- a/Telegram/SourceFiles/core/launcher.cpp
+++ b/Telegram/SourceFiles/core/launcher.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/launcher.h"
+#include "kotato/kotato_settings.h"
#include "kotato/kotato_version.h"
#include "platform/platform_launcher.h"
#include "platform/platform_specific.h"
@@ -356,6 +357,9 @@ void Launcher::initHighDpi() {
}
int Launcher::exec() {
+ // This should be called before init to load default
+ // values and set some options that are not stored in JSON.
+ Kotato::JsonSettings::Start();
init();
if (cLaunchMode() == LaunchModeFixPrevious) {
@@ -367,6 +371,7 @@ int Launcher::exec() {
// Must be started before Platform is started.
Logs::start();
base::options::init(cWorkingDir() + "tdata/experimental_options.json");
+ Kotato::JsonSettings::Load();
// Must be called after options are inited.
initHighDpi();
@@ -408,6 +413,7 @@ int Launcher::exec() {
CrashReports::Finish();
ThirdParty::finish();
Platform::finish();
+ Kotato::JsonSettings::Finish();
Logs::finish();
return result;
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 6fa9d7bb6..a760bc606 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/local_url_handlers.h"
+#include "kotato/kotato_settings_menu.h"
#include "api/api_authorizations.h"
#include "api/api_confirm_phone.h"
#include "api/api_chat_filters.h"
@@ -669,6 +670,8 @@ bool ResolveSettings(
return ::Settings::GlobalTTLId();
} else if (section == u"information"_q) {
return ::Settings::Information::Id();
+ } else if (section == u"kotato"_q) {
+ return ::Settings::Kotato::Id();
}
return ::Settings::Main::Id();
}();
@@ -1255,7 +1258,7 @@ const std::vector &LocalUrlHandlers() {
ResolvePrivatePost
},
{
- u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile)?$"_q,
+ u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/kotato)?$"_q,
ResolveSettings
},
{
diff --git a/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.cpp b/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.cpp
new file mode 100644
index 000000000..80a1c0b25
--- /dev/null
+++ b/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.cpp
@@ -0,0 +1,177 @@
+/*
+This file is part of Kotatogram Desktop,
+the unofficial app based on Telegram Desktop.
+
+For license and copyright information please follow this link:
+https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
+*/
+#include "kotato/boxes/kotato_radio_box.h"
+
+#include "kotato/kotato_settings.h"
+#include "lang/lang_keys.h"
+#include "ui/wrap/vertical_layout.h"
+#include "ui/wrap/padding_wrap.h"
+#include "ui/wrap/wrap.h"
+#include "ui/widgets/checkbox.h"
+#include "ui/widgets/labels.h"
+#include "ui/boxes/confirm_box.h"
+#include "styles/style_layers.h"
+#include "styles/style_boxes.h"
+#include "core/application.h"
+
+namespace Kotato {
+
+RadioBox::RadioBox(
+ QWidget*,
+ const QString &title,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn saveCallback,
+ bool warnRestart)
+: _title(title)
+, _startValue(currentValue)
+, _valueCount(valueCount)
+, _labelGetter(labelGetter)
+, _saveCallback(std::move(saveCallback))
+, _warnRestart(warnRestart)
+, _owned(this)
+, _content(_owned.data()) {
+}
+
+RadioBox::RadioBox(
+ QWidget*,
+ const QString &title,
+ const QString &description,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn saveCallback,
+ bool warnRestart)
+: _title(title)
+, _description(description)
+, _startValue(currentValue)
+, _valueCount(valueCount)
+, _labelGetter(labelGetter)
+, _saveCallback(std::move(saveCallback))
+, _warnRestart(warnRestart)
+, _owned(this)
+, _content(_owned.data()) {
+}
+
+RadioBox::RadioBox(
+ QWidget*,
+ const QString &title,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn descriptionGetter,
+ Fn saveCallback,
+ bool warnRestart)
+: _title(title)
+, _startValue(currentValue)
+, _valueCount(valueCount)
+, _labelGetter(labelGetter)
+, _descriptionGetter(descriptionGetter)
+, _saveCallback(std::move(saveCallback))
+, _warnRestart(warnRestart)
+, _owned(this)
+, _content(_owned.data()) {
+}
+
+RadioBox::RadioBox(
+ QWidget*,
+ const QString &title,
+ const QString &description,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn descriptionGetter,
+ Fn saveCallback,
+ bool warnRestart)
+: _title(title)
+, _description(description)
+, _startValue(currentValue)
+, _valueCount(valueCount)
+, _labelGetter(labelGetter)
+, _descriptionGetter(descriptionGetter)
+, _saveCallback(std::move(saveCallback))
+, _warnRestart(warnRestart)
+, _owned(this)
+, _content(_owned.data()) {
+}
+
+void RadioBox::prepare() {
+ setTitle(rpl::single(_title));
+
+ addButton(tr::lng_settings_save(), [=] { save(); });
+ addButton(tr::lng_cancel(), [=] { closeBox(); });
+
+ if (!_description.isEmpty()) {
+ _content->add(
+ object_ptr(_content, _description, st::boxDividerLabel),
+ style::margins(
+ st::boxPadding.left(),
+ 0,
+ st::boxPadding.right(),
+ st::boxPadding.bottom()));
+ }
+
+ _group = std::make_shared(_startValue);
+
+ for (auto i = 0; i != _valueCount; ++i) {
+ const auto description = _descriptionGetter
+ ? _descriptionGetter(i)
+ : QString();
+
+ _content->add(
+ object_ptr(
+ _content,
+ _group,
+ i,
+ _labelGetter(i),
+ st::autolockButton),
+ style::margins(
+ st::boxPadding.left(),
+ st::boxPadding.bottom(),
+ st::boxPadding.right(),
+ description.isEmpty() ? st::boxPadding.bottom() : 0));
+ if (!description.isEmpty()) {
+ _content->add(
+ object_ptr(_content, description, st::boxDividerLabel),
+ style::margins(
+ st::boxPadding.left()
+ + st::autolockButton.margin.left()
+ + st::autolockButton.margin.right()
+ + st::defaultToggle.width
+ + st::defaultToggle.border * 2,
+ 0,
+ st::boxPadding.right(),
+ st::boxPadding.bottom()));
+ }
+ }
+
+ auto wrap = object_ptr(this, std::move(_owned));
+ setDimensionsToContent(st::boxWidth, wrap.data());
+ setInnerWidget(std::move(wrap));
+}
+
+void RadioBox::save() {
+ _saveCallback(_group->current());
+ if (_warnRestart) {
+ const auto box = std::make_shared>();
+
+ *box = getDelegate()->show(
+ Ui::MakeConfirmBox({
+ .text = tr::lng_settings_need_restart(),
+ .confirmed = [] { Core::Restart(); },
+ .cancelled = crl::guard(this, [=] { closeBox(); box->data()->closeBox(); }),
+ .confirmText = tr::lng_settings_restart_now(),
+ .cancelText = tr::lng_settings_restart_later(),
+ }));
+ } else {
+ closeBox();
+ }
+}
+
+} // namespace Kotato
\ No newline at end of file
diff --git a/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.h b/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.h
new file mode 100644
index 000000000..93ded92b7
--- /dev/null
+++ b/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.h
@@ -0,0 +1,80 @@
+/*
+This file is part of Kotatogram Desktop,
+the unofficial app based on Telegram Desktop.
+
+For license and copyright information please follow this link:
+https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
+*/
+#pragma once
+
+#include "boxes/abstract_box.h"
+
+namespace Ui {
+class RadiobuttonGroup;
+class Radiobutton;
+class FlatLabel;
+class VerticalLayout;
+} // namespace Ui
+
+namespace Kotato {
+
+class RadioBox : public Ui::BoxContent {
+public:
+ RadioBox(
+ QWidget* parent,
+ const QString &title,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn saveCallback,
+ bool warnRestart = false);
+ RadioBox(
+ QWidget* parent,
+ const QString &title,
+ const QString &description,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn saveCallback,
+ bool warnRestart = false);
+ RadioBox(
+ QWidget* parent,
+ const QString &title,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn descriptionGetter,
+ Fn saveCallback,
+ bool warnRestart = false);
+ RadioBox(
+ QWidget* parent,
+ const QString &title,
+ const QString &description,
+ int currentValue,
+ int valueCount,
+ Fn labelGetter,
+ Fn descriptionGetter,
+ Fn saveCallback,
+ bool warnRestart = false);
+
+protected:
+ void prepare() override;
+
+private:
+ void save();
+
+ QString _title;
+ QString _description;
+ int _startValue;
+ int _valueCount;
+ Fn _labelGetter;
+ Fn _descriptionGetter;
+ Fn _saveCallback;
+ bool _warnRestart = false;
+ std::shared_ptr _group;
+
+ object_ptr _owned;
+ not_null _content;
+};
+
+} // namespace Kotato
diff --git a/Telegram/SourceFiles/kotato/kotato_settings.cpp b/Telegram/SourceFiles/kotato/kotato_settings.cpp
new file mode 100644
index 000000000..3a18489a6
--- /dev/null
+++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp
@@ -0,0 +1,770 @@
+/*
+This file is part of Kotatogram Desktop,
+the unofficial app based on Telegram Desktop.
+
+For license and copyright information please follow this link:
+https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
+*/
+#include "kotato/kotato_settings.h"
+
+#include "kotato/kotato_version.h"
+#include "mainwindow.h"
+#include "mainwidget.h"
+#include "window/window_controller.h"
+#include "core/application.h"
+#include "data/data_peer_id.h"
+#include "base/parse_helper.h"
+#include "base/timer.h"
+#include "ui/widgets/input_fields.h"
+#include "data/data_chat_filters.h"
+#include "platform/platform_file_utilities.h"
+
+#include
+#include
+#include
+#include
+
+namespace Kotato {
+namespace JsonSettings {
+namespace {
+
+constexpr auto kWriteJsonTimeout = crl::time(5000);
+
+class Manager : public QObject {
+public:
+ Manager();
+ void load();
+ void fill();
+ void write(bool force = false);
+
+ [[nodiscard]] QVariant get(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ [[nodiscard]] QVariant getWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ [[nodiscard]] QVariantMap getAllWithPending(const QString &key);
+ [[nodiscard]] rpl::producer events(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ [[nodiscard]] rpl::producer eventsWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ void set(
+ const QString &key,
+ QVariant value,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ void setAfterRestart(
+ const QString &key,
+ QVariant value,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ void reset(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ void resetAfterRestart(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+ void writeTimeout();
+
+private:
+ [[nodiscard]] QVariant getDefault(const QString &key);
+
+ void writeDefaultFile();
+ void writeCurrentSettings();
+ bool readCustomFile();
+ void writing();
+
+ base::Timer _jsonWriteTimer;
+
+ rpl::event_stream _eventStream;
+ rpl::event_stream _pendingEventStream;
+ QHash _settingsHashMap;
+ QHash _defaultSettingsHashMap;
+
+};
+
+inline QString MakeMapKey(const QString &key, uint64 accountId, bool isTestAccount) {
+ return (accountId == 0) ? key : key
+ + (isTestAccount ? qsl(":test_") : qsl(":"))
+ + QString::number(accountId);
+}
+
+QVariantMap GetAllWithPending(const QString &key);
+
+enum SettingScope {
+ Global,
+ Account,
+};
+
+enum SettingStorage {
+ None,
+ MainJson,
+};
+
+enum SettingType {
+ BoolSetting,
+ IntSetting,
+ QStringSetting,
+ QJsonArraySetting,
+};
+
+using CheckHandler = Fn;
+
+CheckHandler IntLimit(int min, int max, int defaultValue) {
+ return [=] (QVariant value) -> QVariant {
+ if (value.canConvert()) {
+ auto intValue = value.toInt();
+ if (intValue < min) {
+ return min;
+ } else if (intValue > max) {
+ return max;
+ } else {
+ return value;
+ }
+ } else {
+ return defaultValue;
+ }
+ };
+}
+
+inline CheckHandler IntLimit(int min, int max) {
+ return IntLimit(min, max, min);
+}
+
+CheckHandler IntLimitMin(int min) {
+ return [=] (QVariant value) -> QVariant {
+ if (value.canConvert()) {
+ auto intValue = value.toInt();
+ if (intValue < min) {
+ return min;
+ } else {
+ return value;
+ }
+ } else {
+ return min;
+ }
+ };
+}
+
+
+struct Definition {
+ SettingScope scope = SettingScope::Global;
+ SettingStorage storage = SettingStorage::MainJson;
+ SettingType type = SettingType::BoolSetting;
+ QVariant defaultValue;
+ QVariant fillerValue;
+ CheckHandler limitHandler = nullptr;
+};
+
+const std::map> DefinitionMap {
+
+ // Non-stored settings
+ // Stored settings
+};
+
+using OldOptionKey = QString;
+using NewOptionKey = QString;
+
+const std::map> ReplacedOptionsMap {
+};
+
+QString DefaultFilePath() {
+ return cWorkingDir() + qsl("tdata/kotato-settings-default.json");
+}
+
+QString CustomFilePath() {
+ return cWorkingDir() + qsl("tdata/kotato-settings-custom.json");
+}
+
+bool DefaultFileIsValid() {
+ QFile file(DefaultFilePath());
+ if (!file.open(QIODevice::ReadOnly)) {
+ return false;
+ }
+ auto error = QJsonParseError{ 0, QJsonParseError::NoError };
+ const auto document = QJsonDocument::fromJson(
+ base::parse::stripComments(file.readAll()),
+ &error);
+ file.close();
+
+ if (error.error != QJsonParseError::NoError || !document.isObject()) {
+ return false;
+ }
+ const auto settings = document.object();
+
+ const auto version = settings.constFind(qsl("version"));
+ if (version == settings.constEnd() || (*version).toInt() != AppKotatoVersion) {
+ return false;
+ }
+
+ return true;
+}
+
+void WriteDefaultCustomFile() {
+ const auto path = CustomFilePath();
+ auto input = QFile(":/misc/default_kotato-settings-custom.json");
+ auto output = QFile(path);
+ if (input.open(QIODevice::ReadOnly) && output.open(QIODevice::WriteOnly)) {
+ output.write(input.readAll());
+ }
+}
+
+QByteArray GenerateSettingsJson(bool areDefault = false) {
+ auto settings = QJsonObject();
+
+ auto settingsFoldersLocal = QJsonObject();
+
+ const auto getRef = [&settings] (
+ QStringList &keyParts,
+ const Definition &def) -> QJsonValueRef {
+ const auto firstKey = keyParts.takeFirst();
+ if (!settings.contains(firstKey)) {
+ settings.insert(firstKey, QJsonObject());
+ }
+ auto resultRef = settings[firstKey];
+ for (const auto &key : keyParts) {
+ auto referenced = resultRef.toObject();
+ if (!referenced.contains(key)) {
+ referenced.insert(key, QJsonObject());
+ resultRef = referenced;
+ }
+ resultRef = referenced[key];
+ }
+ return resultRef;
+ };
+
+ const auto getValue = [=] (
+ const QString &key,
+ const Definition &def) -> QJsonValue {
+ auto value = (!areDefault)
+ ? GetWithPending(key)
+ : def.fillerValue.isValid()
+ ? def.fillerValue
+ : def.defaultValue.isValid()
+ ? def.defaultValue
+ : QVariant();
+ switch (def.type) {
+ case SettingType::BoolSetting:
+ return value.isValid() ? value.toBool() : false;
+ case SettingType::IntSetting:
+ return value.isValid() ? value.toInt() : 0;
+ case SettingType::QStringSetting:
+ return value.isValid() ? value.toString() : QString();
+ case SettingType::QJsonArraySetting:
+ return value.isValid() ? value.toJsonArray() : QJsonArray();
+ }
+
+ return QJsonValue();
+ };
+
+ const auto getAccountValue = [=] (const QString &key) -> QJsonValue {
+ if (areDefault) {
+ return QJsonObject();
+ }
+
+ auto values = GetAllWithPending(key);
+ auto resultObject = QJsonObject();
+
+ for (auto i = values.constBegin(); i != values.constEnd(); ++i) {
+ const auto value = i.value();
+ const auto jsonValue = (value.userType() == QMetaType::Bool)
+ ? QJsonValue(value.toBool())
+ : (value.userType() == QMetaType::Int)
+ ? QJsonValue(value.toInt())
+ : (value.userType() == QMetaType::QString)
+ ? QJsonValue(value.toString())
+ : (value.userType() == QMetaType::QJsonArray)
+ ? QJsonValue(value.toJsonArray())
+ : QJsonValue(QJsonValue::Null);
+ resultObject.insert(i.key(), jsonValue);
+ }
+
+ return resultObject;
+ };
+
+ for (const auto &[key, def] : DefinitionMap) {
+ if (def.storage == SettingStorage::None) {
+ continue;
+ }
+
+ auto parts = key.split(QChar('/'));
+ auto value = (def.scope == SettingScope::Account)
+ ? getAccountValue(key)
+ : getValue(key, def);
+ if (parts.size() > 1) {
+ const auto lastKey = parts.takeLast();
+ auto ref = getRef(parts, def);
+ auto referenced = ref.toObject();
+ referenced.insert(lastKey, value);
+ ref = referenced;
+ } else {
+ settings.insert(key, value);
+ }
+ }
+
+ if (areDefault) {
+ settings.insert(qsl("version"), QString::number(AppKotatoVersion));
+ }
+
+ auto document = QJsonDocument();
+ document.setObject(settings);
+ return document.toJson(QJsonDocument::Indented);
+}
+
+std::unique_ptr Data;
+
+QVariantMap GetAllWithPending(const QString &key) {
+ return (Data) ? Data->getAllWithPending(key) : QVariantMap();
+}
+
+} // namespace
+
+Manager::Manager()
+: _jsonWriteTimer([=] { writeTimeout(); }) {
+}
+
+void Manager::load() {
+ if (!DefaultFileIsValid()) {
+ writeDefaultFile();
+ }
+ if (!readCustomFile()) {
+ WriteDefaultCustomFile();
+ }
+}
+
+void Manager::fill() {
+ _settingsHashMap.reserve(DefinitionMap.size());
+ _defaultSettingsHashMap.reserve(DefinitionMap.size());
+
+ const auto addDefaultValue = [&] (const QString &option, QVariant value) {
+ _settingsHashMap.insert(option, value);
+ };
+
+ for (const auto &[key, def] : DefinitionMap) {
+ if (def.scope != SettingScope::Global) {
+ continue;
+ }
+
+ auto defaultValue = def.defaultValue;
+ if (!defaultValue.isValid()) {
+ if (def.type == SettingType::BoolSetting) {
+ defaultValue = false;
+ } else if (def.type == SettingType::IntSetting) {
+ defaultValue = 0;
+ } else if (def.type == SettingType::QStringSetting) {
+ defaultValue = QString();
+ } else if (def.type == SettingType::QJsonArraySetting) {
+ defaultValue = QJsonArray();
+ } else {
+ continue;
+ }
+ }
+
+ addDefaultValue(key, defaultValue);
+ }
+}
+
+void Manager::write(bool force) {
+ if (force && _jsonWriteTimer.isActive()) {
+ _jsonWriteTimer.cancel();
+ writeTimeout();
+ } else if (!force && !_jsonWriteTimer.isActive()) {
+ _jsonWriteTimer.callOnce(kWriteJsonTimeout);
+ }
+}
+
+QVariant Manager::get(const QString &key, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ auto result = _settingsHashMap.contains(mapKey)
+ ? _settingsHashMap.value(mapKey)
+ : QVariant();
+ if (!result.isValid()) {
+ result = _settingsHashMap.contains(key)
+ ? _settingsHashMap.value(key)
+ : getDefault(key);
+ _settingsHashMap.insert(mapKey, result);
+ }
+ return result;
+}
+
+QVariant Manager::getWithPending(const QString &key, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ auto result = _defaultSettingsHashMap.contains(mapKey)
+ ? _defaultSettingsHashMap.value(mapKey)
+ : _settingsHashMap.contains(mapKey)
+ ? _settingsHashMap.value(mapKey)
+ : QVariant();
+ if (!result.isValid()) {
+ result = _settingsHashMap.contains(key)
+ ? _settingsHashMap.value(key)
+ : getDefault(key);
+ _settingsHashMap.insert(mapKey, result);
+ }
+ return result;
+}
+
+QVariantMap Manager::getAllWithPending(const QString &key) {
+ auto resultMap = QVariantMap();
+
+ if (_defaultSettingsHashMap.contains(key) || _settingsHashMap.contains(key)) {
+ resultMap.insert(
+ qsl("0"),
+ _defaultSettingsHashMap.contains(key)
+ ? _defaultSettingsHashMap.value(key)
+ : _settingsHashMap.value(key));
+ return resultMap;
+ }
+
+ const auto prefix = key + qsl(":");
+
+ for (auto i = _settingsHashMap.constBegin(); i != _settingsHashMap.constEnd(); ++i) {
+ const auto mapKey = i.key();
+ if (!mapKey.startsWith(prefix)) {
+ continue;
+ }
+
+ const auto accountKey = mapKey.mid(prefix.size());
+ resultMap.insert(accountKey, i.value());
+ }
+
+ for (auto i = _defaultSettingsHashMap.constBegin(); i != _defaultSettingsHashMap.constEnd(); ++i) {
+ const auto mapKey = i.key();
+ if (!mapKey.startsWith(prefix)) {
+ continue;
+ }
+
+ const auto accountKey = mapKey.mid(prefix.size());
+ resultMap.insert(accountKey, i.value());
+ }
+
+ return resultMap;
+}
+
+QVariant Manager::getDefault(const QString &key) {
+ const auto &defIterator = DefinitionMap.find(key);
+ if (defIterator == DefinitionMap.end()) {
+ return QVariant();
+ }
+ const auto defaultValue = &defIterator->second.defaultValue;
+ const auto settingType = defIterator->second.type;
+ switch (settingType) {
+ case SettingType::QStringSetting:
+ return QVariant(defaultValue->isValid()
+ ? defaultValue->toString()
+ : QString());
+ case SettingType::IntSetting:
+ return QVariant(defaultValue->isValid()
+ ? defaultValue->toInt()
+ : 0);
+ case SettingType::BoolSetting:
+ return QVariant(defaultValue->isValid()
+ ? defaultValue->toBool()
+ : false);
+ case SettingType::QJsonArraySetting:
+ return QVariant(defaultValue->isValid()
+ ? defaultValue->toJsonArray()
+ : QJsonArray());
+ }
+
+ return QVariant();
+}
+
+rpl::producer Manager::events(const QString &key, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ return _eventStream.events() | rpl::filter(rpl::mappers::_1 == mapKey);
+}
+
+rpl::producer Manager::eventsWithPending(const QString &key, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ return _pendingEventStream.events() | rpl::filter(rpl::mappers::_1 == mapKey);
+}
+
+void Manager::set(const QString &key, QVariant value, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ _settingsHashMap.insert(mapKey, value);
+ _eventStream.fire_copy(mapKey);
+}
+
+void Manager::setAfterRestart(const QString &key, QVariant value, uint64 accountId, bool isTestAccount) {
+ const auto mapKey = MakeMapKey(key, accountId, isTestAccount);
+ if (!_settingsHashMap.contains(mapKey)
+ || _settingsHashMap.value(mapKey) != value) {
+ _defaultSettingsHashMap.insert(mapKey, value);
+ } else if (_settingsHashMap.contains(mapKey)
+ && _settingsHashMap.value(mapKey) == value) {
+ _defaultSettingsHashMap.remove(mapKey);
+ }
+ _pendingEventStream.fire_copy(mapKey);
+}
+
+void Manager::reset(const QString &key, uint64 accountId, bool isTestAccount) {
+ set(key, getDefault(key), accountId, isTestAccount);
+}
+
+void Manager::resetAfterRestart(const QString &key, uint64 accountId, bool isTestAccount) {
+ setAfterRestart(key, getDefault(key), accountId, isTestAccount);
+}
+
+bool Manager::readCustomFile() {
+ QFile file(CustomFilePath());
+ if (!file.exists()) {
+ return false;
+ }
+ if (!file.open(QIODevice::ReadOnly)) {
+ return true;
+ }
+ auto error = QJsonParseError{ 0, QJsonParseError::NoError };
+ const auto document = QJsonDocument::fromJson(
+ base::parse::stripComments(file.readAll()),
+ &error);
+ file.close();
+
+ if (error.error != QJsonParseError::NoError) {
+ return true;
+ } else if (!document.isObject()) {
+ return true;
+ }
+ const auto settings = document.object();
+
+ if (settings.isEmpty()) {
+ return true;
+ }
+
+ const auto getObjectValue = [&settings] (
+ QStringList &keyParts,
+ const Definition &def) -> QJsonValue {
+ const auto firstKey = keyParts.takeFirst();
+ if (!settings.contains(firstKey)) {
+ return QJsonValue();
+ }
+ auto resultRef = settings.value(firstKey);
+ for (const auto &key : keyParts) {
+ auto referenced = resultRef.toObject();
+ if (!referenced.contains(key)) {
+ return QJsonValue();
+ }
+ resultRef = referenced.value(key);
+ }
+ return resultRef;
+ };
+
+ const auto prepareAccountOptions = [] (
+ const QString &key,
+ const Definition &def,
+ const QJsonValue &val,
+ Fn callback) {
+
+ if (val.isUndefined()) {
+ return;
+ } else if (def.scope == SettingScope::Account && val.isObject()) {
+ const auto accounts = val.toObject();
+ if (accounts.isEmpty()) {
+ return;
+ }
+
+ for (auto i = accounts.constBegin(); i != accounts.constEnd(); ++i) {
+ auto optionKey = i.key();
+ auto isTestAccount = false;
+ if (optionKey.startsWith("test_")) {
+ isTestAccount = true;
+ optionKey = optionKey.mid(5);
+ }
+ auto accountId = optionKey.toULongLong();
+ callback(key, def, i.value(), accountId, (accountId == 0) ? false : isTestAccount);
+ }
+ } else {
+ callback(key, def, val, 0, false);
+ }
+ };
+
+ const auto setValue = [this] (
+ const QString &key,
+ const Definition &def,
+ const QJsonValue &val,
+ uint64 accountId,
+ bool isTestAccount) {
+
+ const auto defType = def.type;
+ if (defType == SettingType::BoolSetting) {
+ if (val.isBool()) {
+ set(key, val.toBool(), accountId, isTestAccount);
+ } else if (val.isDouble()) {
+ set(key, val.toDouble() != 0.0, accountId, isTestAccount);
+ }
+ } else if (defType == SettingType::IntSetting) {
+ if (val.isDouble()) {
+ auto intValue = qFloor(val.toDouble());
+ set(key,
+ (def.limitHandler)
+ ? def.limitHandler(intValue)
+ : intValue,
+ accountId,
+ isTestAccount);
+ }
+ } else if (defType == SettingType::QStringSetting) {
+ if (val.isString()) {
+ set(key, val.toString(), accountId, isTestAccount);
+ }
+ } else if (defType == SettingType::QJsonArraySetting) {
+ if (val.isArray()) {
+ auto arrayValue = val.toArray();
+ set(key, (def.limitHandler)
+ ? def.limitHandler(arrayValue)
+ : arrayValue,
+ accountId,
+ isTestAccount);
+ }
+ }
+ };
+
+ for (const auto &[oldkey, newkey] : ReplacedOptionsMap) {
+ const auto &defIterator = DefinitionMap.find(newkey);
+ if (defIterator == DefinitionMap.end()) {
+ continue;
+ }
+ auto parts = oldkey.split(QChar('/'));
+ const auto val = (parts.size() > 1)
+ ? getObjectValue(parts, defIterator->second)
+ : settings.value(oldkey);
+
+ if (!val.isUndefined()) {
+ prepareAccountOptions(newkey, defIterator->second, val, setValue);
+ }
+ }
+
+ for (const auto &[key, def] : DefinitionMap) {
+ if (def.storage == SettingStorage::None) {
+ continue;
+ }
+ auto parts = key.split(QChar('/'));
+ const auto val = (parts.size() > 1)
+ ? getObjectValue(parts, def)
+ : settings.value(key);
+
+ if (!val.isUndefined()) {
+ prepareAccountOptions(key, def, val, setValue);
+ }
+ }
+ return true;
+}
+
+void Manager::writeDefaultFile() {
+ auto file = QFile(DefaultFilePath());
+ if (!file.open(QIODevice::WriteOnly)) {
+ return;
+ }
+ const char *defaultHeader = R"HEADER(
+// This is a list of default options for Kotatogram Desktop
+// Please don't modify it, its content is not used in any way
+// You can place your own options in the 'kotato-settings-custom.json' file
+
+)HEADER";
+ file.write(defaultHeader);
+ file.write(GenerateSettingsJson(true));
+}
+
+void Manager::writeCurrentSettings() {
+ auto file = QFile(CustomFilePath());
+ if (!file.open(QIODevice::WriteOnly)) {
+ return;
+ }
+ if (_jsonWriteTimer.isActive()) {
+ writing();
+ }
+ const char *customHeader = R"HEADER(
+// This file was automatically generated from current settings
+// It's better to edit it with app closed, so there will be no rewrites
+// You should restart app to see changes
+
+)HEADER";
+ file.write(customHeader);
+ file.write(GenerateSettingsJson());
+}
+
+void Manager::writeTimeout() {
+ writeCurrentSettings();
+}
+
+void Manager::writing() {
+ _jsonWriteTimer.cancel();
+}
+
+void Start() {
+ if (Data) return;
+
+ Data = std::make_unique();
+ Data->fill();
+}
+
+void Load() {
+ if (!Data) return;
+
+ Data->load();
+}
+
+void Write() {
+ if (!Data) return;
+
+ Data->write();
+}
+
+void Finish() {
+ if (!Data) return;
+
+ Data->write(true);
+}
+
+QVariant Get(const QString &key, uint64 accountId, bool isTestAccount) {
+ return (Data) ? Data->get(key, accountId, isTestAccount) : QVariant();
+}
+
+QVariant GetWithPending(const QString &key, uint64 accountId, bool isTestAccount) {
+ return (Data) ? Data->getWithPending(key, accountId, isTestAccount) : QVariant();
+}
+
+rpl::producer Events(const QString &key, uint64 accountId, bool isTestAccount) {
+ return (Data) ? Data->events(key, accountId, isTestAccount) : rpl::single(QString());
+}
+
+rpl::producer EventsWithPending(const QString &key, uint64 accountId, bool isTestAccount) {
+ return (Data) ? Data->eventsWithPending(key, accountId, isTestAccount) : rpl::single(QString());
+}
+
+void Set(const QString &key, QVariant value, uint64 accountId, bool isTestAccount) {
+ if (!Data) return;
+
+ Data->set(key, value, accountId, isTestAccount);
+}
+
+void SetAfterRestart(const QString &key, QVariant value, uint64 accountId, bool isTestAccount) {
+ if (!Data) return;
+
+ Data->setAfterRestart(key, value, accountId, isTestAccount);
+}
+
+void Reset(const QString &key, uint64 accountId, bool isTestAccount) {
+ if (!Data) return;
+
+ Data->reset(key, accountId, isTestAccount);
+}
+
+void ResetAfterRestart(const QString &key, uint64 accountId, bool isTestAccount) {
+ if (!Data) return;
+
+ Data->resetAfterRestart(key, accountId, isTestAccount);
+}
+
+} // namespace JsonSettings
+} // namespace Kotato
diff --git a/Telegram/SourceFiles/kotato/kotato_settings.h b/Telegram/SourceFiles/kotato/kotato_settings.h
new file mode 100644
index 000000000..c5dacb3ea
--- /dev/null
+++ b/Telegram/SourceFiles/kotato/kotato_settings.h
@@ -0,0 +1,115 @@
+/*
+This file is part of Kotatogram Desktop,
+the unofficial app based on Telegram Desktop.
+
+For license and copyright information please follow this link:
+https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
+*/
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace Kotato {
+namespace JsonSettings {
+
+void Start();
+void Load();
+void Write();
+void Finish();
+
+[[nodiscard]] QVariant Get(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+[[nodiscard]] QVariant GetWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+[[nodiscard]] rpl::producer Events(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+[[nodiscard]] rpl::producer EventsWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+void Set(
+ const QString &key,
+ QVariant value,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+void SetAfterRestart(
+ const QString &key,
+ QVariant value,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+void Reset(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+void ResetAfterRestart(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false);
+
+inline bool GetBool(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return Get(key, accountId, isTestAccount).toBool();
+}
+
+inline int GetInt(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return Get(key, accountId, isTestAccount).toInt();
+}
+
+inline QString GetString(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return Get(key, accountId, isTestAccount).toString();
+}
+
+inline QJsonArray GetJsonArray(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return Get(key, accountId, isTestAccount).toJsonArray();
+}
+
+inline bool GetBoolWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return GetWithPending(key, accountId, isTestAccount).toBool();
+}
+
+inline int GetIntWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return GetWithPending(key, accountId, isTestAccount).toInt();
+}
+
+inline QString GetStringWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return GetWithPending(key, accountId, isTestAccount).toString();
+}
+
+inline QJsonArray GetJsonArrayWithPending(
+ const QString &key,
+ uint64 accountId = 0,
+ bool isTestAccount = false) {
+ return GetWithPending(key, accountId, isTestAccount).toJsonArray();
+}
+
+} // namespace JsonSettings
+} // namespace Kotato
diff --git a/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp
new file mode 100644
index 000000000..15f584a94
--- /dev/null
+++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp
@@ -0,0 +1,167 @@
+/*
+This file is part of Kotatogram Desktop,
+the unofficial app based on Telegram Desktop.
+
+For license and copyright information please follow this link:
+https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
+*/
+#include "kotato/kotato_settings_menu.h"
+
+#include "kotato/kotato_lang.h"
+#include "kotato/kotato_settings.h"
+#include "base/options.h"
+#include "base/platform/base_platform_info.h"
+#include "settings/settings_common.h"
+#include "settings/settings_chat.h"
+#include "ui/wrap/vertical_layout.h"
+#include "ui/wrap/slide_wrap.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/labels.h"
+#include "ui/widgets/checkbox.h"
+#include "ui/widgets/continuous_sliders.h"
+#include "ui/text/text_utilities.h" // Ui::Text::ToUpper
+#include "boxes/connection_box.h"
+#include "kotato/boxes/kotato_fonts_box.h"
+#include "kotato/boxes/kotato_radio_box.h"
+#include "boxes/about_box.h"
+#include "ui/boxes/confirm_box.h"
+#include "platform/platform_specific.h"
+#include "platform/platform_file_utilities.h"
+#include "window/window_peer_menu.h"
+#include "window/window_controller.h"
+#include "window/window_session_controller.h"
+#include "lang/lang_keys.h"
+#include "core/update_checker.h"
+#include "core/application.h"
+#include "storage/localstorage.h"
+#include "data/data_session.h"
+#include "data/data_cloud_themes.h"
+#include "main/main_session.h"
+#include "mainwindow.h"
+#include "styles/style_boxes.h"
+#include "styles/style_calls.h"
+#include "styles/style_settings.h"
+#include "ui/platform/ui_platform_utility.h"
+#include "ui/vertical_list.h"
+
+namespace Settings {
+
+namespace {
+
+
+} // namespace
+
+#define SettingsMenuJsonSwitch(LangKey, Option) container->add(object_ptr