MythTV master
themechooser.cpp
Go to the documentation of this file.
1// C++ headers
2#include <chrono>
3
4// Qt headers
5#include <QCoreApplication>
6#include <QRegularExpression>
7#include <QRunnable>
8
9// MythTV headers
15#include "libmythbase/mythversion.h"
18#include "libmythbase/unziputil.h" // for extractZIP
30
31// Theme Chooser headers
32#include "themechooser.h"
33
34#define LOC QString("ThemeChooser: ")
35#define LOC_WARN QString("ThemeChooser, Warning: ")
36#define LOC_ERR QString("ThemeChooser, Error: ")
37
38static const QRegularExpression kVersionDateRE{"\\.[0-9]{8,}.*"};
39
43class ThemeExtractThread : public QRunnable
44{
45public:
47 QString srcFile, QString destDir) : m_parent(parent),
48 m_srcFile(std::move(srcFile)),
49 m_destDir(std::move(destDir)) {}
50
51 void run() override // QRunnable
52 {
54
55 auto *me = new MythEvent("THEME_INSTALLED", QStringList(m_srcFile));
56 QCoreApplication::postEvent(m_parent, me);
57 }
58
59private:
61 QString m_srcFile;
62 QString m_destDir;
63};
64
70 const QString &name) : MythScreenType(parent, name)
71{
73
74 StorageGroup sgroup("Themes", gCoreContext->GetHostName());
75 m_userThemeDir = sgroup.GetFirstDir(true);
76}
77
79{
81}
82
83static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
84{
85 return s1.fileName().toLower() < s2.fileName().toLower();
86}
87
89{
90 // Load the theme for this screen
91 if (!LoadWindowFromXML("settings-ui.xml", "themechooser", this))
92 return false;
93
94 bool err = false;
95 UIUtilE::Assign(this, m_themes, "themes", &err);
96
97 UIUtilW::Assign(this, m_preview, "preview");
98 UIUtilW::Assign(this, m_fullPreviewStateType, "fullpreviewstate");
99
101 {
102 MythUIGroup *state =
103 dynamic_cast<MythUIGroup *>(m_fullPreviewStateType->GetChild("fullscreen"));
104 if (state)
105 {
107 dynamic_cast<MythUIText *>(state->GetChild("fullscreenname"));
109 dynamic_cast<MythUIImage *>(state->GetChild("fullscreenpreview"));
110 }
111 }
112
113 if (err)
114 {
115 LOG(VB_GENERAL, LOG_ERR, LOC + "Cannot load screen 'themechooser'");
116 return false;
117 }
118
120 this, qOverload<MythUIButtonListItem *>(&ThemeChooser::saveAndReload));
123
125
127
128 return true;
129}
130
132{
133 SetBusyPopupMessage(tr("Loading Installed Themes"));
134
135 QStringList themesSeen;
136 QDir themes(m_userThemeDir);
137 themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
138 themes.setSorting(QDir::Name | QDir::IgnoreCase);
139
140 m_infoList = themes.entryInfoList();
141
142 for (const auto &theme : std::as_const(m_infoList))
143 {
144 if (loadThemeInfo(theme))
145 {
146 themesSeen << theme.fileName();
147 m_themeStatuses[theme.fileName()] = "default";
148 }
149 }
150
151 themes.setPath(GetThemesParentDir());
152 QFileInfoList sharedThemes = themes.entryInfoList();
153 for (const auto &sharedTheme : std::as_const(sharedThemes))
154 {
155 if ((!themesSeen.contains(sharedTheme.fileName())) &&
156 (loadThemeInfo(sharedTheme)))
157 {
158 m_infoList << sharedTheme;
159 themesSeen << sharedTheme.fileName();
160 m_themeStatuses[sharedTheme.fileName()] = "default";
161 }
162 }
163
164 uint major { 0 };
165 uint minor { 0 };
166 bool devel { false };
167 bool parsed = ParseMythSourceVersion(devel, major, minor);
168
169 if (!parsed || devel)
170 {
171 if (!parsed)
172 LOG(VB_GENERAL, LOG_ERR,
173 QString("Invalid MythTV version %1, will use themes from devel")
174 .arg(GetMythSourceVersion()));
175 LOG(VB_GUI, LOG_INFO, QString("Loading themes for devel"));
176 LoadVersion("trunk", themesSeen, true);
177 }
178 else
179 {
180 QString MythVersion { QString::number(major) };
181 LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
182 if (LoadVersion(MythVersion, themesSeen, true))
183 {
184 for (int idx = minor ; idx > 0; idx--)
185 {
186 QString subversion;
187 subversion = MythVersion + "." + QString::number(idx);
188 LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(subversion));
189 LoadVersion(subversion, themesSeen, false);
190 }
191
193
194 // QList doesn't always play well with std::range
195 // NOLINTNEXTLINE(modernize-use-ranges)
196 std::sort(m_infoList.begin(), m_infoList.end(), sortThemeNames);
197 }
198 else
199 {
200
201 LOG(VB_GENERAL, LOG_INFO, QString("Failed to load themes for %1, trying devel").arg(MythVersion));
202 if (!LoadVersion("trunk", themesSeen, true))
203 {
204 LOG(VB_GENERAL, LOG_WARNING, QString("Failed to load themes for devel"));
205 }
206 }
207 }
208}
209
211 QStringList &themesSeen, bool alert_user)
212{
213 QString remoteThemesFile = GetConfDir();
214 remoteThemesFile.append("/tmp/themes.zip");
215 QString themeSite = QString("%1/%2")
216 .arg(gCoreContext->GetSetting("ThemeRepositoryURL",
217 "http://themes.mythtv.org/themes/repository"),
218 version);
219 QString destdir = GetCacheDir().append("/themechooser");
220 QString versiondir = QString("%1/%2").arg(destdir, version);
221 QDir remoteThemesDir(versiondir);
222
223 int downloadFailures =
224 gCoreContext->GetNumSetting("ThemeInfoDownloadFailures", 0);
225 if (QFile::exists(remoteThemesFile))
226 {
227 QFileInfo finfo(remoteThemesFile);
228 if (finfo.lastModified().toUTC() <
229 MythDate::current().addSecs(-600))
230 {
231 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 is over 10 minutes old, forcing "
232 "remote theme list download")
233 .arg(remoteThemesFile));
235 }
236
237 if (!remoteThemesDir.exists())
238 {
240 }
241 }
242 else if (downloadFailures < 2) // (and themes.zip does not exist)
243 {
244 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 does not exist, forcing remote theme "
245 "list download")
246 .arg(remoteThemesFile));
248 }
249
251 {
252 QFile test(remoteThemesFile);
253 if (test.open(QIODevice::WriteOnly))
254 {
255 test.remove();
256 }
257 else
258 {
259 ShowOkPopup(tr("Unable to create '%1'").arg(remoteThemesFile));
260 return false;
261 }
262
263 SetBusyPopupMessage(tr("Refreshing Downloadable Themes Information"));
264
265 QString url = themeSite;
266 url.append("/themes.zip");
267 if (!removeThemeDir(versiondir))
268 ShowOkPopup(tr("Unable to remove '%1'").arg(versiondir));
269 QDir dir;
270 if (!dir.mkpath(destdir))
271 ShowOkPopup(tr("Unable to create '%1'").arg(destdir));
272 bool result = GetMythDownloadManager()->download(url, remoteThemesFile, true);
273
274 LOG(VB_GUI, LOG_INFO, LOC + QString("Downloading '%1' to '%2'").arg(url, remoteThemesFile));
275
276 SetBusyPopupMessage(tr("Extracting Downloadable Themes Information"));
277
278 if (!result || !extractZIP(remoteThemesFile, destdir))
279 {
280 QFile::remove(remoteThemesFile);
281
282 downloadFailures++;
283 gCoreContext->SaveSetting("ThemeInfoDownloadFailures",
284 downloadFailures);
285
286 if (!result)
287 {
288 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to download '%1'").arg(url));
289 if (alert_user)
290 ShowOkPopup(tr("Failed to download '%1'").arg(url));
291 }
292 else
293 {
294 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to unzip '%1' to '%2'").arg(remoteThemesFile, destdir));
295 if (alert_user)
296 ShowOkPopup(tr("Failed to unzip '%1' to '%2'")
297 .arg(remoteThemesFile, destdir));
298 }
299 return false;
300 }
301 LOG(VB_GUI, LOG_INFO, LOC + QString("Unzipped '%1' to '%2'").arg(remoteThemesFile, destdir));
302 }
303
304 QDir themes;
305 themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
306 themes.setSorting(QDir::Name | QDir::IgnoreCase);
307
308 if ((QFile::exists(remoteThemesFile)) &&
309 (remoteThemesDir.exists()))
310 {
311 SetBusyPopupMessage(tr("Loading Downloadable Themes"));
312
313 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 and %2 exist, using cached remote themes list").arg(remoteThemesFile, remoteThemesDir.absolutePath()));
314
315 QString themesPath = remoteThemesDir.absolutePath();
316 themes.setPath(themesPath);
317
318 QFileInfoList downloadableThemes = themes.entryInfoList();
319 for (const auto &dtheme : std::as_const(downloadableThemes))
320 {
321 QString dirName = dtheme.fileName();
322 QString themeName = dirName;
323 QString remoteDir = themeSite;
324 remoteDir.append("/").append(dirName);
325 QString localDir = themes.absolutePath();
326 localDir.append("/").append(dirName);
327
328 ThemeInfo remoteTheme(dtheme.absoluteFilePath());
329
330 if (themesSeen.contains(dirName))
331 {
332 ThemeInfo *localTheme = m_themeNameInfos[dirName];
333
334 themeName = remoteTheme.GetName();
335
336 int rmtMaj = remoteTheme.GetMajorVersion();
337 int rmtMin = remoteTheme.GetMinorVersion();
338 int locMaj = localTheme->GetMajorVersion();
339 int locMin = localTheme->GetMinorVersion();
340
341 if ((rmtMaj > locMaj) ||
342 ((rmtMaj == locMaj) &&
343 (rmtMin > locMin)))
344 {
345 if (loadThemeInfo(dtheme))
346 {
347 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' old version %2.%3, new version %4.%5").arg(themeName).arg(locMaj).arg(locMin).arg(rmtMaj).arg(rmtMin));
348
349 m_infoList << dtheme;
350 m_themeStatuses[themeName] = "updateavailable";
351
352 QFileInfo finfo(remoteTheme.GetPreviewPath());
354 remoteDir.append("/").append(finfo.fileName()),
355 localDir.append("/").append(finfo.fileName()),
356 nullptr);
357 }
358 }
359 else if ((rmtMaj == locMaj) &&
360 (rmtMin == locMin))
361 {
362 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' up to date (%2.%3)").arg(themeName).arg(locMaj).arg(locMin));
363
364 m_themeStatuses[themeName] = "uptodate";
365 }
366 }
367 else
368 {
369 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' (%2.%3) available").arg(themeName).arg(remoteTheme.GetMajorVersion()).arg(remoteTheme.GetMinorVersion()));
370
371 ThemeInfo *tmpTheme = loadThemeInfo(dtheme);
372 if (tmpTheme)
373 {
374 themeName = tmpTheme->GetName();
375 themesSeen << dirName;
376 m_infoList << dtheme;
377 m_themeStatuses[themeName] = "updateavailable";
378
379 QFileInfo finfo(tmpTheme->GetPreviewPath());
381 remoteDir.append("/").append(finfo.fileName()),
382 localDir.append("/").append(finfo.fileName()),
383 nullptr);
384 }
385 }
386 }
387 return true;
388 }
389 return false;
390}
391
393{
394 QString curTheme = gCoreContext->GetSetting("Theme");
395 ThemeInfo *themeinfo = nullptr;
396 ThemeInfo *curThemeInfo = nullptr;
397 MythUIButtonListItem *item = nullptr;
398
399 m_themes->Reset();
400 for (const auto &theme : std::as_const(m_infoList))
401 {
402 if (!m_themeFileNameInfos.contains(theme.filePath()))
403 continue;
404
405 themeinfo = m_themeFileNameInfos[theme.filePath()];
406 if (!themeinfo)
407 continue;
408
409 QString buttonText = QString("%1 %2.%3")
410 .arg(themeinfo->GetName())
411 .arg(themeinfo->GetMajorVersion())
412 .arg(themeinfo->GetMinorVersion());
413
414 item = new MythUIButtonListItem(m_themes, buttonText);
415 if (item)
416 {
417 if (themeinfo->GetDownloadURL().isEmpty())
418 item->DisplayState("local", "themelocation");
419 else
420 item->DisplayState("remote", "themelocation");
421
422 item->DisplayState(themeinfo->GetAspect(), "aspectstate");
423
424 item->DisplayState(m_themeStatuses[themeinfo->GetName()],
425 "themestatus");
426 InfoMap infomap;
427 themeinfo->ToMap(infomap);
428 item->SetTextFromMap(infomap);
429 item->SetData(QVariant::fromValue(themeinfo));
430
431 QString thumbnail = themeinfo->GetPreviewPath();
432 // Downloadable themeinfos have thumbnail copies of their preview images
433 if (!themeinfo->GetDownloadURL().isEmpty())
434 thumbnail = thumbnail.append(".thumb.jpg");
435 item->SetImage(thumbnail);
436
437 if (curTheme == themeinfo->GetDirectoryName())
438 curThemeInfo = themeinfo;
439 }
440 }
441
443
444 if (curThemeInfo)
445 m_themes->SetValueByData(QVariant::fromValue(curThemeInfo));
446
448 if (current)
450
451 QString testFile = m_userThemeDir + "/.test";
452 QFile test(testFile);
453 if (test.open(QIODevice::WriteOnly))
454 {
455 test.remove();
456 }
457 else
458 {
459 ShowOkPopup(tr("Error creating test file, %1 themes directory is "
460 "not writable.")
461 .arg(m_userThemeDir));
462 }
463}
464
466{
467 if (theme.fileName() == "default" || theme.fileName() == "default-wide")
468 return nullptr;
469
470 ThemeInfo *themeinfo = nullptr;
471 if (theme.exists()) // local directory vs http:// or remote URL
472 themeinfo = new ThemeInfo(theme.absoluteFilePath());
473 else
474 themeinfo = new ThemeInfo(theme.filePath());
475
476 if (!themeinfo)
477 return nullptr;
478
479 if (themeinfo->GetName().isEmpty() || ((themeinfo->GetType() & THEME_UI) == 0))
480 {
481 delete themeinfo;
482 return nullptr;
483 }
484
485 m_themeFileNameInfos[theme.filePath()] = themeinfo;
486 m_themeNameInfos[theme.fileName()] = themeinfo;
487
488 return themeinfo;
489}
490
492{
493 if (m_popupMenu)
494 return;
495
496 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
497 QString label = tr("Theme Chooser Menu");
498
500 new MythDialogBox(label, popupStack, "themechoosermenupopup");
501
503
504 if (m_popupMenu->Create())
505 {
506 popupStack->AddScreen(m_popupMenu);
507 }
508 else
509 {
510 delete m_popupMenu;
511 m_popupMenu = nullptr;
512 return;
513 }
514
515 m_popupMenu->SetReturnEvent(this, "popupmenu");
516
518 {
520 {
521 m_popupMenu->AddButton(tr("Hide Fullscreen Preview"),
523 }
524 else
525 {
526 m_popupMenu->AddButton(tr("Show Fullscreen Preview"),
528 }
529 }
530
531 m_popupMenu->AddButton(tr("Refresh Downloadable Themes"),
533
535 if (current)
536 {
537 auto *info = current->GetData().value<ThemeInfo *>();
538
539 if (info)
540 {
541 m_popupMenu->AddButton(tr("Select Theme"),
542 qOverload<>(&ThemeChooser::saveAndReload));
543
544 if (info->GetPreviewPath().startsWith(m_userThemeDir))
545 m_popupMenu->AddButton(tr("Delete Theme"),
547 }
548 }
549
550 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
551 {
552 m_popupMenu->AddButton(tr("Disable Theme Update Notifications"),
554 }
555 else
556 {
557 m_popupMenu->AddButton(tr("Enable Theme Update Notifications"),
559 }
560}
561
562void ThemeChooser::popupClosed([[maybe_unused]] const QString &which,
563 [[maybe_unused]] int result)
564{
565 m_popupMenu = nullptr;
566}
567
568bool ThemeChooser::keyPressEvent(QKeyEvent *event)
569{
570 if (GetFocusWidget()->keyPressEvent(event))
571 return true;
572
573 QStringList actions;
574 bool handled = GetMythMainWindow()->TranslateKeyPress("Theme Chooser", event, actions);
575
576 for (int i = 0; i < actions.size() && !handled; ++i)
577 {
578 const QString& action = actions[i];
579 handled = true;
580
581 if (action == "MENU")
582 {
584 }
585 else if (action == "DELETE")
586 {
587 removeTheme();
588 }
589 else if ((action == "ESCAPE") &&
591 {
593 }
594 else
595 {
596 handled = false;
597 }
598 }
599
600 if (!handled && MythScreenType::keyPressEvent(event))
601 handled = true;
602
603 return handled;
604}
605
607{
609 {
611 {
614
617
619 m_fullPreviewShowing = false;
620 }
621 else
622 {
624 auto *info = item->GetData().value<ThemeInfo *>();
625 if (info)
626 {
628 {
629 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
631 }
632
634 m_fullScreenName->SetText(info->GetName());
635
638 }
639 }
640 }
641}
642
644{
645 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
646 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "0", "");
647 else
648 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "1", "");
649}
650
652{
653 LOG(VB_GUI, LOG_INFO, LOC + "Forcing remote theme list refresh");
655 gCoreContext->SaveSetting("ThemeInfoDownloadFailures", 0);
657}
658
660{
662 if (current)
664}
665
667{
668 auto *info = item->GetData().value<ThemeInfo *>();
669
670 if (!info)
671 return;
672
673 if (!info->GetDownloadURL().isEmpty())
674 {
675 QString testFile = m_userThemeDir + "/.test";
676 QFile test(testFile);
677 if (test.open(QIODevice::WriteOnly))
678 {
679 test.remove();
680 }
681 else
682 {
683 ShowOkPopup(tr("Unable to install theme, %1 themes directory is "
684 "not writable.")
685 .arg(m_userThemeDir));
686 return;
687 }
688
689 QString downloadURL = info->GetDownloadURL();
690 LOG(VB_FILE, LOG_INFO, QString("Download url is %1").arg(downloadURL));
691 QFileInfo qfile(downloadURL);
692 QString baseName = qfile.fileName();
693
694 if (!gCoreContext->GetSetting("ThemeDownloadURL").isEmpty())
695 {
696 QStringList tokens =
697 gCoreContext->GetSetting("ThemeDownloadURL")
698 .split(";", Qt::SkipEmptyParts);
699 QString origURL = downloadURL;
700 downloadURL.replace(tokens[0], tokens[1]);
701 LOG(VB_FILE, LOG_WARNING, LOC + QString("Theme download URL overridden from %1 to %2.").arg(origURL, downloadURL));
702 }
703
704 OpenBusyPopup(tr("Downloading %1 Theme").arg(info->GetName()));
706#if 0
708 "Temp", baseName);
710#else
711 QString localFile = GetConfDir() + "/tmp/" + baseName;
713 m_downloadFile = localFile;
715#endif
716 }
717 else
718 {
719 gCoreContext->SaveSetting("Theme", info->GetDirectoryName());
720 GetMythMainWindow()->JumpTo("Reload Theme");
721 }
722}
723
725{
726 auto *info = item->GetData().value<ThemeInfo *>();
727
728 if (!info)
729 return;
730
731 QFileInfo preview(info->GetPreviewPath());
732 InfoMap infomap;
733 info->ToMap(infomap);
734 SetTextFromMap(infomap);
735 if (m_preview)
736 {
737 if (preview.exists())
738 {
739 m_preview->SetFilename(info->GetPreviewPath());
740 m_preview->Load();
741 }
742 else
743 {
744 m_preview->Reset();
745 }
746 }
748 {
750 {
751 if (preview.exists())
752 {
753 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
755 }
756 else
757 {
759 }
760 }
761
763 m_fullScreenName->SetText(info->GetName());
764 }
765
766 MythUIStateType *themeLocation =
767 dynamic_cast<MythUIStateType *>(GetChild("themelocation"));
768 if (themeLocation)
769 {
770 if (info->GetDownloadURL().isEmpty())
771 themeLocation->DisplayState("local");
772 else
773 themeLocation->DisplayState("remote");
774 }
775
776 MythUIStateType *aspectState =
777 dynamic_cast<MythUIStateType *>(GetChild("aspectstate"));
778 if (aspectState)
779 aspectState->DisplayState(info->GetAspect());
780}
781
782void ThemeChooser::updateProgressBar(int bytesReceived,
783 int bytesTotal)
784{
785 MythUIProgressBar *progressBar =
786 dynamic_cast<MythUIProgressBar *>(GetChild("downloadprogressbar"));
787
788 if (!progressBar)
789 return;
790
791 progressBar->SetUsed(bytesReceived);
792 progressBar->SetTotal(bytesTotal);
793}
794
796{
797 if (e->type() == MythEvent::kMythEventMessage)
798 {
799 auto *me = dynamic_cast<MythEvent *>(e);
800 if (me == nullptr)
801 return;
802
803 QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
804 if (tokens.isEmpty())
805 return;
806
807 if (tokens[0] == "DOWNLOAD_FILE")
808 {
809 QStringList args = me->ExtraDataList();
810 if ((m_downloadState == dsIdle) ||
811 (tokens.size() != 2) ||
812 (!m_downloadTheme) ||
813 (args[1] != m_downloadFile))
814 return;
815
816 if (tokens[1] == "UPDATE")
817 {
818 updateProgressBar(args[2].toInt(), args[3].toInt());
819 }
820 else if (tokens[1] == "FINISHED")
821 {
822 bool remoteFileIsLocal = false;
823 int fileSize = args[2].toInt();
824 int errorCode = args[4].toInt();
825
827
828 QFileInfo file(m_downloadFile);
830 (m_downloadFile.startsWith("myth://")))
831 {
832 // The backend download is finished so start the
833 // frontend download
834 LOG(VB_FILE, LOG_INFO, QString("Download done MBE %1 %2").arg(errorCode).arg(fileSize));
835 if ((errorCode == 0) &&
836 (fileSize > 0))
837 {
839 QString localFile = GetConfDir() + "/tmp/" +
840 file.fileName();
841 file.setFile(localFile);
842
843 if (file.exists())
844 {
845 remoteFileIsLocal = true;
846 m_downloadFile = localFile;
847 }
848 else
849 {
851 m_downloadFile, localFile, this);
852 OpenBusyPopup(tr("Copying %1 Theme Package")
853 .arg(m_downloadTheme->GetName()));
854 m_downloadFile = localFile;
855 return;
856 }
857 }
858 else
859 {
861 ShowOkPopup(tr("ERROR downloading theme package on master backend."));
862 }
863 }
864
866 (file.exists()))
867 {
868 // The frontend download is finished
869 LOG(VB_FILE, LOG_INFO, QString("Download done MFE %1 %2").arg(errorCode).arg(fileSize));
870 // moved error is ok
871 if ((errorCode == 0) &&
872 (fileSize > 0))
873 {
875 auto *extractThread =
879 extractThread, "ThemeExtract");
880
881 if (!remoteFileIsLocal)
883
884 OpenBusyPopup(tr("Installing %1 Theme")
885 .arg(m_downloadTheme->GetName()));
886 }
887 else
888 {
890 ShowOkPopup(tr("ERROR downloading theme package from frontend."));
891 }
892 }
893 }
894 }
895 else if ((me->Message() == "THEME_INSTALLED") &&
896 (m_downloadTheme) &&
898 {
901 QStringList args = me->ExtraDataList();
902
903 if (!args.isEmpty() && !args[0].isEmpty())
904 QFile::remove(args[0]);
905
906 QString event = QString("THEME_INSTALLED PATH %1")
907 .arg(m_userThemeDir +
910
912
913 // Send a message to ourself so we trigger a reload our next chance
914 auto *me2 = new MythEvent("THEME_RELOAD");
915 qApp->postEvent(this, me2);
916 }
917 else if ((me->Message() == "THEME_RELOAD") &&
919 {
920 GetMythMainWindow()->JumpTo("Reload Theme");
921 }
922 }
923}
924
926{
928 if (!current)
929 {
930 ShowOkPopup(tr("Error, no theme selected."));
931 return;
932 }
933
934 auto *info = current->GetData().value<ThemeInfo *>();
935 if (!info)
936 {
937 ShowOkPopup(tr("Error, unable to find current theme."));
938 return;
939 }
940
941 if (!info->GetPreviewPath().startsWith(m_userThemeDir))
942 {
943 ShowOkPopup(tr("%1 is not a user-installed theme and can not "
944 "be deleted.")
945 .arg(info->GetName()));
946 return;
947 }
948
949 removeThemeDir(m_userThemeDir + info->GetDirectoryName());
950
952}
953
954bool ThemeChooser::removeThemeDir(const QString &dirname)
955{
956 if ((!dirname.startsWith(m_userThemeDir)) &&
957 (!dirname.startsWith(GetMythUI()->GetThemeCacheDir())))
958 return true;
959
960 QDir dir(dirname);
961
962 if (!dir.exists())
963 return true;
964
965 dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
966 QFileInfoList list = dir.entryInfoList();
967
968 for (const auto &fi : std::as_const(list))
969 {
970 if (fi.isFile() && !fi.isSymLink())
971 {
972 if (!QFile::remove(fi.absoluteFilePath()))
973 return false;
974 }
975 else if (fi.isDir() && !fi.isSymLink())
976 {
977 if (!removeThemeDir(fi.absoluteFilePath()))
978 return false;
979 }
980 }
981
982 return dir.rmdir(dirname);
983}
984
986
987ThemeUpdateChecker::ThemeUpdateChecker(void) : m_updateTimer(new QTimer(this))
988{
989 uint major { 0 };
990 uint minor { 0 };
991 bool devel { false };
992 bool parsed = ParseMythSourceVersion(devel, major, minor);
993
994 if (!parsed || devel)
995 {
996 m_mythVersions << "trunk";
997 }
998 else
999 {
1000 for (int i = minor ; i > 0; i--)
1001 m_mythVersions << QString("%1.%2").arg(major).arg(i);
1002 m_mythVersions << QString::number(major);
1003 }
1004
1007 "remotethemes/themes.zip",
1008 "Temp");
1009
1010 gCoreContext->SaveSetting("ThemeUpdateStatus", "");
1011
1013
1014 if (qEnvironmentVariableIsSet("MYTHTV_DEBUGMDM"))
1015 {
1016 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every minute");
1017 m_updateTimer->start(1min);
1018 }
1019 else
1020 {
1021 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every hour");
1022 m_updateTimer->start(1h);
1023 }
1024
1025 // Run once 15 seconds from now
1026 QTimer::singleShot(15s, this, &ThemeUpdateChecker::checkForUpdate);
1027}
1028
1030{
1031 if (m_updateTimer)
1032 {
1033 m_updateTimer->stop();
1034 delete m_updateTimer;
1035 m_updateTimer = nullptr;
1036 }
1037}
1038
1040{
1041 if (GetMythUI()->GetCurrentLocation(false, true) != "mainmenu")
1042 return;
1043
1044 ThemeInfo *localTheme = nullptr;
1045
1047 {
1048 QStringList::iterator Iversion;
1049
1050 for (Iversion = m_mythVersions.begin();
1051 Iversion != m_mythVersions.end(); ++Iversion)
1052 {
1053
1054 QString remoteThemeDir =
1057 QString("remotethemes/%1/%2")
1058 .arg(*Iversion,
1059 GetMythUI()->GetThemeName()),
1060 "Temp");
1061
1062 QString infoXML = remoteThemeDir;
1063 infoXML.append("/themeinfo.xml");
1064
1065 LOG(VB_GUI, LOG_INFO, QString("ThemeUpdateChecker Loading '%1'").arg(infoXML));
1066
1067 if (RemoteFile::Exists(infoXML))
1068 {
1069 int locMaj = 0;
1070 int locMin = 0;
1071
1072 auto *remoteTheme = new ThemeInfo(remoteThemeDir);
1073 if (!remoteTheme || remoteTheme->GetType() & THEME_UNKN)
1074 {
1075 LOG(VB_GENERAL, LOG_ERR,
1076 QString("ThemeUpdateChecker::checkForUpdate(): "
1077 "Unable to create ThemeInfo for %1")
1078 .arg(infoXML));
1079 delete remoteTheme;
1080 remoteTheme = nullptr;
1081 return;
1082 }
1083
1084 if (!localTheme)
1085 {
1086 localTheme = new ThemeInfo(GetMythUI()->GetThemeDir());
1087 if (!localTheme || localTheme->GetType() & THEME_UNKN)
1088 {
1089 LOG(VB_GENERAL, LOG_ERR,
1090 "ThemeUpdateChecker::checkForUpdate(): "
1091 "Unable to create ThemeInfo for current theme");
1092 delete localTheme;
1093 localTheme = nullptr;
1094 return;
1095 }
1096 locMaj = localTheme->GetMajorVersion();
1097 locMin = localTheme->GetMinorVersion();
1098 }
1099
1100 int rmtMaj = remoteTheme->GetMajorVersion();
1101 int rmtMin = remoteTheme->GetMinorVersion();
1102
1103 delete remoteTheme;
1104 remoteTheme = nullptr;
1105
1106 if ((rmtMaj > locMaj) ||
1107 ((rmtMaj == locMaj) &&
1108 (rmtMin > locMin)))
1109 {
1111 QString("%1-%2.%3").arg(GetMythUI()->GetThemeName()).arg(rmtMaj).arg(rmtMin);
1112
1113 QString status = gCoreContext->GetSetting("ThemeUpdateStatus");
1114 QString currentLocation = GetMythUI()->GetCurrentLocation(false, true);
1115
1116 if ((!status.startsWith(m_lastKnownThemeVersion)) &&
1117 (currentLocation == "mainmenu"))
1118 {
1119 m_currentVersion = QString("%1.%2")
1120 .arg(locMaj)
1121 .arg(locMin);
1122 m_newVersion = QString("%1.%2").arg(rmtMaj).arg(rmtMin);
1123
1124 gCoreContext->SaveSetting("ThemeUpdateStatus",
1125 m_lastKnownThemeVersion + " notified");
1126
1127 QString message = tr("Version %1 of the %2 theme is now "
1128 "available in the Theme Chooser. "
1129 "The currently installed version "
1130 "is %3.")
1131 .arg(m_newVersion,
1132 GetMythUI()->GetThemeName(),
1134
1135 ShowOkPopup(message);
1136 break;
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 delete localTheme;
1144}
1145
1146/* vim: set expandtab tabstop=4 shiftwidth=4: */
static MThreadPool * globalInstance(void)
void start(QRunnable *runnable, const QString &debugName, int priority=0)
QString GetHostName(void)
void SaveSetting(const QString &key, int newValue)
QString GetSetting(const QString &key, const QString &defaultval="")
void SendSystemEvent(const QString &msg)
bool SaveSettingOnHost(const QString &key, const QString &newValue, const QString &host)
static int GetMasterServerPort(void)
Returns the Master Backend control port If no master server port has been defined in the database,...
static QString GenMythURL(const QString &host=QString(), int port=0, QString path=QString(), const QString &storageGroup=QString())
int GetNumSetting(const QString &key, int defaultval=0)
QString GetMasterHostName(void)
bool GetBoolSetting(const QString &key, bool defaultval=false)
Basic menu dialog, message and a list of options.
void AddButton(const QString &title)
void SetReturnEvent(QObject *retobject, const QString &resultid)
void Closed(QString, int)
bool Create(void) override
void queueDownload(const QString &url, const QString &dest, QObject *caller, bool reload=false)
Adds a url to the download queue.
bool download(const QString &url, const QString &dest, bool reload=false)
Downloads a URL to a file in blocking mode.
This class is used as a container for messages.
Definition: mythevent.h:17
const QString & Message() const
Definition: mythevent.h:65
static const Type kMythEventMessage
Definition: mythevent.h:79
void JumpTo(const QString &Destination, bool Pop=true)
bool TranslateKeyPress(const QString &Context, QKeyEvent *Event, QStringList &Actions, bool AllowJumps=true)
Get a list of actions for a keypress in the given context.
MythScreenStack * GetStack(const QString &Stackname)
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
virtual void AddScreen(MythScreenType *screen, bool allowFade=true)
Screen in which all other widgets are contained and rendered.
void LoadInBackground(const QString &message="")
void OpenBusyPopup(const QString &message="")
void BuildFocusList(void)
MythUIType * GetFocusWidget(void) const
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
bool SetFocusWidget(MythUIType *widget=nullptr)
void SetBusyPopupMessage(const QString &message)
void CloseBusyPopup(void)
void ReloadInBackground(void)
void ResetBusyPopup(void)
void SetData(QVariant data)
void DisplayState(const QString &state, const QString &name)
void SetTextFromMap(const InfoMap &infoMap, const QString &state="")
void SetImage(MythImage *image, const QString &name="")
Sets an image directly, should only be used in special circumstances since it bypasses the cache.
MythUIButtonListItem * GetItemCurrent() const
void Reset() override
Reset the widget to it's original state, should not reset changes made by the theme.
void itemClicked(MythUIButtonListItem *item)
void SetValueByData(const QVariant &data)
void itemSelected(MythUIButtonListItem *item)
virtual void SetTextFromMap(const InfoMap &infoMap)
Create a group of widgets.
Definition: mythuigroup.h:12
Image widget, displays a single image or multiple images in sequence.
Definition: mythuiimage.h:98
bool Load(bool allowLoadInBackground=true, bool forceStat=false)
Load the image(s), wraps ImageLoader::LoadImage()
void SetFilename(const QString &filename)
Must be followed by a call to Load() to load the image.
void Reset(void) override
Reset the image back to the default defined in the theme.
QString GetCurrentLocation(bool FullPath=false, bool MainStackOnly=true)
Progress bar widget.
void SetUsed(int value)
void SetTotal(int value)
This widget is used for grouping other widgets for display when a particular named state is called.
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
bool DisplayState(const QString &name)
All purpose text widget, displays a text string.
Definition: mythuitext.h:29
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
Definition: mythuitext.cpp:65
virtual void SetText(const QString &text)
Definition: mythuitext.cpp:115
MythUIType * GetChild(const QString &name) const
Get a named child of this UIType.
Definition: mythuitype.cpp:130
static bool DeleteFile(const QString &url)
Definition: remotefile.cpp:420
static bool Exists(const QString &url, struct stat *fileinfo)
Definition: remotefile.cpp:463
QString GetFirstDir(bool appendSlash=false) const
View and select installed themes.
Definition: themechooser.h:28
QFileInfoList m_infoList
Definition: themechooser.h:84
void refreshDownloadableThemes(void)
ThemeInfo * loadThemeInfo(const QFileInfo &theme)
bool removeThemeDir(const QString &dirname)
MythUIImage * m_fullScreenPreview
Definition: themechooser.h:82
bool m_fullPreviewShowing
Definition: themechooser.h:79
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
void itemChanged(MythUIButtonListItem *item)
ThemeInfo * m_downloadTheme
Definition: themechooser.h:91
void removeTheme(void)
void updateProgressBar(int bytesReceived, int bytesTotal)
~ThemeChooser() override
void toggleFullscreenPreview(void)
void popupClosed(const QString &which, int result)
MythUIText * m_fullScreenName
Definition: themechooser.h:81
void saveAndReload(void)
MythUIButtonList * m_themes
Definition: themechooser.h:76
QMap< QString, QString > m_themeStatuses
Definition: themechooser.h:90
QString m_downloadFile
Definition: themechooser.h:92
void Init(void) override
Used after calling Load() to assign data to widgets and other UI initilisation which is prohibited in...
QMap< QString, ThemeInfo * > m_themeNameInfos
Definition: themechooser.h:88
MythUIImage * m_preview
Definition: themechooser.h:77
bool m_refreshDownloadableThemes
Definition: themechooser.h:85
bool Create(void) override
DownloadState m_downloadState
Definition: themechooser.h:93
QString m_userThemeDir
Definition: themechooser.h:86
void Load(void) override
Load data which will ultimately be displayed on-screen or used to determine what appears on-screen (S...
bool LoadVersion(const QString &version, QStringList &themesSeen, bool alert_user)
static void toggleThemeUpdateNotifications(void)
QMap< QString, ThemeInfo * > m_themeFileNameInfos
Definition: themechooser.h:89
@ dsDownloadingOnFrontend
Definition: themechooser.h:67
@ dsDownloadingOnBackend
Definition: themechooser.h:66
void customEvent(QEvent *e) override
MythDialogBox * m_popupMenu
Definition: themechooser.h:95
void showPopupMenu(void)
ThemeChooser(MythScreenStack *parent, const QString &name="ThemeChooser")
Creates a new ThemeChooser Screen.
MythUIStateType * m_fullPreviewStateType
Definition: themechooser.h:80
ThemeExtractThread(ThemeChooser *parent, QString srcFile, QString destDir)
void run() override
ThemeChooser * m_parent
QString GetPreviewPath() const
Definition: themeinfo.h:33
QString GetDownloadURL() const
Definition: themeinfo.h:38
QString GetAspect() const
Definition: themeinfo.h:27
int GetMinorVersion() const
Definition: themeinfo.h:36
void ToMap(InfoMap &infoMap) const
Definition: themeinfo.cpp:261
QString GetName() const
Definition: themeinfo.h:29
int GetMajorVersion() const
Definition: themeinfo.h:35
QString GetDirectoryName() const
Definition: themeinfo.cpp:252
int GetType() const
Definition: themeinfo.h:34
QString m_lastKnownThemeVersion
Definition: themechooser.h:115
QString m_currentVersion
Definition: themechooser.h:116
~ThemeUpdateChecker(void) override
QTimer * m_updateTimer
Definition: themechooser.h:112
QStringList m_mythVersions
Definition: themechooser.h:113
void checkForUpdate(void)
static bool LoadWindowFromXML(const QString &xmlfile, const QString &windowname, MythUIType *parent)
unsigned int uint
Definition: compat.h:60
#define minor(X)
Definition: compat.h:58
static bool downloadURL(const QString &url, QByteArray *buffer, QString &finalURL)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythConfirmationDialog * ShowOkPopup(const QString &message, bool showCancel)
Non-blocking version of MythPopupBox::showOkPopup()
QString GetThemesParentDir(void)
Definition: mythdirs.cpp:286
QString GetCacheDir(void)
Returns the base directory for all cached files.
Definition: mythdirs.cpp:295
QString GetConfDir(void)
Definition: mythdirs.cpp:285
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythMainWindow * GetMythMainWindow(void)
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
MythUIHelper * GetMythUI()
const char * GetMythSourceVersion()
Definition: mythversion.cpp:7
bool ParseMythSourceVersion(bool &devel, uint &major, uint &minor, const char *version)
Definition: mythversion.cpp:26
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
string version
Definition: giantbomb.py:185
string themeName
Definition: mythburn.py:216
bool exists(str path)
Definition: xbmcvfs.py:51
QString RemoteDownloadFile(const QString &url, const QString &storageGroup, const QString &filename)
static bool Assign(ContainerType *container, UIType *&item, const QString &name, bool *err=nullptr)
Definition: mythuiutils.h:27
#define LOC
static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
static const QRegularExpression kVersionDateRE
@ THEME_UNKN
Definition: themeinfo.h:14
@ THEME_UI
Definition: themeinfo.h:15
bool extractZIP(QString zipFile, const QString &outDir)
Definition: unziputil.cpp:17