2
0
mirror of https://github.com/kotatogram/kotatogram-desktop synced 2025-08-30 14:17:45 +00:00

[Core] Settings system

This commit is contained in:
RadRussianRus 2022-08-17 20:13:21 +03:00 committed by Eric Kotato
parent f84850d890
commit e951c8064b
18 changed files with 1415 additions and 3 deletions

View File

@ -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

View File

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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": ""
}

View File

@ -58,7 +58,8 @@
</qresource>
<qresource prefix="/misc">
<file alias="default_shortcuts-custom.json">../../default_shortcuts-custom.json</file>
<file alias="io.github.kotatogram.desktop">../../../../lib/xdg/io.github.kotatogram.desktop</file>
<file alias="default_kotato-settings-custom.json">../../default_kotato-settings-custom.json</file>
<file alias="io.github.kotatogram.desktop">../../../../lib/xdg/io.github.kotatogram.desktop</file>
</qresource>
<qresource prefix="/ktg_lang">
<file alias="en.json">../../langs/rewrites/en.json</file>

View File

@ -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;

View File

@ -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<LocalUrlHandler> &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
},
{

View File

@ -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<QString(int)> labelGetter,
Fn<void(int)> 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<QString(int)> labelGetter,
Fn<void(int)> 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<QString(int)> labelGetter,
Fn<QString(int)> descriptionGetter,
Fn<void(int)> 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<QString(int)> labelGetter,
Fn<QString(int)> descriptionGetter,
Fn<void(int)> 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<Ui::FlatLabel>(_content, _description, st::boxDividerLabel),
style::margins(
st::boxPadding.left(),
0,
st::boxPadding.right(),
st::boxPadding.bottom()));
}
_group = std::make_shared<Ui::RadiobuttonGroup>(_startValue);
for (auto i = 0; i != _valueCount; ++i) {
const auto description = _descriptionGetter
? _descriptionGetter(i)
: QString();
_content->add(
object_ptr<Ui::Radiobutton>(
_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<Ui::FlatLabel>(_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<Ui::OverrideMargins>(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<QPointer<BoxContent>>();
*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

View File

@ -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<QString(int)> labelGetter,
Fn<void(int)> saveCallback,
bool warnRestart = false);
RadioBox(
QWidget* parent,
const QString &title,
const QString &description,
int currentValue,
int valueCount,
Fn<QString(int)> labelGetter,
Fn<void(int)> saveCallback,
bool warnRestart = false);
RadioBox(
QWidget* parent,
const QString &title,
int currentValue,
int valueCount,
Fn<QString(int)> labelGetter,
Fn<QString(int)> descriptionGetter,
Fn<void(int)> saveCallback,
bool warnRestart = false);
RadioBox(
QWidget* parent,
const QString &title,
const QString &description,
int currentValue,
int valueCount,
Fn<QString(int)> labelGetter,
Fn<QString(int)> descriptionGetter,
Fn<void(int)> saveCallback,
bool warnRestart = false);
protected:
void prepare() override;
private:
void save();
QString _title;
QString _description;
int _startValue;
int _valueCount;
Fn<QString(int)> _labelGetter;
Fn<QString(int)> _descriptionGetter;
Fn<void(int)> _saveCallback;
bool _warnRestart = false;
std::shared_ptr<Ui::RadiobuttonGroup> _group;
object_ptr<Ui::VerticalLayout> _owned;
not_null<Ui::VerticalLayout*> _content;
};
} // namespace Kotato

View File

@ -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 <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QTimer>
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<QString> events(
const QString &key,
uint64 accountId = 0,
bool isTestAccount = false);
[[nodiscard]] rpl::producer<QString> 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<QString> _eventStream;
rpl::event_stream<QString> _pendingEventStream;
QHash<QString, QVariant> _settingsHashMap;
QHash<QString, QVariant> _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<QVariant(QVariant)>;
CheckHandler IntLimit(int min, int max, int defaultValue) {
return [=] (QVariant value) -> QVariant {
if (value.canConvert<int>()) {
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<int>()) {
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<QString, Definition, std::greater<QString>> DefinitionMap {
// Non-stored settings
// Stored settings
};
using OldOptionKey = QString;
using NewOptionKey = QString;
const std::map<OldOptionKey, NewOptionKey, std::greater<OldOptionKey>> 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<Manager> 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<QString> 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<QString> 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<void(const QString &,
const Definition &,
const QJsonValue &,
uint64,
bool)> 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<Manager>();
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<QString> Events(const QString &key, uint64 accountId, bool isTestAccount) {
return (Data) ? Data->events(key, accountId, isTestAccount) : rpl::single(QString());
}
rpl::producer<QString> 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

View File

@ -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 <rpl/producer.h>
#include <QtCore/QVariant>
#include <QtCore/QJsonArray>
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<QString> Events(
const QString &key,
uint64 accountId = 0,
bool isTestAccount = false);
[[nodiscard]] rpl::producer<QString> 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

View File

@ -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<Button>( \
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<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container) {
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_chats"));
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
}
void SetupKotatoMessages(not_null<Ui::VerticalLayout*> container) {
Ui::AddSubsectionTitle(container, rktr("ktg_settings_messages"));
Ui::AddSkip(container);
}
void SetupKotatoForward(not_null<Ui::VerticalLayout*> container) {
Ui::AddDivider(container);
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_forward"));
Ui::AddSkip(container);
Ui::AddDividerText(container, rktr("ktg_settings_forward_chat_on_click_description"));
}
void SetupKotatoNetwork(not_null<Ui::VerticalLayout*> container) {
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_network"));
Ui::AddSkip(container);
}
void SetupKotatoFolders(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container) {
Ui::AddDivider(container);
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_filters"));
Ui::AddSkip(container);
}
void SetupKotatoSystem(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container) {
Ui::AddDivider(container);
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_system"));
Ui::AddSkip(container);
}
void SetupKotatoOther(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container) {
Ui::AddDivider(container);
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, rktr("ktg_settings_other"));
Ui::AddSkip(container);
}
Kotato::Kotato(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent)
, _controller(controller) {
setupContent(controller);
}
rpl::producer<QString> Kotato::title() {
return rktr("ktg_settings_kotato");
}
void Kotato::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
const auto window = &_controller->window();
}
void Kotato::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(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

View File

@ -0,0 +1,44 @@
/*
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_session.h"
namespace Settings {
void SetupKotatoChats(not_null<Ui::VerticalLayout*> container);
void SetupKotatoMessages(not_null<Ui::VerticalLayout*> container);
void SetupKotatoForward(not_null<Ui::VerticalLayout*> container);
void SetupKotatoNetwork(not_null<Ui::VerticalLayout*> container);
void SetupKotatoFolders(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container);
void SetupKotatoSystem(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container);
void SetupKotatoOther(not_null<Ui::VerticalLayout*> container);
class Kotato : public Section<Kotato> {
public:
Kotato(
QWidget *parent,
not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
void fillTopBarMenu(
const Ui::Menu::MenuCallback &addAction) override;
private:
void setupContent(not_null<Window::SessionController*> controller);
const not_null<Window::SessionController*> _controller;
};
} // namespace Settings

View File

@ -78,6 +78,7 @@ settingsIconChat: icon {{ "settings/chat", settingsIconFg }};
settingsIconInterfaceScale: icon {{ "settings/interface_scale", settingsIconFg }};
settingsIconStickers: icon {{ "settings/stickers", settingsIconFg }};
settingsIconEmoji: icon {{ "settings/emoji", settingsIconFg }};
settingsIconKotato: icon {{ "settings/kotato", settingsIconFg }};
settingsPremiumIconWallpapers: icon {{ "settings/photo", settingsIconFg }};
settingsPremiumIconStories: icon {{ "settings/stories", settingsIconFg }};
@ -679,3 +680,8 @@ settingsChatLinkField: InputField(defaultInputField) {
font: normalFont;
}
ktgSettingsSliderLabel: LabelSimple(defaultLabelSimple) {
textFg: windowFg;
font: boxTextFont;
}

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/settings_common_session.h"
#include "kotato/kotato_settings_menu.h"
#include "api/api_cloud_password.h"
#include "apiwrap.h"
#include "core/application.h"
@ -37,7 +38,8 @@ namespace Settings {
bool HasMenu(Type type) {
return (type == ::Settings::CloudPasswordEmailConfirmId())
|| (type == Main::Id())
|| (type == Chat::Id());
|| (type == Chat::Id())
|| (type == Kotato::Id());
}
} // namespace Settings

View File

@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/settings_main.h"
#include "kotato/kotato_lang.h"
#include "kotato/kotato_settings.h"
#include "kotato/kotato_settings_menu.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "settings/settings_business.h"
@ -388,6 +391,10 @@ void SetupSections(
tr::lng_settings_section_devices(),
Calls::Id(),
{ &st::menuIconUnmute });
addSection(
rktr("ktg_settings_kotato"),
Kotato::Id(),
{ &st::settingsIconKotato });
SetupPowerSavingButton(&controller->window(), container);
SetupLanguageButton(&controller->window(), container);