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