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 std::sort(m_infoList.begin(), m_infoList.end(), sortThemeNames);
195 }
196 else
197 {
198
199 LOG(VB_GENERAL, LOG_INFO, QString("Failed to load themes for %1, trying devel").arg(MythVersion));
200 if (!LoadVersion("trunk", themesSeen, true))
201 {
202 LOG(VB_GENERAL, LOG_WARNING, QString("Failed to load themes for devel"));
203 }
204 }
205 }
206}
207
209 QStringList &themesSeen, bool alert_user)
210{
211 QString remoteThemesFile = GetConfDir();
212 remoteThemesFile.append("/tmp/themes.zip");
213 QString themeSite = QString("%1/%2")
214 .arg(gCoreContext->GetSetting("ThemeRepositoryURL",
215 "http://themes.mythtv.org/themes/repository"),
216 version);
217 QString destdir = GetCacheDir().append("/themechooser");
218 QString versiondir = QString("%1/%2").arg(destdir, version);
219 QDir remoteThemesDir(versiondir);
220
221 int downloadFailures =
222 gCoreContext->GetNumSetting("ThemeInfoDownloadFailures", 0);
223 if (QFile::exists(remoteThemesFile))
224 {
225 QFileInfo finfo(remoteThemesFile);
226 if (finfo.lastModified().toUTC() <
227 MythDate::current().addSecs(-600))
228 {
229 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 is over 10 minutes old, forcing "
230 "remote theme list download")
231 .arg(remoteThemesFile));
233 }
234
235 if (!remoteThemesDir.exists())
236 {
238 }
239 }
240 else if (downloadFailures < 2) // (and themes.zip does not exist)
241 {
242 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 does not exist, forcing remote theme "
243 "list download")
244 .arg(remoteThemesFile));
246 }
247
249 {
250 QFile test(remoteThemesFile);
251 if (test.open(QIODevice::WriteOnly))
252 test.remove();
253 else
254 {
255 ShowOkPopup(tr("Unable to create '%1'").arg(remoteThemesFile));
256 return false;
257 }
258
259 SetBusyPopupMessage(tr("Refreshing Downloadable Themes Information"));
260
261 QString url = themeSite;
262 url.append("/themes.zip");
263 if (!removeThemeDir(versiondir))
264 ShowOkPopup(tr("Unable to remove '%1'").arg(versiondir));
265 QDir dir;
266 if (!dir.mkpath(destdir))
267 ShowOkPopup(tr("Unable to create '%1'").arg(destdir));
268 bool result = GetMythDownloadManager()->download(url, remoteThemesFile, true);
269
270 LOG(VB_GUI, LOG_INFO, LOC + QString("Downloading '%1' to '%2'").arg(url, remoteThemesFile));
271
272 SetBusyPopupMessage(tr("Extracting Downloadable Themes Information"));
273
274 if (!result || !extractZIP(remoteThemesFile, destdir))
275 {
276 QFile::remove(remoteThemesFile);
277
278 downloadFailures++;
279 gCoreContext->SaveSetting("ThemeInfoDownloadFailures",
280 downloadFailures);
281
282 if (!result)
283 {
284 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to download '%1'").arg(url));
285 if (alert_user)
286 ShowOkPopup(tr("Failed to download '%1'").arg(url));
287 }
288 else
289 {
290 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to unzip '%1' to '%2'").arg(remoteThemesFile, destdir));
291 if (alert_user)
292 ShowOkPopup(tr("Failed to unzip '%1' to '%2'")
293 .arg(remoteThemesFile, destdir));
294 }
295 return false;
296 }
297 LOG(VB_GUI, LOG_INFO, LOC + QString("Unzipped '%1' to '%2'").arg(remoteThemesFile, destdir));
298 }
299
300 QDir themes;
301 themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
302 themes.setSorting(QDir::Name | QDir::IgnoreCase);
303
304 if ((QFile::exists(remoteThemesFile)) &&
305 (remoteThemesDir.exists()))
306 {
307 SetBusyPopupMessage(tr("Loading Downloadable Themes"));
308
309 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 and %2 exist, using cached remote themes list").arg(remoteThemesFile, remoteThemesDir.absolutePath()));
310
311 QString themesPath = remoteThemesDir.absolutePath();
312 themes.setPath(themesPath);
313
314 QFileInfoList downloadableThemes = themes.entryInfoList();
315 for (const auto &dtheme : std::as_const(downloadableThemes))
316 {
317 QString dirName = dtheme.fileName();
318 QString themeName = dirName;
319 QString remoteDir = themeSite;
320 remoteDir.append("/").append(dirName);
321 QString localDir = themes.absolutePath();
322 localDir.append("/").append(dirName);
323
324 ThemeInfo remoteTheme(dtheme.absoluteFilePath());
325
326 if (themesSeen.contains(dirName))
327 {
328 ThemeInfo *localTheme = m_themeNameInfos[dirName];
329
330 themeName = remoteTheme.GetName();
331
332 int rmtMaj = remoteTheme.GetMajorVersion();
333 int rmtMin = remoteTheme.GetMinorVersion();
334 int locMaj = localTheme->GetMajorVersion();
335 int locMin = localTheme->GetMinorVersion();
336
337 if ((rmtMaj > locMaj) ||
338 ((rmtMaj == locMaj) &&
339 (rmtMin > locMin)))
340 {
341 if (loadThemeInfo(dtheme))
342 {
343 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));
344
345 m_infoList << dtheme;
346 m_themeStatuses[themeName] = "updateavailable";
347
348 QFileInfo finfo(remoteTheme.GetPreviewPath());
350 remoteDir.append("/").append(finfo.fileName()),
351 localDir.append("/").append(finfo.fileName()),
352 nullptr);
353 }
354 }
355 else if ((rmtMaj == locMaj) &&
356 (rmtMin == locMin))
357 {
358 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' up to date (%2.%3)").arg(themeName).arg(locMaj).arg(locMin));
359
360 m_themeStatuses[themeName] = "uptodate";
361 }
362 }
363 else
364 {
365 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' (%2.%3) available").arg(themeName).arg(remoteTheme.GetMajorVersion()).arg(remoteTheme.GetMinorVersion()));
366
367 ThemeInfo *tmpTheme = loadThemeInfo(dtheme);
368 if (tmpTheme)
369 {
370 themeName = tmpTheme->GetName();
371 themesSeen << dirName;
372 m_infoList << dtheme;
373 m_themeStatuses[themeName] = "updateavailable";
374
375 QFileInfo finfo(tmpTheme->GetPreviewPath());
377 remoteDir.append("/").append(finfo.fileName()),
378 localDir.append("/").append(finfo.fileName()),
379 nullptr);
380 }
381 }
382 }
383 return true;
384 }
385 return false;
386}
387
389{
390 QString curTheme = gCoreContext->GetSetting("Theme");
391 ThemeInfo *themeinfo = nullptr;
392 ThemeInfo *curThemeInfo = nullptr;
393 MythUIButtonListItem *item = nullptr;
394
395 m_themes->Reset();
396 for (const auto &theme : std::as_const(m_infoList))
397 {
398 if (!m_themeFileNameInfos.contains(theme.filePath()))
399 continue;
400
401 themeinfo = m_themeFileNameInfos[theme.filePath()];
402 if (!themeinfo)
403 continue;
404
405 QString buttonText = QString("%1 %2.%3")
406 .arg(themeinfo->GetName())
407 .arg(themeinfo->GetMajorVersion())
408 .arg(themeinfo->GetMinorVersion());
409
410 item = new MythUIButtonListItem(m_themes, buttonText);
411 if (item)
412 {
413 if (themeinfo->GetDownloadURL().isEmpty())
414 item->DisplayState("local", "themelocation");
415 else
416 item->DisplayState("remote", "themelocation");
417
418 item->DisplayState(themeinfo->GetAspect(), "aspectstate");
419
420 item->DisplayState(m_themeStatuses[themeinfo->GetName()],
421 "themestatus");
422 InfoMap infomap;
423 themeinfo->ToMap(infomap);
424 item->SetTextFromMap(infomap);
425 item->SetData(QVariant::fromValue(themeinfo));
426
427 QString thumbnail = themeinfo->GetPreviewPath();
428 // Downloadable themeinfos have thumbnail copies of their preview images
429 if (!themeinfo->GetDownloadURL().isEmpty())
430 thumbnail = thumbnail.append(".thumb.jpg");
431 item->SetImage(thumbnail);
432
433 if (curTheme == themeinfo->GetDirectoryName())
434 curThemeInfo = themeinfo;
435 }
436 }
437
439
440 if (curThemeInfo)
441 m_themes->SetValueByData(QVariant::fromValue(curThemeInfo));
442
444 if (current)
446
447 QString testFile = m_userThemeDir + "/.test";
448 QFile test(testFile);
449 if (test.open(QIODevice::WriteOnly))
450 test.remove();
451 else
452 {
453 ShowOkPopup(tr("Error creating test file, %1 themes directory is "
454 "not writable.")
455 .arg(m_userThemeDir));
456 }
457}
458
460{
461 if (theme.fileName() == "default" || theme.fileName() == "default-wide")
462 return nullptr;
463
464 ThemeInfo *themeinfo = nullptr;
465 if (theme.exists()) // local directory vs http:// or remote URL
466 themeinfo = new ThemeInfo(theme.absoluteFilePath());
467 else
468 themeinfo = new ThemeInfo(theme.filePath());
469
470 if (!themeinfo)
471 return nullptr;
472
473 if (themeinfo->GetName().isEmpty() || ((themeinfo->GetType() & THEME_UI) == 0))
474 {
475 delete themeinfo;
476 return nullptr;
477 }
478
479 m_themeFileNameInfos[theme.filePath()] = themeinfo;
480 m_themeNameInfos[theme.fileName()] = themeinfo;
481
482 return themeinfo;
483}
484
486{
487 if (m_popupMenu)
488 return;
489
490 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
491 QString label = tr("Theme Chooser Menu");
492
494 new MythDialogBox(label, popupStack, "themechoosermenupopup");
495
497
498 if (m_popupMenu->Create())
499 popupStack->AddScreen(m_popupMenu);
500 else
501 {
502 delete m_popupMenu;
503 m_popupMenu = nullptr;
504 return;
505 }
506
507 m_popupMenu->SetReturnEvent(this, "popupmenu");
508
510 {
512 {
513 m_popupMenu->AddButton(tr("Hide Fullscreen Preview"),
515 }
516 else
517 {
518 m_popupMenu->AddButton(tr("Show Fullscreen Preview"),
520 }
521 }
522
523 m_popupMenu->AddButton(tr("Refresh Downloadable Themes"),
525
527 if (current)
528 {
529 auto *info = current->GetData().value<ThemeInfo *>();
530
531 if (info)
532 {
533 m_popupMenu->AddButton(tr("Select Theme"),
534 qOverload<>(&ThemeChooser::saveAndReload));
535
536 if (info->GetPreviewPath().startsWith(m_userThemeDir))
537 m_popupMenu->AddButton(tr("Delete Theme"),
539 }
540 }
541
542 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
543 {
544 m_popupMenu->AddButton(tr("Disable Theme Update Notifications"),
546 }
547 else
548 {
549 m_popupMenu->AddButton(tr("Enable Theme Update Notifications"),
551 }
552}
553
554void ThemeChooser::popupClosed([[maybe_unused]] const QString &which,
555 [[maybe_unused]] int result)
556{
557 m_popupMenu = nullptr;
558}
559
560bool ThemeChooser::keyPressEvent(QKeyEvent *event)
561{
562 if (GetFocusWidget()->keyPressEvent(event))
563 return true;
564
565 QStringList actions;
566 bool handled = GetMythMainWindow()->TranslateKeyPress("Theme Chooser", event, actions);
567
568 for (int i = 0; i < actions.size() && !handled; ++i)
569 {
570 const QString& action = actions[i];
571 handled = true;
572
573 if (action == "MENU")
575 else if (action == "DELETE")
576 removeTheme();
577 else if ((action == "ESCAPE") &&
579 {
581 }
582 else
583 {
584 handled = false;
585 }
586 }
587
588 if (!handled && MythScreenType::keyPressEvent(event))
589 handled = true;
590
591 return handled;
592}
593
595{
597 {
599 {
602
605
607 m_fullPreviewShowing = false;
608 }
609 else
610 {
612 auto *info = item->GetData().value<ThemeInfo *>();
613 if (info)
614 {
616 {
617 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
619 }
620
622 m_fullScreenName->SetText(info->GetName());
623
626 }
627 }
628 }
629}
630
632{
633 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
634 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "0", "");
635 else
636 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "1", "");
637}
638
640{
641 LOG(VB_GUI, LOG_INFO, LOC + "Forcing remote theme list refresh");
643 gCoreContext->SaveSetting("ThemeInfoDownloadFailures", 0);
645}
646
648{
650 if (current)
652}
653
655{
656 auto *info = item->GetData().value<ThemeInfo *>();
657
658 if (!info)
659 return;
660
661 if (!info->GetDownloadURL().isEmpty())
662 {
663 QString testFile = m_userThemeDir + "/.test";
664 QFile test(testFile);
665 if (test.open(QIODevice::WriteOnly))
666 test.remove();
667 else
668 {
669 ShowOkPopup(tr("Unable to install theme, %1 themes directory is "
670 "not writable.")
671 .arg(m_userThemeDir));
672 return;
673 }
674
675 QString downloadURL = info->GetDownloadURL();
676 LOG(VB_FILE, LOG_INFO, QString("Download url is %1").arg(downloadURL));
677 QFileInfo qfile(downloadURL);
678 QString baseName = qfile.fileName();
679
680 if (!gCoreContext->GetSetting("ThemeDownloadURL").isEmpty())
681 {
682 QStringList tokens =
683 gCoreContext->GetSetting("ThemeDownloadURL")
684 .split(";", Qt::SkipEmptyParts);
685 QString origURL = downloadURL;
686 downloadURL.replace(tokens[0], tokens[1]);
687 LOG(VB_FILE, LOG_WARNING, LOC + QString("Theme download URL overridden from %1 to %2.").arg(origURL, downloadURL));
688 }
689
690 OpenBusyPopup(tr("Downloading %1 Theme").arg(info->GetName()));
692#if 0
694 "Temp", baseName);
696#else
697 QString localFile = GetConfDir() + "/tmp/" + baseName;
699 m_downloadFile = localFile;
701#endif
702 }
703 else
704 {
705 gCoreContext->SaveSetting("Theme", info->GetDirectoryName());
706 GetMythMainWindow()->JumpTo("Reload Theme");
707 }
708}
709
711{
712 auto *info = item->GetData().value<ThemeInfo *>();
713
714 if (!info)
715 return;
716
717 QFileInfo preview(info->GetPreviewPath());
718 InfoMap infomap;
719 info->ToMap(infomap);
720 SetTextFromMap(infomap);
721 if (m_preview)
722 {
723 if (preview.exists())
724 {
725 m_preview->SetFilename(info->GetPreviewPath());
726 m_preview->Load();
727 }
728 else
729 {
730 m_preview->Reset();
731 }
732 }
734 {
736 {
737 if (preview.exists())
738 {
739 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
741 }
742 else
743 {
745 }
746 }
747
749 m_fullScreenName->SetText(info->GetName());
750 }
751
752 MythUIStateType *themeLocation =
753 dynamic_cast<MythUIStateType *>(GetChild("themelocation"));
754 if (themeLocation)
755 {
756 if (info->GetDownloadURL().isEmpty())
757 themeLocation->DisplayState("local");
758 else
759 themeLocation->DisplayState("remote");
760 }
761
762 MythUIStateType *aspectState =
763 dynamic_cast<MythUIStateType *>(GetChild("aspectstate"));
764 if (aspectState)
765 aspectState->DisplayState(info->GetAspect());
766}
767
768void ThemeChooser::updateProgressBar(int bytesReceived,
769 int bytesTotal)
770{
771 MythUIProgressBar *progressBar =
772 dynamic_cast<MythUIProgressBar *>(GetChild("downloadprogressbar"));
773
774 if (!progressBar)
775 return;
776
777 progressBar->SetUsed(bytesReceived);
778 progressBar->SetTotal(bytesTotal);
779}
780
782{
783 if (e->type() == MythEvent::kMythEventMessage)
784 {
785 auto *me = dynamic_cast<MythEvent *>(e);
786 if (me == nullptr)
787 return;
788
789 QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
790 if (tokens.isEmpty())
791 return;
792
793 if (tokens[0] == "DOWNLOAD_FILE")
794 {
795 QStringList args = me->ExtraDataList();
796 if ((m_downloadState == dsIdle) ||
797 (tokens.size() != 2) ||
798 (!m_downloadTheme) ||
799 (args[1] != m_downloadFile))
800 return;
801
802 if (tokens[1] == "UPDATE")
803 {
804 updateProgressBar(args[2].toInt(), args[3].toInt());
805 }
806 else if (tokens[1] == "FINISHED")
807 {
808 bool remoteFileIsLocal = false;
809 int fileSize = args[2].toInt();
810 int errorCode = args[4].toInt();
811
813
814 QFileInfo file(m_downloadFile);
816 (m_downloadFile.startsWith("myth://")))
817 {
818 // The backend download is finished so start the
819 // frontend download
820 LOG(VB_FILE, LOG_INFO, QString("Download done MBE %1 %2").arg(errorCode).arg(fileSize));
821 if ((errorCode == 0) &&
822 (fileSize > 0))
823 {
825 QString localFile = GetConfDir() + "/tmp/" +
826 file.fileName();
827 file.setFile(localFile);
828
829 if (file.exists())
830 {
831 remoteFileIsLocal = true;
832 m_downloadFile = localFile;
833 }
834 else
835 {
837 m_downloadFile, localFile, this);
838 OpenBusyPopup(tr("Copying %1 Theme Package")
839 .arg(m_downloadTheme->GetName()));
840 m_downloadFile = localFile;
841 return;
842 }
843 }
844 else
845 {
847 ShowOkPopup(tr("ERROR downloading theme package on master backend."));
848 }
849 }
850
852 (file.exists()))
853 {
854 // The frontend download is finished
855 LOG(VB_FILE, LOG_INFO, QString("Download done MFE %1 %2").arg(errorCode).arg(fileSize));
856 // moved error is ok
857 if ((errorCode == 0) &&
858 (fileSize > 0))
859 {
861 auto *extractThread =
865 extractThread, "ThemeExtract");
866
867 if (!remoteFileIsLocal)
869
870 OpenBusyPopup(tr("Installing %1 Theme")
871 .arg(m_downloadTheme->GetName()));
872 }
873 else
874 {
876 ShowOkPopup(tr("ERROR downloading theme package from frontend."));
877 }
878 }
879 }
880 }
881 else if ((me->Message() == "THEME_INSTALLED") &&
882 (m_downloadTheme) &&
884 {
887 QStringList args = me->ExtraDataList();
888
889 if (!args.isEmpty() && !args[0].isEmpty())
890 QFile::remove(args[0]);
891
892 QString event = QString("THEME_INSTALLED PATH %1")
893 .arg(m_userThemeDir +
896
898
899 // Send a message to ourself so we trigger a reload our next chance
900 auto *me2 = new MythEvent("THEME_RELOAD");
901 qApp->postEvent(this, me2);
902 }
903 else if ((me->Message() == "THEME_RELOAD") &&
905 {
906 GetMythMainWindow()->JumpTo("Reload Theme");
907 }
908 }
909}
910
912{
914 if (!current)
915 {
916 ShowOkPopup(tr("Error, no theme selected."));
917 return;
918 }
919
920 auto *info = current->GetData().value<ThemeInfo *>();
921 if (!info)
922 {
923 ShowOkPopup(tr("Error, unable to find current theme."));
924 return;
925 }
926
927 if (!info->GetPreviewPath().startsWith(m_userThemeDir))
928 {
929 ShowOkPopup(tr("%1 is not a user-installed theme and can not "
930 "be deleted.")
931 .arg(info->GetName()));
932 return;
933 }
934
935 removeThemeDir(m_userThemeDir + info->GetDirectoryName());
936
938}
939
940bool ThemeChooser::removeThemeDir(const QString &dirname)
941{
942 if ((!dirname.startsWith(m_userThemeDir)) &&
943 (!dirname.startsWith(GetMythUI()->GetThemeCacheDir())))
944 return true;
945
946 QDir dir(dirname);
947
948 if (!dir.exists())
949 return true;
950
951 dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
952 QFileInfoList list = dir.entryInfoList();
953
954 for (const auto &fi : std::as_const(list))
955 {
956 if (fi.isFile() && !fi.isSymLink())
957 {
958 if (!QFile::remove(fi.absoluteFilePath()))
959 return false;
960 }
961 else if (fi.isDir() && !fi.isSymLink())
962 {
963 if (!removeThemeDir(fi.absoluteFilePath()))
964 return false;
965 }
966 }
967
968 return dir.rmdir(dirname);
969}
970
972
973ThemeUpdateChecker::ThemeUpdateChecker(void) : m_updateTimer(new QTimer(this))
974{
975 uint major { 0 };
976 uint minor { 0 };
977 bool devel { false };
978 bool parsed = ParseMythSourceVersion(devel, major, minor);
979
980 if (!parsed || devel)
981 {
982 m_mythVersions << "trunk";
983 }
984 else
985 {
986 for (int i = minor ; i > 0; i--)
987 m_mythVersions << QString("%1.%2").arg(major).arg(i);
988 m_mythVersions << QString::number(major);
989 }
990
993 "remotethemes/themes.zip",
994 "Temp");
995
996 gCoreContext->SaveSetting("ThemeUpdateStatus", "");
997
999
1000 if (qEnvironmentVariableIsSet("MYTHTV_DEBUGMDM"))
1001 {
1002 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every minute");
1003 m_updateTimer->start(1min);
1004 }
1005 else
1006 {
1007 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every hour");
1008 m_updateTimer->start(1h);
1009 }
1010
1011 // Run once 15 seconds from now
1012 QTimer::singleShot(15s, this, &ThemeUpdateChecker::checkForUpdate);
1013}
1014
1016{
1017 if (m_updateTimer)
1018 {
1019 m_updateTimer->stop();
1020 delete m_updateTimer;
1021 m_updateTimer = nullptr;
1022 }
1023}
1024
1026{
1027 if (GetMythUI()->GetCurrentLocation(false, true) != "mainmenu")
1028 return;
1029
1030 ThemeInfo *localTheme = nullptr;
1031
1033 {
1034 QStringList::iterator Iversion;
1035
1036 for (Iversion = m_mythVersions.begin();
1037 Iversion != m_mythVersions.end(); ++Iversion)
1038 {
1039
1040 QString remoteThemeDir =
1043 QString("remotethemes/%1/%2")
1044 .arg(*Iversion,
1045 GetMythUI()->GetThemeName()),
1046 "Temp");
1047
1048 QString infoXML = remoteThemeDir;
1049 infoXML.append("/themeinfo.xml");
1050
1051 LOG(VB_GUI, LOG_INFO, QString("ThemeUpdateChecker Loading '%1'").arg(infoXML));
1052
1053 if (RemoteFile::Exists(infoXML))
1054 {
1055 int locMaj = 0;
1056 int locMin = 0;
1057
1058 auto *remoteTheme = new ThemeInfo(remoteThemeDir);
1059 if (!remoteTheme || remoteTheme->GetType() & THEME_UNKN)
1060 {
1061 LOG(VB_GENERAL, LOG_ERR,
1062 QString("ThemeUpdateChecker::checkForUpdate(): "
1063 "Unable to create ThemeInfo for %1")
1064 .arg(infoXML));
1065 delete remoteTheme;
1066 remoteTheme = nullptr;
1067 return;
1068 }
1069
1070 if (!localTheme)
1071 {
1072 localTheme = new ThemeInfo(GetMythUI()->GetThemeDir());
1073 if (!localTheme || localTheme->GetType() & THEME_UNKN)
1074 {
1075 LOG(VB_GENERAL, LOG_ERR,
1076 "ThemeUpdateChecker::checkForUpdate(): "
1077 "Unable to create ThemeInfo for current theme");
1078 delete localTheme;
1079 localTheme = nullptr;
1080 return;
1081 }
1082 locMaj = localTheme->GetMajorVersion();
1083 locMin = localTheme->GetMinorVersion();
1084 }
1085
1086 int rmtMaj = remoteTheme->GetMajorVersion();
1087 int rmtMin = remoteTheme->GetMinorVersion();
1088
1089 delete remoteTheme;
1090 remoteTheme = nullptr;
1091
1092 if ((rmtMaj > locMaj) ||
1093 ((rmtMaj == locMaj) &&
1094 (rmtMin > locMin)))
1095 {
1097 QString("%1-%2.%3").arg(GetMythUI()->GetThemeName()).arg(rmtMaj).arg(rmtMin);
1098
1099 QString status = gCoreContext->GetSetting("ThemeUpdateStatus");
1100 QString currentLocation = GetMythUI()->GetCurrentLocation(false, true);
1101
1102 if ((!status.startsWith(m_lastKnownThemeVersion)) &&
1103 (currentLocation == "mainmenu"))
1104 {
1105 m_currentVersion = QString("%1.%2")
1106 .arg(locMaj)
1107 .arg(locMin);
1108 m_newVersion = QString("%1.%2").arg(rmtMaj).arg(rmtMin);
1109
1110 gCoreContext->SaveSetting("ThemeUpdateStatus",
1111 m_lastKnownThemeVersion + " notified");
1112
1113 QString message = tr("Version %1 of the %2 theme is now "
1114 "available in the Theme Chooser. "
1115 "The currently installed version "
1116 "is %3.")
1117 .arg(m_newVersion,
1118 GetMythUI()->GetThemeName(),
1120
1121 ShowOkPopup(message);
1122 break;
1123 }
1124 }
1125 }
1126 }
1127 }
1128
1129 delete localTheme;
1130}
1131
1132/* 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:138
static bool DeleteFile(const QString &url)
Definition: remotefile.cpp:418
static bool Exists(const QString &url, struct stat *fileinfo)
Definition: remotefile.cpp:461
QString GetFirstDir(bool appendSlash=false) const
View and select installed themes.
Definition: themechooser.h:28
QFileInfoList m_infoList
Definition: themechooser.h:82
void refreshDownloadableThemes(void)
ThemeInfo * loadThemeInfo(const QFileInfo &theme)
bool removeThemeDir(const QString &dirname)
MythUIImage * m_fullScreenPreview
Definition: themechooser.h:80
bool m_fullPreviewShowing
Definition: themechooser.h:77
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
void itemChanged(MythUIButtonListItem *item)
ThemeInfo * m_downloadTheme
Definition: themechooser.h:89
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:79
void saveAndReload(void)
MythUIButtonList * m_themes
Definition: themechooser.h:74
QMap< QString, QString > m_themeStatuses
Definition: themechooser.h:88
QString m_downloadFile
Definition: themechooser.h:90
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:86
MythUIImage * m_preview
Definition: themechooser.h:75
bool m_refreshDownloadableThemes
Definition: themechooser.h:83
bool Create(void) override
DownloadState m_downloadState
Definition: themechooser.h:91
QString m_userThemeDir
Definition: themechooser.h:84
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:87
@ dsDownloadingOnFrontend
Definition: themechooser.h:65
@ dsDownloadingOnBackend
Definition: themechooser.h:64
void customEvent(QEvent *e) override
MythDialogBox * m_popupMenu
Definition: themechooser.h:93
void showPopupMenu(void)
ThemeChooser(MythScreenStack *parent, const QString &name="ThemeChooser")
Creates a new ThemeChooser Screen.
MythUIStateType * m_fullPreviewStateType
Definition: themechooser.h:78
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:113
QString m_currentVersion
Definition: themechooser.h:114
~ThemeUpdateChecker(void) override
QTimer * m_updateTimer
Definition: themechooser.h:110
QStringList m_mythVersions
Definition: themechooser.h:111
void checkForUpdate(void)
static bool LoadWindowFromXML(const QString &xmlfile, const QString &windowname, MythUIType *parent)
unsigned int uint
Definition: compat.h:68
#define minor(X)
Definition: compat.h:65
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:264
QString GetCacheDir(void)
Returns the base directory for all cached files.
Definition: mythdirs.cpp:273
QString GetConfDir(void)
Definition: mythdirs.cpp:263
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:217
STL namespace.
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