From e951c8064b7fb0f8cb48b5cf24027f46e799c07c Mon Sep 17 00:00:00 2001 From: RadRussianRus Date: Wed, 17 Aug 2022 20:13:21 +0300 Subject: [PATCH] [Core] Settings system --- Telegram/CMakeLists.txt | 6 + .../default_kotato-settings-custom.json | 20 + Telegram/Resources/icons/settings/kotato.png | Bin 0 -> 721 bytes .../Resources/icons/settings/kotato@2x.png | Bin 0 -> 1094 bytes .../Resources/icons/settings/kotato@3x.png | Bin 0 -> 1153 bytes Telegram/Resources/langs/rewrites/en.json | 8 + Telegram/Resources/qrc/telegram/telegram.qrc | 3 +- Telegram/SourceFiles/core/launcher.cpp | 6 + .../SourceFiles/core/local_url_handlers.cpp | 5 +- .../kotato/boxes/kotato_radio_box.cpp | 177 ++++ .../kotato/boxes/kotato_radio_box.h | 80 ++ .../SourceFiles/kotato/kotato_settings.cpp | 770 ++++++++++++++++++ Telegram/SourceFiles/kotato/kotato_settings.h | 115 +++ .../kotato/kotato_settings_menu.cpp | 167 ++++ .../SourceFiles/kotato/kotato_settings_menu.h | 44 + Telegram/SourceFiles/settings/settings.style | 6 + .../settings/settings_common_session.cpp | 4 +- .../SourceFiles/settings/settings_main.cpp | 7 + 18 files changed, 1415 insertions(+), 3 deletions(-) create mode 100644 Telegram/Resources/default_kotato-settings-custom.json create mode 100644 Telegram/Resources/icons/settings/kotato.png create mode 100644 Telegram/Resources/icons/settings/kotato@2x.png create mode 100644 Telegram/Resources/icons/settings/kotato@3x.png create mode 100644 Telegram/SourceFiles/kotato/boxes/kotato_radio_box.cpp create mode 100644 Telegram/SourceFiles/kotato/boxes/kotato_radio_box.h create mode 100644 Telegram/SourceFiles/kotato/kotato_settings.cpp create mode 100644 Telegram/SourceFiles/kotato/kotato_settings.h create mode 100644 Telegram/SourceFiles/kotato/kotato_settings_menu.cpp create mode 100644 Telegram/SourceFiles/kotato/kotato_settings_menu.h 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 0000000000000000000000000000000000000000..23c74dbecdfc2872c43115ba7ecec1ecd9933624 GIT binary patch literal 721 zcmV;?0xtcDP)yV{0G_!whDrv2qISg0AG!S^^Oq|B$;LQew>+enK=XYU{=eSZ9d55 zi$*x$_bf+OJkD1-anp~Bv6yD+bD>a>mgDOlo2qvcs#oiMO-FYkX=-5W5T4d_qbNKp z9NZ}Cig$$jv`j1^d?Y+>$N`0~l^(D0i{f;RKNK60@I2U6Pfxmm;|;83jGXYOus4@2 zX@BQYH*qENON&wmWe#ByCV(FgaZ0R{rO1TTykyqg=lk_7P!NwMf1NUhIMQUqJ1%NT zx+bNjL{G|>rL1!QpQo9cnyhtq`Jw&UvfhWmxeL{bWqlu8R`moNPjH#9`wNoO%%|v; zy5>Cu_ZFOA)wTE@Tx`R@qaHJ2zmh|yJ(YsDGw6vz?=5t#*0TCr=kx(0oAN|Auzvs} zvfsV-1Lfss{-xxUjQ{`u32;bRa{vGf6951U69E94oEQKA0RBltK~y-6?T)dkMqnJq z-{X`d86+93tYUi&l7%}kxe1d?F(`{0P-3?kP)6K=jRA>shEu2aoUilg)HnK#|KIaG z{Q+aI48vT;u*-y^D7J0C;G-Z2vMjrdz5gW$f>2e}aU9?GB}u|@d|8%hnzn8G z#}}^aj^p_8z&y{M=RIp`n#OS)06-9gD2g5@P|`G&BnbdO)3jk2KZZz>EX%U%x_#f# z&T$-Z9EV}Jt}A*!&vO`tzVCUS|NRupvPDs#>0uajUH`JOELT;9+O};)QT(pIp<5V+ zS(XJMJkPVP>pai*eMhZnnh-*QAeyF0k_5FkH?QCmwz|6FLBE=%00000NkvXXu0mjf D^9)CD literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/kotato@2x.png b/Telegram/Resources/icons/settings/kotato@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..04b3997dc8727ae0bf2107b4f49e38878ab465ba GIT binary patch literal 1094 zcmV-M1iAZ(P)yV{0G_!whDrv2qISg0AG!S^^Oq|B$;LQew>+enK=XYU{=eSZ9d55 zi$*x$_bf+OJkD1-anp~Bv6yD+bD>a>mgDOlo2qvcs#oiMO-FYkX=-5W5T4d_qbNKp z9NZ}Cig$$jv`j1^d?Y+>$N`0~l^(D0i{f;RKNK60@I2U6Pfxmm;|;83jGXYOus4@2 zX@BQYH*qENON&wmWe#ByCV(FgaZ0R{rO1TTykyqg=lk_7P!NwMf1NUhIMQUqJ1%NT zx+bNjL{G|>rL1!QpQo9cnyhtq`Jw&UvfhWmxeL{bWqlu8R`moNPjH#9`wNoO%%|v; zy5>Cu_ZFOA)wTE@Tx`R@qaHJ2zmh|yJ(YsDGw6vz?=5t#*0TCr=kx(0oAN|Auzvs} zvfsV-1Lfss{-xxUjQ{`u32;bRa{vGf6951U69E94oEQKA0&_`3K~z|U?UX%e>QESl zPj9QlXs}S|pBZdK9bA-v;80u&)=9MBq&O7OL2z|&5nLRcTe_4kf|I3CI+RX2bX37X zR0@h#8zG=X6NTgsa!bLh(Q7UpdY&=K`+mfL{_E>&rBazpCY$&zV`pc_VzE3uJ+Uk+tN_pR#bVLra-k^tUxY3%FYERCDk@*0 zC@Psu{^Ljt!?arM{BwiBkWQ!9Rr$)}3sA4{=QnRUMQr|Xl`$B-EQ~iChzU- z1p9pBwHX4obc)ZHTg+d{h%T=pYmSz2Zzxa;}hr={YueffGG8786TCEj> zJkQ_U+-z@eiwgk&E-o&*-L7ck{rx>jl3#%feIAdeR4OfM zq$nztN*x>=%m-B}RWKN=)oP2folYkjjUov0*SX6sMg&0ufq>KLBnSdU(b>vctyVst zzq`BZ^?FOPN~O~4^*S65olb{gm`o<)IIh#_9?Ncm1Y5zzB48of z_$vGd+6uM`f}jW@R{j8AjfC}%5fUVsW%hoYnRA&r1NLB6%b9IH$mNShIN`16>mHk`cN3~t>wQf}cOq$OVCxW`)^wvNJS!aBDC&xLg!{Bi zEFpX(JZ{JVg|C$!uknlGbd5h08jtYwUx@Tjmimn~_3=TSFtCG$&* zQU_%YVG<^Q9}jU#tdph4gw(ub*4*d&^({~kk0yVeGKM(PWW+lzYDu~#rKUtr%9o|A za{r&FnVOocb$9up{n@hKhrziE)rw_(A6r)S1RPIrnXmf`lGDtm=#{$WJp}g_oL|+o z_#Rwr!@#2+Gh)AzL#92Ig10m1i9+u!bgkC1`djDp0V137L^rU103)*Bz4im;1nw6wJU|NjUG2m%5E^YimXMMX?Z zOs%c0%gf6d85v1QNvo@?_4V~yT3S3jJZo!f2?+@Y2M0<@N`-}mtgNiPy}i-V(ZImK zl$4ZUU|=5~A8u}L;o;%^{r#Ajm>?h^EG#Vf`T1U6URG9Cii(QI$HyinCVhQ!qcoBqStvcX#yk^pB5^6ciMB zd3oO6-sk7%G&D4Hbac+n&i3~9ot>RBGBN}N1c!%*XlQ6VJ39;v3@a-uo}QkUmzR5c zdrnSHY4|=z00009a7bBm000XU000XU0RWnu7ytkPM@d9MR9M69l~+^3Ko~`_g)68* z0TmQuLG0KoDA)jd$KHGI{(j9UI0>5&WPSV2OEPosp4r{(Y)mGCAP9o^PjYfOiSqI} zi3$WxA~OoPcq~>FaS;^*C0s-z02>ES>6Zs(*?VkuSp-M69;bkB$}9e=sC2ojsy#>| z)R?8(y39TG4PfAH^kt@L5}`qJOD3LH=pY&Qv_Y5l4xO?7&g5&C2kGdxB+9-Xm8che z{ql3bH3$O?4Ufp}KoFzqDUAW4uzP$02AQ0ijzsMcQx_N08Zt8*nbXSV7Z}^L$gEnI zH#fI_!|fe)x~qK*_VyLcgF{7nbgW2E^e*7j zv-3pnLb^=3Qm(EO>CNq3np1u642EOzhsUR9X8hvtORsP59|8K^69hpJgaJMQ?9wgx Tx{viD00000NkvXXu0mjfRJId< literal 0 HcmV?d00001 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