summaryrefslogtreecommitdiffstats
path: root/xbmc/addons/gui
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/addons/gui')
-rw-r--r--xbmc/addons/gui/CMakeLists.txt13
-rw-r--r--xbmc/addons/gui/GUIDialogAddonInfo.cpp940
-rw-r--r--xbmc/addons/gui/GUIDialogAddonInfo.h162
-rw-r--r--xbmc/addons/gui/GUIDialogAddonSettings.cpp484
-rw-r--r--xbmc/addons/gui/GUIDialogAddonSettings.h54
-rw-r--r--xbmc/addons/gui/GUIHelpers.cpp49
-rw-r--r--xbmc/addons/gui/GUIHelpers.h41
-rw-r--r--xbmc/addons/gui/GUIViewStateAddonBrowser.cpp64
-rw-r--r--xbmc/addons/gui/GUIViewStateAddonBrowser.h21
-rw-r--r--xbmc/addons/gui/GUIWindowAddonBrowser.cpp644
-rw-r--r--xbmc/addons/gui/GUIWindowAddonBrowser.h115
-rw-r--r--xbmc/addons/gui/skin/CMakeLists.txt7
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.cpp97
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.h110
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.cpp222
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.h77
-rw-r--r--xbmc/addons/gui/skin/SkinTimers.dox164
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)
+
+*/