diff options
Diffstat (limited to 'xbmc/addons/gui')
-rw-r--r-- | xbmc/addons/gui/CMakeLists.txt | 13 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIDialogAddonInfo.cpp | 940 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIDialogAddonInfo.h | 162 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIDialogAddonSettings.cpp | 484 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIDialogAddonSettings.h | 54 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIHelpers.cpp | 49 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIHelpers.h | 41 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIViewStateAddonBrowser.cpp | 64 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIViewStateAddonBrowser.h | 21 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIWindowAddonBrowser.cpp | 644 | ||||
-rw-r--r-- | xbmc/addons/gui/GUIWindowAddonBrowser.h | 115 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/CMakeLists.txt | 7 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimer.cpp | 97 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimer.h | 110 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimerManager.cpp | 222 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimerManager.h | 77 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimers.dox | 164 |
17 files changed, 3264 insertions, 0 deletions
diff --git a/xbmc/addons/gui/CMakeLists.txt b/xbmc/addons/gui/CMakeLists.txt new file mode 100644 index 0000000..2a9c463 --- /dev/null +++ b/xbmc/addons/gui/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES GUIDialogAddonInfo.cpp + GUIDialogAddonSettings.cpp + GUIHelpers.cpp + GUIViewStateAddonBrowser.cpp + GUIWindowAddonBrowser.cpp) + +set(HEADERS GUIDialogAddonInfo.h + GUIDialogAddonSettings.h + GUIHelpers.h + GUIViewStateAddonBrowser.h + GUIWindowAddonBrowser.h) + +core_add_library(addons_gui) diff --git a/xbmc/addons/gui/GUIDialogAddonInfo.cpp b/xbmc/addons/gui/GUIDialogAddonInfo.cpp new file mode 100644 index 0000000..3acdbf5 --- /dev/null +++ b/xbmc/addons/gui/GUIDialogAddonInfo.cpp @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogAddonInfo.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "addons/AddonDatabase.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "addons/AddonRepos.h" +#include "addons/AddonSystemSettings.h" +#include "addons/AudioDecoder.h" +#include "addons/ExtsMimeSupportList.h" +#include "addons/IAddon.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "addons/gui/GUIDialogAddonSettings.h" +#include "addons/gui/GUIHelpers.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "games/GameUtils.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "interfaces/builtins/Builtins.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pictures/GUIWindowSlideShow.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Digest.h" +#include "utils/JobManager.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <functional> +#include <sstream> +#include <utility> + +#define CONTROL_BTN_INSTALL 6 +#define CONTROL_BTN_ENABLE 7 +#define CONTROL_BTN_UPDATE 8 +#define CONTROL_BTN_SETTINGS 9 +#define CONTROL_BTN_DEPENDENCIES 10 +#define CONTROL_BTN_SELECT 12 +#define CONTROL_BTN_AUTOUPDATE 13 +#define CONTROL_BTN_VERSIONS 14 +#define CONTROL_LIST_SCREENSHOTS 50 + +using namespace KODI; +using namespace ADDON; +using namespace KODI::ADDONS; +using namespace XFILE; +using namespace KODI::MESSAGING; + +CGUIDialogAddonInfo::CGUIDialogAddonInfo(void) + : CGUIDialog(WINDOW_DIALOG_ADDON_INFO, "DialogAddonInfo.xml") +{ + m_item = CFileItemPtr(new CFileItem); + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogAddonInfo::~CGUIDialogAddonInfo(void) = default; + +bool CGUIDialogAddonInfo::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTN_UPDATE) + { + OnUpdate(); + return true; + } + else if (iControl == CONTROL_BTN_INSTALL) + { + const auto& itemAddonInfo = m_item->GetAddonInfo(); + if (!CServiceBroker::GetAddonMgr().IsAddonInstalled( + itemAddonInfo->ID(), itemAddonInfo->Origin(), itemAddonInfo->Version())) + { + OnInstall(); + return true; + } + else + { + m_silentUninstall = false; + OnUninstall(); + return true; + } + } + else if (iControl == CONTROL_BTN_SELECT) + { + OnSelect(); + return true; + } + else if (iControl == CONTROL_BTN_ENABLE) + { + OnEnableDisable(); + return true; + } + else if (iControl == CONTROL_BTN_SETTINGS) + { + OnSettings(); + return true; + } + else if (iControl == CONTROL_BTN_DEPENDENCIES) + { + ShowDependencyList(Reactivate::CHOICE_YES, EntryPoint::SHOW_DEPENDENCIES); + return true; + } + else if (iControl == CONTROL_BTN_AUTOUPDATE) + { + OnToggleAutoUpdates(); + return true; + } + else if (iControl == CONTROL_LIST_SCREENSHOTS) + { + if (message.GetParam1() == ACTION_SELECT_ITEM || + message.GetParam1() == ACTION_MOUSE_LEFT_CLICK) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl); + OnMessage(msg); + int start = msg.GetParam1(); + if (start >= 0 && start < static_cast<int>(m_item->GetAddonInfo()->Screenshots().size())) + CGUIWindowSlideShow::RunSlideShow(m_item->GetAddonInfo()->Screenshots(), start); + } + } + else if (iControl == CONTROL_BTN_VERSIONS) + { + OnSelectVersion(); + return true; + } + } + break; + default: + break; + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogAddonInfo::OnAction(const CAction& action) +{ + if (action.GetID() == ACTION_SHOW_INFO) + { + Close(); + return true; + } + return CGUIDialog::OnAction(action); +} + +void CGUIDialogAddonInfo::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + BuildDependencyList(); + UpdateControls(PerformButtonFocus::CHOICE_YES); +} + +void CGUIDialogAddonInfo::UpdateControls(PerformButtonFocus performButtonFocus) +{ + if (!m_item) + return; + + const auto& itemAddonInfo = m_item->GetAddonInfo(); + bool isInstalled = CServiceBroker::GetAddonMgr().IsAddonInstalled( + itemAddonInfo->ID(), itemAddonInfo->Origin(), itemAddonInfo->Version()); + m_addonEnabled = + m_localAddon && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_localAddon->ID()); + bool canDisable = + isInstalled && CServiceBroker::GetAddonMgr().CanAddonBeDisabled(m_localAddon->ID()); + bool canInstall = !isInstalled && itemAddonInfo->LifecycleState() != AddonLifecycleState::BROKEN; + bool canUninstall = m_localAddon && CServiceBroker::GetAddonMgr().CanUninstall(m_localAddon); + + bool isUpdate = (!isInstalled && CServiceBroker::GetAddonMgr().IsAddonInstalled( + itemAddonInfo->ID(), itemAddonInfo->Origin())); + + bool showUpdateButton = m_localAddon && + CServiceBroker::GetAddonMgr().IsAutoUpdateable(m_localAddon->ID()) && + m_item->GetProperty("Addon.HasUpdate").asBoolean(); + + if (isInstalled) + { + SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24037); // uninstall + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_INSTALL, canUninstall); + } + else + { + if (isUpdate) + { + SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24138); // update + } + else + { + SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24038); // install + } + + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_INSTALL, canInstall); + if (canInstall && performButtonFocus == PerformButtonFocus::CHOICE_YES) + { + SET_CONTROL_FOCUS(CONTROL_BTN_INSTALL, 0); + } + } + + if (showUpdateButton) + { + SET_CONTROL_VISIBLE(CONTROL_BTN_UPDATE); + SET_CONTROL_HIDDEN(CONTROL_BTN_VERSIONS); + } + else + { + SET_CONTROL_VISIBLE(CONTROL_BTN_VERSIONS); + SET_CONTROL_HIDDEN(CONTROL_BTN_UPDATE); + } + + if (m_addonEnabled) + { + SET_CONTROL_LABEL(CONTROL_BTN_ENABLE, 24021); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_ENABLE, canDisable); + } + else + { + SET_CONTROL_LABEL(CONTROL_BTN_ENABLE, 24022); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_ENABLE, isInstalled); + } + + bool autoUpdatesOn = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_ADDONS_AUTOUPDATES) == AUTO_UPDATES_ON; + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_AUTOUPDATE, isInstalled && autoUpdatesOn); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_AUTOUPDATE, + isInstalled && autoUpdatesOn && + CServiceBroker::GetAddonMgr().IsAutoUpdateable(m_localAddon->ID())); + SET_CONTROL_LABEL(CONTROL_BTN_AUTOUPDATE, 21340); + + const bool active = m_localAddon && CAddonSystemSettings::GetInstance().IsActive(*m_localAddon); + CONTROL_ENABLE_ON_CONDITION( + CONTROL_BTN_SELECT, + m_addonEnabled && (CanShowSupportList() || CanOpen() || CanRun() || (CanUse() && !active))); + + int label; + if (CanShowSupportList()) + label = 21484; + else if (CanUse()) + label = 21480; + else if (CanOpen()) + label = 21478; + else + label = 21479; + SET_CONTROL_LABEL(CONTROL_BTN_SELECT, label); + + const bool hasSettings = m_localAddon && m_localAddon->CanHaveAddonOrInstanceSettings(); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_SETTINGS, isInstalled && hasSettings); + if (isInstalled && hasSettings && performButtonFocus == PerformButtonFocus::CHOICE_YES) + { + SET_CONTROL_FOCUS(CONTROL_BTN_SETTINGS, 0); + } + + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_DEPENDENCIES, !m_depsInstalledWithAvailable.empty()); + + CFileItemList items; + for (const auto& screenshot : m_item->GetAddonInfo()->Screenshots()) + { + auto item = std::make_shared<CFileItem>(""); + item->SetArt("thumb", screenshot); + items.Add(std::move(item)); + } + CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST_SCREENSHOTS, 0, 0, &items); + OnMessage(msg); +} + +static const std::string LOCAL_CACHE = + "\\0_local_cache"; // \0 to give it the lowest priority when sorting + +int CGUIDialogAddonInfo::AskForVersion(std::vector<std::pair<CAddonVersion, std::string>>& versions) +{ + auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + dialog->Reset(); + dialog->SetHeading(CVariant{21338}); + dialog->SetUseDetails(true); + + for (const auto& versionInfo : versions) + { + CFileItem item(StringUtils::Format(g_localizeStrings.Get(21339), versionInfo.first.asString())); + if (m_localAddon && m_localAddon->Version() == versionInfo.first && + m_item->GetAddonInfo()->Origin() == versionInfo.second) + item.Select(true); + + AddonPtr repo; + if (versionInfo.second == LOCAL_CACHE) + { + item.SetLabel2(g_localizeStrings.Get(24095)); + item.SetArt("icon", "DefaultAddonRepository.png"); + dialog->Add(item); + } + else if (CServiceBroker::GetAddonMgr().GetAddon(versionInfo.second, repo, AddonType::REPOSITORY, + OnlyEnabled::CHOICE_YES)) + { + item.SetLabel2(repo->Name()); + item.SetArt("icon", repo->Icon()); + dialog->Add(item); + } + } + + dialog->Open(); + return dialog->IsConfirmed() ? dialog->GetSelectedItem() : -1; +} + +void CGUIDialogAddonInfo::OnUpdate() +{ + const auto& itemAddonInfo = m_item->GetAddonInfo(); + const std::string& addonId = itemAddonInfo->ID(); + const std::string& origin = m_item->GetProperty("Addon.ValidUpdateOrigin").asString(); + const CAddonVersion& version = + static_cast<CAddonVersion>(m_item->GetProperty("Addon.ValidUpdateVersion").asString()); + + Close(); + if (!m_depsInstalledWithAvailable.empty() && + !ShowDependencyList(Reactivate::CHOICE_NO, EntryPoint::UPDATE)) + return; + + CAddonInstaller::GetInstance().Install(addonId, version, origin); +} + +void CGUIDialogAddonInfo::OnSelectVersion() +{ + if (!m_item->HasAddonInfo()) + return; + + const std::string& processAddonId = m_item->GetAddonInfo()->ID(); + EntryPoint entryPoint = m_localAddon ? EntryPoint::UPDATE : EntryPoint::INSTALL; + + // get all compatible versions of an addon-id regardless of their origin + std::vector<std::shared_ptr<IAddon>> compatibleVersions = + CServiceBroker::GetAddonMgr().GetCompatibleVersions(processAddonId); + + std::vector<std::pair<CAddonVersion, std::string>> versions; + versions.reserve(compatibleVersions.size()); + + for (const auto& compatibleVersion : compatibleVersions) + versions.emplace_back(compatibleVersion->Version(), compatibleVersion->Origin()); + + CAddonDatabase database; + database.Open(); + + CFileItemList items; + if (XFILE::CDirectory::GetDirectory("special://home/addons/packages/", items, ".zip", + DIR_FLAG_NO_FILE_DIRS)) + { + for (int i = 0; i < items.Size(); ++i) + { + std::string packageId; + std::string versionString; + if (CAddonVersion::SplitFileName(packageId, versionString, items[i]->GetLabel())) + { + if (packageId == processAddonId) + { + std::string hash; + std::string path(items[i]->GetPath()); + if (database.GetPackageHash(processAddonId, items[i]->GetPath(), hash)) + { + std::string sha256 = CUtil::GetFileDigest(path, KODI::UTILITY::CDigest::Type::SHA256); + + // don't offer locally cached packages that result in an invalid version. + // usually this happens when the package filename gets malformed on the fs + // e.g. downloading "http://localhost/a+b.zip" ends up in "a b.zip" + const CAddonVersion version(versionString); + if (StringUtils::EqualsNoCase(sha256, hash) && !version.empty()) + versions.emplace_back(version, LOCAL_CACHE); + } + } + } + } + } + + if (versions.empty()) + HELPERS::ShowOKDialogText(CVariant{21341}, CVariant{21342}); + else + { + int i = AskForVersion(versions); + if (i != -1) + { + Close(); + + if (versions[i].second == LOCAL_CACHE) + { + CAddonInstaller::GetInstance().InstallFromZip( + StringUtils::Format("special://home/addons/packages/{}-{}.zip", processAddonId, + versions[i].first.asString())); + } + else + { + if (!m_depsInstalledWithAvailable.empty() && + !ShowDependencyList(Reactivate::CHOICE_NO, entryPoint)) + return; + CAddonInstaller::GetInstance().Install(processAddonId, versions[i].first, + versions[i].second); + } + } + } +} + +void CGUIDialogAddonInfo::OnToggleAutoUpdates() +{ + CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), CONTROL_BTN_AUTOUPDATE); + if (OnMessage(msg)) + { + bool selected = msg.GetParam1() == 1; + if (selected) + CServiceBroker::GetAddonMgr().RemoveAllUpdateRulesFromList(m_localAddon->ID()); + else + CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_localAddon->ID(), + AddonUpdateRule::USER_DISABLED_AUTO_UPDATE); + + bool showUpdateButton = (selected && m_item->GetProperty("Addon.HasUpdate").asBoolean()); + + if (showUpdateButton) + { + SET_CONTROL_VISIBLE(CONTROL_BTN_UPDATE); + SET_CONTROL_HIDDEN(CONTROL_BTN_VERSIONS); + } + else + { + SET_CONTROL_VISIBLE(CONTROL_BTN_VERSIONS); + SET_CONTROL_HIDDEN(CONTROL_BTN_UPDATE); + } + + CServiceBroker::GetAddonMgr().PublishEventAutoUpdateStateChanged(m_localAddon->ID()); + } +} + +void CGUIDialogAddonInfo::OnInstall() +{ + if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) + return; + + if (!m_item->HasAddonInfo()) + return; + + const auto& itemAddonInfo = m_item->GetAddonInfo(); + const std::string& origin = itemAddonInfo->Origin(); + + if (m_localAddon && CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode() != + AddonRepoUpdateMode::ANY_REPOSITORY) + { + if (m_localAddon->Origin() != origin && m_localAddon->Origin() != ORIGIN_SYSTEM) + { + const std::string& header = g_localizeStrings.Get(19098); // Warning! + const std::string origin = + !m_localAddon->Origin().empty() ? m_localAddon->Origin() : g_localizeStrings.Get(39029); + const std::string text = + StringUtils::Format(g_localizeStrings.Get(39028), m_localAddon->Name(), origin, + m_localAddon->Version().asString()); + + if (CGUIDialogYesNo::ShowAndGetInput(header, text)) + { + m_silentUninstall = true; + OnUninstall(); + } + else + { + return; + } + } + } + + const std::string& addonId = itemAddonInfo->ID(); + const CAddonVersion& version = itemAddonInfo->Version(); + + Close(); + if (!m_depsInstalledWithAvailable.empty() && + !ShowDependencyList(Reactivate::CHOICE_NO, EntryPoint::INSTALL)) + return; + + CAddonInstaller::GetInstance().Install(addonId, version, origin); +} + +void CGUIDialogAddonInfo::OnSelect() +{ + if (!m_localAddon) + return; + + if (CanShowSupportList()) + { + ShowSupportList(); + return; + } + + Close(); + + if (CanOpen() || CanRun()) + CBuiltins::GetInstance().Execute("RunAddon(" + m_localAddon->ID() + ")"); + else if (CanUse()) + CAddonSystemSettings::GetInstance().SetActive(m_localAddon->Type(), m_localAddon->ID()); +} + +bool CGUIDialogAddonInfo::CanOpen() const +{ + return m_localAddon && m_localAddon->Type() == AddonType::PLUGIN; +} + +bool CGUIDialogAddonInfo::CanRun() const +{ + if (m_localAddon) + { + if (m_localAddon->Type() == AddonType::SCRIPT) + return true; + + if (GAME::CGameUtils::IsStandaloneGame(m_localAddon)) + return true; + } + + return false; +} + +bool CGUIDialogAddonInfo::CanUse() const +{ + return m_localAddon && (m_localAddon->Type() == AddonType::SKIN || + m_localAddon->Type() == AddonType::SCREENSAVER || + m_localAddon->Type() == AddonType::VISUALIZATION || + m_localAddon->Type() == AddonType::SCRIPT_WEATHER || + m_localAddon->Type() == AddonType::RESOURCE_LANGUAGE || + m_localAddon->Type() == AddonType::RESOURCE_UISOUNDS || + m_localAddon->Type() == AddonType::AUDIOENCODER); +} + +bool CGUIDialogAddonInfo::CanShowSupportList() const +{ + return m_localAddon && (m_localAddon->Type() == AddonType::AUDIODECODER || + m_localAddon->Type() == AddonType::IMAGEDECODER); +} + +bool CGUIDialogAddonInfo::PromptIfDependency(int heading, int line2) +{ + if (!m_localAddon) + return false; + + VECADDONS addons; + std::vector<std::string> deps; + CServiceBroker::GetAddonMgr().GetAddons(addons); + for (VECADDONS::const_iterator it = addons.begin(); it != addons.end(); ++it) + { + auto i = + std::find_if((*it)->GetDependencies().begin(), (*it)->GetDependencies().end(), + [&](const DependencyInfo& other) { return other.id == m_localAddon->ID(); }); + if (i != (*it)->GetDependencies().end() && !i->optional) // non-optional dependency + deps.push_back((*it)->Name()); + } + + if (!deps.empty()) + { + std::string line0 = StringUtils::Format(g_localizeStrings.Get(24046), m_localAddon->Name()); + std::string line1 = StringUtils::Join(deps, ", "); + HELPERS::ShowOKDialogLines(CVariant{heading}, CVariant{std::move(line0)}, + CVariant{std::move(line1)}, CVariant{line2}); + return true; + } + return false; +} + +void CGUIDialogAddonInfo::OnUninstall() +{ + if (!m_localAddon.get()) + return; + + if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) + return; + + // ensure the addon is not a dependency of other installed addons + if (PromptIfDependency(24037, 24047)) + return; + + // prompt user to be sure + if (!m_silentUninstall && !CGUIDialogYesNo::ShowAndGetInput(CVariant{24037}, CVariant{750})) + return; + + bool removeData = false; + if (CDirectory::Exists(m_localAddon->Profile())) + removeData = CGUIDialogYesNo::ShowAndGetInput(CVariant{24037}, CVariant{39014}); + + CAddonInstaller::GetInstance().UnInstall(m_localAddon, removeData); + + Close(); +} + +void CGUIDialogAddonInfo::OnEnableDisable() +{ + if (!m_localAddon) + return; + + if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) + return; + + if (m_addonEnabled) + { + if (PromptIfDependency(24075, 24091)) + return; //required. can't disable + + CServiceBroker::GetAddonMgr().DisableAddon(m_localAddon->ID(), AddonDisabledReason::USER); + } + else + { + // Check user want to enable if lifecycle not normal + if (!ADDON::GUI::CHelpers::DialogAddonLifecycleUseAsk(m_localAddon)) + return; + + CServiceBroker::GetAddonMgr().EnableAddon(m_localAddon->ID()); + } + + UpdateControls(PerformButtonFocus::CHOICE_NO); +} + +void CGUIDialogAddonInfo::OnSettings() +{ + CGUIDialogAddonSettings::ShowForAddon(m_localAddon); +} + +bool CGUIDialogAddonInfo::ShowDependencyList(Reactivate reactivate, EntryPoint entryPoint) +{ + if (entryPoint != EntryPoint::INSTALL || m_showDepDialogOnInstall) + { + auto pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + CFileItemList items; + + for (const auto& it : m_depsInstalledWithAvailable) + { + // All combinations of depAddon and localAddon validity are possible and information + // must be displayed even when there is no depAddon. + // info_addon is the add-on to take the information to display (name, icon) from. The + // version in the repository is preferred because it might contain more recent data. + + std::shared_ptr<IAddon> infoAddon = it.m_available ? it.m_available : it.m_installed; + + if (infoAddon) + { + if (entryPoint != EntryPoint::UPDATE || !it.IsInstalledUpToDate()) + { + const CFileItemPtr item = std::make_shared<CFileItem>(infoAddon->Name()); + int messageId = 24180; // minversion only + + // dep not installed locally, but it is available from a repo! + // make sure only non-optional add-ons that meet versionMin are + // announced for installation + + if (!it.m_installed) + { + if (entryPoint != EntryPoint::SHOW_DEPENDENCIES && !it.m_depInfo.optional) + { + if (it.m_depInfo.versionMin <= it.m_available->Version()) + { + messageId = 24181; // => install + } + else + { + messageId = 24185; // => not available, only lower versions available in the repos + } + } + } + else // dep is installed locally + { + messageId = 24182; // => installed + + if (!it.IsInstalledUpToDate()) + { + messageId = 24183; // => update to + } + } + + if (entryPoint == EntryPoint::SHOW_DEPENDENCIES || + infoAddon->MainType() != AddonType::SCRIPT_MODULE || + !CAddonRepos::IsFromOfficialRepo(infoAddon, CheckAddonPath::CHOICE_NO)) + { + item->SetLabel2(StringUtils::Format( + g_localizeStrings.Get(messageId), it.m_depInfo.versionMin.asString(), + it.m_installed ? it.m_installed->Version().asString() : "", + it.m_available ? it.m_available->Version().asString() : "", + it.m_depInfo.optional ? g_localizeStrings.Get(24184) : "")); + + item->SetArt("icon", infoAddon->Icon()); + item->SetProperty("addon_id", it.m_depInfo.id); + items.Add(item); + } + } + } + else + { + const CFileItemPtr item = std::make_shared<CFileItem>(it.m_depInfo.id); + item->SetLabel2(g_localizeStrings.Get(10005)); // Not available + items.Add(item); + } + } + + if (!items.IsEmpty()) + { + CFileItemPtr backup_item = GetCurrentListItem(); + while (true) + { + pDialog->Reset(); + pDialog->SetHeading(reactivate == Reactivate::CHOICE_YES ? 39024 : 39020); + pDialog->SetUseDetails(true); + for (auto& it : items) + pDialog->Add(*it); + pDialog->EnableButton(reactivate == Reactivate::CHOICE_NO, 186); + pDialog->SetButtonFocus(true); + pDialog->Open(); + + if (pDialog->IsButtonPressed()) + return true; + + if (pDialog->IsConfirmed()) + { + const CFileItemPtr& item = pDialog->GetSelectedFileItem(); + std::string addon_id = item->GetProperty("addon_id").asString(); + std::shared_ptr<IAddon> depAddon; + if (CServiceBroker::GetAddonMgr().FindInstallableById(addon_id, depAddon)) + { + Close(); + ShowForItem(std::make_shared<CFileItem>(depAddon)); + } + } + else + break; + } + SetItem(backup_item); + if (reactivate == Reactivate::CHOICE_YES) + Open(); + + return false; + } + } + + return true; +} + +void CGUIDialogAddonInfo::ShowSupportList() +{ + std::vector<KODI::ADDONS::AddonSupportEntry> list; + if (CanShowSupportList()) + list = + CServiceBroker::GetExtsMimeSupportList().GetSupportedExtsAndMimeTypes(m_localAddon->ID()); + + auto pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + CFileItemList items; + for (const auto& entry : list) + { + // Ignore included extension about track support + if (StringUtils::EndsWith(entry.m_name, KODI_ADDON_AUDIODECODER_TRACK_EXT)) + continue; + + std::string label; + if (entry.m_type == AddonSupportType::Extension) + label = StringUtils::Format(g_localizeStrings.Get(21346), entry.m_name); + else if (entry.m_type == AddonSupportType::Mimetype) + label = StringUtils::Format(g_localizeStrings.Get(21347), entry.m_name); + else + label = entry.m_name; + + const CFileItemPtr item = std::make_shared<CFileItem>(label); + item->SetLabel2(entry.m_description); + if (!entry.m_icon.empty()) + item->SetArt("icon", entry.m_icon); + else if (entry.m_type == AddonSupportType::Extension) + item->SetArt("icon", "DefaultExtensionInfo.png"); + else if (entry.m_type == AddonSupportType::Mimetype) + item->SetArt("icon", "DefaultMimetypeInfo.png"); + item->SetProperty("addon_id", m_localAddon->ID()); + items.Add(item); + } + + pDialog->Reset(); + pDialog->SetHeading(21485); + pDialog->SetUseDetails(true); + for (auto& it : items) + pDialog->Add(*it); + pDialog->SetButtonFocus(true); + pDialog->Open(); +} + +bool CGUIDialogAddonInfo::ShowForItem(const CFileItemPtr& item) +{ + if (!item) + return false; + + CGUIDialogAddonInfo* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonInfo>( + WINDOW_DIALOG_ADDON_INFO); + if (!dialog) + return false; + if (!dialog->SetItem(item)) + return false; + + dialog->Open(); + return true; +} + +bool CGUIDialogAddonInfo::SetItem(const CFileItemPtr& item) +{ + if (!item || !item->HasAddonInfo()) + return false; + + m_item = std::make_shared<CFileItem>(*item); + m_localAddon.reset(); + if (CServiceBroker::GetAddonMgr().GetAddon(item->GetAddonInfo()->ID(), m_localAddon, + OnlyEnabled::CHOICE_NO)) + { + CLog::Log(LOGDEBUG, "{} - Addon with id {} not found locally.", __FUNCTION__, + item->GetAddonInfo()->ID()); + } + return true; +} + +void CGUIDialogAddonInfo::BuildDependencyList() +{ + if (!m_item) + return; + + m_showDepDialogOnInstall = false; + m_depsInstalledWithAvailable.clear(); + m_deps = CServiceBroker::GetAddonMgr().GetDepsRecursive(m_item->GetAddonInfo()->ID(), + OnlyEnabledRootAddon::CHOICE_NO); + + for (const auto& dep : m_deps) + { + std::shared_ptr<IAddon> addonInstalled; + std::shared_ptr<IAddon> addonAvailable; + + // Find add-on in local installation + if (!CServiceBroker::GetAddonMgr().GetAddon(dep.id, addonInstalled, OnlyEnabled::CHOICE_YES)) + { + addonInstalled = nullptr; + } + + // Find add-on in repositories + if (!CServiceBroker::GetAddonMgr().FindInstallableById(dep.id, addonAvailable)) + { + addonAvailable = nullptr; + } + + if (!addonInstalled) + { + + // after pushing the install button the dependency install dialog + // will be opened only if... + // - dependencies are unavailable (for informational purposes) OR + // - the dependency is not a script/module OR + // - the script/module is not available at an official repo + if (!addonAvailable || addonAvailable->MainType() != AddonType::SCRIPT_MODULE || + !CAddonRepos::IsFromOfficialRepo(addonAvailable, CheckAddonPath::CHOICE_NO)) + { + m_showDepDialogOnInstall = true; + } + } + else + { + + // only display dialog if updates for already installed dependencies will install + if (addonAvailable && addonAvailable->Version() > addonInstalled->Version()) + { + m_showDepDialogOnInstall = true; + } + } + + m_depsInstalledWithAvailable.emplace_back(dep, addonInstalled, addonAvailable); + } + + std::sort(m_depsInstalledWithAvailable.begin(), m_depsInstalledWithAvailable.end(), + [](const auto& a, const auto& b) { + // 1. "not installed/available" go to the bottom first + const bool depAInstalledOrAvailable = + a.m_installed != nullptr || a.m_available != nullptr; + const bool depBInstalledOrAvailable = + b.m_installed != nullptr || b.m_available != nullptr; + + if (depAInstalledOrAvailable != depBInstalledOrAvailable) + { + return !depAInstalledOrAvailable; + } + + // 2. then optional add-ons to top + if (a.m_depInfo.optional != b.m_depInfo.optional) + { + return a.m_depInfo.optional; + } + + // 3. addon type asc, except scripts/modules at the bottom + const std::shared_ptr<IAddon>& depA = a.m_installed ? a.m_installed : a.m_available; + const std::shared_ptr<IAddon>& depB = b.m_installed ? b.m_installed : b.m_available; + + if (depA && depB) + { + const AddonType typeA = depA->MainType(); + const AddonType typeB = depB->MainType(); + if (typeA != typeB) + { + if ((typeA == AddonType::SCRIPT_MODULE) == (typeB == AddonType::SCRIPT_MODULE)) + { + // both are scripts/modules or neither one is => sort by addon type asc + return typeA < typeB; + } + else + { + // At this point, either: + // A is script/module and B is not, or A is not script/module and B is. + // the script/module goes to the bottom + return typeA != AddonType::SCRIPT_MODULE; + } + } + } + + // 4. finally order by addon-id + return a.m_depInfo.id < b.m_depInfo.id; + }); +} + +bool CInstalledWithAvailable::IsInstalledUpToDate() const +{ + if (m_installed) + { + if (!m_available || m_available->Version() == m_installed->Version()) + { + return true; + } + } + + return false; +} diff --git a/xbmc/addons/gui/GUIDialogAddonInfo.h b/xbmc/addons/gui/GUIDialogAddonInfo.h new file mode 100644 index 0000000..b28a858 --- /dev/null +++ b/xbmc/addons/gui/GUIDialogAddonInfo.h @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/addoninfo/AddonInfo.h" +#include "guilib/GUIDialog.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +namespace ADDON +{ +class IAddon; +using AddonPtr = std::shared_ptr<IAddon>; + +} // namespace ADDON + +enum class Reactivate : bool +{ + CHOICE_YES = true, + CHOICE_NO = false, +}; + +enum class PerformButtonFocus : bool +{ + CHOICE_YES = true, + CHOICE_NO = false, +}; + +enum class EntryPoint : int +{ + INSTALL, + UPDATE, + SHOW_DEPENDENCIES, +}; + +struct CInstalledWithAvailable +{ + CInstalledWithAvailable(const ADDON::DependencyInfo& depInfo, + const std::shared_ptr<ADDON::IAddon>& installed, + const std::shared_ptr<ADDON::IAddon>& available) + : m_depInfo(depInfo), m_installed(installed), m_available(available) + { + } + + /*! + * @brief Returns true if the currently installed dependency version is up to date + * or the dependency is not available from a repository + */ + bool IsInstalledUpToDate() const; + + ADDON::DependencyInfo m_depInfo; + std::shared_ptr<ADDON::IAddon> m_installed; + std::shared_ptr<ADDON::IAddon> m_available; +}; + +class CGUIDialogAddonInfo : public CGUIDialog +{ +public: + CGUIDialogAddonInfo(void); + ~CGUIDialogAddonInfo(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + + CFileItemPtr GetCurrentListItem(int offset = 0) override { return m_item; } + bool HasListItems() const override { return true; } + + static bool ShowForItem(const CFileItemPtr& item); + +private: + void OnInitWindow() override; + + /*! + * @brief Set the item to display addon info on. + * + * @param[in] item to display + * @return true if we can display information, false otherwise + */ + bool SetItem(const CFileItemPtr& item); + void UpdateControls(PerformButtonFocus performButtonFocus); + + void OnUpdate(); + void OnSelectVersion(); + void OnInstall(); + void OnUninstall(); + void OnEnableDisable(); + void OnSettings(); + void OnSelect(); + void OnToggleAutoUpdates(); + int AskForVersion(std::vector<std::pair<ADDON::CAddonVersion, std::string>>& versions); + + /*! + * @brief Returns true if current addon can be opened (i.e is a plugin) + */ + bool CanOpen() const; + + /*! + * @brief Returns true if current addon can be run (i.e is a script) + */ + bool CanRun() const; + + /*! + * @brief Returns true if current addon is of a type that can only have one active + * in use at a time and can be changed (e.g skins) + */ + bool CanUse() const; + + /*! + * @brief Returns true if current addon can be show list about supported parts + */ + bool CanShowSupportList() const; + + /*! + * @brief check if the add-on is a dependency of others, and if so prompt the user. + * + * @param[in] heading the label for the heading of the prompt dialog + * @param[in] line2 the action that could not be completed. + * @return true if prompted, false otherwise. + */ + bool PromptIfDependency(int heading, int line2); + + /*! + * @brief Show a dialog with the addon's dependencies. + * + * @param[in] reactivate If true, reactivate info dialog when done + * @param[in] entryPoint INSTALL, UPDATE or SHOW_DEPENDENCIES + * @return True if okay was selected, false otherwise + */ + bool ShowDependencyList(Reactivate reactivate, EntryPoint entryPoint); + + /*! + * @brief Show a dialog with the addon's supported extensions and mimetypes. + */ + void ShowSupportList(); + + /*! + * @brief Used to build up the dependency list shown by @ref ShowDependencyList() + */ + void BuildDependencyList(); + + CFileItemPtr m_item; + ADDON::AddonPtr m_localAddon; + bool m_addonEnabled = false; + + /*!< a switch to force @ref OnUninstall() to proceed without user interaction. + * useful for cases like where another repo’s version of an addon must + * be removed before installing a new version. + */ + bool m_silentUninstall = false; + + bool m_showDepDialogOnInstall = false; + std::vector<ADDON::DependencyInfo> m_deps; + std::vector<CInstalledWithAvailable> m_depsInstalledWithAvailable; +}; diff --git a/xbmc/addons/gui/GUIDialogAddonSettings.cpp b/xbmc/addons/gui/GUIDialogAddonSettings.cpp new file mode 100644 index 0000000..bc794d4 --- /dev/null +++ b/xbmc/addons/gui/GUIDialogAddonSettings.cpp @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogAddonSettings.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/settings/AddonSettings.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "input/Key.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/SettingSection.h" +#include "settings/lib/SettingsManager.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "view/ViewStateSettings.h" + +#define CONTROL_BTN_LEVELS 20 + +using namespace ADDON; +using namespace KODI::MESSAGING; + +CGUIDialogAddonSettings::CGUIDialogAddonSettings() + : CGUIDialogSettingsManagerBase(WINDOW_DIALOG_ADDON_SETTINGS, "DialogAddonSettings.xml") +{ +} + +bool CGUIDialogAddonSettings::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + if (message.GetSenderId() == CONTROL_SETTINGS_CUSTOM_BUTTON) + { + OnResetSettings(); + return true; + } + break; + } + + case GUI_MSG_SETTING_UPDATED: + { + const std::string& settingId = message.GetStringParam(0); + const std::string& settingValue = message.GetStringParam(1); + const ADDON::AddonInstanceId instanceId = message.GetParam1(); + + if (instanceId != m_instanceId) + { + CLog::Log(LOGERROR, + "CGUIDialogAddonSettings::{}: Set value \"{}\" from add-on \"{}\" called with " + "invalid instance id (given: {}, needed: {})", + __func__, m_addon->ID(), settingId, instanceId, m_instanceId); + break; + } + + std::shared_ptr<CSetting> setting = GetSettingsManager()->GetSetting(settingId); + if (setting != nullptr) + { + setting->FromString(settingValue); + return true; + } + break; + } + + default: + break; + } + + return CGUIDialogSettingsManagerBase::OnMessage(message); +} + +bool CGUIDialogAddonSettings::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_SETTINGS_LEVEL_CHANGE: + { + // Test if we can access the new level + if (!g_passwordManager.CheckSettingLevelLock( + CViewStateSettings::GetInstance().GetNextSettingLevel(), true)) + return false; + + CViewStateSettings::GetInstance().CycleSettingLevel(); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + // try to keep the current position + std::string oldCategory; + if (m_iCategory >= 0 && m_iCategory < static_cast<int>(m_categories.size())) + oldCategory = m_categories[m_iCategory]->GetId(); + + SET_CONTROL_LABEL(CONTROL_BTN_LEVELS, + 10036 + + static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel())); + // only re-create the categories, the settings will be created later + SetupControls(false); + + m_iCategory = 0; + // try to find the category that was previously selected + if (!oldCategory.empty()) + { + for (int i = 0; i < static_cast<int>(m_categories.size()); i++) + { + if (m_categories[i]->GetId() == oldCategory) + { + m_iCategory = i; + break; + } + } + } + + CreateSettings(); + return true; + } + + default: + break; + } + + return CGUIDialogSettingsManagerBase::OnAction(action); +} + +bool CGUIDialogAddonSettings::ShowForAddon(const ADDON::AddonPtr& addon, + bool saveToDisk /* = true */) +{ + if (!addon) + { + CLog::LogF(LOGERROR, "No addon given!"); + return false; + } + + if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) + return false; + + if (addon->SupportsInstanceSettings()) + return ShowForMultipleInstances(addon, saveToDisk); + else + return ShowForSingleInstance(addon, saveToDisk); +} + +bool CGUIDialogAddonSettings::ShowForSingleInstance( + const ADDON::AddonPtr& addon, + bool saveToDisk, + ADDON::AddonInstanceId instanceId /* = ADDON::ADDON_SETTINGS_ID */) +{ + if (!addon->HasSettings(instanceId)) + { + // addon does not support settings, inform user + HELPERS::ShowOKDialogText(CVariant{24000}, CVariant{24030}); + return false; + } + + // Create the dialog + CGUIDialogAddonSettings* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>( + WINDOW_DIALOG_ADDON_SETTINGS); + if (!dialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_ADDON_SETTINGS instance!"); + return false; + } + + dialog->m_addon = addon; + dialog->m_instanceId = instanceId; + dialog->m_saveToDisk = saveToDisk; + + dialog->Open(); + + if (!dialog->IsConfirmed()) + { + addon->ReloadSettings(instanceId); + return false; + } + + if (saveToDisk) + addon->SaveSettings(instanceId); + + return true; +} + +bool CGUIDialogAddonSettings::ShowForMultipleInstances(const ADDON::AddonPtr& addon, + bool saveToDisk) +{ + CGUIDialogSelect* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!dialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT instance!"); + return false; + } + + int lastSelected = -1; + while (true) + { + std::vector<ADDON::AddonInstanceId> ids = addon->GetKnownInstanceIds(); + std::sort(ids.begin(), ids.end(), [](const auto& a, const auto& b) { return a < b; }); + + dialog->Reset(); + dialog->SetHeading(10012); // Add-on configurations and settings + dialog->SetUseDetails(false); + + CFileItemList itemsInstances; + ADDON::AddonInstanceId highestId = 0; + for (const auto& id : ids) + { + std::string name; + addon->GetSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, name, id); + if (name.empty()) + name = g_localizeStrings.Get(13205); // Unknown + + bool enabled = false; + addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabled, id); + + const std::string label = StringUtils::Format( + g_localizeStrings.Get(10020), name, + g_localizeStrings.Get(enabled ? 305 : 13106)); // Edit "config name" [enabled state] + + const CFileItemPtr item = std::make_shared<CFileItem>(label); + item->SetProperty("id", id); + item->SetProperty("name", name); + itemsInstances.Add(item); + + if (id > highestId) + highestId = id; + } + + CFileItemList itemsGeneral; + + const ADDON::AddonInstanceId addInstanceId = highestId + 1; + const ADDON::AddonInstanceId removeInstanceId = highestId + 2; + + CFileItemPtr item = + std::make_shared<CFileItem>(g_localizeStrings.Get(10014)); // Add add-on configuration + item->SetProperty("id", addInstanceId); + itemsGeneral.Add(item); + + if (ids.size() > 1) // Forbid removal of last instance + { + item = + std::make_shared<CFileItem>(g_localizeStrings.Get(10015)); // Remove add-on configuration + item->SetProperty("id", removeInstanceId); + itemsGeneral.Add(item); + } + + if (addon->HasSettings(ADDON_SETTINGS_ID)) + { + item = std::make_shared<CFileItem>(g_localizeStrings.Get(10013)); // Edit Add-on settings + item->SetProperty("id", ADDON_SETTINGS_ID); + itemsGeneral.Add(item); + } + + for (auto& it : itemsGeneral) + dialog->Add(*it); + + for (auto& it : itemsInstances) + dialog->Add(*it); + + // Select last selected item, first instance config item or first item + if (lastSelected >= 0) + dialog->SetSelected(lastSelected); + else + dialog->SetSelected(itemsInstances.Size() > 0 ? itemsGeneral.Size() : 0); + + dialog->Open(); + + if (dialog->IsButtonPressed() || !dialog->IsConfirmed()) + break; + + lastSelected = dialog->GetSelectedItem(); + + item = dialog->GetSelectedFileItem(); + ADDON::AddonInstanceId instanceId = item->GetProperty("id").asInteger(); + + if (instanceId == addInstanceId) + { + instanceId = highestId + 1; + + addon->GetSettings(instanceId); + addon->UpdateSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, "", instanceId); + addon->UpdateSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, true, instanceId); + addon->SaveSettings(instanceId); + + if (ShowForSingleInstance(addon, saveToDisk, instanceId)) + { + CServiceBroker::GetAddonMgr().PublishInstanceAdded(addon->ID(), instanceId); + } + else + { + // Remove instance settings if not succeeded (e.g. dialog cancelled) + addon->DeleteInstanceSettings(instanceId); + } + } + else if (instanceId == removeInstanceId) + { + dialog->Reset(); + dialog->SetHeading(10010); // Select add-on configuration to remove + dialog->SetUseDetails(false); + + for (auto& it : itemsInstances) + { + CFileItem item(*it); + item.SetLabel((*it).GetProperty("name").asString()); + dialog->Add(item); + } + + dialog->SetSelected(0); + dialog->Open(); + + if (!dialog->IsButtonPressed() && dialog->IsConfirmed()) + { + item = dialog->GetSelectedFileItem(); + const std::string label = StringUtils::Format( + g_localizeStrings.Get(10019), + item->GetProperty("name") + .asString()); // Do you want to remove the add-on configuration "config name"? + + if (CGUIDialogYesNo::ShowAndGetInput(10009, // Confirm add-on configuration removal + label)) + { + instanceId = item->GetProperty("id").asInteger(); + addon->DeleteInstanceSettings(instanceId); + CServiceBroker::GetAddonMgr().PublishInstanceRemoved(addon->ID(), instanceId); + } + } + } + else + { + // edit instance settings or edit addon settings selected; open settings dialog + + bool enabled = false; + addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabled, instanceId); + + if (ShowForSingleInstance(addon, saveToDisk, instanceId) && instanceId != ADDON_SETTINGS_ID) + { + // Publish new/removed instance configuration and start the use of new instance + bool enabledNow = false; + addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabledNow, instanceId); + if (enabled != enabledNow) + { + if (enabledNow) + CServiceBroker::GetAddonMgr().PublishInstanceAdded(addon->ID(), instanceId); + else + CServiceBroker::GetAddonMgr().PublishInstanceRemoved(addon->ID(), instanceId); + } + } + } + + // refresh selection dialog content... + + } // while (true) + + return true; +} + +void CGUIDialogAddonSettings::SaveAndClose() +{ + if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) + return; + + // get the dialog + CGUIDialogAddonSettings* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>( + WINDOW_DIALOG_ADDON_SETTINGS); + if (dialog == nullptr || !dialog->IsActive()) + return; + + // check if we need to save the settings + if (dialog->m_saveToDisk && dialog->m_addon != nullptr) + dialog->m_addon->SaveSettings(dialog->m_instanceId); + + // close the dialog + dialog->Close(); +} + +std::string CGUIDialogAddonSettings::GetCurrentAddonID() const +{ + if (m_addon == nullptr) + return ""; + + return m_addon->ID(); +} + +void CGUIDialogAddonSettings::SetupView() +{ + if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr) + return; + + auto settings = m_addon->GetSettings(m_instanceId); + if (!settings->IsLoaded()) + return; + + CGUIDialogSettingsManagerBase::SetupView(); + + // set addon id as window property + SetProperty("Addon.ID", m_addon->ID()); + + // set heading + SetHeading(StringUtils::Format("$LOCALIZE[10004] - {}", + m_addon->Name())); // "Settings - AddonName" + + // set control labels + SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CUSTOM_BUTTON, 409); + SET_CONTROL_LABEL(CONTROL_BTN_LEVELS, + 10036 + static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel())); +} + +std::string CGUIDialogAddonSettings::GetLocalizedString(uint32_t labelId) const +{ + std::string label = g_localizeStrings.GetAddonString(m_addon->ID(), labelId); + if (!label.empty()) + return label; + + return CGUIDialogSettingsManagerBase::GetLocalizedString(labelId); +} + +std::string CGUIDialogAddonSettings::GetSettingsLabel(const std::shared_ptr<ISetting>& setting) +{ + if (setting == nullptr) + return ""; + + std::string label = GetLocalizedString(setting->GetLabel()); + if (!label.empty()) + return label; + + // try the addon settings + label = m_addon->GetSettings(m_instanceId)->GetSettingLabel(setting->GetLabel()); + if (!label.empty()) + return label; + + return CGUIDialogSettingsManagerBase::GetSettingsLabel(setting); +} + +int CGUIDialogAddonSettings::GetSettingLevel() const +{ + return static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel()); +} + +std::shared_ptr<CSettingSection> CGUIDialogAddonSettings::GetSection() +{ + const auto settingsManager = GetSettingsManager(); + if (settingsManager == nullptr) + return nullptr; + + const auto sections = settingsManager->GetSections(); + if (!sections.empty()) + return sections.front(); + + return nullptr; +} + +CSettingsManager* CGUIDialogAddonSettings::GetSettingsManager() const +{ + if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr) + return nullptr; + + return m_addon->GetSettings(m_instanceId)->GetSettingsManager(); +} + +void CGUIDialogAddonSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr) + return; + + m_addon->GetSettings(m_instanceId)->OnSettingAction(setting); +} diff --git a/xbmc/addons/gui/GUIDialogAddonSettings.h b/xbmc/addons/gui/GUIDialogAddonSettings.h new file mode 100644 index 0000000..a3cf664 --- /dev/null +++ b/xbmc/addons/gui/GUIDialogAddonSettings.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" +#include "settings/dialogs/GUIDialogSettingsManagerBase.h" + +class CGUIDialogAddonSettings : public CGUIDialogSettingsManagerBase +{ +public: + CGUIDialogAddonSettings(); + ~CGUIDialogAddonSettings() override = default; + + // specializations of CGUIControl + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + + static bool ShowForAddon(const ADDON::AddonPtr& addon, bool saveToDisk = true); + static void SaveAndClose(); + + std::string GetCurrentAddonID() const; + +protected: + // implementation of CGUIDialogSettingsBase + void SetupView() override; + std::string GetLocalizedString(uint32_t labelId) const override; + std::string GetSettingsLabel(const std::shared_ptr<ISetting>& setting) override; + int GetSettingLevel() const override; + std::shared_ptr<CSettingSection> GetSection() override; + + // implementation of CGUIDialogSettingsManagerBase + bool AllowResettingSettings() const override { return false; } + bool Save() override { return true; } + CSettingsManager* GetSettingsManager() const override; + + // implementation of ISettingCallback + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + +private: + static bool ShowForSingleInstance(const ADDON::AddonPtr& addon, + bool saveToDisk, + ADDON::AddonInstanceId instanceId = ADDON::ADDON_SETTINGS_ID); + static bool ShowForMultipleInstances(const ADDON::AddonPtr& addon, bool saveToDisk); + + ADDON::AddonPtr m_addon; + ADDON::AddonInstanceId m_instanceId{ADDON::ADDON_SETTINGS_ID}; + bool m_saveToDisk = false; +}; diff --git a/xbmc/addons/gui/GUIHelpers.cpp b/xbmc/addons/gui/GUIHelpers.cpp new file mode 100644 index 0000000..62fb9d3 --- /dev/null +++ b/xbmc/addons/gui/GUIHelpers.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIHelpers.h" + +#include "addons/IAddon.h" +#include "addons/addoninfo/AddonInfo.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" + +using namespace ADDON; +using namespace ADDON::GUI; + +bool CHelpers::DialogAddonLifecycleUseAsk(const std::shared_ptr<const IAddon>& addon) +{ + int header_nr; + int text_nr; + switch (addon->LifecycleState()) + { + case AddonLifecycleState::BROKEN: + header_nr = 24164; + text_nr = 24165; + break; + case AddonLifecycleState::DEPRECATED: + header_nr = 24166; + text_nr = 24167; + break; + default: + header_nr = 0; + text_nr = 0; + break; + } + if (header_nr > 0) + { + std::string header = StringUtils::Format(g_localizeStrings.Get(header_nr), addon->ID()); + std::string text = + StringUtils::Format(g_localizeStrings.Get(text_nr), addon->LifecycleStateDescription()); + if (!CGUIDialogYesNo::ShowAndGetInput(header, text)) + return false; + } + + return true; +} diff --git a/xbmc/addons/gui/GUIHelpers.h b/xbmc/addons/gui/GUIHelpers.h new file mode 100644 index 0000000..63310ae --- /dev/null +++ b/xbmc/addons/gui/GUIHelpers.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <memory> + +namespace ADDON +{ + +class IAddon; + +namespace GUI +{ + +class CHelpers +{ +public: + /*! + * @brief This shows an Yes/No dialog with information about the add-on if it is + * not in the normal status. + * + * This asks the user whether he really wants to use the add-on and informs with + * text why the other status is. + * + * @note The dialog is currently displayed for @ref AddonLifecycleState::BROKEN + * and @ref AddonLifecycleState::DEPRECATED. + * + * @param[in] addon Class of the add-on to be checked + * @return True if user activation is desired, false if not + */ + static bool DialogAddonLifecycleUseAsk(const std::shared_ptr<const IAddon>& addon); +}; + +} /* namespace GUI */ +} /* namespace ADDON */ diff --git a/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp b/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp new file mode 100644 index 0000000..5504f33 --- /dev/null +++ b/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIViewStateAddonBrowser.h" + +#include "FileItem.h" +#include "filesystem/File.h" +#include "guilib/WindowIDs.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "view/ViewState.h" +#include "windowing/GraphicContext.h" + +using namespace XFILE; +using namespace ADDON; + +CGUIViewStateAddonBrowser::CGUIViewStateAddonBrowser(const CFileItemList& items) + : CGUIViewState(items) +{ + if (URIUtils::PathEquals(items.GetPath(), "addons://")) + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); + SetSortMethod(SortByNone); + } + else if (URIUtils::PathEquals(items.GetPath(), "addons://recently_updated/", true)) + { + AddSortMethod(SortByLastUpdated, 12014, LABEL_MASKS("%L", "%v", "%L", "%v"), + SortAttributeIgnoreFolders, SortOrderDescending); + } + else + { + AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, + LABEL_MASKS("%L", "%s", "%L", "%s")); + + if (StringUtils::StartsWith(items.GetPath(), "addons://sources/")) + AddSortMethod(SortByLastUsed, 12012, LABEL_MASKS("%L", "%u", "%L", "%u"), + SortAttributeIgnoreFolders, SortOrderDescending); //Label, Last used + + if (StringUtils::StartsWith(items.GetPath(), "addons://user/") && + items.GetContent() == "addons") + AddSortMethod(SortByInstallDate, 12013, LABEL_MASKS("%L", "%i", "%L", "%i"), + SortAttributeIgnoreFolders, SortOrderDescending); + + SetSortMethod(SortByLabel); + } + SetViewAsControl(DEFAULT_VIEW_AUTO); + + LoadViewState(items.GetPath(), WINDOW_ADDON_BROWSER); +} + +void CGUIViewStateAddonBrowser::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_ADDON_BROWSER); +} + +std::string CGUIViewStateAddonBrowser::GetExtensions() +{ + return ""; +} diff --git a/xbmc/addons/gui/GUIViewStateAddonBrowser.h b/xbmc/addons/gui/GUIViewStateAddonBrowser.h new file mode 100644 index 0000000..e7cc564 --- /dev/null +++ b/xbmc/addons/gui/GUIViewStateAddonBrowser.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "view/GUIViewState.h" + +class CGUIViewStateAddonBrowser : public CGUIViewState +{ +public: + explicit CGUIViewStateAddonBrowser(const CFileItemList& items); + +protected: + void SaveViewState() override; + std::string GetExtensions() override; +}; diff --git a/xbmc/addons/gui/GUIWindowAddonBrowser.cpp b/xbmc/addons/gui/GUIWindowAddonBrowser.cpp new file mode 100644 index 0000000..2f522e7 --- /dev/null +++ b/xbmc/addons/gui/GUIWindowAddonBrowser.cpp @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowAddonBrowser.h" + +#include "ContextMenuManager.h" +#include "FileItem.h" +#include "GUIDialogAddonInfo.h" +#include "GUIUserMessages.h" +#include "LangInfo.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "addons/AddonSystemSettings.h" +#include "addons/IAddon.h" +#include "addons/RepositoryUpdater.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/AddonsDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "messaging/helpers/DialogHelper.h" +#include "platform/Platform.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "threads/IRunnable.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <utility> + +#define CONTROL_SETTINGS 5 +#define CONTROL_FOREIGNFILTER 7 +#define CONTROL_BROKENFILTER 8 +#define CONTROL_CHECK_FOR_UPDATES 9 + +using namespace ADDON; +using namespace XFILE; + +CGUIWindowAddonBrowser::CGUIWindowAddonBrowser(void) + : CGUIMediaWindow(WINDOW_ADDON_BROWSER, "AddonBrowser.xml") +{ +} + +CGUIWindowAddonBrowser::~CGUIWindowAddonBrowser() = default; + +bool CGUIWindowAddonBrowser::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_DEINIT: + { + CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this); + CServiceBroker::GetAddonMgr().Events().Unsubscribe(this); + + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + } + break; + case GUI_MSG_WINDOW_INIT: + { + CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this, + &CGUIWindowAddonBrowser::OnEvent); + CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIWindowAddonBrowser::OnEvent); + + SetProperties(); + } + break; + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_FOREIGNFILTER) + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->ToggleBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER); + settings->Save(); + Refresh(); + return true; + } + else if (iControl == CONTROL_BROKENFILTER) + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->ToggleBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER); + settings->Save(); + Refresh(); + return true; + } + else if (iControl == CONTROL_CHECK_FOR_UPDATES) + { + CServiceBroker::GetRepositoryUpdater().CheckForUpdates(true); + return true; + } + else if (iControl == CONTROL_SETTINGS) + { + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SETTINGS_SYSTEM, + "addons"); + return true; + } + else if (m_viewControl.HasControl(iControl)) // list/thumb control + { + // get selected item + int iItem = m_viewControl.GetSelectedItem(); + int iAction = message.GetParam1(); + + // iItem is checked for validity inside these routines + if (iAction == ACTION_SHOW_INFO) + { + if (!m_vecItems->Get(iItem)->GetProperty("Addon.ID").empty()) + return CGUIDialogAddonInfo::ShowForItem((*m_vecItems)[iItem]); + return false; + } + } + } + break; + case GUI_MSG_NOTIFY_ALL: + { + if (message.GetParam1() == GUI_MSG_UPDATE_ITEM && IsActive() && + message.GetNumStringParams() == 1) + { // update this item + for (int i = 0; i < m_vecItems->Size(); ++i) + { + CFileItemPtr item = m_vecItems->Get(i); + if (item->GetProperty("Addon.ID") == message.GetStringParam()) + { + UpdateStatus(item); + FormatAndSort(*m_vecItems); + return true; + } + } + } + else if (message.GetParam1() == GUI_MSG_UPDATE && IsActive()) + SetProperties(); + } + break; + default: + break; + } + return CGUIMediaWindow::OnMessage(message); +} + +void CGUIWindowAddonBrowser::SetProperties() +{ + auto lastUpdated = CServiceBroker::GetRepositoryUpdater().LastUpdated(); + SetProperty("Updated", lastUpdated.IsValid() ? lastUpdated.GetAsLocalizedDateTime() + : g_localizeStrings.Get(21337)); +} + +class UpdateAddons : public IRunnable +{ + void Run() override + { + for (const auto& addon : CServiceBroker::GetAddonMgr().GetAvailableUpdates()) + CAddonInstaller::GetInstance().InstallOrUpdate(addon->ID(), BackgroundJob::CHOICE_YES, + ModalJob::CHOICE_NO); + } +}; + +class UpdateAllowedAddons : public IRunnable +{ + void Run() override + { + for (const auto& addon : CServiceBroker::GetAddonMgr().GetAvailableUpdates()) + if (CServiceBroker::GetAddonMgr().IsAutoUpdateable(addon->ID())) + CAddonInstaller::GetInstance().InstallOrUpdate(addon->ID(), BackgroundJob::CHOICE_YES, + ModalJob::CHOICE_NO); + } +}; + +void CGUIWindowAddonBrowser::OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event) +{ + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CGUIWindowAddonBrowser::OnEvent(const ADDON::AddonEvent& event) +{ + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CGUIWindowAddonBrowser::InstallFromZip() +{ + using namespace KODI::MESSAGING::HELPERS; + + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES)) + { + if (ShowYesNoDialogText(13106, 36617, 186, 10004) == DialogResponse::CHOICE_YES) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow( + WINDOW_SETTINGS_SYSTEM, CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES); + } + else + { + // pop up filebrowser to grab an installed folder + VECSOURCES shares = *CMediaSourceSettings::GetInstance().GetSources("files"); + CServiceBroker::GetMediaManager().GetLocalDrives(shares); + CServiceBroker::GetMediaManager().GetNetworkLocations(shares); + std::string path; + if (CGUIDialogFileBrowser::ShowAndGetFile(shares, "*.zip", g_localizeStrings.Get(24041), path)) + { + CAddonInstaller::GetInstance().InstallFromZip(path); + } + } +} + +bool CGUIWindowAddonBrowser::OnClick(int iItem, const std::string& player) +{ + CFileItemPtr item = m_vecItems->Get(iItem); + if (item->GetPath() == "addons://install/") + { + InstallFromZip(); + return true; + } + if (item->GetPath() == "addons://update_all/") + { + UpdateAddons updater; + CGUIDialogBusy::Wait(&updater, 100, true); + return true; + } + if (item->GetPath() == "addons://update_allowed/") + { + UpdateAllowedAddons updater; + CGUIDialogBusy::Wait(&updater, 100, true); + return true; + } + if (!item->m_bIsFolder) + { + // cancel a downloading job + if (item->HasProperty("Addon.Downloading")) + { + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{24000}, item->GetProperty("Addon.Name"), + CVariant{24066}, CVariant{""})) + { + if (CAddonInstaller::GetInstance().Cancel(item->GetProperty("Addon.ID").asString())) + Refresh(); + } + return true; + } + + CGUIDialogAddonInfo::ShowForItem(item); + return true; + } + if (item->IsPath("addons://search/")) + { + Update(item->GetPath()); + return true; + } + + return CGUIMediaWindow::OnClick(iItem, player); +} + +void CGUIWindowAddonBrowser::UpdateButtons() +{ + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + SET_CONTROL_SELECTED(GetID(), CONTROL_FOREIGNFILTER, + settings->GetBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER)); + SET_CONTROL_SELECTED(GetID(), CONTROL_BROKENFILTER, + settings->GetBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER)); + CONTROL_ENABLE(CONTROL_CHECK_FOR_UPDATES); + CONTROL_ENABLE(CONTROL_SETTINGS); + + bool allowFilter = CAddonsDirectory::IsRepoDirectory(CURL(m_vecItems->GetPath())); + CONTROL_ENABLE_ON_CONDITION(CONTROL_FOREIGNFILTER, allowFilter); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BROKENFILTER, allowFilter); + + CGUIMediaWindow::UpdateButtons(); +} + +static bool IsForeign(const std::string& languages) +{ + if (languages.empty()) + return false; + + for (const auto& lang : StringUtils::Split(languages, " ")) + { + if (lang == "en" || lang == g_langInfo.GetLocale().GetLanguageCode() || + lang == g_langInfo.GetLocale().ToShortString()) + return false; + + // for backwards compatibility + if (lang == "no" && g_langInfo.GetLocale().ToShortString() == "nb_NO") + return false; + } + return true; +} + +bool CGUIWindowAddonBrowser::GetDirectory(const std::string& strDirectory, CFileItemList& items) +{ + bool result = CGUIMediaWindow::GetDirectory(strDirectory, items); + + if (result && CAddonsDirectory::IsRepoDirectory(CURL(strDirectory))) + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER)) + { + int i = 0; + while (i < items.Size()) + { + auto prop = items[i]->GetProperty("Addon.Language"); + if (!prop.isNull() && IsForeign(prop.asString())) + items.Remove(i); + else + ++i; + } + } + if (settings->GetBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER)) + { + for (int i = items.Size() - 1; i >= 0; i--) + { + if (items[i]->GetAddonInfo() && + items[i]->GetAddonInfo()->LifecycleState() == AddonLifecycleState::BROKEN) + { + //check if it's installed + AddonPtr addon; + if (!CServiceBroker::GetAddonMgr().GetAddon(items[i]->GetProperty("Addon.ID").asString(), + addon, OnlyEnabled::CHOICE_YES)) + items.Remove(i); + } + } + } + } + + for (int i = 0; i < items.Size(); ++i) + UpdateStatus(items[i]); + + return result; +} + +void CGUIWindowAddonBrowser::UpdateStatus(const CFileItemPtr& item) +{ + if (!item || item->m_bIsFolder) + return; + + unsigned int percent; + bool downloadFinshed; + if (CAddonInstaller::GetInstance().GetProgress(item->GetProperty("Addon.ID").asString(), percent, + downloadFinshed)) + { + std::string progress = StringUtils::Format( + !downloadFinshed ? g_localizeStrings.Get(24042) : g_localizeStrings.Get(24044), percent); + item->SetProperty("Addon.Status", progress); + item->SetProperty("Addon.Downloading", true); + } + else + item->ClearProperty("Addon.Downloading"); +} + +bool CGUIWindowAddonBrowser::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath)) + return false; + + m_thumbLoader.Load(*m_vecItems); + + return true; +} + +int CGUIWindowAddonBrowser::SelectAddonID(AddonType type, + std::string& addonID, + bool showNone /* = false */, + bool showDetails /* = true */, + bool showInstalled /* = true */, + bool showInstallable /*= false */, + bool showMore /* = true */) +{ + std::vector<AddonType> types; + types.push_back(type); + return SelectAddonID(types, addonID, showNone, showDetails, showInstalled, showInstallable, + showMore); +} + +int CGUIWindowAddonBrowser::SelectAddonID(AddonType type, + std::vector<std::string>& addonIDs, + bool showNone /* = false */, + bool showDetails /* = true */, + bool multipleSelection /* = true */, + bool showInstalled /* = true */, + bool showInstallable /* = false */, + bool showMore /* = true */) +{ + std::vector<AddonType> types; + types.push_back(type); + return SelectAddonID(types, addonIDs, showNone, showDetails, multipleSelection, showInstalled, + showInstallable, showMore); +} + +int CGUIWindowAddonBrowser::SelectAddonID(const std::vector<AddonType>& types, + std::string& addonID, + bool showNone /* = false */, + bool showDetails /* = true */, + bool showInstalled /* = true */, + bool showInstallable /* = false */, + bool showMore /* = true */) +{ + std::vector<std::string> addonIDs; + if (!addonID.empty()) + addonIDs.push_back(addonID); + int retval = SelectAddonID(types, addonIDs, showNone, showDetails, false, showInstalled, + showInstallable, showMore); + if (!addonIDs.empty()) + addonID = addonIDs.at(0); + else + addonID = ""; + return retval; +} + +int CGUIWindowAddonBrowser::SelectAddonID(const std::vector<AddonType>& types, + std::vector<std::string>& addonIDs, + bool showNone /* = false */, + bool showDetails /* = true */, + bool multipleSelection /* = true */, + bool showInstalled /* = true */, + bool showInstallable /* = false */, + bool showMore /* = true */) +{ + // if we shouldn't show neither installed nor installable addons the list will be empty + if (!showInstalled && !showInstallable) + return -1; + + // can't show the "Get More" button if we already show installable addons + if (showInstallable) + showMore = false; + + CGUIDialogSelect* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!dialog) + return -1; + + // get rid of any invalid addon types + std::vector<AddonType> validTypes(types.size()); + std::copy_if(types.begin(), types.end(), validTypes.begin(), + [](AddonType type) { return type != AddonType::UNKNOWN; }); + + if (validTypes.empty()) + return -1; + + // get all addons to show + VECADDONS addons; + if (showInstalled) + { + for (std::vector<AddonType>::const_iterator type = validTypes.begin(); type != validTypes.end(); + ++type) + { + VECADDONS typeAddons; + if (*type == AddonType::AUDIO) + CAddonsDirectory::GetScriptsAndPlugins("audio", typeAddons); + else if (*type == AddonType::EXECUTABLE) + CAddonsDirectory::GetScriptsAndPlugins("executable", typeAddons); + else if (*type == AddonType::IMAGE) + CAddonsDirectory::GetScriptsAndPlugins("image", typeAddons); + else if (*type == AddonType::VIDEO) + CAddonsDirectory::GetScriptsAndPlugins("video", typeAddons); + else if (*type == AddonType::GAME) + CAddonsDirectory::GetScriptsAndPlugins("game", typeAddons); + else + CServiceBroker::GetAddonMgr().GetAddons(typeAddons, *type); + + addons.insert(addons.end(), typeAddons.begin(), typeAddons.end()); + } + } + + if (showInstallable || showMore) + { + VECADDONS installableAddons; + if (CServiceBroker::GetAddonMgr().GetInstallableAddons(installableAddons)) + { + for (auto addon = installableAddons.begin(); addon != installableAddons.end();) + { + AddonPtr pAddon = *addon; + + // check if the addon matches one of the provided addon types + bool matchesType = false; + for (std::vector<AddonType>::const_iterator type = validTypes.begin(); + type != validTypes.end(); ++type) + { + if (pAddon->HasType(*type)) + { + matchesType = true; + break; + } + } + + if (matchesType) + { + ++addon; + continue; + } + + addon = installableAddons.erase(addon); + } + + if (showInstallable) + addons.insert(addons.end(), installableAddons.begin(), installableAddons.end()); + else if (showMore) + showMore = !installableAddons.empty(); + } + } + + if (addons.empty() && !showNone) + return -1; + + // turn the addons into items + std::map<std::string, AddonPtr> addonMap; + CFileItemList items; + for (const auto& addon : addons) + { + const CFileItemPtr item(CAddonsDirectory::FileItemFromAddon(addon, addon->ID())); + item->SetLabel2(addon->Summary()); + if (!items.Contains(item->GetPath())) + { + items.Add(item); + addonMap.insert(std::make_pair(item->GetPath(), addon)); + } + } + + if (items.IsEmpty() && !showNone) + return -1; + + std::string heading; + for (std::vector<AddonType>::const_iterator type = validTypes.begin(); type != validTypes.end(); + ++type) + { + if (!heading.empty()) + heading += ", "; + heading += CAddonInfo::TranslateType(*type, true); + } + + dialog->SetHeading(CVariant{std::move(heading)}); + dialog->Reset(); + dialog->SetUseDetails(showDetails); + + if (multipleSelection) + { + showNone = false; + showMore = false; + dialog->EnableButton(true, 186); + } + else if (showMore) + dialog->EnableButton(true, 21452); + + if (showNone) + { + CFileItemPtr item(new CFileItem("", false)); + item->SetLabel(g_localizeStrings.Get(231)); + item->SetLabel2(g_localizeStrings.Get(24040)); + item->SetArt("icon", "DefaultAddonNone.png"); + item->SetSpecialSort(SortSpecialOnTop); + items.Add(item); + } + items.Sort(SortByLabel, SortOrderAscending); + + if (!addonIDs.empty()) + { + for (std::vector<std::string>::const_iterator it = addonIDs.begin(); it != addonIDs.end(); ++it) + { + CFileItemPtr item = items.Get(*it); + if (item) + item->Select(true); + } + } + dialog->SetItems(items); + dialog->SetMultiSelection(multipleSelection); + dialog->Open(); + + // if the "Get More" button has been pressed and we haven't shown the + // installable addons so far show a list of installable addons + if (showMore && dialog->IsButtonPressed()) + return SelectAddonID(types, addonIDs, showNone, showDetails, multipleSelection, false, true, + false); + + if (!dialog->IsConfirmed()) + return 0; + + addonIDs.clear(); + for (int i : dialog->GetSelectedItems()) + { + const CFileItemPtr& item = items.Get(i); + + // check if one of the selected addons needs to be installed + if (showInstallable) + { + std::map<std::string, AddonPtr>::const_iterator itAddon = addonMap.find(item->GetPath()); + if (itAddon != addonMap.end()) + { + const AddonPtr& addon = itAddon->second; + + // if the addon isn't installed we need to install it + if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addon->ID())) + { + AddonPtr installedAddon; + if (!CAddonInstaller::GetInstance().InstallModal(addon->ID(), installedAddon, + InstallModalPrompt::CHOICE_NO)) + continue; + } + + // if the addon is disabled we need to enable it + if (CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID())) + CServiceBroker::GetAddonMgr().EnableAddon(addon->ID()); + } + } + + addonIDs.push_back(item->GetPath()); + } + return 1; +} + +std::string CGUIWindowAddonBrowser::GetStartFolder(const std::string& dir) +{ + if (StringUtils::StartsWith(dir, "addons://")) + { + if (StringUtils::StartsWith(dir, "addons://default_binary_addons_source/")) + { + const bool all = CServiceBroker::GetPlatform().SupportsUserInstalledBinaryAddons(); + std::string startDir = dir; + StringUtils::Replace(startDir, "/default_binary_addons_source/", all ? "/all/" : "/user/"); + return startDir; + } + else + return dir; + } + + return CGUIMediaWindow::GetStartFolder(dir); +} diff --git a/xbmc/addons/gui/GUIWindowAddonBrowser.h b/xbmc/addons/gui/GUIWindowAddonBrowser.h new file mode 100644 index 0000000..7f605ee --- /dev/null +++ b/xbmc/addons/gui/GUIWindowAddonBrowser.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ThumbLoader.h" +#include "addons/RepositoryUpdater.h" +#include "windows/GUIMediaWindow.h" + +#include <string> +#include <vector> + +class CFileItemList; + +namespace ADDON +{ +enum class AddonType; +struct AddonEvent; +} + +class CGUIWindowAddonBrowser : public CGUIMediaWindow +{ +public: + CGUIWindowAddonBrowser(void); + ~CGUIWindowAddonBrowser(void) override; + bool OnMessage(CGUIMessage& message) override; + + /*! + * @brief Popup a selection dialog with a list of addons of the given type + * + * @param[in] type the type of addon wanted + * @param[in] addonID [in/out] the addon ID of the (pre) selected item + * @param[in] showNone whether there should be a "None" item in the list (defaults to false) + * @param[in] showDetails whether to show details of the addons or not + * @param[in] showInstalled whether installed addons should be in the list + * @param[in] showInstallable whether installable addons should be in the list + * @param[in] showMore whether to show the "Get More" button (only makes sense + * if showInstalled is true and showInstallable is false) + * @return 1 if an addon was selected or multiple selection was specified, 2 if + * "Get More" was chosen, 0 if the selection process was cancelled or -1 + * if an error occurred or + */ + static int SelectAddonID(ADDON::AddonType type, + std::string& addonID, + bool showNone = false, + bool showDetails = true, + bool showInstalled = true, + bool showInstallable = false, + bool showMore = true); + static int SelectAddonID(const std::vector<ADDON::AddonType>& types, + std::string& addonID, + bool showNone = false, + bool showDetails = true, + bool showInstalled = true, + bool showInstallable = false, + bool showMore = true); + /*! + * @brief Popup a selection dialog with a list of addons of the given type + * + * @param[in] type the type of addon wanted + * @param[in] addonIDs [in/out] array of (pre) selected addon IDs + * @param[in] showNone whether there should be a "None" item in the list (defaults to false) + * @param[in] showDetails whether to show details of the addons or not + * @param[in] multipleSelection allow selection of multiple addons, if set to + * true showNone will automatically switch to false + * @param[in] showInstalled whether installed addons should be in the list + * @param[in] showInstallable whether installable addons should be in the list + * @param[in] showMore whether to show the "Get More" button (only makes sense + * if showInstalled is true and showInstallable is false) + * @return 1 if an addon was selected or multiple selection was specified, 2 if + * "Get More" was chosen, 0 if the selection process was cancelled or -1 + * if an error occurred or + */ + static int SelectAddonID(ADDON::AddonType type, + std::vector<std::string>& addonIDs, + bool showNone = false, + bool showDetails = true, + bool multipleSelection = true, + bool showInstalled = true, + bool showInstallable = false, + bool showMore = true); + static int SelectAddonID(const std::vector<ADDON::AddonType>& types, + std::vector<std::string>& addonIDs, + bool showNone = false, + bool showDetails = true, + bool multipleSelection = true, + bool showInstalled = true, + bool showInstallable = false, + bool showMore = true); + + bool UseFileDirectories() override { return false; } + + static void InstallFromZip(); + +protected: + bool OnClick(int iItem, const std::string& player = "") override; + void UpdateButtons() override; + bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + std::string GetStartFolder(const std::string& dir) override; + + std::string GetRootPath() const override { return "addons://"; } + +private: + void SetProperties(); + void UpdateStatus(const CFileItemPtr& item); + void OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event); + void OnEvent(const ADDON::AddonEvent& event); + CProgramThumbLoader m_thumbLoader; +}; diff --git a/xbmc/addons/gui/skin/CMakeLists.txt b/xbmc/addons/gui/skin/CMakeLists.txt new file mode 100644 index 0000000..916cd94 --- /dev/null +++ b/xbmc/addons/gui/skin/CMakeLists.txt @@ -0,0 +1,7 @@ +set(SOURCES SkinTimer.cpp + SkinTimerManager.cpp) + +set(HEADERS SkinTimer.h + SkinTimerManager.h) + +core_add_library(addons_gui_skin) diff --git a/xbmc/addons/gui/skin/SkinTimer.cpp b/xbmc/addons/gui/skin/SkinTimer.cpp new file mode 100644 index 0000000..c4e88b7 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimer.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "SkinTimer.h" + +#include "interfaces/info/Info.h" + +CSkinTimer::CSkinTimer(const std::string& name, + const INFO::InfoPtr& startCondition, + const INFO::InfoPtr& resetCondition, + const INFO::InfoPtr& stopCondition, + const CGUIAction& startActions, + const CGUIAction& stopActions, + bool resetOnStart) + : m_name{name}, + m_startCondition{startCondition}, + m_resetCondition{resetCondition}, + m_stopCondition{stopCondition}, + m_startActions{startActions}, + m_stopActions{stopActions}, + m_resetOnStart{resetOnStart} +{ +} + +void CSkinTimer::Start() +{ + if (m_resetOnStart) + { + CStopWatch::StartZero(); + } + else + { + CStopWatch::Start(); + } + OnStart(); +} + +void CSkinTimer::Reset() +{ + CStopWatch::Reset(); +} + +void CSkinTimer::Stop() +{ + CStopWatch::Stop(); + OnStop(); +} + +bool CSkinTimer::VerifyStartCondition() const +{ + return m_startCondition && m_startCondition->Get(INFO::DEFAULT_CONTEXT); +} + +bool CSkinTimer::VerifyResetCondition() const +{ + return m_resetCondition && m_resetCondition->Get(INFO::DEFAULT_CONTEXT); +} + +bool CSkinTimer::VerifyStopCondition() const +{ + return m_stopCondition && m_stopCondition->Get(INFO::DEFAULT_CONTEXT); +} + +INFO::InfoPtr CSkinTimer::GetStartCondition() const +{ + return m_startCondition; +} + +INFO::InfoPtr CSkinTimer::GetResetCondition() const +{ + return m_resetCondition; +} + +INFO::InfoPtr CSkinTimer::GetStopCondition() const +{ + return m_stopCondition; +} + +void CSkinTimer::OnStart() +{ + if (m_startActions.HasAnyActions()) + { + m_startActions.ExecuteActions(); + } +} + +void CSkinTimer::OnStop() +{ + if (m_stopActions.HasAnyActions()) + { + m_stopActions.ExecuteActions(); + } +} diff --git a/xbmc/addons/gui/skin/SkinTimer.h b/xbmc/addons/gui/skin/SkinTimer.h new file mode 100644 index 0000000..d838ef0 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimer.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/GUIAction.h" +#include "interfaces/info/InfoExpression.h" +#include "utils/Stopwatch.h" + +#include <memory> +#include <string> + +class TiXmlElement; + +/*! \brief Skin timers are skin objects that dependent on time and can be fully controlled from skins either using boolean + * conditions or builtin functions. This class represents the Skin Timer object. + * \sa Skin_Timers + */ +class CSkinTimer : public CStopWatch +{ +public: + /*! \brief Skin timer constructor + * \param name - the name of the timer + * \param startCondition - the boolean info expression to start the timer (may be null) + * \param resetCondition - the boolean info expression to reset the timer (may be null) + * \param stopCondition - the boolean info expression to stop the timer (may be null) + * \param startActions - the builtin functions to execute on timer start (actions may be empty) + * \param stopActions - the builtin functions to execute on timer stop (actions may be empty) + * \param resetOnStart - if the timer should be reset when started (i.e. start from zero if true or resumed if false) + */ + CSkinTimer(const std::string& name, + const INFO::InfoPtr& startCondition, + const INFO::InfoPtr& resetCondition, + const INFO::InfoPtr& stopCondition, + const CGUIAction& startActions, + const CGUIAction& stopActions, + bool resetOnStart); + + /*! \brief Default skin timer destructor */ + virtual ~CSkinTimer() = default; + + /*! \brief Start the skin timer */ + void Start(); + + /*! \brief Resets the skin timer so that the elapsed time of the timer is 0 */ + void Reset(); + + /*! \brief stops the skin timer */ + void Stop(); + + /*! \brief Getter for the timer start boolean condition/expression + * \return the start boolean condition/expression (may be null) + */ + INFO::InfoPtr GetStartCondition() const; + + /*! \brief Getter for the timer reset boolean condition/expression + * \return the reset boolean condition/expression (may be null) + */ + INFO::InfoPtr GetResetCondition() const; + + /*! \brief Getter for the timer start boolean condition/expression + * \return the start boolean condition/expression (may be null) + */ + INFO::InfoPtr GetStopCondition() const; + + /*! \brief Evaluates the timer start boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be started + * \return true if the condition is true, false otherwise + */ + bool VerifyStartCondition() const; + + /*! \brief Evaluates the timer reset boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be reset to 0 + * \return true if the condition is true, false otherwise + */ + bool VerifyResetCondition() const; + + /*! \brief Evaluates the timer stop boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be stopped + * \return true if the condition is true, false otherwise + */ + bool VerifyStopCondition() const; + +private: + /*! \brief Called when this timer is started */ + void OnStart(); + + /*! \brief Called when this timer is stopped */ + void OnStop(); + + /*! The name of the skin timer */ + std::string m_name; + /*! The info boolean expression that automatically starts the timer if evaluated true */ + INFO::InfoPtr m_startCondition; + /*! The info boolean expression that automatically resets the timer if evaluated true */ + INFO::InfoPtr m_resetCondition; + /*! The info boolean expression that automatically stops the timer if evaluated true */ + INFO::InfoPtr m_stopCondition; + /*! The builtin functions to be executed when the timer is started */ + CGUIAction m_startActions; + /*! The builtin functions to be executed when the timer is stopped */ + CGUIAction m_stopActions; + /*! if the timer should be reset on start (or just resumed) */ + bool m_resetOnStart{false}; +}; diff --git a/xbmc/addons/gui/skin/SkinTimerManager.cpp b/xbmc/addons/gui/skin/SkinTimerManager.cpp new file mode 100644 index 0000000..663f5aa --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimerManager.cpp @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SkinTimerManager.h" + +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIAction.h" +#include "guilib/GUIComponent.h" +#include "utils/StringUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/log.h" + +#include <chrono> +#include <mutex> + +using namespace std::chrono_literals; + +void CSkinTimerManager::LoadTimers(const std::string& path) +{ + CXBMCTinyXML doc; + if (!doc.LoadFile(path)) + { + CLog::LogF(LOGWARNING, "Could not load timers file {}: {} (row: {}, col: {})", path, + doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol()); + return; + } + + TiXmlElement* root = doc.RootElement(); + if (!root || !StringUtils::EqualsNoCase(root->Value(), "timers")) + { + CLog::LogF(LOGERROR, "Error loading timers file {}: Root element <timers> required.", path); + return; + } + + const TiXmlElement* timerNode = root->FirstChildElement("timer"); + while (timerNode) + { + LoadTimerInternal(timerNode); + timerNode = timerNode->NextSiblingElement("timer"); + } +} + +void CSkinTimerManager::LoadTimerInternal(const TiXmlElement* node) +{ + if ((!node->FirstChild("name") || !node->FirstChild("name")->FirstChild() || + node->FirstChild("name")->FirstChild()->ValueStr().empty())) + { + CLog::LogF(LOGERROR, "Missing required field name for valid skin. Ignoring timer."); + return; + } + + std::string timerName = node->FirstChild("name")->FirstChild()->Value(); + if (m_timers.count(timerName) > 0) + { + CLog::LogF(LOGWARNING, + "Ignoring timer with name {} - another timer with the same name already exists", + timerName); + return; + } + + // timer start + INFO::InfoPtr startInfo{nullptr}; + bool resetOnStart{false}; + if (node->FirstChild("start") && node->FirstChild("start")->FirstChild() && + !node->FirstChild("start")->FirstChild()->ValueStr().empty()) + { + startInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("start")->FirstChild()->ValueStr()); + // check if timer needs to be reset after start + if (node->FirstChildElement("start")->Attribute("reset") && + StringUtils::EqualsNoCase(node->FirstChildElement("start")->Attribute("reset"), "true")) + { + resetOnStart = true; + } + } + + // timer reset + INFO::InfoPtr resetInfo{nullptr}; + if (node->FirstChild("reset") && node->FirstChild("reset")->FirstChild() && + !node->FirstChild("reset")->FirstChild()->ValueStr().empty()) + { + resetInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("reset")->FirstChild()->ValueStr()); + } + // timer stop + INFO::InfoPtr stopInfo{nullptr}; + if (node->FirstChild("stop") && node->FirstChild("stop")->FirstChild() && + !node->FirstChild("stop")->FirstChild()->ValueStr().empty()) + { + stopInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("stop")->FirstChild()->ValueStr()); + } + + // process onstart actions + CGUIAction startActions; + startActions.EnableSendThreadMessageMode(); + const TiXmlElement* onStartElement = node->FirstChildElement("onstart"); + while (onStartElement) + { + if (onStartElement->FirstChild()) + { + const std::string conditionalActionAttribute = + onStartElement->Attribute("condition") != nullptr ? onStartElement->Attribute("condition") + : ""; + startActions.Append(CGUIAction::CExecutableAction{conditionalActionAttribute, + onStartElement->FirstChild()->Value()}); + } + onStartElement = onStartElement->NextSiblingElement("onstart"); + } + + // process onstop actions + CGUIAction stopActions; + stopActions.EnableSendThreadMessageMode(); + const TiXmlElement* onStopElement = node->FirstChildElement("onstop"); + while (onStopElement) + { + if (onStopElement->FirstChild()) + { + const std::string conditionalActionAttribute = + onStopElement->Attribute("condition") != nullptr ? onStopElement->Attribute("condition") + : ""; + stopActions.Append(CGUIAction::CExecutableAction{conditionalActionAttribute, + onStopElement->FirstChild()->Value()}); + } + onStopElement = onStopElement->NextSiblingElement("onstop"); + } + + m_timers[timerName] = std::make_unique<CSkinTimer>(CSkinTimer( + timerName, startInfo, resetInfo, stopInfo, startActions, stopActions, resetOnStart)); +} + +bool CSkinTimerManager::TimerIsRunning(const std::string& timer) const +{ + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return false; + } + return m_timers.at(timer)->IsRunning(); +} + +float CSkinTimerManager::GetTimerElapsedSeconds(const std::string& timer) const +{ + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return 0; + } + return m_timers.at(timer)->GetElapsedSeconds(); +} + +void CSkinTimerManager::TimerStart(const std::string& timer) const +{ + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return; + } + m_timers.at(timer)->Start(); +} + +void CSkinTimerManager::TimerStop(const std::string& timer) const +{ + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return; + } + m_timers.at(timer)->Stop(); +} + +void CSkinTimerManager::Stop() +{ + // skintimers, as infomanager clients register info conditions/expressions in the infomanager. + // The infomanager is linked to skins, being initialized or cleared when + // skins are loaded (or unloaded). All the registered boolean conditions from + // skin timers will end up being removed when the skin is unloaded. However, to + // self-contain this component unregister them all here. + for (auto const& [key, val] : m_timers) + { + const std::unique_ptr<CSkinTimer>::pointer timer = val.get(); + if (timer->GetStartCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStartCondition()); + } + if (timer->GetStopCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStopCondition()); + } + if (timer->GetResetCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetResetCondition()); + } + } + m_timers.clear(); +} + +void CSkinTimerManager::Process() +{ + for (const auto& [key, val] : m_timers) + { + const std::unique_ptr<CSkinTimer>::pointer timer = val.get(); + if (!timer->IsRunning() && timer->VerifyStartCondition()) + { + timer->Start(); + } + else if (timer->IsRunning() && timer->VerifyStopCondition()) + { + timer->Stop(); + } + if (timer->GetElapsedSeconds() > 0 && timer->VerifyResetCondition()) + { + timer->Reset(); + } + } +} diff --git a/xbmc/addons/gui/skin/SkinTimerManager.h b/xbmc/addons/gui/skin/SkinTimerManager.h new file mode 100644 index 0000000..fdf44d1 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimerManager.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "SkinTimer.h" + +#include <map> +#include <memory> +#include <string> + +/*! \brief CSkinTimerManager is the container and manager for Skin timers. Its role is that of + * checking if the timer boolean conditions are valid, start or stop timers and execute the respective + * builtin actions linked to the timer lifecycle + * \note This component should only be called by the main/rendering thread + * \sa Skin_Timers + * \sa CSkinTimer + */ +class CSkinTimerManager +{ +public: + /*! \brief Skin timer manager constructor */ + CSkinTimerManager() = default; + + /*! \brief Default skin timer manager destructor */ + ~CSkinTimerManager() = default; + + /*! \brief Loads all the skin timers + * \param path - the path for the skin Timers.xml file + */ + void LoadTimers(const std::string& path); + + /*! \brief Stops the manager */ + void Stop(); + + /*! \brief Checks if the timer with name `timer` is running + \param timer the name of the skin timer + \return true if the given timer exists and is running, false otherwise + */ + bool TimerIsRunning(const std::string& timer) const; + + /*! \brief Get the elapsed seconds since the timer with name `timer` was started + \param timer the name of the skin timer + \return the elapsed time in seconds the given timer is running (0 if not running or if it does not exist) + */ + float GetTimerElapsedSeconds(const std::string& timer) const; + + /*! \brief Starts/Enables a given skin timer + \param timer the name of the skin timer + */ + void TimerStart(const std::string& timer) const; + + /*! \brief Stops/Disables a given skin timer + \param timer the name of the skin timer + */ + void TimerStop(const std::string& timer) const; + + // CThread methods + + /*! \brief Run the main manager processing loop */ + void Process(); + +private: + /*! \brief Loads a specific timer + * \note Called internally from LoadTimers + * \param node - the XML representation of a skin timer object + */ + void LoadTimerInternal(const TiXmlElement* node); + + /*! Container for the skin timers */ + std::map<std::string, std::unique_ptr<CSkinTimer>> m_timers; +}; diff --git a/xbmc/addons/gui/skin/SkinTimers.dox b/xbmc/addons/gui/skin/SkinTimers.dox new file mode 100644 index 0000000..0d6f171 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimers.dox @@ -0,0 +1,164 @@ +/*! + +\page Skin_Timers Skin Timers +\brief **Programatic time-based resources for Skins** + +\tableofcontents + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect1 Description + +Skin timers are skin resources that are dependent on time and can be fully controlled from skins either using +\link page_List_of_built_in_functions **Builtin functions**\endlink or +\link modules__infolabels_boolean_conditions **Infolabels and Boolean conditions**\endlink. One can see them +as stopwatches that can be activated and deactivated automatically depending on the value of info expressions or simply activated/deactivated +manually from builtins. +The framework was created to allow skins to control the visibility of windows (and controls) depending on +the elapsed time of timers the skin defines. Skin timers allow multiple use cases in skins, previously only available via the execution +of python scripts: +- Closing a specific window after x seconds have elapsed +- Controlling the visibility of a group (or triggering an animation) depending on the elapsed time of a given timer +- Defining a buffer time window that is kept activated for a short period of time (e.g. keep controls visible for x seconds after a player seek) +- Executing timed actions (on timer stop or timer start) +- etc + +Skin timers are defined in the `Timers.xml` file within the xml directory of the skin. The file has the following "schema": + +~~~~~~~~~~~~~{.xml} +<timers> + <timer>...</timer> + <timer>...</timer> +</timers> +~~~~~~~~~~~~~ + +see \link Skin_Timers_sect2 the examples section\endlink and \link Skin_Timers_sect3 the list of available tags\endlink for concrete details. + +\skinning_v20 Added skin timers + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect2 Examples + +The following example illustrates the simplest possible skin timer. This timer is completely manual (it has to be manually started and stopped): + +~~~~~~~~~~~~~{.xml} +<timer> + <name>mymanualtimer</name> + <description>100% manual timer</description> +</timer> +~~~~~~~~~~~~~ + +This timer can be controlled from your skin by executing the \link Builtin_SkinStartTimer `Skin.TimerStart(mymanualtimer)` builtin\endlink or +\link Builtin_SkinStopTimer `Skin.TimerStop(mymanualtimer)` builtin\endlink. You can define the visibility of skin elements based on the internal +properties of the timer, such as the fact that the timer is active/running using \link Skin_TimerIsRunning `Skin.TimerIsRunning(mymanualtimer)` info\endlink +or depending on the elapsed time (e.g. 5 seconds) using the \link Skin_TimerElapsedSecs Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualtimer),5) info\endlink. + +The following timer is a variation of the previous timer but with the added ability of being automatically stopped by the skinning engine after a maximum of elapsed +5 seconds without having to issue the `Skin.TimerStop(mymanualtimer)` builtin: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>mymanualautocloseabletimer</name> + <description>100% manual autocloseable timer</description> + <stop>Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)</stop> +</timer> +~~~~~~~~~~~~~ + +This type of timer is particularly useful if you want to automatically close a specific window (or triggering a close animation) after x time has elapsed, +while guaranteeing the timer is also stopped. See the example below: + +~~~~~~~~~~~~~{.xml} +<?xml version="1.0" encoding="utf-8"?> +<window type="dialog" id="1109"> + <onload>Skin.TimerStart(mymanualautocloseabletimer)</onload> + ... + <controls> + <control type="group"> + <animation effect="slide" start="0,0" end="0,-80" time="300" condition="Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)">Conditional</animation> + ... + </control> + </controls> +</window> +~~~~~~~~~~~~~ + +The following timer presents a notification (for 1 sec) whenever the timer is activated or deactivated: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>manualtimerwithactions</name> + <description>100% manual timer with actions</description> + <onstart>Notification(skintimer, My timer was started, 1000)</onstart> + <onstop>Notification(skintimer, My timer was stopped, 1000)</onstop> +</timer> +~~~~~~~~~~~~~ + +The following timer is an example of a completely automatic timer. The timer is automatically activated or deactivated based on the value +of boolean info expressions. In this particular example, the timer is automatically started whenever the Player is playing a file (if not already running). It is stopped if +there is no file being played (and of course if previously running). Since the timer can be activated/deactivated multiple times, `reset="true"` ensures the timer is +always reset to 0 on each start operation. Whenever the timer is started or stopped, notifications are issued. + +~~~~~~~~~~~~~{.xml} +<timer> + <name>myautomatictimer</name> + <description>Player state checker</description> + <start reset="true">Player.Playing</start> + <stop>!Player.Playing</stop> + <onstart>Notification(skintimer, Player has started playing a file, 1000)</onstart> + <onstop>Notification(skintimer, Player is no longer playing a file, 1000)</onstop> +</timer> +~~~~~~~~~~~~~ + +In certain situations you might want to reset your timer without having to stop and start. For instance, if you want to stop the timer after 5 seconds +but have the timer resetting to 0 seconds if the user provides some input to Kodi. For such cases the `<reset/>` condition can be used: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>windowtimer</name> + <description>Reset on idle</description> + <start reset="true">Window.IsActive(mywindow)</start> + <reset>Window.IsActive(mywindow) + !System.IdleTime(1) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 1)</reset> + <stop>!Window.IsActive(mywindow) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 5)</stop> + <onstop>Dialog.Close(mywindow)</onstop> +</timer> +~~~~~~~~~~~~~ + +Finer conditional granularity can also be applied to the `onstop` or `onstart` actions. This allows the skinner to create generic timers which respect a +limited set of conditions but trigger different actions depending on a condition applied only to the action. +The following timer plays the trailer of a given item when the user is in the videos window, the item has a trailer, the player is not playing and the +global idle time is greater than 3 seconds. +As you can see, the first action (notification) is triggered for any item. The actual playback, on the other hand, will only play if the focused +item has the label "MyAwesomeMovie". + +~~~~~~~~~~~~~{.xml} +<timer> + <name>trailer_autoplay_idle_timer</name> + <start reset="true">System.IdleTime(3) + Window.IsVisible(videos) + !Player.HasMedia + !String.IsEmpty(ListItem.Trailer)</start> + <onstart>Notification(skintimer try play, $INFO[ListItem.Trailer], 1000)</onstart> + <onstart condition="String.IsEqual(ListItem.Label,MyAwesomeMovie)">PlayMedia($INFO[ListItem.Trailer],1,noresume)</onstart> +</timer> +~~~~~~~~~~~~~ + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect3 Available tags + +Skin timers have the following available tags: + +| Tag | Description | +|--------------:|:--------------------------------------------------------------| +| name | The unique name of the timer. The name is used as the id of the timer, hence needs to be unique. <b>(required)</b> +| description | The description of the timer, a helper string. <b>(optional)</b> +| start | An info bool expression that the skinning engine should use to automatically start the timer <b>(optional)</b> +| reset | An info bool expression that the skinning engine should use to automatically reset the timer <b>(optional)</b> +| stop | An info bool expression that the skinning engine should use to automatically stop the timer <b>(optional)</b> +| onstart | A builtin function that the skinning engine should execute when the timer is started <b>(optional)</b><b>(can be repeated)</b>. Supports an additional `"condition"` as element attribute. +| onstop | A builtin function that the skinning engine should execute when the timer is stopped <b>(optional)</b><b>(can be repeated)</b>. Supports an additional `"condition"` as element attribute. + +@note If multiple onstart or onstop actions exist, their execution is triggered sequentially. +@note Both onstart and onstop actions support fine-grained conditional granularity by specifying a "condition" attribute (see the examples above). + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect4 See also +#### Development: + +- [Skinning](http://kodi.wiki/view/Skinning) + +*/ |