# HG changeset patch # User Paper # Date 1699034762 14400 # Node ID c537996cf67b0dbadcf14cb2cfc45016be4aa94b # Parent f5940a575d83614759d3dabae034bf348217d980 *: multitude of config changes 1. theme is now configurable from the settings menu (but you have to restart for it to apply) 2. config is now stored in an INI file, with no method of conversion from json (this repo is private-ish anyway) diff -r f5940a575d83 -r c537996cf67b CMakeLists.txt --- a/CMakeLists.txt Fri Nov 03 09:43:04 2023 -0400 +++ b/CMakeLists.txt Fri Nov 03 14:06:02 2023 -0400 @@ -122,7 +122,7 @@ set_property(TARGET minori PROPERTY AUTOMOC ON) set_property(TARGET minori PROPERTY AUTORCC ON) -target_include_directories(minori PUBLIC ${CURL_INCLUDE_DIRS} PRIVATE include dep/pugixml/src dep/animia/include dep/anitomy) +target_include_directories(minori PUBLIC ${CURL_INCLUDE_DIRS} PRIVATE include dep/pugixml/src dep/animia/include dep/anitomy dep/mini) if(USE_QT6) target_include_directories(minori PUBLIC ${Qt6Widgets_INCLUDE_DIRS}) else() diff -r f5940a575d83 -r c537996cf67b dep/mini/ini.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dep/mini/ini.h Fri Nov 03 14:06:02 2023 -0400 @@ -0,0 +1,789 @@ +/* + * The MIT License (MIT) + * Copyright (c) 2018 Danijel Durakovic + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +/////////////////////////////////////////////////////////////////////////////// +// +// /mINI/ v0.9.14 +// An INI file reader and writer for the modern age. +// +/////////////////////////////////////////////////////////////////////////////// +// +// A tiny utility library for manipulating INI files with a straightforward +// API and a minimal footprint. It conforms to the (somewhat) standard INI +// format - sections and keys are case insensitive and all leading and +// trailing whitespace is ignored. Comments are lines that begin with a +// semicolon. Trailing comments are allowed on section lines. +// +// Files are read on demand, upon which data is kept in memory and the file +// is closed. This utility supports lazy writing, which only writes changes +// and updates to a file and preserves custom formatting and comments. A lazy +// write invoked by a write() call will read the output file, find what +// changes have been made and update the file accordingly. If you only need to +// generate files, use generate() instead. Section and key order is preserved +// on read, write and insert. +// +/////////////////////////////////////////////////////////////////////////////// +// +// /* BASIC USAGE EXAMPLE: */ +// +// /* read from file */ +// mINI::INIFile file("myfile.ini"); +// mINI::INIStructure ini; +// file.read(ini); +// +// /* read value; gets a reference to actual value in the structure. +// if key or section don't exist, a new empty value will be created */ +// std::string& value = ini["section"]["key"]; +// +// /* read value safely; gets a copy of value in the structure. +// does not alter the structure */ +// std::string value = ini.get("section").get("key"); +// +// /* set or update values */ +// ini["section"]["key"] = "value"; +// +// /* set multiple values */ +// ini["section2"].set({ +// {"key1", "value1"}, +// {"key2", "value2"} +// }); +// +// /* write updates back to file, preserving comments and formatting */ +// file.write(ini); +// +// /* or generate a file (overwrites the original) */ +// file.generate(ini); +// +/////////////////////////////////////////////////////////////////////////////// +// +// Long live the INI file!!! +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef MINI_INI_H_ +#define MINI_INI_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mINI +{ + namespace INIStringUtil + { + const char* const whitespaceDelimiters = " \t\n\r\f\v"; + inline void trim(std::string& str) + { + str.erase(str.find_last_not_of(whitespaceDelimiters) + 1); + str.erase(0, str.find_first_not_of(whitespaceDelimiters)); + } +#ifndef MINI_CASE_SENSITIVE + inline void toLower(std::string& str) + { + std::transform(str.begin(), str.end(), str.begin(), [](const char c) { + return static_cast(std::tolower(c)); + }); + } +#endif + inline void replace(std::string& str, std::string const& a, std::string const& b) + { + if (!a.empty()) + { + std::size_t pos = 0; + while ((pos = str.find(a, pos)) != std::string::npos) + { + str.replace(pos, a.size(), b); + pos += b.size(); + } + } + } +#ifdef _WIN32 + const char* const endl = "\r\n"; +#else + const char* const endl = "\n"; +#endif + } + + template + class INIMap + { + private: + using T_DataIndexMap = std::unordered_map; + using T_DataItem = std::pair; + using T_DataContainer = std::vector; + using T_MultiArgs = typename std::vector>; + + T_DataIndexMap dataIndexMap; + T_DataContainer data; + + inline std::size_t setEmpty(std::string& key) + { + std::size_t index = data.size(); + dataIndexMap[key] = index; + data.emplace_back(key, T()); + return index; + } + + public: + using const_iterator = typename T_DataContainer::const_iterator; + + INIMap() { } + + INIMap(INIMap const& other) + { + std::size_t data_size = other.data.size(); + for (std::size_t i = 0; i < data_size; ++i) + { + auto const& key = other.data[i].first; + auto const& obj = other.data[i].second; + data.emplace_back(key, obj); + } + dataIndexMap = T_DataIndexMap(other.dataIndexMap); + } + + T& operator[](std::string key) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + bool hasIt = (it != dataIndexMap.end()); + std::size_t index = (hasIt) ? it->second : setEmpty(key); + return data[index].second; + } + T get(std::string key) const + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it == dataIndexMap.end()) + { + return T(); + } + return T(data[it->second].second); + } + bool has(std::string key) const + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + return (dataIndexMap.count(key) == 1); + } + void set(std::string key, T obj) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it != dataIndexMap.end()) + { + data[it->second].second = obj; + } + else + { + dataIndexMap[key] = data.size(); + data.emplace_back(key, obj); + } + } + void set(T_MultiArgs const& multiArgs) + { + for (auto const& it : multiArgs) + { + auto const& key = it.first; + auto const& obj = it.second; + set(key, obj); + } + } + bool remove(std::string key) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it != dataIndexMap.end()) + { + std::size_t index = it->second; + data.erase(data.begin() + index); + dataIndexMap.erase(it); + for (auto& it2 : dataIndexMap) + { + auto& vi = it2.second; + if (vi > index) + { + vi--; + } + } + return true; + } + return false; + } + void clear() + { + data.clear(); + dataIndexMap.clear(); + } + std::size_t size() const + { + return data.size(); + } + const_iterator begin() const { return data.begin(); } + const_iterator end() const { return data.end(); } + }; + + using INIStructure = INIMap>; + + namespace INIParser + { + using T_ParseValues = std::pair; + + enum class PDataType : char + { + PDATA_NONE, + PDATA_COMMENT, + PDATA_SECTION, + PDATA_KEYVALUE, + PDATA_UNKNOWN + }; + + inline PDataType parseLine(std::string line, T_ParseValues& parseData) + { + parseData.first.clear(); + parseData.second.clear(); + INIStringUtil::trim(line); + if (line.empty()) + { + return PDataType::PDATA_NONE; + } + char firstCharacter = line[0]; + if (firstCharacter == ';') + { + return PDataType::PDATA_COMMENT; + } + if (firstCharacter == '[') + { + auto commentAt = line.find_first_of(';'); + if (commentAt != std::string::npos) + { + line = line.substr(0, commentAt); + } + auto closingBracketAt = line.find_last_of(']'); + if (closingBracketAt != std::string::npos) + { + auto section = line.substr(1, closingBracketAt - 1); + INIStringUtil::trim(section); + parseData.first = section; + return PDataType::PDATA_SECTION; + } + } + auto lineNorm = line; + INIStringUtil::replace(lineNorm, "\\=", " "); + auto equalsAt = lineNorm.find_first_of('='); + if (equalsAt != std::string::npos) + { + auto key = line.substr(0, equalsAt); + INIStringUtil::trim(key); + INIStringUtil::replace(key, "\\=", "="); + auto value = line.substr(equalsAt + 1); + INIStringUtil::trim(value); + parseData.first = key; + parseData.second = value; + return PDataType::PDATA_KEYVALUE; + } + return PDataType::PDATA_UNKNOWN; + } + } + + class INIReader + { + public: + using T_LineData = std::vector; + using T_LineDataPtr = std::shared_ptr; + + bool isBOM = false; + + private: + std::ifstream fileReadStream; + T_LineDataPtr lineData; + + T_LineData readFile() + { + fileReadStream.seekg(0, std::ios::end); + const std::size_t fileSize = static_cast(fileReadStream.tellg()); + fileReadStream.seekg(0, std::ios::beg); + if (fileSize >= 3) { + const char header[3] = { + static_cast(fileReadStream.get()), + static_cast(fileReadStream.get()), + static_cast(fileReadStream.get()) + }; + isBOM = ( + header[0] == static_cast(0xEF) && + header[1] == static_cast(0xBB) && + header[2] == static_cast(0xBF) + ); + } + else { + isBOM = false; + } + std::string fileContents; + fileContents.resize(fileSize); + fileReadStream.seekg(isBOM ? 3 : 0, std::ios::beg); + fileReadStream.read(&fileContents[0], fileSize); + fileReadStream.close(); + T_LineData output; + if (fileSize == 0) + { + return output; + } + std::string buffer; + buffer.reserve(50); + for (std::size_t i = 0; i < fileSize; ++i) + { + char& c = fileContents[i]; + if (c == '\n') + { + output.emplace_back(buffer); + buffer.clear(); + continue; + } + if (c != '\0' && c != '\r') + { + buffer += c; + } + } + output.emplace_back(buffer); + return output; + } + + public: + INIReader(std::string const& filename, bool keepLineData = false) + { + fileReadStream.open(filename, std::ios::in | std::ios::binary); + if (keepLineData) + { + lineData = std::make_shared(); + } + } + ~INIReader() { } + + bool operator>>(INIStructure& data) + { + if (!fileReadStream.is_open()) + { + return false; + } + T_LineData fileLines = readFile(); + std::string section; + bool inSection = false; + INIParser::T_ParseValues parseData; + for (auto const& line : fileLines) + { + auto parseResult = INIParser::parseLine(line, parseData); + if (parseResult == INIParser::PDataType::PDATA_SECTION) + { + inSection = true; + data[section = parseData.first]; + } + else if (inSection && parseResult == INIParser::PDataType::PDATA_KEYVALUE) + { + auto const& key = parseData.first; + auto const& value = parseData.second; + data[section][key] = value; + } + if (lineData && parseResult != INIParser::PDataType::PDATA_UNKNOWN) + { + if (parseResult == INIParser::PDataType::PDATA_KEYVALUE && !inSection) + { + continue; + } + lineData->emplace_back(line); + } + } + return true; + } + T_LineDataPtr getLines() + { + return lineData; + } + }; + + class INIGenerator + { + private: + std::ofstream fileWriteStream; + + public: + bool prettyPrint = false; + + INIGenerator(std::string const& filename) + { + fileWriteStream.open(filename, std::ios::out | std::ios::binary); + } + ~INIGenerator() { } + + bool operator<<(INIStructure const& data) + { + if (!fileWriteStream.is_open()) + { + return false; + } + if (!data.size()) + { + return true; + } + auto it = data.begin(); + for (;;) + { + auto const& section = it->first; + auto const& collection = it->second; + fileWriteStream + << "[" + << section + << "]"; + if (collection.size()) + { + fileWriteStream << INIStringUtil::endl; + auto it2 = collection.begin(); + for (;;) + { + auto key = it2->first; + INIStringUtil::replace(key, "=", "\\="); + auto value = it2->second; + INIStringUtil::trim(value); + fileWriteStream + << key + << ((prettyPrint) ? " = " : "=") + << value; + if (++it2 == collection.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + } + } + if (++it == data.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + if (prettyPrint) + { + fileWriteStream << INIStringUtil::endl; + } + } + return true; + } + }; + + class INIWriter + { + private: + using T_LineData = std::vector; + using T_LineDataPtr = std::shared_ptr; + + std::string filename; + + T_LineData getLazyOutput(T_LineDataPtr const& lineData, INIStructure& data, INIStructure& original) + { + T_LineData output; + INIParser::T_ParseValues parseData; + std::string sectionCurrent; + bool parsingSection = false; + bool continueToNextSection = false; + bool discardNextEmpty = false; + bool writeNewKeys = false; + std::size_t lastKeyLine = 0; + for (auto line = lineData->begin(); line != lineData->end(); ++line) + { + if (!writeNewKeys) + { + auto parseResult = INIParser::parseLine(*line, parseData); + if (parseResult == INIParser::PDataType::PDATA_SECTION) + { + if (parsingSection) + { + writeNewKeys = true; + parsingSection = false; + --line; + continue; + } + sectionCurrent = parseData.first; + if (data.has(sectionCurrent)) + { + parsingSection = true; + continueToNextSection = false; + discardNextEmpty = false; + output.emplace_back(*line); + lastKeyLine = output.size(); + } + else + { + continueToNextSection = true; + discardNextEmpty = true; + continue; + } + } + else if (parseResult == INIParser::PDataType::PDATA_KEYVALUE) + { + if (continueToNextSection) + { + continue; + } + if (data.has(sectionCurrent)) + { + auto& collection = data[sectionCurrent]; + auto const& key = parseData.first; + auto const& value = parseData.second; + if (collection.has(key)) + { + auto outputValue = collection[key]; + if (value == outputValue) + { + output.emplace_back(*line); + } + else + { + INIStringUtil::trim(outputValue); + auto lineNorm = *line; + INIStringUtil::replace(lineNorm, "\\=", " "); + auto equalsAt = lineNorm.find_first_of('='); + auto valueAt = lineNorm.find_first_not_of( + INIStringUtil::whitespaceDelimiters, + equalsAt + 1 + ); + std::string outputLine = line->substr(0, valueAt); + if (prettyPrint && equalsAt + 1 == valueAt) + { + outputLine += " "; + } + outputLine += outputValue; + output.emplace_back(outputLine); + } + lastKeyLine = output.size(); + } + } + } + else + { + if (discardNextEmpty && line->empty()) + { + discardNextEmpty = false; + } + else if (parseResult != INIParser::PDataType::PDATA_UNKNOWN) + { + output.emplace_back(*line); + } + } + } + if (writeNewKeys || std::next(line) == lineData->end()) + { + T_LineData linesToAdd; + if (data.has(sectionCurrent) && original.has(sectionCurrent)) + { + auto const& collection = data[sectionCurrent]; + auto const& collectionOriginal = original[sectionCurrent]; + for (auto const& it : collection) + { + auto key = it.first; + if (collectionOriginal.has(key)) + { + continue; + } + auto value = it.second; + INIStringUtil::replace(key, "=", "\\="); + INIStringUtil::trim(value); + linesToAdd.emplace_back( + key + ((prettyPrint) ? " = " : "=") + value + ); + } + } + if (!linesToAdd.empty()) + { + output.insert( + output.begin() + lastKeyLine, + linesToAdd.begin(), + linesToAdd.end() + ); + } + if (writeNewKeys) + { + writeNewKeys = false; + --line; + } + } + } + for (auto const& it : data) + { + auto const& section = it.first; + if (original.has(section)) + { + continue; + } + if (prettyPrint && output.size() > 0 && !output.back().empty()) + { + output.emplace_back(); + } + output.emplace_back("[" + section + "]"); + auto const& collection = it.second; + for (auto const& it2 : collection) + { + auto key = it2.first; + auto value = it2.second; + INIStringUtil::replace(key, "=", "\\="); + INIStringUtil::trim(value); + output.emplace_back( + key + ((prettyPrint) ? " = " : "=") + value + ); + } + } + return output; + } + + public: + bool prettyPrint = false; + + INIWriter(std::string const& filename) + : filename(filename) + { + } + ~INIWriter() { } + + bool operator<<(INIStructure& data) + { + struct stat buf; + bool fileExists = (stat(filename.c_str(), &buf) == 0); + if (!fileExists) + { + INIGenerator generator(filename); + generator.prettyPrint = prettyPrint; + return generator << data; + } + INIStructure originalData; + T_LineDataPtr lineData; + bool readSuccess = false; + bool fileIsBOM = false; + { + INIReader reader(filename, true); + if ((readSuccess = reader >> originalData)) + { + lineData = reader.getLines(); + fileIsBOM = reader.isBOM; + } + } + if (!readSuccess) + { + return false; + } + T_LineData output = getLazyOutput(lineData, data, originalData); + std::ofstream fileWriteStream(filename, std::ios::out | std::ios::binary); + if (fileWriteStream.is_open()) + { + if (fileIsBOM) { + const char utf8_BOM[3] = { + static_cast(0xEF), + static_cast(0xBB), + static_cast(0xBF) + }; + fileWriteStream.write(utf8_BOM, 3); + } + if (output.size()) + { + auto line = output.begin(); + for (;;) + { + fileWriteStream << *line; + if (++line == output.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + } + } + return true; + } + return false; + } + }; + + class INIFile + { + private: + std::string filename; + + public: + INIFile(std::string const& filename) + : filename(filename) + { } + + ~INIFile() { } + + bool read(INIStructure& data) const + { + if (data.size()) + { + data.clear(); + } + if (filename.empty()) + { + return false; + } + INIReader reader(filename); + return reader >> data; + } + bool generate(INIStructure const& data, bool pretty = false) const + { + if (filename.empty()) + { + return false; + } + INIGenerator generator(filename); + generator.prettyPrint = pretty; + return generator << data; + } + bool write(INIStructure& data, bool pretty = false) const + { + if (filename.empty()) + { + return false; + } + INIWriter writer(filename); + writer.prettyPrint = pretty; + return writer << data; + } + }; +} + +#endif // MINI_INI_H_ diff -r f5940a575d83 -r c537996cf67b include/core/config.h --- a/include/core/config.h Fri Nov 03 09:43:04 2023 -0400 +++ b/include/core/config.h Fri Nov 03 14:06:02 2023 -0400 @@ -34,12 +34,8 @@ } anilist; }; -#define WIDEIFY_EX(x) L##x -#define WIDEIFY(x) WIDEIFY_EX(x) #define CONFIG_DIR "minori" -#define CONFIG_WDIR WIDEIFY(CONFIG_DIR) -#define CONFIG_NAME "config.json" -#define CONFIG_WNAME WIDEIFY(CONFIG_NAME) +#define CONFIG_NAME "config.ini" #define MAX_LINE_LENGTH 256 diff -r f5940a575d83 -r c537996cf67b include/core/strings.h --- a/include/core/strings.h Fri Nov 03 09:43:04 2023 -0400 +++ b/include/core/strings.h Fri Nov 03 14:06:02 2023 -0400 @@ -33,6 +33,9 @@ QString ToQString(const std::string& string); QString ToQString(const std::wstring& wstring); +/* arithmetic :) */ +int ToInt(const std::string& str, int def = 0); + }; // namespace Strings #endif // __core__strings_h \ No newline at end of file diff -r f5940a575d83 -r c537996cf67b include/gui/dialog/settings.h --- a/include/gui/dialog/settings.h Fri Nov 03 09:43:04 2023 -0400 +++ b/include/gui/dialog/settings.h Fri Nov 03 14:06:02 2023 -0400 @@ -2,6 +2,7 @@ #define __gui__dialog__settings_h #include "core/anime.h" +#include "core/config.h" #include #include @@ -43,6 +44,7 @@ private: QWidget* CreateAnimeListWidget(); + Themes theme; Anime::TitleLanguage language; bool display_aired_episodes; bool display_available_episodes; diff -r f5940a575d83 -r c537996cf67b src/core/config.cc --- a/src/core/config.cc Fri Nov 03 09:43:04 2023 -0400 +++ b/src/core/config.cc Fri Nov 03 14:06:02 2023 -0400 @@ -3,34 +3,55 @@ * parses the config... lol **/ #include "core/config.h" +#include "core/strings.h" #include "core/anime.h" #include "core/filesystem.h" #include "core/json.h" #include "gui/translate/anime.h" #include "gui/translate/config.h" +#include "ini.h" // mINI +#include #include #include #include #include #include +/* I'm not exactly fond of using JSON for a config file, but it's better than + no config I guess. I'd like to have something more readable, e.g. YAML or + even INI. */ + +static bool string_to_bool(const std::string& s, bool def = false) { + bool b; + std::istringstream is(Strings::ToLower(s)); + is >> std::boolalpha >> b; + return b; +} + +static std::string bool_to_string(bool b) { + std::ostringstream stream; + stream << std::boolalpha << b; + return stream.str(); +} + int Config::Load() { Filesystem::Path cfg_path = Filesystem::GetConfigPath(); if (!cfg_path.Exists()) return 0; - std::ifstream config(cfg_path.GetPath(), std::ifstream::in); - auto config_js = nlohmann::json::parse(config); - service = Translate::ToService(JSON::GetString(config_js, "/General/Service"_json_pointer, "None")); - anime_list.language = Translate::ToLanguage(JSON::GetString(config_js, "/Anime List/Title language"_json_pointer, "Romaji")); - anime_list.display_aired_episodes = JSON::GetBoolean(config_js, "/Anime List/Display only aired episodes"_json_pointer, true); - anime_list.display_available_episodes = JSON::GetBoolean(config_js, "/Anime List/Display only available episodes in library"_json_pointer, true); - anime_list.highlight_anime_if_available = JSON::GetBoolean(config_js, "/Anime List/Highlight anime if available"_json_pointer, true); - anime_list.highlighted_anime_above_others = JSON::GetBoolean(config_js, "/Anime List/Display highlighted anime above others"_json_pointer); - anilist.auth_token = JSON::GetString(config_js, "/Authorization/AniList/Auth Token"_json_pointer); - anilist.username = JSON::GetString(config_js, "/Authorization/AniList/Username"_json_pointer); - anilist.user_id = JSON::GetInt(config_js, "/Authorization/AniList/User ID"_json_pointer); - theme = Translate::ToTheme(JSON::GetString(config_js, "/Appearance/Theme"_json_pointer, "Default")); - config.close(); + mINI::INIFile file(cfg_path.GetPath()); + mINI::INIStructure ini; + file.read(ini); + + service = Translate::ToService(ini.get("General").get("Service")); + anime_list.language = Translate::ToLanguage(ini.get("Anime List").get("Title language")); + anime_list.display_aired_episodes = string_to_bool(ini.get("Anime List").get("Display only aired episodes"), true); + anime_list.display_available_episodes = string_to_bool(ini.get("Anime List").get("Display only available episodes in library"), true); + anime_list.highlight_anime_if_available = string_to_bool(ini.get("Anime List").get("Highlight anime if available"), true); + anime_list.highlighted_anime_above_others = string_to_bool(ini.get("Anime List").get("Display highlighted anime above others")); + anilist.auth_token = ini.get("AniList").get("Auth Token"); + anilist.user_id = Strings::ToInt(ini.get("AniList").get("User ID")); + theme = Translate::ToTheme(ini.get("Appearance").get("Theme")); + return 0; } @@ -38,32 +59,21 @@ Filesystem::Path cfg_path = Filesystem::GetConfigPath(); if (!cfg_path.GetParent().Exists()) cfg_path.GetParent().CreateDirectories(); - std::ofstream config(cfg_path.GetPath(), std::ofstream::out | std::ofstream::trunc); - /* clang-format off */ - nlohmann::json config_js = { - {"General", { - {"Service", Translate::ToString(service)} - }}, - {"Anime List", { - {"Title language", Translate::ToString(anime_list.language)}, - {"Display only aired episodes", anime_list.display_aired_episodes}, - {"Display only available episodes in library", anime_list.display_available_episodes}, - {"Highlight anime if available", anime_list.highlight_anime_if_available}, - {"Display highlighted anime above others", anime_list.highlighted_anime_above_others} - }}, - {"Authorization", { - {"AniList", { - {"Auth Token", anilist.auth_token}, - {"Username", anilist.username}, - {"User ID", anilist.user_id} - }} - }}, - {"Appearance", { - {"Theme", Translate::ToString(theme)} - }} - }; - /* clang-format on */ - config << std::setw(4) << config_js << std::endl; - config.close(); + + mINI::INIFile file(cfg_path.GetPath()); + mINI::INIStructure ini; + + ini["General"]["Service"] = Translate::ToString(service); + ini["Anime List"]["Title language"] = Translate::ToString(anime_list.language); + ini["Anime List"]["Display only aired episodes"] = bool_to_string(anime_list.display_aired_episodes); + ini["Anime List"]["Display only available episodes in library"] = bool_to_string(anime_list.display_available_episodes); + ini["Anime List"]["Highlight anime if available"] = bool_to_string(anime_list.highlight_anime_if_available); + ini["Anime List"]["Display highlighted anime above others"] = bool_to_string(anime_list.highlighted_anime_above_others); + ini["AniList"]["Auth Token"] = anilist.auth_token; + ini["AniList"]["User ID"] = std::to_string(anilist.user_id); + ini["Appearance"]["Theme"] = Translate::ToString(theme); + + file.generate(ini); + return 0; } diff -r f5940a575d83 -r c537996cf67b src/core/strings.cc --- a/src/core/strings.cc Fri Nov 03 09:43:04 2023 -0400 +++ b/src/core/strings.cc Fri Nov 03 14:06:02 2023 -0400 @@ -3,6 +3,7 @@ **/ #include "core/strings.h" #include +#include #include #include #include @@ -133,4 +134,15 @@ return QString::fromWCharArray(wstring.c_str(), wstring.length()); } +int ToInt(const std::string& str, int def) { + int tmp = 0; + try { + tmp = std::stoi(str); + } catch (std::invalid_argument const& ex) { + qDebug() << "Failed to parse int from std::string: no number found in " << ToQString(str) << " defaulting to " << def; + tmp = def; + } + return tmp; +} + } // namespace Strings diff -r f5940a575d83 -r c537996cf67b src/gui/dialog/settings/application.cc --- a/src/gui/dialog/settings/application.cc Fri Nov 03 09:43:04 2023 -0400 +++ b/src/gui/dialog/settings/application.cc Fri Nov 03 14:06:02 2023 -0400 @@ -1,5 +1,6 @@ #include "core/session.h" #include "gui/dialog/settings.h" +#include "gui/dark_theme.h" #include #include #include @@ -55,6 +56,15 @@ [this](int index) { language = static_cast(index); }); lang_combo_box->setCurrentIndex(static_cast(language)); + QLabel* theme_combo_box_label = new QLabel(tr("Application theme:"), appearance_group_box); + QComboBox* theme_combo_box = new QComboBox(appearance_group_box); + theme_combo_box->addItem(tr("Default")); + theme_combo_box->addItem(tr("Light")); + theme_combo_box->addItem(tr("Dark")); + connect(theme_combo_box, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int index) { theme = static_cast(index); }); + theme_combo_box->setCurrentIndex(static_cast(theme)); + QCheckBox* hl_anime_box = new QCheckBox(tr("Highlight anime if next episode is available in library folders"), appearance_group_box); QCheckBox* hl_above_anime_box = new QCheckBox(tr("Display highlighted anime above others"), appearance_group_box); @@ -73,6 +83,8 @@ QVBoxLayout* appearance_layout = new QVBoxLayout(appearance_group_box); appearance_layout->addWidget(lang_combo_box_label); appearance_layout->addWidget(lang_combo_box); + appearance_layout->addWidget(theme_combo_box_label); + appearance_layout->addWidget(theme_combo_box); appearance_layout->addWidget(hl_anime_box); appearance_layout->addWidget(hl_above_anime_box); @@ -111,10 +123,13 @@ session.config.anime_list.highlight_anime_if_available = highlight_anime_if_available; session.config.anime_list.display_aired_episodes = display_aired_episodes; session.config.anime_list.display_available_episodes = display_available_episodes; + session.config.theme = theme; + DarkTheme::SetTheme(session.config.theme); } SettingsPageApplication::SettingsPageApplication(QWidget* parent) : SettingsPage(parent, tr("Application")) { language = session.config.anime_list.language; + theme = session.config.theme; highlighted_anime_above_others = session.config.anime_list.highlighted_anime_above_others; highlight_anime_if_available = session.config.anime_list.highlight_anime_if_available; display_aired_episodes = session.config.anime_list.display_aired_episodes;