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"
20#include "libmythbase/unziputil.h" // for extractZIP
32
33// Theme Chooser headers
34#include "themechooser.h"
35
36#define LOC QString("ThemeChooser: ")
37#define LOC_WARN QString("ThemeChooser, Warning: ")
38#define LOC_ERR QString("ThemeChooser, Error: ")
39
40static const QRegularExpression kVersionDateRE{"\\.[0-9]{8,}.*"};
41
45class ThemeExtractThread : public QRunnable
46{
47public:
49 QString srcFile, QString destDir) : m_parent(parent),
50 m_srcFile(std::move(srcFile)),
51 m_destDir(std::move(destDir)) {}
52
53 void run() override // QRunnable
54 {
56
57 auto *me = new MythEvent("THEME_INSTALLED", QStringList(m_srcFile));
58 QCoreApplication::postEvent(m_parent, me);
59 }
60
61private:
63 QString m_srcFile;
64 QString m_destDir;
65};
66
72 const QString &name) : MythScreenType(parent, name)
73{
75
76 StorageGroup sgroup("Themes", gCoreContext->GetHostName());
77 m_userThemeDir = sgroup.GetFirstDir(true);
78}
79
81{
83}
84
85static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
86{
87 return s1.fileName().toLower() < s2.fileName().toLower();
88}
89
91{
92 // Load the theme for this screen
93 if (!LoadWindowFromXML("settings-ui.xml", "themechooser", this))
94 return false;
95
96 bool err = false;
97 UIUtilE::Assign(this, m_themes, "themes", &err);
98
99 UIUtilW::Assign(this, m_preview, "preview");
100 UIUtilW::Assign(this, m_fullPreviewStateType, "fullpreviewstate");
101
103 {
104 MythUIGroup *state =
105 dynamic_cast<MythUIGroup *>(m_fullPreviewStateType->GetChild("fullscreen"));
106 if (state)
107 {
109 dynamic_cast<MythUIText *>(state->GetChild("fullscreenname"));
111 dynamic_cast<MythUIImage *>(state->GetChild("fullscreenpreview"));
112 }
113 }
114
115 if (err)
116 {
117 LOG(VB_GENERAL, LOG_ERR, LOC + "Cannot load screen 'themechooser'");
118 return false;
119 }
120
122 this, qOverload<MythUIButtonListItem *>(&ThemeChooser::saveAndReload));
125
127
129
130 return true;
131}
132
134{
135 SetBusyPopupMessage(tr("Loading Installed Themes"));
136
137 QStringList themesSeen;
138 QDir themes(m_userThemeDir);
139 themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
140 themes.setSorting(QDir::Name | QDir::IgnoreCase);
141
142 m_infoList = themes.entryInfoList();
143
144 for (const auto &theme : std::as_const(m_infoList))
145 {
146 if (loadThemeInfo(theme))
147 {
148 themesSeen << theme.fileName();
149 m_themeStatuses[theme.fileName()] = "default";
150 }
151 }
152
153 themes.setPath(GetThemesParentDir());
154 QFileInfoList sharedThemes = themes.entryInfoList();
155 for (const auto &sharedTheme : std::as_const(sharedThemes))
156 {
157 if ((!themesSeen.contains(sharedTheme.fileName())) &&
158 (loadThemeInfo(sharedTheme)))
159 {
160 m_infoList << sharedTheme;
161 themesSeen << sharedTheme.fileName();
162 m_themeStatuses[sharedTheme.fileName()] = "default";
163 }
164 }
165
166 uint major { 0 };
167 uint minor { 0 };
168 bool devel { false };
169 bool parsed = ParseMythSourceVersion(devel, major, minor);
170
171 if (!parsed || devel)
172 {
173 if (!parsed)
174 LOG(VB_GENERAL, LOG_ERR,
175 QString("Invalid MythTV version %1, will use themes from devel")
176 .arg(GetMythSourceVersion()));
177 LOG(VB_GUI, LOG_INFO, QString("Loading themes for devel"));
178 LoadVersion("trunk", themesSeen, true);
179 }
180 else
181 {
182 QString MythVersion { QString::number(major) };
183 LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
184 if (LoadVersion(MythVersion, themesSeen, true))
185 {
186 for (int idx = minor ; idx > 0; idx--)
187 {
188 QString subversion;
189 subversion = MythVersion + "." + QString::number(idx);
190 LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(subversion));
191 LoadVersion(subversion, themesSeen, false);
192 }
193
195
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 test.remove();
255 else
256 {
257 ShowOkPopup(tr("Unable to create '%1'").arg(remoteThemesFile));
258 return false;
259 }
260
261 SetBusyPopupMessage(tr("Refreshing Downloadable Themes Information"));
262
263 QString url = themeSite;
264 url.append("/themes.zip");
265 if (!removeThemeDir(versiondir))
266 ShowOkPopup(tr("Unable to remove '%1'").arg(versiondir));
267 QDir dir;
268 if (!dir.mkpath(destdir))
269 ShowOkPopup(tr("Unable to create '%1'").arg(destdir));
270 bool result = GetMythDownloadManager()->download(url, remoteThemesFile, true);
271
272 LOG(VB_GUI, LOG_INFO, LOC + QString("Downloading '%1' to '%2'").arg(url, remoteThemesFile));
273
274 SetBusyPopupMessage(tr("Extracting Downloadable Themes Information"));
275
276 if (!result || !extractZIP(remoteThemesFile, destdir))
277 {
278 QFile::remove(remoteThemesFile);
279
280 downloadFailures++;
281 gCoreContext->SaveSetting("ThemeInfoDownloadFailures",
282 downloadFailures);
283
284 if (!result)
285 {
286 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to download '%1'").arg(url));
287 if (alert_user)
288 ShowOkPopup(tr("Failed to download '%1'").arg(url));
289 }
290 else
291 {
292 LOG(VB_GUI, LOG_ERR, LOC + QString("Failed to unzip '%1' to '%2'").arg(remoteThemesFile, destdir));
293 if (alert_user)
294 ShowOkPopup(tr("Failed to unzip '%1' to '%2'")
295 .arg(remoteThemesFile, destdir));
296 }
297 return false;
298 }
299 LOG(VB_GUI, LOG_INFO, LOC + QString("Unzipped '%1' to '%2'").arg(remoteThemesFile, destdir));
300 }
301
302 QDir themes;
303 themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
304 themes.setSorting(QDir::Name | QDir::IgnoreCase);
305
306 if ((QFile::exists(remoteThemesFile)) &&
307 (remoteThemesDir.exists()))
308 {
309 SetBusyPopupMessage(tr("Loading Downloadable Themes"));
310
311 LOG(VB_GUI, LOG_INFO, LOC + QString("%1 and %2 exist, using cached remote themes list").arg(remoteThemesFile, remoteThemesDir.absolutePath()));
312
313 QString themesPath = remoteThemesDir.absolutePath();
314 themes.setPath(themesPath);
315
316 QFileInfoList downloadableThemes = themes.entryInfoList();
317 for (const auto &dtheme : std::as_const(downloadableThemes))
318 {
319 QString dirName = dtheme.fileName();
320 QString themeName = dirName;
321 QString remoteDir = themeSite;
322 remoteDir.append("/").append(dirName);
323 QString localDir = themes.absolutePath();
324 localDir.append("/").append(dirName);
325
326 ThemeInfo remoteTheme(dtheme.absoluteFilePath());
327
328 if (themesSeen.contains(dirName))
329 {
330 ThemeInfo *localTheme = m_themeNameInfos[dirName];
331
332 themeName = remoteTheme.GetName();
333
334 int rmtMaj = remoteTheme.GetMajorVersion();
335 int rmtMin = remoteTheme.GetMinorVersion();
336 int locMaj = localTheme->GetMajorVersion();
337 int locMin = localTheme->GetMinorVersion();
338
339 if ((rmtMaj > locMaj) ||
340 ((rmtMaj == locMaj) &&
341 (rmtMin > locMin)))
342 {
343 if (loadThemeInfo(dtheme))
344 {
345 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));
346
347 m_infoList << dtheme;
348 m_themeStatuses[themeName] = "updateavailable";
349
350 QFileInfo finfo(remoteTheme.GetPreviewPath());
352 remoteDir.append("/").append(finfo.fileName()),
353 localDir.append("/").append(finfo.fileName()),
354 nullptr);
355 }
356 }
357 else if ((rmtMaj == locMaj) &&
358 (rmtMin == locMin))
359 {
360 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' up to date (%2.%3)").arg(themeName).arg(locMaj).arg(locMin));
361
362 m_themeStatuses[themeName] = "uptodate";
363 }
364 }
365 else
366 {
367 LOG(VB_GUI, LOG_DEBUG, LOC + QString("'%1' (%2.%3) available").arg(themeName).arg(remoteTheme.GetMajorVersion()).arg(remoteTheme.GetMinorVersion()));
368
369 ThemeInfo *tmpTheme = loadThemeInfo(dtheme);
370 if (tmpTheme)
371 {
372 themeName = tmpTheme->GetName();
373 themesSeen << dirName;
374 m_infoList << dtheme;
375 m_themeStatuses[themeName] = "updateavailable";
376
377 QFileInfo finfo(tmpTheme->GetPreviewPath());
379 remoteDir.append("/").append(finfo.fileName()),
380 localDir.append("/").append(finfo.fileName()),
381 nullptr);
382 }
383 }
384 }
385 return true;
386 }
387 return false;
388}
389
391{
392 QString curTheme = gCoreContext->GetSetting("Theme");
393 ThemeInfo *themeinfo = nullptr;
394 ThemeInfo *curThemeInfo = nullptr;
395 MythUIButtonListItem *item = nullptr;
396
397 m_themes->Reset();
398 for (const auto &theme : std::as_const(m_infoList))
399 {
400 if (!m_themeFileNameInfos.contains(theme.filePath()))
401 continue;
402
403 themeinfo = m_themeFileNameInfos[theme.filePath()];
404 if (!themeinfo)
405 continue;
406
407 QString buttonText = QString("%1 %2.%3")
408 .arg(themeinfo->GetName())
409 .arg(themeinfo->GetMajorVersion())
410 .arg(themeinfo->GetMinorVersion());
411
412 item = new MythUIButtonListItem(m_themes, buttonText);
413 if (item)
414 {
415 if (themeinfo->GetDownloadURL().isEmpty())
416 item->DisplayState("local", "themelocation");
417 else
418 item->DisplayState("remote", "themelocation");
419
420 item->DisplayState(themeinfo->GetAspect(), "aspectstate");
421
422 item->DisplayState(m_themeStatuses[themeinfo->GetName()],
423 "themestatus");
424 InfoMap infomap;
425 themeinfo->ToMap(infomap);
426 item->SetTextFromMap(infomap);
427 item->SetData(QVariant::fromValue(themeinfo));
428
429 QString thumbnail = themeinfo->GetPreviewPath();
430 // Downloadable themeinfos have thumbnail copies of their preview images
431 if (!themeinfo->GetDownloadURL().isEmpty())
432 thumbnail = thumbnail.append(".thumb.jpg");
433 item->SetImage(thumbnail);
434
435 if (curTheme == themeinfo->GetDirectoryName())
436 curThemeInfo = themeinfo;
437 }
438 }
439
441
442 if (curThemeInfo)
443 m_themes->SetValueByData(QVariant::fromValue(curThemeInfo));
444
446 if (current)
448
449 QString testFile = m_userThemeDir + "/.test";
450 QFile test(testFile);
451 if (test.open(QIODevice::WriteOnly))
452 test.remove();
453 else
454 {
455 ShowOkPopup(tr("Error creating test file, %1 themes directory is "
456 "not writable.")
457 .arg(m_userThemeDir));
458 }
459}
460
462{
463 if (theme.fileName() == "default" || theme.fileName() == "default-wide")
464 return nullptr;
465
466 ThemeInfo *themeinfo = nullptr;
467 if (theme.exists()) // local directory vs http:// or remote URL
468 themeinfo = new ThemeInfo(theme.absoluteFilePath());
469 else
470 themeinfo = new ThemeInfo(theme.filePath());
471
472 if (!themeinfo)
473 return nullptr;
474
475 if (themeinfo->GetName().isEmpty() || ((themeinfo->GetType() & THEME_UI) == 0))
476 {
477 delete themeinfo;
478 return nullptr;
479 }
480
481 m_themeFileNameInfos[theme.filePath()] = themeinfo;
482 m_themeNameInfos[theme.fileName()] = themeinfo;
483
484 return themeinfo;
485}
486
488{
489 if (m_popupMenu)
490 return;
491
492 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
493 QString label = tr("Theme Chooser Menu");
494
496 new MythDialogBox(label, popupStack, "themechoosermenupopup");
497
499
500 if (m_popupMenu->Create())
501 popupStack->AddScreen(m_popupMenu);
502 else
503 {
504 delete m_popupMenu;
505 m_popupMenu = nullptr;
506 return;
507 }
508
509 m_popupMenu->SetReturnEvent(this, "popupmenu");
510
512 {
514 {
515 m_popupMenu->AddButton(tr("Hide Fullscreen Preview"),
517 }
518 else
519 {
520 m_popupMenu->AddButton(tr("Show Fullscreen Preview"),
522 }
523 }
524
525 m_popupMenu->AddButton(tr("Refresh Downloadable Themes"),
527
529 if (current)
530 {
531 auto *info = current->GetData().value<ThemeInfo *>();
532
533 if (info)
534 {
535 m_popupMenu->AddButton(tr("Select Theme"),
536 qOverload<>(&ThemeChooser::saveAndReload));
537
538 if (info->GetPreviewPath().startsWith(m_userThemeDir))
539 m_popupMenu->AddButton(tr("Delete Theme"),
541 }
542 }
543
544 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
545 {
546 m_popupMenu->AddButton(tr("Disable Theme Update Notifications"),
548 }
549 else
550 {
551 m_popupMenu->AddButton(tr("Enable Theme Update Notifications"),
553 }
554}
555
556void ThemeChooser::popupClosed([[maybe_unused]] const QString &which,
557 [[maybe_unused]] int result)
558{
559 m_popupMenu = nullptr;
560}
561
562bool ThemeChooser::keyPressEvent(QKeyEvent *event)
563{
564 if (GetFocusWidget()->keyPressEvent(event))
565 return true;
566
567 QStringList actions;
568 bool handled = GetMythMainWindow()->TranslateKeyPress("Theme Chooser", event, actions);
569
570 for (int i = 0; i < actions.size() && !handled; ++i)
571 {
572 const QString& action = actions[i];
573 handled = true;
574
575 if (action == "MENU")
577 else if (action == "DELETE")
578 removeTheme();
579 else if ((action == "ESCAPE") &&
581 {
583 }
584 else
585 {
586 handled = false;
587 }
588 }
589
590 if (!handled && MythScreenType::keyPressEvent(event))
591 handled = true;
592
593 return handled;
594}
595
597{
599 {
601 {
604
607
609 m_fullPreviewShowing = false;
610 }
611 else
612 {
614 auto *info = item->GetData().value<ThemeInfo *>();
615 if (info)
616 {
618 {
619 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
621 }
622
624 m_fullScreenName->SetText(info->GetName());
625
628 }
629 }
630 }
631}
632
634{
635 if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
636 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "0", "");
637 else
638 gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "1", "");
639}
640
642{
643 LOG(VB_GUI, LOG_INFO, LOC + "Forcing remote theme list refresh");
645 gCoreContext->SaveSetting("ThemeInfoDownloadFailures", 0);
647}
648
650{
652 if (current)
654}
655
657{
658 auto *info = item->GetData().value<ThemeInfo *>();
659
660 if (!info)
661 return;
662
663 if (!info->GetDownloadURL().isEmpty())
664 {
665 QString testFile = m_userThemeDir + "/.test";
666 QFile test(testFile);
667 if (test.open(QIODevice::WriteOnly))
668 test.remove();
669 else
670 {
671 ShowOkPopup(tr("Unable to install theme, %1 themes directory is "
672 "not writable.")
673 .arg(m_userThemeDir));
674 return;
675 }
676
677 QString downloadURL = info->GetDownloadURL();
678 LOG(VB_FILE, LOG_INFO, QString("Download url is %1").arg(downloadURL));
679 QFileInfo qfile(downloadURL);
680 QString baseName = qfile.fileName();
681
682 if (!gCoreContext->GetSetting("ThemeDownloadURL").isEmpty())
683 {
684 QStringList tokens =
685 gCoreContext->GetSetting("ThemeDownloadURL")
686 .split(";", Qt::SkipEmptyParts);
687 QString origURL = downloadURL;
688 downloadURL.replace(tokens[0], tokens[1]);
689 LOG(VB_FILE, LOG_WARNING, LOC + QString("Theme download URL overridden from %1 to %2.").arg(origURL, downloadURL));
690 }
691
692 OpenBusyPopup(tr("Downloading %1 Theme").arg(info->GetName()));
694#if 0
696 "Temp", baseName);
698#else
699 QString localFile = GetConfDir() + "/tmp/" + baseName;
701 m_downloadFile = localFile;
703#endif
704 }
705 else
706 {
707 gCoreContext->SaveSetting("Theme", info->GetDirectoryName());
708 GetMythMainWindow()->JumpTo("Reload Theme");
709 }
710}
711
713{
714 auto *info = item->GetData().value<ThemeInfo *>();
715
716 if (!info)
717 return;
718
719 QFileInfo preview(info->GetPreviewPath());
720 InfoMap infomap;
721 info->ToMap(infomap);
722 SetTextFromMap(infomap);
723 if (m_preview)
724 {
725 if (preview.exists())
726 {
727 m_preview->SetFilename(info->GetPreviewPath());
728 m_preview->Load();
729 }
730 else
731 {
732 m_preview->Reset();
733 }
734 }
736 {
738 {
739 if (preview.exists())
740 {
741 m_fullScreenPreview->SetFilename(info->GetPreviewPath());
743 }
744 else
745 {
747 }
748 }
749
751 m_fullScreenName->SetText(info->GetName());
752 }
753
754 MythUIStateType *themeLocation =
755 dynamic_cast<MythUIStateType *>(GetChild("themelocation"));
756 if (themeLocation)
757 {
758 if (info->GetDownloadURL().isEmpty())
759 themeLocation->DisplayState("local");
760 else
761 themeLocation->DisplayState("remote");
762 }
763
764 MythUIStateType *aspectState =
765 dynamic_cast<MythUIStateType *>(GetChild("aspectstate"));
766 if (aspectState)
767 aspectState->DisplayState(info->GetAspect());
768}
769
770void ThemeChooser::updateProgressBar(int bytesReceived,
771 int bytesTotal)
772{
773 MythUIProgressBar *progressBar =
774 dynamic_cast<MythUIProgressBar *>(GetChild("downloadprogressbar"));
775
776 if (!progressBar)
777 return;
778
779 progressBar->SetUsed(bytesReceived);
780 progressBar->SetTotal(bytesTotal);
781}
782
784{
785 if (e->type() == MythEvent::kMythEventMessage)
786 {
787 auto *me = dynamic_cast<MythEvent *>(e);
788 if (me == nullptr)
789 return;
790
791 QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
792 if (tokens.isEmpty())
793 return;
794
795 if (tokens[0] == "DOWNLOAD_FILE")
796 {
797 QStringList args = me->ExtraDataList();
798 if ((m_downloadState == dsIdle) ||
799 (tokens.size() != 2) ||
800 (!m_downloadTheme) ||
801 (args[1] != m_downloadFile))
802 return;
803
804 if (tokens[1] == "UPDATE")
805 {
806 updateProgressBar(args[2].toInt(), args[3].toInt());
807 }
808 else if (tokens[1] == "FINISHED")
809 {
810 bool remoteFileIsLocal = false;
811 int fileSize = args[2].toInt();
812 int errorCode = args[4].toInt();
813
815
816 QFileInfo file(m_downloadFile);
818 (m_downloadFile.startsWith("myth://")))
819 {
820 // The backend download is finished so start the
821 // frontend download
822 LOG(VB_FILE, LOG_INFO, QString("Download done MBE %1 %2").arg(errorCode).arg(fileSize));
823 if ((errorCode == 0) &&
824 (fileSize > 0))
825 {
827 QString localFile = GetConfDir() + "/tmp/" +
828 file.fileName();
829 file.setFile(localFile);
830
831 if (file.exists())
832 {
833 remoteFileIsLocal = true;
834 m_downloadFile = localFile;
835 }
836 else
837 {
839 m_downloadFile, localFile, this);
840 OpenBusyPopup(tr("Copying %1 Theme Package")
841 .arg(m_downloadTheme->GetName()));
842 m_downloadFile = localFile;
843 return;
844 }
845 }
846 else
847 {
849 ShowOkPopup(tr("ERROR downloading theme package on master backend."));
850 }
851 }
852
854 (file.exists()))
855 {
856 // The frontend download is finished
857 LOG(VB_FILE, LOG_INFO, QString("Download done MFE %1 %2").arg(errorCode).arg(fileSize));
858 // moved error is ok
859 if ((errorCode == 0) &&
860 (fileSize > 0))
861 {
863 auto *extractThread =
867 extractThread, "ThemeExtract");
868
869 if (!remoteFileIsLocal)
871
872 OpenBusyPopup(tr("Installing %1 Theme")
873 .arg(m_downloadTheme->GetName()));
874 }
875 else
876 {
878 ShowOkPopup(tr("ERROR downloading theme package from frontend."));
879 }
880 }
881 }
882 }
883 else if ((me->Message() == "THEME_INSTALLED") &&
884 (m_downloadTheme) &&
886 {
889 QStringList args = me->ExtraDataList();
890
891 if (!args.isEmpty() && !args[0].isEmpty())
892 QFile::remove(args[0]);
893
894 QString event = QString("THEME_INSTALLED PATH %1")
895 .arg(m_userThemeDir +
898
900
901 // Send a message to ourself so we trigger a reload our next chance
902 auto *me2 = new MythEvent("THEME_RELOAD");
903 qApp->postEvent(this, me2);
904 }
905 else if ((me->Message() == "THEME_RELOAD") &&
907 {
908 GetMythMainWindow()->JumpTo("Reload Theme");
909 }
910 }
911}
912
914{
916 if (!current)
917 {
918 ShowOkPopup(tr("Error, no theme selected."));
919 return;
920 }
921
922 auto *info = current->GetData().value<ThemeInfo *>();
923 if (!info)
924 {
925 ShowOkPopup(tr("Error, unable to find current theme."));
926 return;
927 }
928
929 if (!info->GetPreviewPath().startsWith(m_userThemeDir))
930 {
931 ShowOkPopup(tr("%1 is not a user-installed theme and can not "
932 "be deleted.")
933 .arg(info->GetName()));
934 return;
935 }
936
937 removeThemeDir(m_userThemeDir + info->GetDirectoryName());
938
940}
941
942bool ThemeChooser::removeThemeDir(const QString &dirname)
943{
944 if ((!dirname.startsWith(m_userThemeDir)) &&
945 (!dirname.startsWith(GetMythUI()->GetThemeCacheDir())))
946 return true;
947
948 QDir dir(dirname);
949
950 if (!dir.exists())
951 return true;
952
953 dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
954 QFileInfoList list = dir.entryInfoList();
955
956 for (const auto &fi : std::as_const(list))
957 {
958 if (fi.isFile() && !fi.isSymLink())
959 {
960 if (!QFile::remove(fi.absoluteFilePath()))
961 return false;
962 }
963 else if (fi.isDir() && !fi.isSymLink())
964 {
965 if (!removeThemeDir(fi.absoluteFilePath()))
966 return false;
967 }
968 }
969
970 return dir.rmdir(dirname);
971}
972
974
975ThemeUpdateChecker::ThemeUpdateChecker(void) : m_updateTimer(new QTimer(this))
976{
977 uint major { 0 };
978 uint minor { 0 };
979 bool devel { false };
980 bool parsed = ParseMythSourceVersion(devel, major, minor);
981
982 if (!parsed || devel)
983 {
984 m_mythVersions << "trunk";
985 }
986 else
987 {
988 for (int i = minor ; i > 0; i--)
989 m_mythVersions << QString("%1.%2").arg(major).arg(i);
990 m_mythVersions << QString::number(major);
991 }
992
995 "remotethemes/themes.zip",
996 "Temp");
997
998 gCoreContext->SaveSetting("ThemeUpdateStatus", "");
999
1001
1002 if (qEnvironmentVariableIsSet("MYTHTV_DEBUGMDM"))
1003 {
1004 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every minute");
1005 m_updateTimer->start(1min);
1006 }
1007 else
1008 {
1009 LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every hour");
1010 m_updateTimer->start(1h);
1011 }
1012
1013 // Run once 15 seconds from now
1014 QTimer::singleShot(15s, this, &ThemeUpdateChecker::checkForUpdate);
1015}
1016
1018{
1019 if (m_updateTimer)
1020 {
1021 m_updateTimer->stop();
1022 delete m_updateTimer;
1023 m_updateTimer = nullptr;
1024 }
1025}
1026
1028{
1029 if (GetMythUI()->GetCurrentLocation(false, true) != "mainmenu")
1030 return;
1031
1032 ThemeInfo *localTheme = nullptr;
1033
1035 {
1036 QStringList::iterator Iversion;
1037
1038 for (Iversion = m_mythVersions.begin();
1039 Iversion != m_mythVersions.end(); ++Iversion)
1040 {
1041
1042 QString remoteThemeDir =
1045 QString("remotethemes/%1/%2")
1046 .arg(*Iversion,
1047 GetMythUI()->GetThemeName()),
1048 "Temp");
1049
1050 QString infoXML = remoteThemeDir;
1051 infoXML.append("/themeinfo.xml");
1052
1053 LOG(VB_GUI, LOG_INFO, QString("ThemeUpdateChecker Loading '%1'").arg(infoXML));
1054
1055 if (RemoteFile::Exists(infoXML))
1056 {
1057 int locMaj = 0;
1058 int locMin = 0;
1059
1060 auto *remoteTheme = new ThemeInfo(remoteThemeDir);
1061 if (!remoteTheme || remoteTheme->GetType() & THEME_UNKN)
1062 {
1063 LOG(VB_GENERAL, LOG_ERR,
1064 QString("ThemeUpdateChecker::checkForUpdate(): "
1065 "Unable to create ThemeInfo for %1")
1066 .arg(infoXML));
1067 delete remoteTheme;
1068 remoteTheme = nullptr;
1069 return;
1070 }
1071
1072 if (!localTheme)
1073 {
1074 localTheme = new ThemeInfo(GetMythUI()->GetThemeDir());
1075 if (!localTheme || localTheme->GetType() & THEME_UNKN)
1076 {
1077 LOG(VB_GENERAL, LOG_ERR,
1078 "ThemeUpdateChecker::checkForUpdate(): "
1079 "Unable to create ThemeInfo for current theme");
1080 delete localTheme;
1081 localTheme = nullptr;
1082 return;
1083 }
1084 locMaj = localTheme->GetMajorVersion();
1085 locMin = localTheme->GetMinorVersion();
1086 }
1087
1088 int rmtMaj = remoteTheme->GetMajorVersion();
1089 int rmtMin = remoteTheme->GetMinorVersion();
1090
1091 delete remoteTheme;
1092 remoteTheme = nullptr;
1093
1094 if ((rmtMaj > locMaj) ||
1095 ((rmtMaj == locMaj) &&
1096 (rmtMin > locMin)))
1097 {
1099 QString("%1-%2.%3").arg(GetMythUI()->GetThemeName()).arg(rmtMaj).arg(rmtMin);
1100
1101 QString status = gCoreContext->GetSetting("ThemeUpdateStatus");
1102 QString currentLocation = GetMythUI()->GetCurrentLocation(false, true);
1103
1104 if ((!status.startsWith(m_lastKnownThemeVersion)) &&
1105 (currentLocation == "mainmenu"))
1106 {
1107 m_currentVersion = QString("%1.%2")
1108 .arg(locMaj)
1109 .arg(locMin);
1110 m_newVersion = QString("%1.%2").arg(rmtMaj).arg(rmtMin);
1111
1112 gCoreContext->SaveSetting("ThemeUpdateStatus",
1113 m_lastKnownThemeVersion + " notified");
1114
1115 QString message = tr("Version %1 of the %2 theme is now "
1116 "available in the Theme Chooser. "
1117 "The currently installed version "
1118 "is %3.")
1119 .arg(m_newVersion,
1120 GetMythUI()->GetThemeName(),
1122
1123 ShowOkPopup(message);
1124 break;
1125 }
1126 }
1127 }
1128 }
1129 }
1130
1131 delete localTheme;
1132}
1133
1134/* 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)
#define minor(X)
Definition: compat.h:74
unsigned int uint
Definition: freesurround.h:24
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)
Definition: remoteutil.cpp:609
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