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 <QRunnable>
8 #include <QRegExp>
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  const QString &srcFile, const QString &destDir) :
47  m_parent(parent),
48  m_srcFile(srcFile),
49  m_destDir(destDir) {}
50 
51  void run() override // QRunnable
52  {
54 
55  MythEvent *me =
56  new MythEvent("THEME_INSTALLED", QStringList(m_srcFile));
57  QCoreApplication::postEvent(m_parent, me);
58  }
59 
60  private:
61  ThemeChooser *m_parent {nullptr};
62  QString m_srcFile;
63  QString m_destDir;
64 };
65 
66 
72  const QString &name) :
73  MythScreenType(parent, name)
74 {
76 
77  StorageGroup sgroup("Themes", gCoreContext->GetHostName());
78  m_userThemeDir = sgroup.GetFirstDir(true);
79 }
80 
82 {
84 }
85 
86 static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
87 {
88  return s1.fileName().toLower() < s2.fileName().toLower();
89 }
90 
91 
93 {
94  // Load the theme for this screen
95  if (!LoadWindowFromXML("settings-ui.xml", "themechooser", this))
96  return false;
97 
98  bool err = false;
99  UIUtilE::Assign(this, m_themes, "themes", &err);
100 
101  UIUtilW::Assign(this, m_preview, "preview");
102  UIUtilW::Assign(this, m_fullPreviewStateType, "fullpreviewstate");
103 
105  {
106  MythUIGroup *state =
107  dynamic_cast<MythUIGroup*>
108  (m_fullPreviewStateType->GetChild("fullscreen"));
109  if (state)
110  {
112  dynamic_cast<MythUIText*>(state->GetChild("fullscreenname"));
114  dynamic_cast<MythUIImage*>(state->GetChild("fullscreenpreview"));
115  }
116  }
117 
118  if (err)
119  {
120  LOG(VB_GENERAL, LOG_ERR, LOC + "Cannot load screen 'themechooser'");
121  return false;
122  }
123 
124  connect(m_themes, SIGNAL(itemClicked(MythUIButtonListItem*)),
125  this, SLOT(saveAndReload(MythUIButtonListItem*)));
126  connect(m_themes, SIGNAL(itemSelected(MythUIButtonListItem*)),
127  this, SLOT(itemChanged(MythUIButtonListItem*)));
128 
129  BuildFocusList();
130 
132 
133  return true;
134 }
135 
137 {
138  SetBusyPopupMessage(tr("Loading Installed Themes"));
139 
140  QStringList themesSeen;
141  QDir themes(m_userThemeDir);
142  themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
143  themes.setSorting(QDir::Name | QDir::IgnoreCase);
144 
145  m_infoList = themes.entryInfoList();
146 
147  for( QFileInfoList::iterator it = m_infoList.begin();
148  it != m_infoList.end();
149  ++it )
150  {
151  if (loadThemeInfo(*it))
152  {
153  themesSeen << (*it).fileName();
154  m_themeStatuses[(*it).fileName()] = "default";
155  }
156  }
157 
158  themes.setPath(GetThemesParentDir());
159  QFileInfoList sharedThemes = themes.entryInfoList();
160  for( QFileInfoList::iterator it = sharedThemes.begin();
161  it != sharedThemes.end();
162  ++it )
163  {
164  if ((!themesSeen.contains((*it).fileName())) &&
165  (loadThemeInfo(*it)))
166  {
167  m_infoList << *it;
168  themesSeen << (*it).fileName();
169  m_themeStatuses[(*it).fileName()] = "default";
170  }
171  }
172 
173  // MYTH_SOURCE_VERSION - examples v29-pre-574-g92517f5, v29-Pre, v29.1-21-ge26a33c
174  QString MythVersion(MYTH_SOURCE_VERSION);
175  QRegExp trunkver("v[0-9]+-pre.*",Qt::CaseInsensitive);
176  QRegExp validver("v[0-9]+.*",Qt::CaseInsensitive);
177 
178  if (!validver.exactMatch(MythVersion))
179  {
180  LOG(VB_GENERAL, LOG_ERR, QString("Invalid MythTV version %1, will use themes from trunk").arg(MythVersion));
181  MythVersion = "trunk";
182  }
183  if (trunkver.exactMatch(MythVersion))
184  MythVersion = "trunk";
185 
186  if (MythVersion == "trunk")
187  {
188  LoadVersion(MythVersion, themesSeen, true);
189  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
190  }
191  else
192  {
193 
194  MythVersion = MYTH_BINARY_VERSION; // Example: 29.20161017-1
195  // Remove the date part and the rest, eg 29.20161017-1 -> 29
196  MythVersion.replace(QRegExp("\\.[0-9]{8,}.*"), "");
197  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
198  LoadVersion(MythVersion, themesSeen, true);
199 
200  // If a version of the theme for this tag exists, use it...
201  // MYTH_SOURCE_VERSION - examples v29-pre-574-g92517f5, v29-Pre, v29.1-21-ge26a33c
202  QRegExp subexp("v[0-9]+\\.([0-9]+)-*");
203  // This captures the subversion, i.e. the number after a dot
204  int pos = subexp.indexIn(MYTH_SOURCE_VERSION);
205  if (pos > -1)
206  {
207  QString subversion;
208  int idx = subexp.cap(1).toInt();
209  for ( ; idx > 0; --idx)
210  {
211  subversion = MythVersion + "." + QString::number(idx);
212  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(subversion));
213  LoadVersion(subversion, themesSeen, false);
214  }
215  }
216  }
217 
218  ResetBusyPopup();
219 
220  std::sort(m_infoList.begin(), m_infoList.end(), sortThemeNames);
221 }
222 
223 void ThemeChooser::LoadVersion(const QString &version,
224  QStringList &themesSeen, bool alert_user)
225 {
226  QString remoteThemesFile = GetConfDir();
227  remoteThemesFile.append("/tmp/themes.zip");
228  QString themeSite = QString("%1/%2")
229  .arg(gCoreContext->GetSetting("ThemeRepositoryURL",
230  "http://themes.mythtv.org/themes/repository")).arg(version);
231  QString destdir = GetCacheDir().append("/themechooser/");
232  QString versiondir = QString("%1/%2").arg(destdir).arg(version);
233  QDir remoteThemesDir(versiondir);
234 
235  int downloadFailures =
236  gCoreContext->GetNumSetting("ThemeInfoDownloadFailures", 0);
237  if (QFile::exists(remoteThemesFile))
238  {
239  QFileInfo finfo(remoteThemesFile);
240  if (finfo.lastModified().toUTC() <
241  MythDate::current().addSecs(-600))
242  {
243  LOG(VB_GUI, LOG_INFO, LOC +
244  QString("%1 is over 10 minutes old, forcing "
245  "remote theme list download").arg(remoteThemesFile));
247  }
248 
249  if (!remoteThemesDir.exists())
251  }
252  else if (downloadFailures < 2) // (and themes.zip does not exist)
253  {
254  LOG(VB_GUI, LOG_INFO, LOC +
255  QString("%1 does not exist, forcing remote theme "
256  "list download").arg(remoteThemesFile));
258  }
259 
261  {
262  QFile test(remoteThemesFile);
263  if (test.open(QIODevice::WriteOnly))
264  test.remove();
265  else
266  {
267  ShowOkPopup(tr("Unable to create '%1'").arg(remoteThemesFile));
268  return;
269  }
270 
271  SetBusyPopupMessage(tr("Refreshing Downloadable Themes Information"));
272 
273  QString url = themeSite;
274  url.append("/themes.zip");
275  if (!removeThemeDir(versiondir))
276  ShowOkPopup(tr("Unable to remove '%1'").arg(versiondir));
277  QDir dir;
278  if (!dir.mkpath(destdir))
279  ShowOkPopup(tr("Unable to create '%1'").arg(destdir));
280  bool result = GetMythDownloadManager()->download(url, remoteThemesFile, true);
281 
282  LOG(VB_GUI, LOG_INFO, LOC +
283  QString("Downloading '%1' to '%2'").arg(url).arg(remoteThemesFile));
284 
285  SetBusyPopupMessage(tr("Extracting Downloadable Themes Information"));
286 
287  if (!result || !extractZIP(remoteThemesFile, destdir))
288  {
289  QFile::remove(remoteThemesFile);
290 
291  downloadFailures++;
292  gCoreContext->SaveSetting("ThemeInfoDownloadFailures",
293  downloadFailures);
294 
295  if (!result)
296  {
297  LOG(VB_GUI, LOG_ERR, LOC +
298  QString("Failed to download '%1'").arg(url));
299  if (alert_user)
300  ShowOkPopup(tr("Failed to download '%1'").arg(url));
301  }
302  else
303  {
304  LOG(VB_GUI, LOG_ERR, LOC +
305  QString("Failed to unzip '%1' to '%2'")
306  .arg(remoteThemesFile).arg(destdir));
307  if (alert_user)
308  ShowOkPopup(tr("Failed to unzip '%1' to '%2'")
309  .arg(remoteThemesFile).arg(destdir));
310  }
311  }
312  else
313  LOG(VB_GUI, LOG_INFO, LOC +
314  QString("Unzipped '%1' to '%2'")
315  .arg(remoteThemesFile)
316  .arg(destdir));
317  }
318 
319  QDir themes;
320  themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
321  themes.setSorting(QDir::Name | QDir::IgnoreCase);
322 
323  if ((QFile::exists(remoteThemesFile)) &&
324  (remoteThemesDir.exists()))
325  {
326  SetBusyPopupMessage(tr("Loading Downloadable Themes"));
327 
328  LOG(VB_GUI, LOG_INFO, LOC +
329  QString("%1 and %2 exist, using cached remote themes list")
330  .arg(remoteThemesFile).arg(remoteThemesDir.absolutePath()));
331 
332  QString themesPath = remoteThemesDir.absolutePath();
333  themes.setPath(themesPath);
334 
335  QFileInfoList downloadableThemes = themes.entryInfoList();
336  for( QFileInfoList::iterator it = downloadableThemes.begin();
337  it != downloadableThemes.end();
338  ++it )
339  {
340  QString dirName = (*it).fileName();
341  QString themeName = dirName;
342  QString remoteDir = themeSite;
343  remoteDir.append("/").append(dirName);
344  QString localDir = themes.absolutePath();
345  localDir.append("/").append(dirName);
346 
347  ThemeInfo remoteTheme((*it).absoluteFilePath());
348 
349  if (themesSeen.contains(dirName))
350  {
351  ThemeInfo *localTheme = m_themeNameInfos[dirName];
352 
353  themeName = remoteTheme.GetName();
354 
355  int rmtMaj = remoteTheme.GetMajorVersion();
356  int rmtMin = remoteTheme.GetMinorVersion();
357  int locMaj = localTheme->GetMajorVersion();
358  int locMin = localTheme->GetMinorVersion();
359 
360  if ((rmtMaj > locMaj) ||
361  ((rmtMaj == locMaj) &&
362  (rmtMin > locMin)))
363  {
364  if (loadThemeInfo(*it))
365  {
366  LOG(VB_GUI, LOG_DEBUG, LOC +
367  QString("'%1' old version %2.%3, new version %4.%5")
368  .arg(themeName).arg(locMaj).arg(locMin)
369  .arg(rmtMaj).arg(rmtMin));
370 
371  m_infoList << *it;
372  m_themeStatuses[themeName] = "updateavailable";
373 
374  QFileInfo finfo(remoteTheme.GetPreviewPath());
376  remoteDir.append("/").append(finfo.fileName()),
377  localDir.append("/").append(finfo.fileName()),
378  nullptr);
379  }
380  }
381  else if ((rmtMaj == locMaj) &&
382  (rmtMin == locMin))
383  {
384  LOG(VB_GUI, LOG_DEBUG, LOC +
385  QString("'%1' up to date (%2.%3)")
386  .arg(themeName).arg(locMaj).arg(locMin));
387 
388  m_themeStatuses[themeName] = "uptodate";
389  }
390  }
391  else
392  {
393  LOG(VB_GUI, LOG_DEBUG, LOC +
394  QString("'%1' (%2.%3) available")
395  .arg(themeName)
396  .arg(remoteTheme.GetMajorVersion())
397  .arg(remoteTheme.GetMinorVersion()));
398 
399  ThemeInfo *tmpTheme = loadThemeInfo(*it);
400  if (tmpTheme)
401  {
402  themeName = tmpTheme->GetName();
403  themesSeen << dirName;
404  m_infoList << *it;
405  m_themeStatuses[themeName] = "updateavailable";
406 
407  QFileInfo finfo(tmpTheme->GetPreviewPath());
409  remoteDir.append("/").append(finfo.fileName()),
410  localDir.append("/").append(finfo.fileName()),
411  nullptr);
412  }
413  }
414  }
415  }
416 
417 }
418 
420 {
421  QString curTheme = gCoreContext->GetSetting("Theme");
422  ThemeInfo *themeinfo = nullptr;
423  ThemeInfo *curThemeInfo = nullptr;
424  MythUIButtonListItem *item = nullptr;
425 
426  m_themes->Reset();
427  for( QFileInfoList::iterator it = m_infoList.begin();
428  it != m_infoList.end();
429  ++it )
430  {
431  QFileInfo &theme = *it;
432 
433  if (!m_themeFileNameInfos.contains(theme.filePath()))
434  continue;
435 
436  themeinfo = m_themeFileNameInfos[theme.filePath()];
437  if (!themeinfo)
438  continue;
439 
440  QString buttonText = QString("%1 %2.%3")
441  .arg(themeinfo->GetName())
442  .arg(themeinfo->GetMajorVersion())
443  .arg(themeinfo->GetMinorVersion());
444 
445  item = new MythUIButtonListItem(m_themes, buttonText);
446  if (item)
447  {
448  if (themeinfo->GetDownloadURL().isEmpty())
449  item->DisplayState("local", "themelocation");
450  else
451  item->DisplayState("remote", "themelocation");
452 
453  item->DisplayState(themeinfo->GetAspect(), "aspectstate");
454 
455  item->DisplayState(m_themeStatuses[themeinfo->GetName()],
456  "themestatus");
457  InfoMap infomap;
458  themeinfo->ToMap(infomap);
459  item->SetTextFromMap(infomap);
460  item->SetData(qVariantFromValue(themeinfo));
461 
462  QString thumbnail = themeinfo->GetPreviewPath();
463  QFileInfo fInfo(thumbnail);
464  // Downloadable themeinfos have thumbnail copies of their preview images
465  if (!themeinfo->GetDownloadURL().isEmpty())
466  thumbnail = thumbnail.append(".thumb.jpg");
467  item->SetImage(thumbnail);
468 
469  if (curTheme == themeinfo->GetDirectoryName())
470  curThemeInfo = themeinfo;
471  }
472  }
473 
475 
476  if (curThemeInfo)
477  m_themes->SetValueByData(qVariantFromValue(curThemeInfo));
478 
480  if (current)
482 
483  QString testFile = m_userThemeDir + "/.test";
484  QFile test(testFile);
485  if (test.open(QIODevice::WriteOnly))
486  test.remove();
487  else
488  {
489  ShowOkPopup(tr("Error creating test file, %1 themes directory is "
490  "not writable.").arg(m_userThemeDir));
491  }
492 }
493 
495 {
496  if (theme.fileName() == "default" || theme.fileName() == "default-wide")
497  return nullptr;
498 
499  ThemeInfo *themeinfo = nullptr;
500  if (theme.exists()) // local directory vs http:// or remote URL
501  themeinfo = new ThemeInfo(theme.absoluteFilePath());
502  else
503  themeinfo = new ThemeInfo(theme.filePath());
504 
505  if (!themeinfo)
506  return nullptr;
507 
508  if (themeinfo->GetName().isEmpty() || ((themeinfo->GetType() & THEME_UI) == 0))
509  {
510  delete themeinfo;
511  return nullptr;
512  }
513 
514  m_themeFileNameInfos[theme.filePath()] = themeinfo;
515  m_themeNameInfos[theme.fileName()] = themeinfo;
516 
517  return themeinfo;
518 }
519 
521 {
522  if (m_popupMenu)
523  return;
524 
525  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
526  QString label = tr("Theme Chooser Menu");
527 
528  m_popupMenu =
529  new MythDialogBox(label, popupStack, "themechoosermenupopup");
530 
531  connect(m_popupMenu, SIGNAL(Closed(QString, int)), SLOT(popupClosed(QString, int)));
532 
533  if (m_popupMenu->Create())
534  popupStack->AddScreen(m_popupMenu);
535  else
536  {
537  delete m_popupMenu;
538  m_popupMenu = nullptr;
539  return;
540  }
541 
542  m_popupMenu->SetReturnEvent(this, "popupmenu");
543 
545  {
547  m_popupMenu->AddButton(tr("Hide Fullscreen Preview"),
548  SLOT(toggleFullscreenPreview()));
549  else
550  m_popupMenu->AddButton(tr("Show Fullscreen Preview"),
551  SLOT(toggleFullscreenPreview()));
552  }
553 
554  m_popupMenu->AddButton(tr("Refresh Downloadable Themes"),
555  SLOT(refreshDownloadableThemes()));
556 
558  if (current)
559  {
560  ThemeInfo *info = current->GetData().value<ThemeInfo *>();
561 
562  if (info)
563  {
564  m_popupMenu->AddButton(tr("Select Theme"),
565  SLOT(saveAndReload()));
566 
567  if (info->GetPreviewPath().startsWith(m_userThemeDir))
568  m_popupMenu->AddButton(tr("Delete Theme"),
569  SLOT(removeTheme()));
570  }
571  }
572 
573  if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
574  m_popupMenu->AddButton(tr("Disable Theme Update Notifications"),
576  else
577  m_popupMenu->AddButton(tr("Enable Theme Update Notifications"),
579 }
580 
581 void ThemeChooser::popupClosed(const QString& which, int result)
582 {
583  (void)which;
584  (void)result;
585 
586  m_popupMenu = nullptr;
587 }
588 
589 bool ThemeChooser::keyPressEvent(QKeyEvent *event)
590 {
591  if (GetFocusWidget()->keyPressEvent(event))
592  return true;
593 
594  QStringList actions;
595  bool handled = GetMythMainWindow()->TranslateKeyPress("Theme Chooser", event, actions);
596 
597  for (int i = 0; i < actions.size() && !handled; ++i)
598  {
599  QString action = actions[i];
600  handled = true;
601 
602  if (action == "MENU")
603  showPopupMenu();
604  else if (action == "DELETE")
605  removeTheme();
606  else if ((action == "ESCAPE") &&
608  {
610  }
611  else
612  handled = false;
613  }
614 
615  if (!handled && MythScreenType::keyPressEvent(event))
616  handled = true;
617 
618  return handled;
619 }
620 
622 {
624  {
626  {
629 
630  if (m_fullScreenName)
632 
634  m_fullPreviewShowing = false;
635  }
636  else
637  {
639  ThemeInfo *info = item->GetData().value<ThemeInfo*>();
640  if (info)
641  {
643  {
646  }
647 
648  if (m_fullScreenName)
649  m_fullScreenName->SetText(info->GetName());
650 
651  m_fullPreviewStateType->DisplayState("fullscreen");
652  m_fullPreviewShowing = true;
653  }
654  }
655  }
656 }
657 
659 {
660  if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
661  gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "0", "");
662  else
663  gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "1", "");
664 }
665 
667 {
668  LOG(VB_GUI, LOG_INFO, LOC + "Forcing remote theme list refresh");
670  gCoreContext->SaveSetting("ThemeInfoDownloadFailures", 0);
672 }
673 
675 {
677  if (current)
679 }
680 
682 {
683  ThemeInfo *info = item->GetData().value<ThemeInfo *>();
684 
685  if (!info)
686  return;
687 
688  if (!info->GetDownloadURL().isEmpty())
689  {
690  QString testFile = m_userThemeDir + "/.test";
691  QFile test(testFile);
692  if (test.open(QIODevice::WriteOnly))
693  test.remove();
694  else
695  {
696  ShowOkPopup(tr("Unable to install theme, %1 themes directory is "
697  "not writable.").arg(m_userThemeDir));
698  return;
699  }
700 
701  QString downloadURL = info->GetDownloadURL();
702  QFileInfo qfile(downloadURL);
703  QString baseName = qfile.fileName();
704 
705  if (!gCoreContext->GetSetting("ThemeDownloadURL").isEmpty())
706  {
707  QStringList tokens =
708  gCoreContext->GetSetting("ThemeDownloadURL")
709  .split(";", QString::SkipEmptyParts);
710  QString origURL = downloadURL;
711  downloadURL.replace(tokens[0], tokens[1]);
712  LOG(VB_FILE, LOG_WARNING, LOC +
713  QString("Theme download URL overridden from %1 to %2.")
714  .arg(origURL).arg(downloadURL));
715  }
716 
717  OpenBusyPopup(tr("Downloading %1 Theme").arg(info->GetName()));
718  m_downloadTheme = info;
719 #if 0
721  "Temp", baseName);
723 #else
724  QString localFile = GetConfDir() + "/tmp/" + baseName;
725  GetMythDownloadManager()->queueDownload(downloadURL, localFile, this);
726  m_downloadFile = localFile;
728 #endif
729  }
730  else
731  {
732  gCoreContext->SaveSetting("Theme", info->GetDirectoryName());
733  GetMythMainWindow()->JumpTo("Reload Theme");
734  }
735 }
736 
738 {
739  ThemeInfo *info = item->GetData().value<ThemeInfo*>();
740 
741  if (!info)
742  return;
743 
744  QFileInfo preview(info->GetPreviewPath());
745  InfoMap infomap;
746  info->ToMap(infomap);
747  SetTextFromMap(infomap);
748  if (m_preview)
749  {
750  if (preview.exists())
751  {
753  m_preview->Load();
754  }
755  else
756  m_preview->Reset();
757  }
759  {
761  {
762  if (preview.exists())
763  {
766  }
767  else
769  }
770 
771  if (m_fullScreenName)
772  m_fullScreenName->SetText(info->GetName());
773  }
774 
775  MythUIStateType *themeLocation =
776  dynamic_cast<MythUIStateType*>(GetChild("themelocation"));
777  if (themeLocation)
778  {
779  if (info->GetDownloadURL().isEmpty())
780  themeLocation->DisplayState("local");
781  else
782  themeLocation->DisplayState("remote");
783  }
784 
785  MythUIStateType *aspectState =
786  dynamic_cast<MythUIStateType*>(GetChild("aspectstate"));
787  if (aspectState)
788  aspectState->DisplayState(info->GetAspect());
789 }
790 
791 void ThemeChooser::updateProgressBar(int bytesReceived,
792  int bytesTotal)
793 {
794  MythUIProgressBar *progressBar =
795  dynamic_cast<MythUIProgressBar *>(GetChild("downloadprogressbar"));
796 
797  if (!progressBar)
798  return;
799 
800  progressBar->SetUsed(bytesReceived);
801  progressBar->SetTotal(bytesTotal);
802 }
803 
805 {
806  if (e->type() == MythEvent::MythEventMessage)
807  {
808  MythEvent *me = static_cast<MythEvent *>(e);
809  QStringList tokens = me->Message().split(" ", QString::SkipEmptyParts);
810 
811  if (tokens.isEmpty())
812  return;
813 
814  if (tokens[0] == "DOWNLOAD_FILE")
815  {
816  QStringList args = me->ExtraDataList();
817  if ((m_downloadState == dsIdle) ||
818  (tokens.size() != 2) ||
819  (!m_downloadTheme) ||
820  (args[1] != m_downloadFile))
821  return;
822 
823  if (tokens[1] == "UPDATE")
824  {
825  updateProgressBar(args[2].toInt(), args[3].toInt());
826  }
827  else if (tokens[1] == "FINISHED")
828  {
829  bool remoteFileIsLocal = false;
830  int fileSize = args[2].toInt();
831  int errorCode = args[4].toInt();
832 
833  CloseBusyPopup();
834 
835  QFileInfo file(m_downloadFile);
837  (m_downloadFile.startsWith("myth://")))
838  {
839  // The backend download is finished so start the
840  // frontend download
841  if ((errorCode == 0) &&
842  (fileSize > 0))
843  {
845  QString localFile = GetConfDir() + "/tmp/" +
846  file.fileName();
847  file.setFile(localFile);
848 
849  if (file.exists())
850  {
851  remoteFileIsLocal = true;
852  m_downloadFile = localFile;
853  }
854  else
855  {
857  m_downloadFile, localFile, this);
858  OpenBusyPopup(tr("Copying %1 Theme Package")
859  .arg(m_downloadTheme->GetName()));
860  m_downloadFile = localFile;
861  return;
862  }
863  }
864  else
865  {
867  ShowOkPopup(tr("ERROR downloading theme package on master backend."));
868  }
869  }
870 
872  (file.exists()))
873  {
874  // The frontend download is finished
875  if ((errorCode == 0) &&
876  (fileSize > 0))
877  {
879  ThemeExtractThread *extractThread =
883  extractThread, "ThemeExtract");
884 
885  if (!remoteFileIsLocal)
887 
888  OpenBusyPopup(tr("Installing %1 Theme")
889  .arg(m_downloadTheme->GetName()));
890  }
891  else
892  {
894  ShowOkPopup(tr("ERROR downloading theme package from master backend."));
895  }
896  }
897  }
898  }
899  else if ((me->Message() == "THEME_INSTALLED") &&
900  (m_downloadTheme) &&
902  {
904  CloseBusyPopup();
905  QStringList args = me->ExtraDataList();
906  QFile::remove(args[0]);
907 
908  QString event = QString("THEME_INSTALLED PATH %1")
909  .arg(m_userThemeDir +
912 
914 
915  // Send a message to ourself so we trigger a reload our next chance
916  MythEvent *me2 = new MythEvent("THEME_RELOAD");
917  qApp->postEvent(this, me2);
918  }
919  else if ((me->Message() == "THEME_RELOAD") &&
920  (m_downloadState == dsIdle))
921  {
922  GetMythMainWindow()->JumpTo("Reload Theme");
923  }
924  }
925 }
926 
928 {
930  if (!current)
931  {
932  ShowOkPopup(tr("Error, no theme selected."));
933  return;
934  }
935 
936  ThemeInfo *info = current->GetData().value<ThemeInfo *>();
937  if (!info)
938  {
939  ShowOkPopup(tr("Error, unable to find current theme."));
940  return;
941  }
942 
943  if (!info->GetPreviewPath().startsWith(m_userThemeDir))
944  {
945  ShowOkPopup(tr("%1 is not a user-installed theme and can not "
946  "be deleted.").arg(info->GetName()));
947  return;
948  }
949 
951 
953 }
954 
955 bool ThemeChooser::removeThemeDir(const QString &dirname)
956 {
957  if ((!dirname.startsWith(m_userThemeDir)) &&
958  (!dirname.startsWith(GetMythUI()->GetThemeCacheDir())))
959  return true;
960 
961  QDir dir(dirname);
962 
963  if (!dir.exists())
964  return true;
965 
966  dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
967  QFileInfoList list = dir.entryInfoList();
968  QFileInfoList::const_iterator it = list.begin();
969 
970  while (it != list.end())
971  {
972  const QFileInfo *fi = &(*it++);
973  if (fi->isFile() && !fi->isSymLink())
974  {
975  if (!QFile::remove(fi->absoluteFilePath()))
976  return false;
977  }
978  else if (fi->isDir() && !fi->isSymLink())
979  {
980  if (!removeThemeDir(fi->absoluteFilePath()))
981  return false;
982  }
983  }
984 
985  return dir.rmdir(dirname);
986 }
987 
989 
991  m_updateTimer(new QTimer(this))
992 {
993  QString version = MYTH_SOURCE_PATH;
994 
995  if (!version.isEmpty() && !version.startsWith("fixes/"))
996  // Treat devel branches as master
997  m_mythVersions << "trunk";
998  else
999  {
1000  version = MYTH_BINARY_VERSION; // Example: 0.25.20101017-1
1001  version.replace(QRegExp("\\.[0-9]{8,}.*"), "");
1002 
1003  // If a version of the theme for this tag exists, use it...
1004  QRegExp subexp("v[0-9]+.[0-9]+.([0-9]+)-*");
1005  int pos = subexp.indexIn(MYTH_SOURCE_VERSION);
1006  if (pos > -1)
1007  {
1008  QString subversion;
1009  int idx = subexp.cap(1).toInt();
1010  for ( ; idx > 0; --idx)
1011  m_mythVersions << version + "." + QString::number(idx);
1012  }
1014  }
1015 
1018  "remotethemes/themes.zip",
1019  "Temp");
1020 
1021  gCoreContext->SaveSetting("ThemeUpdateStatus", "");
1022 
1023  connect(m_updateTimer, SIGNAL(timeout()), SLOT(checkForUpdate()));
1024 
1025  if (getenv("MYTHTV_DEBUGMDM"))
1026  {
1027  LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every minute");
1028  m_updateTimer->start(60 * 1000); // Run once a minute
1029  }
1030  else
1031  {
1032  LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every hour");
1033  m_updateTimer->start(60 * 60 * 1000); // Run once an hour
1034  }
1035 
1036  // Run once 15 seconds from now
1037  QTimer::singleShot(15 * 1000, this, SLOT(checkForUpdate()));
1038 }
1039 
1041 {
1042  if (m_updateTimer)
1043  {
1044  m_updateTimer->stop();
1045  delete m_updateTimer;
1046  m_updateTimer = nullptr;
1047  }
1048 }
1049 
1051 {
1052  if (GetMythUI()->GetCurrentLocation(false, true) != "mainmenu")
1053  return;
1054 
1055  ThemeInfo *localTheme = nullptr;
1056 
1058  {
1059  QStringList::iterator Iversion;
1060 
1061  for (Iversion = m_mythVersions.begin();
1062  Iversion != m_mythVersions.end(); ++Iversion)
1063  {
1064 
1065  QString remoteThemeDir =
1068  QString("remotethemes/%1/%2")
1069  .arg(*Iversion)
1070  .arg(GetMythUI()->GetThemeName()),
1071  "Temp");
1072 
1073  QString infoXML = remoteThemeDir;
1074  infoXML.append("/themeinfo.xml");
1075 
1076  LOG(VB_GUI, LOG_INFO, QString("ThemeUpdateChecker Loading '%1'")
1077  .arg(infoXML));
1078 
1079  if (RemoteFile::Exists(infoXML))
1080  {
1081  int locMaj = 0;
1082  int locMin = 0;
1083 
1084  ThemeInfo *remoteTheme = new ThemeInfo(remoteThemeDir);
1085  if (!remoteTheme || remoteTheme->GetType() & THEME_UNKN)
1086  {
1087  LOG(VB_GENERAL, LOG_ERR,
1088  QString("ThemeUpdateChecker::checkForUpdate(): "
1089  "Unable to create ThemeInfo for %1")
1090  .arg(infoXML));
1091  delete remoteTheme;
1092  remoteTheme = nullptr;
1093  return;
1094  }
1095 
1096  if (!localTheme)
1097  {
1098  localTheme = new ThemeInfo(GetMythUI()->GetThemeDir());
1099  if (!localTheme || localTheme->GetType() & THEME_UNKN)
1100  {
1101  LOG(VB_GENERAL, LOG_ERR,
1102  "ThemeUpdateChecker::checkForUpdate(): "
1103  "Unable to create ThemeInfo for current theme");
1104  delete localTheme;
1105  localTheme = nullptr;
1106  return;
1107  }
1108  locMaj = localTheme->GetMajorVersion();
1109  locMin = localTheme->GetMinorVersion();
1110  }
1111 
1112  int rmtMaj = remoteTheme->GetMajorVersion();
1113  int rmtMin = remoteTheme->GetMinorVersion();
1114 
1115  delete remoteTheme;
1116  remoteTheme = nullptr;
1117 
1118  if ((rmtMaj > locMaj) ||
1119  ((rmtMaj == locMaj) &&
1120  (rmtMin > locMin)))
1121  {
1123  QString("%1-%2.%3").arg(GetMythUI()->GetThemeName())
1124  .arg(rmtMaj).arg(rmtMin);
1125 
1126  QString status = gCoreContext->GetSetting
1127  ("ThemeUpdateStatus");
1128  QString currentLocation = GetMythUI()->GetCurrentLocation
1129  (false, true);
1130 
1131  if ((!status.startsWith(m_lastKnownThemeVersion)) &&
1132  (currentLocation == "mainmenu"))
1133  {
1134  m_currentVersion = QString("%1.%2")
1135  .arg(locMaj).arg(locMin);
1136  m_newVersion = QString("%1.%2").arg(rmtMaj).arg(rmtMin);
1137 
1138  gCoreContext->SaveSetting("ThemeUpdateStatus",
1140  + " notified");
1141 
1142  QString message = tr("Version %1 of the %2 theme is now "
1143  "available in the Theme Chooser. "
1144  "The currently installed version "
1145  "is %3.")
1146  .arg(m_newVersion)
1147  .arg(GetMythUI()->GetThemeName())
1148  .arg(m_currentVersion);
1149 
1150  ShowOkPopup(message);
1151  break;
1152  }
1153  }
1154  }
1155  }
1156  }
1157 
1158  delete localTheme;
1159 }
1160 
1161 /* 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)
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.
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:136
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)
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
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
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:84
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
const char * name
Definition: ParseText.cpp:328
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
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
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...
const QString & Message() const
Definition: mythevent.h:58
ThemeExtractThread(ThemeChooser *parent, const QString &srcFile, const QString &destDir)
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
const QStringList & ExtraDataList() const
Definition: mythevent.h:60
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)
void LoadInBackground(QString message="")
bool Create(void) override