diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0f78e6703..168770431 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -773,8 +773,14 @@ PRIVATE intro/intro_step.h intro/intro_widget.cpp intro/intro_widget.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/settings_kotato.png b/Telegram/Resources/icons/settings/settings_kotato.png new file mode 100644 index 000000000..5cbf718c2 Binary files /dev/null and b/Telegram/Resources/icons/settings/settings_kotato.png differ diff --git a/Telegram/Resources/icons/settings/settings_kotato@2x.png b/Telegram/Resources/icons/settings/settings_kotato@2x.png new file mode 100644 index 000000000..2e871fcf6 Binary files /dev/null and b/Telegram/Resources/icons/settings/settings_kotato@2x.png differ diff --git a/Telegram/Resources/icons/settings/settings_kotato@3x.png b/Telegram/Resources/icons/settings/settings_kotato@3x.png new file mode 100644 index 000000000..b187ccb2e Binary files /dev/null and b/Telegram/Resources/icons/settings/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 0df2274bf..e83d242c0 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -85,6 +85,7 @@ ../../default_shortcuts-custom.json + ../../default_kotato-settings-custom.json ../../../../lib/xdg/kotatogramdesktop.desktop diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index 1cd9d1d1c..59a31dece 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" @@ -316,6 +317,9 @@ void Launcher::init() { } 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) { @@ -327,6 +331,7 @@ int Launcher::exec() { // Must be started before Platform is started. Logs::start(this); base::options::init(cWorkingDir() + "tdata/experimental_options.json"); + Kotato::JsonSettings::Load(); if (Logs::DebugEnabled()) { const auto openalLogPath = QDir::toNativeSeparators( @@ -363,6 +368,7 @@ int Launcher::exec() { CrashReports::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 485455ed8..332dc4f10 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -386,6 +386,8 @@ bool ResolveSettings( ? ::Settings::Type::Folders : (section == qstr("devices")) ? ::Settings::Type::Sessions + : (section == qstr("kotato")) + ? ::Settings::Type::Kotato : ::Settings::Type::Main; controller->showSettings(type); return true; @@ -681,7 +683,7 @@ const std::vector &LocalUrlHandlers() { ResolvePrivatePost }, { - qsl("^settings(/folders|/devices|/language)?$"), + qsl("^settings(/folders|/devices|/language|/kotato)?$"), ResolveSettings }, { diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index b57ffd69a..f028a03a1 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include "kotato/kotato_lang.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" @@ -643,6 +644,8 @@ rpl::producer TitleValue( return tr::lng_settings_section_call_settings(); case Section::SettingsType::Experimental: return tr::lng_settings_experimental(); + case Section::SettingsType::Kotato: + return rktr("ktg_settings_kotato"); } Unexpected("Bad settings type in Info::TitleValue()"); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index bcb40f632..ac4f39024 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -393,7 +393,8 @@ void WrapWidget::createTopBar() { // addProfileNotificationsButton(); } else if (section.type() == Section::Type::Settings && (section.settingsType() == Section::SettingsType::Main - || section.settingsType() == Section::SettingsType::Chat)) { + || section.settingsType() == Section::SettingsType::Chat + || section.settingsType() == Section::SettingsType::Kotato)) { addTopBarMenuButton(); } else if (section.type() == Section::Type::Settings && section.settingsType() == Section::SettingsType::Information) { 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..0f1c77f7b --- /dev/null +++ b/Telegram/SourceFiles/kotato/boxes/kotato_radio_box.cpp @@ -0,0 +1,176 @@ +/* +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->value()); + if (_warnRestart) { + const auto box = std::make_shared>(); + + *box = getDelegate()->show( + Box( + tr::lng_settings_need_restart(tr::now), + tr::lng_settings_restart_now(tr::now), + tr::lng_settings_restart_later(tr::now), + [] { Core::Restart(); }, + [=] { closeBox(); })); + } 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..6c3ec157e --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -0,0 +1,771 @@ +/* +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 "facades.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..1b474eb24 --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp @@ -0,0 +1,154 @@ +/* +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_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 "facades.h" +#include "styles/style_settings.h" +#include "ui/platform/ui_platform_utility.h" + +namespace Settings { + +namespace { + + +} // namespace + +#define SettingsMenuJsonSwitch(LangKey, Option) AddButton( \ + container, \ + rktr(#LangKey), \ + st::settingsButton \ +)->toggleOn( \ + rpl::single(::Kotato::JsonSettings::GetBool(#Option)) \ +)->toggledValue( \ +) | rpl::filter([](bool enabled) { \ + return (enabled != ::Kotato::JsonSettings::GetBool(#Option)); \ +}) | rpl::start_with_next([](bool enabled) { \ + ::Kotato::JsonSettings::Set(#Option, enabled); \ + ::Kotato::JsonSettings::Write(); \ +}, container->lifetime()); + +void SetupKotatoChats( + not_null controller, + not_null container) { + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_chats")); + + + AddSkip(container); + AddDivider(container); + AddSkip(container); +} + +void SetupKotatoMessages(not_null container) { + AddSubsectionTitle(container, rktr("ktg_settings_messages")); + + AddSkip(container); +} + +void SetupKotatoForward(not_null container) { + AddDivider(container); + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_forward")); + + + AddSkip(container); + AddDividerText(container, rktr("ktg_settings_forward_chat_on_click_description")); +} + +void SetupKotatoNetwork(not_null container) { + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_network")); + + + AddSkip(container); +} + +void SetupKotatoFolders( + not_null controller, + not_null container) { + AddDivider(container); + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_filters")); + + + AddSkip(container); +} + +void SetupKotatoSystem( + not_null controller, + not_null container) { + AddDivider(container); + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_system")); + + + AddSkip(container); +} + +void SetupKotatoOther( + not_null controller, + not_null container) { + AddDivider(container); + AddSkip(container); + AddSubsectionTitle(container, rktr("ktg_settings_other")); + +} + +Kotato::Kotato( + QWidget *parent, + not_null controller) +: Section(parent) { + setupContent(controller); +} + +void Kotato::setupContent(not_null controller) { + const auto content = Ui::CreateChild(this); + + SetupKotatoChats(controller, content); + SetupKotatoMessages(content); + SetupKotatoForward(content); + SetupKotatoNetwork(content); + SetupKotatoFolders(controller, content); + SetupKotatoSystem(controller, content); + SetupKotatoOther(controller, content); + + Ui::ResizeFitChild(this, content); +} + +} // namespace Settings + diff --git a/Telegram/SourceFiles/kotato/kotato_settings_menu.h b/Telegram/SourceFiles/kotato/kotato_settings_menu.h new file mode 100644 index 000000000..66194d31d --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.h @@ -0,0 +1,39 @@ +/* +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 "settings/settings_common.h" + +class BoxContent; + +namespace Settings { + +void SetupKotatoChats(not_null container); +void SetupKotatoMessages(not_null container); +void SetupKotatoForward(not_null container); +void SetupKotatoNetwork(not_null container); +void SetupKotatoFolders( + not_null controller, + not_null container); +void SetupKotatoSystem( + not_null controller, + not_null container); +void SetupKotatoOther(not_null container); + +class Kotato : public Section { +public: + Kotato( + QWidget *parent, + not_null controller); + +private: + void setupContent(not_null controller); + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 69ecc186c..20ea09da3 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -63,6 +63,7 @@ settingsIconFaq: icon {{ "settings/settings_faq", menuIconFg }}; settingsIconStickers: icon {{ "settings/settings_stickers", menuIconFg }}; settingsIconEmoji: icon {{ "settings/settings_emoji", menuIconFg }}; settingsIconThemes: icon {{ "settings/settings_themes", menuIconFg }}; +settingsIconKotato: icon {{ "settings/settings_kotato", menuIconFg }}; settingsSetPhotoSkip: 7px; diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 0780809ad..41514655e 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_folders.h" #include "settings/settings_calls.h" #include "settings/settings_experimental.h" +#include "kotato/kotato_settings_menu.h" #include "core/application.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -62,6 +63,8 @@ object_ptr
CreateSection( return object_ptr(parent, controller); case Type::Experimental: return object_ptr(parent, controller); + case Type::Kotato: + return object_ptr(parent, controller); } Unexpected("Settings section type in Widget::createInnerWidget."); } @@ -223,16 +226,18 @@ void FillMenu( Core::App().domain().addActivated(MTP::Environment{}); }, &st::menuIconAddAccount); } - if (!controller->session().supportMode()) { + if (type != Type::Kotato && !controller->session().supportMode()) { addAction( tr::lng_settings_information(tr::now), [=] { showOther(Type::Information); }, &st::menuIconInfo); } - addAction( - tr::lng_settings_logout(tr::now), - [=] { window->showLogoutConfirmation(); }, - &st::menuIconLeave); + if (type != Type::Kotato) { + addAction( + tr::lng_settings_logout(tr::now), + [=] { window->showLogoutConfirmation(); }, + &st::menuIconLeave); + } } } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index c4520ad1b..11a009d78 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -42,6 +42,7 @@ enum class Type { Folders, Calls, Experimental, + Kotato, }; using Button = Ui::SettingsButton; diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 9d8b2d6ba..371b65f10 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "kotato/kotato_lang.h" #include "settings/settings_common.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" @@ -159,6 +160,10 @@ void SetupSections( tr::lng_settings_advanced(), Type::Advanced, &st::settingsIconGeneral); + addSection( + rktr("ktg_settings_kotato"), + Type::Kotato, + &st::settingsIconKotato); SetupLanguageButton(container);