// SPDX-License-Identifier: MIT // // ES-DE Frontend // DateTimeComponent.cpp // // Provides the date and time, in absolute (actual date) or relative // (delta from current date and time) form. // Used by the gamelist views. // #include "components/DateTimeComponent.h" #include "Log.h" #include "Settings.h" #include "utils/LocalizationUtil.h" #include "utils/StringUtil.h" DateTimeComponent::DateTimeComponent() : mRenderer {Renderer::getInstance()} , mClockAccumulator {0} , mClockMode {false} , mDisplayRelative {false} , mBackgroundHorizontalPadding {0.0f, 0.0f} , mBackgroundVerticalPadding {0.0f, 0.0f} , mClockBgColor {0x00000000} , mClockBgColorEnd {0x00000000} , mClockColorGradientHorizontal {true} { // ISO 8601 date format. setFormat("%Y-%m-%d"); } DateTimeComponent::DateTimeComponent(const std::string& text, const std::shared_ptr<Font>& font, unsigned int color, Alignment horizontalAlignment, glm::vec3 pos, glm::vec2 size, unsigned int bgcolor) : TextComponent {text, font, color, horizontalAlignment, ALIGN_CENTER, glm::vec2 {1, 0}, pos, size, bgcolor} , mRenderer {Renderer::getInstance()} , mClockAccumulator {0} , mClockMode {false} , mDisplayRelative {false} , mBackgroundHorizontalPadding {0.0f, 0.0f} , mBackgroundVerticalPadding {0.0f, 0.0f} , mClockBgColor {0x00000000} , mClockBgColorEnd {0x00000000} , mClockColorGradientHorizontal {true} { // ISO 8601 date format. setFormat("%Y-%m-%d"); } void DateTimeComponent::setValue(const std::string& val) { mTime = val; onTextChanged(); } std::string DateTimeComponent::getValue() const { // Return time value as a string. return mTime; } void DateTimeComponent::setFormat(const std::string& format) { mFormat = format; onTextChanged(); } void DateTimeComponent::setDisplayRelative(bool displayRelative) { mDisplayRelative = displayRelative; onTextChanged(); } void DateTimeComponent::onTextChanged() { mText = getDisplayString(); TextComponent::onTextChanged(); } std::string DateTimeComponent::getDisplayString() const { if (mClockMode) return (Utils::Time::timeToString(Utils::Time::DateTime {Utils::Time::now()}.getTime(), mFormat)); if (mDisplayRelative) { // Workaround to handle Unix epoch for different time zones. if (mTime.getTime() < 82800) { if (mDefaultValue == "") return _p("theme", "never"); else return mDefaultValue; } Utils::Time::DateTime now {Utils::Time::now()}; Utils::Time::Duration dur {now.getTime() - mTime.getTime()}; std::string buf; if (dur.getDays() > 0) { buf = Utils::String::format(_np("theme", "%i day ago", "%i days ago", dur.getDays()), dur.getDays()); } else if (dur.getHours() > 0) { buf = Utils::String::format(_np("theme", "%i hour ago", "%i hours ago", dur.getHours()), dur.getHours()); } else if (dur.getMinutes() > 0) { buf = Utils::String::format( _np("theme", "%i minute ago", "%i minutes ago", dur.getMinutes()), dur.getMinutes()); } else { buf = Utils::String::format( _np("theme", "%i second ago", "%i seconds ago", dur.getSeconds()), dur.getSeconds()); } return std::string(buf); } if (mTime.getTime() == 0) { if (mDefaultValue == "") return _p("theme", "unknown"); else return mDefaultValue; } return Utils::Time::timeToString(mTime.getTime(), mFormat); } void DateTimeComponent::update(int deltaTime) { updateSelf(deltaTime); if (!mClockMode || (mClockMode && !Settings::getInstance()->getBool("DisplayClock"))) return; mClockAccumulator += deltaTime; if (mClockAccumulator >= 500) { mClockAccumulator = 0; mTime = Utils::Time::now(); const std::string newTime {Utils::Time::timeToString(mTime, mFormat)}; // The setValue() function with its text cache rebuild is an expensive operation so we only // call this when the actual date/time string needs updating. if (newTime != mText) setValue(newTime); } } void DateTimeComponent::render(const glm::mat4& parentTrans) { if (mClockMode && !Settings::getInstance()->getBool("DisplayClock")) return; if (mClockMode && mClockBgColor != 0x00000000) { glm::mat4 trans {parentTrans * getTransform()}; trans = glm::translate(trans, glm::vec3 {-mBackgroundHorizontalPadding.x, -mBackgroundVerticalPadding.x, 0.0f}); mRenderer->setMatrix(trans); mRenderer->drawRect( 0.0f, 0.0f, mSize.x + mBackgroundHorizontalPadding.x + mBackgroundHorizontalPadding.y, mSize.y + mBackgroundVerticalPadding.x + mBackgroundVerticalPadding.y, mClockBgColor, mClockBgColorEnd, mClockColorGradientHorizontal, mThemeOpacity, 1.0f, Renderer::BlendFactor::SRC_ALPHA, Renderer::BlendFactor::ONE_MINUS_SRC_ALPHA, mBackgroundCornerRadius); } // Render the component. TextComponent::render(parentTrans); } void DateTimeComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) { using namespace ThemeFlags; std::string elementType {"datetime"}; std::string componentName {"DateTimeComponent"}; if (element.substr(0, 6) == "clock_") { mClockMode = true; elementType = "clock"; componentName = "ClockComponent"; // Apply default clock settings as the theme may not define any configuration for it. setFont(Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT)); setLineSpacing(1.0f); const glm::vec2 scale { getParent() ? getParent()->getSize() : glm::vec2 {mRenderer->getScreenWidth(), mRenderer->getScreenHeight()}}; setPosition(0.018f * scale.x, 0.016f * scale.y); mSize.y = mFont->getLetterHeight(); setColor(0xFFFFFFFF); setFormat("%H:%M"); } GuiComponent::applyTheme(theme, view, element, properties); const ThemeData::ThemeElement* elem {theme->getElement(view, element, elementType)}; if (!elem) return; if (mClockMode && elem->has("scope")) { const std::string& scope {elem->get<std::string>("scope")}; if (scope == "shared") { mComponentScope = ComponentScope::SHARED; } else if (scope == "view") { mComponentScope = ComponentScope::VIEW; } else if (scope == "menu") { mComponentScope = ComponentScope::MENU; } else if (scope == "none") { mComponentScope = ComponentScope::NONE; } else { LOG(LogWarning) << componentName << ": Invalid theme configuration, property " "\"scope\" for element \"" << element.substr(6) << "\" defined as \"" << scope << "\""; } } if (properties & ThemeFlags::POSITION && elem->has("stationary")) { const std::string& stationary {elem->get<std::string>("stationary")}; if (stationary == "never") mStationary = Stationary::NEVER; else if (stationary == "always") mStationary = Stationary::ALWAYS; else if (stationary == "withinView") mStationary = Stationary::WITHIN_VIEW; else if (stationary == "betweenViews") mStationary = Stationary::BETWEEN_VIEWS; else LOG(LogWarning) << "DateTimeComponent: Invalid theme configuration, property " "\"stationary\" for element \"" << element.substr(9) << "\" defined as \"" << stationary << "\""; } if (properties & COLOR && elem->has("color")) setColor(elem->get<unsigned int>("color")); setRenderBackground(false); if (properties & COLOR && elem->has("backgroundColor")) { if (mClockMode) { mClockBgColor = elem->get<unsigned int>("backgroundColor"); if (elem->has("backgroundColorEnd")) mClockBgColorEnd = elem->get<unsigned int>("backgroundColorEnd"); else mClockBgColorEnd = mClockBgColor; if (elem->has("backgroundGradientType")) { const std::string& backgroundGradientType { elem->get<std::string>("backgroundGradientType")}; if (backgroundGradientType == "horizontal") { mClockColorGradientHorizontal = true; } else if (backgroundGradientType == "vertical") { mClockColorGradientHorizontal = false; } else { mClockColorGradientHorizontal = true; LOG(LogWarning) << componentName << ": Invalid theme configuration, property " "\"backgroundGradientType\" for element \"" << element.substr(6) << "\" defined as \"" << backgroundGradientType << "\""; } } } else { setBackgroundColor(elem->get<unsigned int>("backgroundColor")); setRenderBackground(true); } } if (!mClockMode && elem->has("backgroundMargins")) { setBackgroundMargins(glm::clamp(elem->get<glm::vec2>("backgroundMargins"), 0.0f, 0.5f) * mRenderer->getScreenWidth()); } if (mClockMode && elem->has("backgroundHorizontalPadding")) { const glm::vec2 backgroundHorizontalPadding { glm::clamp(elem->get<glm::vec2>("backgroundHorizontalPadding"), 0.0f, 0.2f)}; mBackgroundHorizontalPadding.x = backgroundHorizontalPadding.x * mRenderer->getScreenWidth(); mBackgroundHorizontalPadding.y = backgroundHorizontalPadding.y * mRenderer->getScreenWidth(); } if (mClockMode && elem->has("backgroundVerticalPadding")) { const glm::vec2 backgroundVerticalPadding { glm::clamp(elem->get<glm::vec2>("backgroundVerticalPadding"), 0.0f, 0.2f)}; mBackgroundVerticalPadding.x = backgroundVerticalPadding.x * mRenderer->getScreenHeight(); mBackgroundVerticalPadding.y = backgroundVerticalPadding.y * mRenderer->getScreenHeight(); } if (elem->has("backgroundCornerRadius")) { setBackgroundCornerRadius( glm::clamp(elem->get<float>("backgroundCornerRadius"), 0.0f, 0.5f) * mRenderer->getScreenWidth()); } if (properties & ALIGNMENT && elem->has("horizontalAlignment")) { const std::string& horizontalAlignment {elem->get<std::string>("horizontalAlignment")}; if (horizontalAlignment == "left") setHorizontalAlignment(ALIGN_LEFT); else if (horizontalAlignment == "center") setHorizontalAlignment(ALIGN_CENTER); else if (horizontalAlignment == "right") setHorizontalAlignment(ALIGN_RIGHT); else LOG(LogWarning) << componentName << ": Invalid theme configuration, property " "\"horizontalAlignment\" for element \"" << element.substr(elementType == "clock" ? 6 : 9) << "\" defined as \"" << horizontalAlignment << "\""; } if (properties & ALIGNMENT && elem->has("verticalAlignment")) { const std::string& verticalAlignment {elem->get<std::string>("verticalAlignment")}; if (verticalAlignment == "top") setVerticalAlignment(ALIGN_TOP); else if (verticalAlignment == "center") setVerticalAlignment(ALIGN_CENTER); else if (verticalAlignment == "bottom") setVerticalAlignment(ALIGN_BOTTOM); else LOG(LogWarning) << componentName << ": Invalid theme configuration, property " "\"verticalAlignment\" for element \"" << element.substr(elementType == "clock" ? 6 : 9) << "\" defined as \"" << verticalAlignment << "\""; } if (properties & METADATA && elem->has("metadata")) { mThemeMetadata = ""; const std::string& metadata {elem->get<std::string>("metadata")}; if (metadata == "releasedate" || metadata == "lastplayed") { if (elem->has("defaultValue")) { const std::string& defaultValue {elem->get<std::string>("defaultValue")}; if (defaultValue == ":space:") mDefaultValue = " "; else mDefaultValue = defaultValue; } mThemeMetadata = metadata; } else { LOG(LogWarning) << "DateTimeComponent: Invalid theme configuration, property " "\"metadata\" for element \"" << element.substr(9) << "\" defined as \"" << metadata << "\""; } } if (mThemeMetadata == "lastplayed") setDisplayRelative(true); if (elem->has("displayRelative")) setDisplayRelative(elem->get<bool>("displayRelative")); if (properties & LETTER_CASE && elem->has("letterCase")) { const std::string& letterCase {elem->get<std::string>("letterCase")}; if (letterCase == "uppercase") { setUppercase(true); } else if (letterCase == "lowercase") { setLowercase(true); } else if (letterCase == "capitalize") { setCapitalize(true); } else if (letterCase != "none") { LOG(LogWarning) << componentName << ": Invalid theme configuration, property " "\"letterCase\" for element \"" << element.substr(9) << "\" defined as \"" << letterCase << "\""; } } float maxHeight {0.0f}; bool hasSize {false}; if (elem->has("size")) { const glm::vec2 size {elem->get<glm::vec2>("size")}; if (size.x != 0.0f && size.y != 0.0f) { maxHeight = mSize.y * 2.0f; hasSize = true; } } if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f)); if (getAutoCalcExtent() == glm::ivec2 {1, 0} && !hasSize) mSize.y = 0.0f; setFont(Font::getFromTheme(elem, properties, mFont, maxHeight)); mSize = glm::round(mSize); if (elem->has("format")) setFormat(elem->get<std::string>("format")); else if (mClockMode) setFormat("%H:%M"); }