diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c90dda662..cfd357321 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -845,8 +845,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/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 1b360d805..7833c71cc 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -46,6 +46,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 371f7d152..82ff511de 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" @@ -317,6 +318,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) { @@ -328,6 +332,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( @@ -364,6 +369,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 c1b5947d7..0706da71f 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_text_entities.h" @@ -472,6 +473,8 @@ bool ResolveSettings( return ::Settings::Chat::Id(); } else if (section == qstr("change_number")) { return ::Settings::ChangePhone::Id(); + } else if (section == qstr("kotato")) { + return ::Settings::Kotato::Id(); } return ::Settings::Main::Id(); }(); @@ -828,7 +831,7 @@ const std::vector &LocalUrlHandlers() { ResolvePrivatePost }, { - qsl("^settings(/language|/devices|/folders|/privacy|/themes|/change_number)?$"), + qsl("^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/kotato)?$"), ResolveSettings }, { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 7069f79a8..649c46c67 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/info_wrap_widget.h" +#include "kotato/kotato_settings_menu.h" #include "info/profile/info_profile_widget.h" #include "info/profile/info_profile_values.h" #include "info/media/info_media_widget.h" @@ -421,7 +422,8 @@ void WrapWidget::createTopBar() { && (section.settingsType() == ::Settings::CloudPasswordEmailConfirmId() || section.settingsType() == ::Settings::Main::Id() - || section.settingsType() == ::Settings::Chat::Id())) { + || section.settingsType() == ::Settings::Chat::Id() + || section.settingsType() == ::Settings::Kotato::Id())) { addTopBarMenuButton(); } else if (section.type() == Section::Type::Downloads) { auto &manager = Core::App().downloadManager(); 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..55f25ee27 --- /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->value()); + 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..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..7e2a37ee4 --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp @@ -0,0 +1,158 @@ +/* +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::settingsButtonNoIcon \ +)->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); +} + +rpl::producer Kotato::title() { + return rktr("ktg_settings_kotato"); +} + +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..74396408e --- /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" + +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); + + [[nodiscard]] rpl::producer title() override; + +private: + void setupContent(not_null controller); + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 9bf34ae08..72c8f5b42 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -68,6 +68,7 @@ settingsIconTips: icon {{ "settings/tips", settingsIconFg }}; settingsIconStickers: icon {{ "settings/stickers", settingsIconFg }}; settingsIconEmoji: icon {{ "settings/emoji", settingsIconFg }}; settingsIconThemes: icon {{ "settings/palette", settingsIconFg }}; +settingsIconKotato: icon {{ "settings/kotato", settingsIconFg }}; settingsIconGroup: icon {{ "settings/group", settingsIconFg }}; settingsIconChannel: icon {{ "settings/channel", settingsIconFg }}; settingsIconUser: icon {{ "settings/user", settingsIconFg }}; diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index ab540f48d..41bb32940 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -19,6 +19,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 "core/core_cloud_password.h" #include "ui/wrap/padding_wrap.h" @@ -332,18 +333,20 @@ void FillMenu( Core::App().domain().addActivated(MTP::Environment{}); }, &st::menuIconAddAccount); } - if (!controller->session().supportMode()) { + if (type != Kotato::Id() && !controller->session().supportMode()) { addAction( tr::lng_settings_information(tr::now), [=] { showOther(Information::Id()); }, &st::menuIconInfo); } - addAction({ - .text = tr::lng_settings_logout(tr::now), - .handler = [=] { window->showLogoutConfirmation(); }, - .icon = &st::menuIconLeaveAttention, - .isAttention = true, - }); + if (type != Kotato::Id()) { + addAction({ + .text = tr::lng_settings_logout(tr::now), + .handler = [=] { window->showLogoutConfirmation(); }, + .icon = &st::menuIconLeaveAttention, + .isAttention = true, + }); + } } } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 506127de2..e9d141f24 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "kotato/kotato_lang.h" +#include "kotato/kotato_settings_menu.h" #include "settings/settings_common.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" @@ -366,6 +368,10 @@ void SetupSections( tr::lng_settings_section_call_settings(), Calls::Id(), { &st::settingsIconCalls, kIconGreen }); + addSection( + rktr("ktg_settings_kotato"), + Kotato::Id(), + { &st::settingsIconKotato, kIconPurple }); SetupLanguageButton(container);