🚧 update
This commit is contained in:
parent
4c2e31ae6b
commit
d1c92376c4
|
@ -1,5 +1,7 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
##
|
##
|
||||||
## PROJECT
|
## PROJECT
|
||||||
## name and version
|
## name and version
|
||||||
|
@ -15,6 +17,9 @@ message(STATUS "BUILD: ${CMAKE_BUILD_TYPE}")
|
||||||
##
|
##
|
||||||
## INCLUDE
|
## INCLUDE
|
||||||
##
|
##
|
||||||
|
include(CompilerWarnings)
|
||||||
|
include(EnableCcache)
|
||||||
|
include(ClangTidy)
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
|
@ -22,13 +27,16 @@ pkg_check_modules(GTK3 REQUIRED gtkmm-3.0)
|
||||||
|
|
||||||
FetchContent_Declare(fmt
|
FetchContent_Declare(fmt
|
||||||
GIT_REPOSITORY "https://github.com/fmtlib/fmt.git"
|
GIT_REPOSITORY "https://github.com/fmtlib/fmt.git"
|
||||||
GIT_TAG "d9fd695ac737f84f7de2d0a2aa346b25efb9afbf"
|
GIT_TAG "2742611cad4aee6b1a5638bd1ebf132908f4a3d9"
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(fmt)
|
FetchContent_MakeAvailable(fmt)
|
||||||
|
|
||||||
##
|
##
|
||||||
## CONFIGURATION
|
## CONFIGURATION
|
||||||
##
|
##
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")
|
||||||
|
|
||||||
if(UNIX AND CMAKE_GENERATOR STREQUAL "Ninja")
|
if(UNIX AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
set(CMAKE_CXX_FLAGS "-fcolor-diagnostics ${CMAKE_CXX_FLAGS}")
|
set(CMAKE_CXX_FLAGS "-fcolor-diagnostics ${CMAKE_CXX_FLAGS}")
|
||||||
|
@ -40,13 +48,19 @@ if(UNIX AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fwhole-program")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(project_warnings INTERFACE)
|
||||||
|
set_project_warnings(project_warnings)
|
||||||
|
|
||||||
##
|
##
|
||||||
## Target
|
## Target
|
||||||
##
|
##
|
||||||
add_executable(${PROJECT_NAME}
|
add_executable(${PROJECT_NAME}
|
||||||
src/main.cc
|
src/hello.cpp src/hello.hpp
|
||||||
|
src/main.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
|
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
option(ENABLE_TIDY "Enable clang-tidy [default: OFF]" OFF)
|
||||||
|
if(ENABLE_TIDY)
|
||||||
|
find_program(CLANG_TIDY_EXE
|
||||||
|
NAMES clang-tidy-9 clang-tidy-8 clang-tidy-7 clang-tidy
|
||||||
|
DOC "Path to clang-tidy executable")
|
||||||
|
if(NOT CLANG_TIDY_EXE)
|
||||||
|
message(STATUS "[clang-tidy] Not found.")
|
||||||
|
else()
|
||||||
|
message(STATUS "[clang-tidy] found: ${CLANG_TIDY_EXE}")
|
||||||
|
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message(STATUS "[clang-tidy] Disabled.")
|
||||||
|
endif()
|
|
@ -0,0 +1,76 @@
|
||||||
|
function(set_project_warnings project_name)
|
||||||
|
option(WARNINGS_AS_ERROR "Treat compiler warnings as error" ON)
|
||||||
|
|
||||||
|
set(MSVC_WARNINGS
|
||||||
|
/W4 # Base
|
||||||
|
/w14242 # Conversion
|
||||||
|
/w14254 # Operator convers.
|
||||||
|
/w14263 # Func member doesn't override
|
||||||
|
/w14265 # class has vfuncs, but destructor is not
|
||||||
|
/w14287 # unsigned/negative constant mismatch
|
||||||
|
/we4289 # nonstandard extension used: loop control var
|
||||||
|
/w14296 # expression is always 'boolean_value'
|
||||||
|
/w14311 # pointer trunc from one tipe to another
|
||||||
|
/w14545 # expression before comma evaluates to a function which missign an argument list
|
||||||
|
|
||||||
|
/w14546 # function call before comma missing argument list
|
||||||
|
/w14547 # operator before comma has no effect; expected operator with side-effect
|
||||||
|
/w14549 # operator before comma has no effect; did you intend operator?
|
||||||
|
|
||||||
|
/w14555 # expresion has no effect; expected expression with side-effect
|
||||||
|
/w14619 # pragma warning
|
||||||
|
/w14640 # Enable warning on thread; static member
|
||||||
|
|
||||||
|
/w14826 # Conversion from one tipe to another is sign-extended cause unexpected runtime behavior.
|
||||||
|
/w14928 # illegal copy-initialization; more than user-defined.
|
||||||
|
/X
|
||||||
|
/constexpr
|
||||||
|
)
|
||||||
|
|
||||||
|
set(CLANG_WARNINGS
|
||||||
|
-Wall
|
||||||
|
-Wextra # standard
|
||||||
|
-Wshadow
|
||||||
|
|
||||||
|
-Wnon-virtual-dtor
|
||||||
|
|
||||||
|
-Wold-style-cast # c-style cast
|
||||||
|
-Wcast-align
|
||||||
|
-Wunused
|
||||||
|
-Woverloaded-virtual
|
||||||
|
|
||||||
|
-Wpedantic # non-standard C++
|
||||||
|
-Wconversion # type conversion that may lose data
|
||||||
|
-Wsign-conversion
|
||||||
|
-Wnull-dereference
|
||||||
|
-Wdouble-promotion # float to double
|
||||||
|
|
||||||
|
-Wformat=2
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WARNINGS_AS_ERRORS)
|
||||||
|
set(CLANG_WARNINGS ${CLANG_WARNINGS} -Werror)
|
||||||
|
set(MSVC_WARNINGS ${MSVC_WARNINGS} /WX)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(GCC_WARNINGS
|
||||||
|
${CLANG_WARNINGS}
|
||||||
|
-Wmisleading-indentation
|
||||||
|
|
||||||
|
-Wduplicated-cond
|
||||||
|
-Wduplicated-branches
|
||||||
|
-Wlogical-op
|
||||||
|
|
||||||
|
-Wuseless-cast
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set(PROJECT_WARNINGS ${MSVC_WARNINGS})
|
||||||
|
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
set(PROJECT_WARNINGS ${CLANG_WARNINGS})
|
||||||
|
else()
|
||||||
|
set(PROJECT_WARNINGS ${GCC_WARNINGS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_options(${project_name} INTERFACE ${PROJECT_WARNINGS})
|
||||||
|
endfunction()
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Setup ccache.
|
||||||
|
#
|
||||||
|
# The ccache is auto-enabled if the tool is found.
|
||||||
|
# To disable set -DCCACHE=OFF option.
|
||||||
|
if(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER)
|
||||||
|
find_program(CCACHE ccache DOC "ccache tool path; set to OFF to disable")
|
||||||
|
if(CCACHE)
|
||||||
|
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE})
|
||||||
|
message(STATUS "[ccache] Enabled: ${CCACHE}")
|
||||||
|
else()
|
||||||
|
message(STATUS "[ccache] Disabled.")
|
||||||
|
endif()
|
||||||
|
endif()
|
|
@ -0,0 +1,42 @@
|
||||||
|
-std=gnu++17
|
||||||
|
-DFMT_LOCALE
|
||||||
|
-I/usr/include/gtkmm-3.0
|
||||||
|
-I/usr/lib/gtkmm-3.0/include
|
||||||
|
-I/usr/include/giomm-2.4
|
||||||
|
-I/usr/lib/giomm-2.4/include
|
||||||
|
-I/usr/include/glib-2.0
|
||||||
|
-I/usr/lib/glib-2.0/include
|
||||||
|
-I/usr/include/libmount
|
||||||
|
-I/usr/include/blkid
|
||||||
|
-I/usr/include/glibmm-2.4
|
||||||
|
-I/usr/lib/glibmm-2.4/include
|
||||||
|
-I/usr/include/sigc++-2.0
|
||||||
|
-I/usr/lib/sigc++-2.0/include
|
||||||
|
-I/usr/include/gtk-3.0
|
||||||
|
-I/usr/include/pango-1.0
|
||||||
|
-I/usr/include/harfbuzz
|
||||||
|
-I/usr/include/freetype2
|
||||||
|
-I/usr/include/libpng16
|
||||||
|
-I/usr/include/fribidi
|
||||||
|
-I/usr/include/cairo
|
||||||
|
-I/usr/include/lzo
|
||||||
|
-I/usr/include/pixman-1
|
||||||
|
-I/usr/include/gdk-pixbuf-2.0
|
||||||
|
-I/usr/include/gio-unix-2.0
|
||||||
|
-I/usr/include/cloudproviders
|
||||||
|
-I/usr/include/atk-1.0
|
||||||
|
-I/usr/include/at-spi2-atk/2.0
|
||||||
|
-I/usr/include/dbus-1.0
|
||||||
|
-I/usr/lib/dbus-1.0/include
|
||||||
|
-I/usr/include/at-spi-2.0
|
||||||
|
-I/usr/include/cairomm-1.0
|
||||||
|
-I/usr/lib/cairomm-1.0/include
|
||||||
|
-I/usr/include/pangomm-1.4
|
||||||
|
-I/usr/lib/pangomm-1.4/include
|
||||||
|
-I/usr/include/atkmm-1.6
|
||||||
|
-I/usr/lib/atkmm-1.6/include
|
||||||
|
-I/usr/include/gtk-3.0/unix-print
|
||||||
|
-I/usr/include/gdkmm-3.0
|
||||||
|
-I/usr/lib/gdkmm-3.0/include
|
||||||
|
-Isrc
|
||||||
|
-xc++
|
|
@ -0,0 +1,362 @@
|
||||||
|
#include "hello.hpp"
|
||||||
|
#include "helper.hpp"
|
||||||
|
|
||||||
|
#include <libintl.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Hello* g_refHello;
|
||||||
|
|
||||||
|
std::string fix_path(std::string&& path) noexcept {
|
||||||
|
if (path[0] != '~') {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
replace_all(path, "~", Glib::get_home_dir().c_str());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json read_json(const std::string_view& path) {
|
||||||
|
// read a JSON file
|
||||||
|
std::ifstream i(fix_path(path.data()));
|
||||||
|
nlohmann::json j;
|
||||||
|
i >> j;
|
||||||
|
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_json(const std::string_view& path, const nlohmann::json& content) {
|
||||||
|
// write data to JSON file
|
||||||
|
std::ofstream o(fix_path(path.data()));
|
||||||
|
o << content << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read information from the lsb-release file.
|
||||||
|
//
|
||||||
|
// @Returns args from lsb-release file
|
||||||
|
std::array<std::string, 2> get_lsb_infos() {
|
||||||
|
std::unordered_map<std::string, std::string> lsb{};
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream lsb_release("/etc/lsb-release");
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(lsb_release, line)) {
|
||||||
|
if (line.find('=') != std::string::npos) {
|
||||||
|
auto var = tokenize(line, "=");
|
||||||
|
remove_all(var.first, "DISTRIB_");
|
||||||
|
remove_all(var.second, "\"");
|
||||||
|
lsb[var.first] = var.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << e.what() << '\n';
|
||||||
|
return {"not CachyOS", "0.0"};
|
||||||
|
}
|
||||||
|
return {lsb["ID"], lsb["RELEASE"]};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Hello::Hello(int argc, char** argv) {
|
||||||
|
set_title("CachyOS Hello");
|
||||||
|
set_border_width(6);
|
||||||
|
if (argc > 1 && (strncmp(argv[1], "--dev", 5) == 0)) {
|
||||||
|
m_dev = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_refHello = this;
|
||||||
|
|
||||||
|
auto screen = Gdk::Screen::get_default();
|
||||||
|
|
||||||
|
// Load preferences
|
||||||
|
if (m_dev) {
|
||||||
|
m_preferences = read_json("data/preferences.json");
|
||||||
|
m_preferences["data_path"] = "data/";
|
||||||
|
m_preferences["desktop_path"] = fmt::format("{}/{}.desktop", fs::current_path().string(), m_app);
|
||||||
|
m_preferences["locale_path"] = "locale/";
|
||||||
|
m_preferences["ui_path"] = fmt::format("ui/{}.glade", m_app);
|
||||||
|
m_preferences["style_path"] = "ui/style.css";
|
||||||
|
} else {
|
||||||
|
m_preferences = read_json(fmt::format("/usr/share/{}/data/preferences.json", m_app));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get saved infos
|
||||||
|
const auto& save_path = fix_path(m_preferences["save_path"]);
|
||||||
|
m_save = (!fs::exists(save_path)) ? nlohmann::json({{"locale", ""}}) : read_json(save_path);
|
||||||
|
|
||||||
|
// Import Css
|
||||||
|
auto provider = Gtk::CssProvider::create();
|
||||||
|
provider->load_from_path(m_preferences["style_path"]);
|
||||||
|
Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
|
|
||||||
|
// Init window
|
||||||
|
m_builder = Gtk::Builder::create_from_file(m_preferences["ui_path"]);
|
||||||
|
gtk_builder_add_callback_symbol(m_builder->gobj(), "on_languages_changed", G_CALLBACK(on_languages_changed));
|
||||||
|
gtk_builder_add_callback_symbol(m_builder->gobj(), "on_action_clicked", G_CALLBACK(on_action_clicked));
|
||||||
|
gtk_builder_add_callback_symbol(m_builder->gobj(), "on_btn_clicked", G_CALLBACK(on_btn_clicked));
|
||||||
|
gtk_builder_add_callback_symbol(m_builder->gobj(), "on_link_clicked", G_CALLBACK(on_link_clicked));
|
||||||
|
gtk_builder_add_callback_symbol(m_builder->gobj(), "on_delete_window", G_CALLBACK(on_delete_window));
|
||||||
|
gtk_builder_connect_signals(m_builder->gobj(), nullptr);
|
||||||
|
Gtk::Window* ref_window;
|
||||||
|
m_builder->get_widget("window", ref_window);
|
||||||
|
gobject_ = reinterpret_cast<GObject*>(ref_window->gobj());
|
||||||
|
|
||||||
|
// Subtitle of headerbar
|
||||||
|
Gtk::HeaderBar* header;
|
||||||
|
m_builder->get_widget("headerbar", header);
|
||||||
|
const auto& lsb_info = get_lsb_infos();
|
||||||
|
header->set_subtitle(lsb_info[0] + " " + lsb_info[1]);
|
||||||
|
|
||||||
|
// Load images
|
||||||
|
if (fs::is_regular_file(m_preferences["logo_path"])) {
|
||||||
|
const auto& logo = Gdk::Pixbuf::create_from_file(m_preferences["logo_path"]);
|
||||||
|
set_icon(logo);
|
||||||
|
|
||||||
|
Gtk::Image* image;
|
||||||
|
m_builder->get_widget("distriblogo", image);
|
||||||
|
image->set(logo);
|
||||||
|
|
||||||
|
Gtk::AboutDialog* dialog;
|
||||||
|
m_builder->get_widget("aboutdialog", dialog);
|
||||||
|
dialog->set_logo(logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk::Box* social_box;
|
||||||
|
m_builder->get_widget("social", social_box);
|
||||||
|
for (const auto& btn : social_box->get_children()) {
|
||||||
|
const auto& name = btn->get_name();
|
||||||
|
const auto& icon_path = fmt::format("{}img/{}.png", m_preferences["data_path"], name.c_str());
|
||||||
|
Gtk::Image* image;
|
||||||
|
m_builder->get_widget(name, image);
|
||||||
|
image->set(icon_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk::Grid* homepage_grid;
|
||||||
|
m_builder->get_widget("homepage", homepage_grid);
|
||||||
|
for (const auto& widget : homepage_grid->get_children()) {
|
||||||
|
if (!G_TYPE_CHECK_INSTANCE_TYPE(widget->gobj(), GTK_TYPE_BUTTON)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& casted_widget = Glib::wrap(GTK_BUTTON(widget->gobj()));
|
||||||
|
if (gtk_button_get_image_position(casted_widget->gobj()) != GtkPositionType::GTK_POS_RIGHT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& image_path = fmt::format("{}img/external-link.png", m_preferences["data_path"]);
|
||||||
|
Gtk::Image image;
|
||||||
|
image.set(image_path);
|
||||||
|
image.set_margin_start(2);
|
||||||
|
casted_widget->set_image(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pages
|
||||||
|
m_pages = fmt::format("{}pages/{}", m_preferences["data_path"], m_preferences["default_locale"]);
|
||||||
|
|
||||||
|
for (const auto& page : fs::directory_iterator(m_pages)) {
|
||||||
|
auto* scrolled_window = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
auto* viewport = gtk_viewport_new(nullptr, nullptr);
|
||||||
|
gtk_container_set_border_width(GTK_CONTAINER(viewport), 10);
|
||||||
|
auto* label = gtk_label_new(nullptr);
|
||||||
|
gtk_label_set_line_wrap(GTK_LABEL(label), true);
|
||||||
|
auto* image = gtk_image_new_from_icon_name("go-previous", GTK_ICON_SIZE_BUTTON);
|
||||||
|
auto* backBtn = gtk_button_new();
|
||||||
|
gtk_button_set_image(GTK_BUTTON(backBtn), image);
|
||||||
|
gtk_widget_set_name(backBtn, "home");
|
||||||
|
g_signal_connect(backBtn, "clicked", G_CALLBACK(&on_btn_clicked), nullptr);
|
||||||
|
|
||||||
|
auto* grid = GTK_GRID(gtk_grid_new());
|
||||||
|
gtk_grid_attach(grid, backBtn, 0, 1, 1, 1);
|
||||||
|
gtk_grid_attach(grid, label, 1, 2, 1, 1);
|
||||||
|
gtk_container_add(GTK_CONTAINER(viewport), GTK_WIDGET(grid));
|
||||||
|
gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(viewport));
|
||||||
|
gtk_widget_show_all(scrolled_window);
|
||||||
|
|
||||||
|
Glib::RefPtr<Glib::Object> stack = m_builder->get_object("stack");
|
||||||
|
const auto& child_name = page.path().filename().string() + "page";
|
||||||
|
gtk_stack_add_named(GTK_STACK(stack->gobj()), scrolled_window, child_name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init translation
|
||||||
|
const std::string& locale_path = m_preferences["locale_path"];
|
||||||
|
bindtextdomain(m_app, locale_path.c_str());
|
||||||
|
textdomain(m_app);
|
||||||
|
Gtk::ComboBoxText* languages;
|
||||||
|
m_builder->get_widget("languages", languages);
|
||||||
|
languages->set_active_id(get_best_locale());
|
||||||
|
|
||||||
|
// Set autostart switcher state
|
||||||
|
m_autostart = fs::is_regular_file(fix_path(m_preferences["autostart_path"]));
|
||||||
|
Gtk::Switch* autostart_switch;
|
||||||
|
m_builder->get_widget("autostart", autostart_switch);
|
||||||
|
autostart_switch->set_active(m_autostart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the best locale, based on user's preferences.
|
||||||
|
auto Hello::get_best_locale() const noexcept -> std::string {
|
||||||
|
const auto& binary_path = fmt::format("{}{}{}.mo", m_preferences["locale_path"], "{}/LC_MESSAGES/", m_app);
|
||||||
|
const auto& saved_locale = fmt::vformat(binary_path, fmt::make_format_args(m_save["locale"]));
|
||||||
|
if (fs::is_regular_file(saved_locale)) {
|
||||||
|
return m_save["locale"];
|
||||||
|
} else if (m_save["locale"] == m_preferences["default_locale"]) {
|
||||||
|
return m_preferences["default_locale"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& locale_name = std::locale("").name();
|
||||||
|
std::string sys_locale = locale_name.substr(0, locale_name.find('.'));
|
||||||
|
const auto& user_locale = fmt::vformat(binary_path, fmt::make_format_args(sys_locale));
|
||||||
|
const auto& two_letters = sys_locale.substr(0, 2);
|
||||||
|
|
||||||
|
// If user's locale is supported
|
||||||
|
if (fs::is_regular_file(user_locale)) {
|
||||||
|
if (sys_locale.find('_') != std::string::npos) {
|
||||||
|
replace_all(sys_locale, "_", "-");
|
||||||
|
}
|
||||||
|
return sys_locale;
|
||||||
|
}
|
||||||
|
// If two first letters of user's locale is supported (ex: en_US -> en)
|
||||||
|
else if (fs::is_regular_file(fmt::vformat(binary_path, fmt::make_format_args(two_letters)))) {
|
||||||
|
return two_letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_preferences["default_locale"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets locale of ui and pages.
|
||||||
|
void Hello::set_locale(const std::string_view& use_locale) noexcept {
|
||||||
|
fmt::print(
|
||||||
|
"┌{0:─^{2}}┐\n"
|
||||||
|
"│{1: ^{2}}│\n"
|
||||||
|
"└{0:─^{2}}┘\n",
|
||||||
|
"", fmt::format("Locale changed to {}", use_locale), 40);
|
||||||
|
|
||||||
|
bind_textdomain_codeset(m_app, use_locale.data());
|
||||||
|
|
||||||
|
m_save["locale"] = use_locale;
|
||||||
|
|
||||||
|
// Real-time locale changing
|
||||||
|
/* clang-format off */
|
||||||
|
nlohmann::json elts = {
|
||||||
|
{"comments", {"aboutdialog"}},
|
||||||
|
{"label", {
|
||||||
|
"autostartlabel",
|
||||||
|
"development",
|
||||||
|
"discover",
|
||||||
|
"donate",
|
||||||
|
"firstcategory",
|
||||||
|
"forum",
|
||||||
|
"install",
|
||||||
|
"installlabel",
|
||||||
|
"involved",
|
||||||
|
"mailling",
|
||||||
|
"readme",
|
||||||
|
"release",
|
||||||
|
"secondcategory",
|
||||||
|
"thirdcategory",
|
||||||
|
"welcomelabel",
|
||||||
|
"welcometitle",
|
||||||
|
"wiki"}
|
||||||
|
},
|
||||||
|
{"tooltip_text", {
|
||||||
|
"about",
|
||||||
|
"development",
|
||||||
|
"discover",
|
||||||
|
"donate",
|
||||||
|
"forum",
|
||||||
|
"mailling",
|
||||||
|
"wiki"}
|
||||||
|
}};
|
||||||
|
/* clang-format on */
|
||||||
|
|
||||||
|
for (const auto& method : elts.items()) {
|
||||||
|
if (!m_default_texts.contains(method.key())) {
|
||||||
|
m_default_texts[method.key()] = {};
|
||||||
|
}
|
||||||
|
for (const auto& elt : elts[method.key()].items()) {
|
||||||
|
const std::string& elt_value = elt.value();
|
||||||
|
if (!m_default_texts[method.key()].contains(elt_value)) {
|
||||||
|
Gtk::Widget* item;
|
||||||
|
m_builder->get_widget(elt_value, item);
|
||||||
|
gchar* item_buf;
|
||||||
|
g_object_get(G_OBJECT(item->gobj()), method.key().c_str(), &item_buf, nullptr);
|
||||||
|
m_default_texts[method.key()][elt_value] = item_buf;
|
||||||
|
g_free(item_buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change content of pages
|
||||||
|
for (const auto& page : fs::directory_iterator(m_pages)) {
|
||||||
|
Gtk::Stack* stack;
|
||||||
|
m_builder->get_widget("stack", stack);
|
||||||
|
const auto& child = stack->get_child_by_name((page.path().filename().string() + "page").c_str());
|
||||||
|
if (child == nullptr) {
|
||||||
|
fmt::print(stderr, "child not found\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& first_child = reinterpret_cast<Gtk::Container*>(child)->get_children();
|
||||||
|
const auto& second_child = reinterpret_cast<Gtk::Container*>(first_child[0])->get_children();
|
||||||
|
const auto& third_child = reinterpret_cast<Gtk::Container*>(second_child[0])->get_children();
|
||||||
|
|
||||||
|
const auto& label = reinterpret_cast<Gtk::Label*>(third_child[0]);
|
||||||
|
label->set_markup(get_page(page.path().filename().string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Hello::get_page(const std::string& name) const noexcept -> std::string {
|
||||||
|
auto filename = fmt::format("{}pages/{}/{}", m_preferences["data_path"], m_save["locale"], name);
|
||||||
|
if (!fs::is_regular_file(filename)) {
|
||||||
|
filename = fmt::format("{}pages/{}/{}", m_preferences["data_path"], m_preferences["default_locale"], name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return read_whole_file(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
void Hello::on_languages_changed(GtkComboBox* combobox) noexcept {
|
||||||
|
const auto& active_id = gtk_combo_box_get_active_id(combobox);
|
||||||
|
g_refHello->set_locale(active_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hello::on_action_clicked(GtkWidget* widget) noexcept {
|
||||||
|
const auto& name = gtk_widget_get_name(widget);
|
||||||
|
if (strncmp(name, "install", 7) == 0) {
|
||||||
|
fmt::print("install\n");
|
||||||
|
return;
|
||||||
|
} else if (strncmp(name, "autostart", 9) == 0) {
|
||||||
|
const auto& action = Glib::wrap(GTK_SWITCH(widget));
|
||||||
|
//set_autostart(action->get_active());
|
||||||
|
fmt::print("autostart\n");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk::AboutDialog* dialog;
|
||||||
|
g_refHello->m_builder->get_widget("aboutdialog", dialog);
|
||||||
|
dialog->set_decorated(false);
|
||||||
|
dialog->run();
|
||||||
|
dialog->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hello::on_btn_clicked(GtkWidget* widget) noexcept {
|
||||||
|
const auto& name = gtk_widget_get_name(widget);
|
||||||
|
Gtk::Stack* stack;
|
||||||
|
g_refHello->m_builder->get_widget("stack", stack);
|
||||||
|
stack->set_visible_child(fmt::format("{}page", name).c_str());
|
||||||
|
}
|
||||||
|
void Hello::on_link_clicked(GtkWidget* widget) noexcept {
|
||||||
|
const auto& name = gtk_widget_get_name(widget);
|
||||||
|
const std::string uri = g_refHello->m_preferences["urls"][name];
|
||||||
|
gtk_show_uri_on_window(nullptr, uri.c_str(), GDK_CURRENT_TIME, nullptr);
|
||||||
|
}
|
||||||
|
void Hello::on_delete_window(GtkWidget* /*widget*/) noexcept {
|
||||||
|
write_json(g_refHello->m_preferences["save_path"].get<std::string>(), g_refHello->m_save);
|
||||||
|
const auto& application = g_refHello->get_application();
|
||||||
|
application->quit();
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
#ifndef HELLO_HPP_
|
||||||
|
#define HELLO_HPP_
|
||||||
|
|
||||||
|
#include <gtkmm.h>
|
||||||
|
#include <json.hpp>
|
||||||
|
|
||||||
|
class Hello final : public Gtk::Window {
|
||||||
|
public:
|
||||||
|
Hello(int argc, char** argv);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Handlers
|
||||||
|
static void on_languages_changed(GtkComboBox* combobox) noexcept;
|
||||||
|
static void on_action_clicked(GtkWidget* widget) noexcept;
|
||||||
|
static void on_btn_clicked(GtkWidget* widget) noexcept;
|
||||||
|
static void on_link_clicked(GtkWidget* widget) noexcept;
|
||||||
|
static void on_delete_window(GtkWidget* /*widget*/) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr auto m_app = "cachyos-hello";
|
||||||
|
bool m_dev{};
|
||||||
|
bool m_autostart{};
|
||||||
|
|
||||||
|
std::string m_pages;
|
||||||
|
Glib::RefPtr<Gtk::Builder> m_builder;
|
||||||
|
nlohmann::json m_preferences;
|
||||||
|
nlohmann::json m_save;
|
||||||
|
nlohmann::json m_default_texts;
|
||||||
|
|
||||||
|
auto get_best_locale() const noexcept -> std::string;
|
||||||
|
void set_locale(const std::string_view& use_locale) noexcept;
|
||||||
|
auto get_page(const std::string& name) const noexcept -> std::string;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // HELLO_HPP_
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Helper macroses
|
||||||
|
#ifndef HELPER_HPP_
|
||||||
|
#define HELPER_HPP_
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
inline std::pair<std::string, std::string> tokenize(std::string& str, const std::string_view& delim) {
|
||||||
|
int start{};
|
||||||
|
int end = str.find(delim.data());
|
||||||
|
std::string key;
|
||||||
|
while (end != -1) {
|
||||||
|
key = str.substr(start, end - start);
|
||||||
|
start = end + delim.length();
|
||||||
|
end = str.find(delim.data(), start);
|
||||||
|
}
|
||||||
|
return {key, str.substr(start, end - start)};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::size_t replace_all(std::string& inout, const std::string_view& what, const std::string_view& with) {
|
||||||
|
std::size_t count{};
|
||||||
|
std::size_t pos{};
|
||||||
|
while (inout.npos != (pos = inout.find(what.data(), pos, what.length()))) {
|
||||||
|
inout.replace(pos, what.length(), with.data(), with.length());
|
||||||
|
pos += with.length(), ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::size_t remove_all(std::string& inout, const std::string_view& what) {
|
||||||
|
return replace_all(inout, what, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto read_whole_file(const std::string_view& path) noexcept -> std::string {
|
||||||
|
static constexpr auto read_size = 4096;
|
||||||
|
std::ifstream stream{path.data()};
|
||||||
|
stream.exceptions(std::ios_base::badbit);
|
||||||
|
|
||||||
|
std::string file{};
|
||||||
|
std::string buf(read_size, '\0');
|
||||||
|
while (stream.read(&buf[0], read_size)) {
|
||||||
|
file.append(buf, 0, stream.gcount());
|
||||||
|
}
|
||||||
|
file.append(buf, 0, stream.gcount());
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // HELPER_HPP_
|
226
src/main.cc
226
src/main.cc
|
@ -1,226 +0,0 @@
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include <fmt/core.h>
|
|
||||||
#include <gtkmm.h>
|
|
||||||
#include <json.hpp>
|
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
Glib::RefPtr<Gtk::Application> g_app;
|
|
||||||
Glib::RefPtr<Gtk::Builder> g_refGlade;
|
|
||||||
Gtk::Window* g_refWindow;
|
|
||||||
nlohmann::json preferences;
|
|
||||||
|
|
||||||
std::pair<std::string, std::string> tokenize(std::string str, std::string delim) {
|
|
||||||
int start = 0;
|
|
||||||
int end = str.find(delim);
|
|
||||||
std::string key{};
|
|
||||||
while (end != -1) {
|
|
||||||
key = str.substr(start, end - start);
|
|
||||||
start = end + delim.size();
|
|
||||||
end = str.find(delim, start);
|
|
||||||
}
|
|
||||||
return {key, str.substr(start, end - start)};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t replace_all(std::string& inout, std::string_view what, std::string_view with) {
|
|
||||||
std::size_t count{};
|
|
||||||
for (std::string::size_type pos{};
|
|
||||||
inout.npos != (pos = inout.find(what.data(), pos, what.length()));
|
|
||||||
pos += with.length(), ++count) {
|
|
||||||
inout.replace(pos, what.length(), with.data(), with.length());
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t remove_all(std::string& inout, std::string_view what) {
|
|
||||||
return replace_all(inout, what, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read informations from the lsb-release file.
|
|
||||||
//
|
|
||||||
// @Returns args from lsb-release file
|
|
||||||
std::array<std::string, 2> get_lsb_infos() {
|
|
||||||
std::unordered_map<std::string, std::string> lsb{};
|
|
||||||
|
|
||||||
try {
|
|
||||||
std::ifstream lsb_release("/etc/lsb-release");
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(lsb_release, line)) {
|
|
||||||
if (line.find("=") != std::string::npos) {
|
|
||||||
auto var = tokenize(line, "=");
|
|
||||||
remove_all(var.first, "DISTRIB_");
|
|
||||||
remove_all(var.second, "\"");
|
|
||||||
lsb[var.first] = var.second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << e.what() << '\n';
|
|
||||||
return {"not CachyOS", "0.0"};
|
|
||||||
}
|
|
||||||
return {lsb["ID"], lsb["RELEASE"]};
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_action_clicked(GtkWidget* widget) {
|
|
||||||
const auto& name = gtk_widget_get_name(widget);
|
|
||||||
if (strncmp(name, "install", 7) == 0) {
|
|
||||||
fmt::print("install\n");
|
|
||||||
return;
|
|
||||||
} else if (strncmp(name, "autostart", 9) == 0) {
|
|
||||||
//const auto& action = GTK_ACTION(widget);
|
|
||||||
//set_autostart(action->get_active());
|
|
||||||
fmt::print("autostart\n");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk::AboutDialog* dialog;
|
|
||||||
g_refGlade->get_widget<Gtk::AboutDialog>("aboutdialog", std::ref(dialog));
|
|
||||||
dialog->set_decorated(false);
|
|
||||||
dialog->run();
|
|
||||||
dialog->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_btn_clicked(GtkWidget* widget) {
|
|
||||||
const auto& name = gtk_widget_get_name(widget);
|
|
||||||
Gtk::Stack* stack;
|
|
||||||
g_refGlade->get_widget<Gtk::Stack>("stack", std::ref(stack));
|
|
||||||
stack->set_visible_child(fmt::format("{}page", name).c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_link_clicked(GtkWidget* widget) {
|
|
||||||
const auto& name = gtk_widget_get_name(widget);
|
|
||||||
const std::string uri = preferences["urls"][name];
|
|
||||||
gtk_show_uri_on_window(nullptr, uri.c_str(), GDK_CURRENT_TIME, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_delete_window(GtkWidget* widget) {
|
|
||||||
g_app->quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json read_json(const std::string& path) {
|
|
||||||
// read a JSON file
|
|
||||||
std::ifstream i(path);
|
|
||||||
nlohmann::json j;
|
|
||||||
i >> j;
|
|
||||||
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
g_app = Gtk::Application::create();
|
|
||||||
auto screen = Gdk::Screen::get_default();
|
|
||||||
|
|
||||||
bool is_dev = false;
|
|
||||||
if (argc > 1 && (strncmp(argv[1], "--dev", 5) == 0)) {
|
|
||||||
is_dev = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load preferences
|
|
||||||
if (is_dev) {
|
|
||||||
preferences = read_json("data/preferences.json");
|
|
||||||
preferences["data_path"] = "data/";
|
|
||||||
preferences["desktop_path"] = (fs::current_path() / "cachyos-hello.desktop").string();
|
|
||||||
preferences["locale_path"] = "locale/";
|
|
||||||
preferences["ui_path"] = "ui/cachyos-hello.glade";
|
|
||||||
preferences["style_path"] = "ui/style.css";
|
|
||||||
} else {
|
|
||||||
preferences = read_json("/usr/share/cachyos-hello/data/preferences.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get saved infos
|
|
||||||
//const auto& save = read_json(preferences["save_path"]);
|
|
||||||
|
|
||||||
// Import Css
|
|
||||||
auto provider = Gtk::CssProvider::create();
|
|
||||||
provider->load_from_path(preferences["style_path"]);
|
|
||||||
Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
||||||
|
|
||||||
// Init window
|
|
||||||
g_refGlade = Gtk::Builder::create_from_file(preferences["ui_path"]);
|
|
||||||
gtk_builder_add_callback_symbol(g_refGlade->gobj(), "on_action_clicked", G_CALLBACK(on_action_clicked));
|
|
||||||
gtk_builder_add_callback_symbol(g_refGlade->gobj(), "on_btn_clicked", G_CALLBACK(on_btn_clicked));
|
|
||||||
gtk_builder_add_callback_symbol(g_refGlade->gobj(), "on_link_clicked", G_CALLBACK(on_link_clicked));
|
|
||||||
gtk_builder_add_callback_symbol(g_refGlade->gobj(), "on_delete_window", G_CALLBACK(on_delete_window));
|
|
||||||
gtk_builder_connect_signals(g_refGlade->gobj(), nullptr);
|
|
||||||
g_refGlade->get_widget<Gtk::Window>("window", std::ref(g_refWindow));
|
|
||||||
|
|
||||||
// Subtitle of headerbar
|
|
||||||
Gtk::HeaderBar* header;
|
|
||||||
g_refGlade->get_widget<Gtk::HeaderBar>("headerbar", std::ref(header));
|
|
||||||
const auto& lsb_info = get_lsb_infos();
|
|
||||||
header->set_subtitle(lsb_info[0] + " " + lsb_info[1]);
|
|
||||||
|
|
||||||
// Load images
|
|
||||||
if (fs::is_regular_file(preferences["logo_path"])) {
|
|
||||||
const auto& logo = Gdk::Pixbuf::create_from_file(preferences["logo_path"]);
|
|
||||||
g_refWindow->set_icon(logo);
|
|
||||||
|
|
||||||
Gtk::Image* image;
|
|
||||||
g_refGlade->get_widget<Gtk::Image>("distriblogo", std::ref(image));
|
|
||||||
image->set(logo);
|
|
||||||
|
|
||||||
Gtk::AboutDialog* dialog;
|
|
||||||
g_refGlade->get_widget<Gtk::AboutDialog>("aboutdialog", std::ref(dialog));
|
|
||||||
dialog->set_logo(logo);
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk::Box* social_box;
|
|
||||||
g_refGlade->get_widget<Gtk::Box>("social", std::ref(social_box));
|
|
||||||
for (const auto& btn : social_box->get_children()) {
|
|
||||||
const auto& name = btn->get_name();
|
|
||||||
const auto& icon_path = fmt::format("{}img/{}.png", preferences["data_path"], name.c_str());
|
|
||||||
Gtk::Image* image;
|
|
||||||
g_refGlade->get_widget<Gtk::Image>(name, image);
|
|
||||||
image->set(icon_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk::Grid* homepage_grid;
|
|
||||||
g_refGlade->get_widget<Gtk::Grid>("homepage", std::ref(homepage_grid));
|
|
||||||
for (const auto& widget : homepage_grid->get_children()) {
|
|
||||||
if (!G_TYPE_CHECK_INSTANCE_TYPE(widget->gobj(), GTK_TYPE_BUTTON)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto& casted_widget = GTK_BUTTON(widget->gobj());
|
|
||||||
if (gtk_button_get_image_position(casted_widget) != GtkPositionType::GTK_POS_RIGHT) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk::Image image(fmt::format("{}/img/external-link.png", preferences["data_path"]));
|
|
||||||
image.set_margin_start(2);
|
|
||||||
gtk_button_set_image(casted_widget, (GtkWidget*)image.gobj());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create pages
|
|
||||||
const auto& pages = fmt::format("{}/pages/{}", preferences["data_path"], preferences["default_locale"]);
|
|
||||||
|
|
||||||
for (const auto& page : fs::directory_iterator(pages)) {
|
|
||||||
Gtk::ScrolledWindow scrolled_window;
|
|
||||||
Gtk::Viewport viewport(Gtk::Adjustment::create(1, 1, 1), Gtk::Adjustment::create(1, 1, 1));
|
|
||||||
Gtk::Label label;
|
|
||||||
label.set_line_wrap(true);
|
|
||||||
Gtk::Image image(Gtk::Stock::GO_BACK, Gtk::ICON_SIZE_BUTTON);
|
|
||||||
Gtk::Button backBtn;
|
|
||||||
backBtn.set_image(image);
|
|
||||||
backBtn.set_name("home");
|
|
||||||
backBtn.signal_clicked().connect(sigc::bind(sigc::ptr_fun(on_btn_clicked), (GtkWidget*)&backBtn));
|
|
||||||
|
|
||||||
Gtk::Grid grid;
|
|
||||||
grid.attach(backBtn, 0, 1, 1, 1);
|
|
||||||
grid.attach(label, 1, 2, 1, 1);
|
|
||||||
viewport.add(grid);
|
|
||||||
scrolled_window.add(viewport);
|
|
||||||
scrolled_window.show_all();
|
|
||||||
|
|
||||||
Gtk::Stack* stack;
|
|
||||||
g_refGlade->get_widget<Gtk::Stack>("stack", std::ref(stack));
|
|
||||||
stack->add(scrolled_window, page.path().stem().string() + "page");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shows the window and returns when it is closed.
|
|
||||||
return g_app->run(*g_refWindow);
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
#include "hello.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
auto app = Gtk::Application::create();
|
||||||
|
|
||||||
|
Hello hello(argc, argv);
|
||||||
|
|
||||||
|
// Shows the window and returns when it is closed.
|
||||||
|
return app->run(hello);
|
||||||
|
}
|
Loading…
Reference in New Issue