1""" 2 @file 3 @brief This file loads the About dialog (i.e about Openshot Project) 4 @author Jonathan Thomas <jonathan@openshot.org> 5 @author Olivier Girard <olivier@openshot.org> 6 7 @section LICENSE 8 9 Copyright (c) 2008-2018 OpenShot Studios, LLC 10 (http://www.openshotstudios.com). This file is part of 11 OpenShot Video Editor (http://www.openshot.org), an open-source project 12 dedicated to delivering high quality video editing and animation solutions 13 to the world. 14 15 OpenShot Video Editor is free software: you can redistribute it and/or modify 16 it under the terms of the GNU General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version. 19 20 OpenShot Video Editor is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU General Public License for more details. 24 25 You should have received a copy of the GNU General Public License 26 along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>. 27 """ 28 29import os 30import codecs 31import re 32from functools import partial 33 34from PyQt5.QtCore import Qt 35from PyQt5.QtWidgets import QDialog 36 37from classes import info, ui_util 38from classes.logger import log 39from classes.app import get_app 40from classes.metrics import track_metric_screen 41from windows.views.credits_treeview import CreditsTreeView 42from windows.views.changelog_treeview import ChangelogTreeView 43 44import json 45import datetime 46 47import openshot 48 49 50def parse_changelog(changelog_path): 51 """Read changelog entries from provided file path.""" 52 changelog_list = [] 53 if not os.path.exists(changelog_path): 54 return None 55 # Attempt to open changelog with utf-8, and then utf-16 (for unix / windows support) 56 for encoding_name in ('utf_8', 'utf_16'): 57 try: 58 with codecs.open( 59 changelog_path, 'r', encoding=encoding_name 60 ) as changelog_file: 61 for line in changelog_file: 62 changelog_list.append({ 63 'hash': line[:9].strip(), 64 'date': line[9:20].strip(), 65 'author': line[20:45].strip(), 66 'subject': line[45:].strip(), 67 }) 68 break 69 except Exception: 70 log.warning('Failed to parse log file %s with encoding %s' % (changelog_path, encoding_name)) 71 return changelog_list 72 73 74def parse_new_changelog(changelog_path): 75 """Parse changelog data from specified new-format file.""" 76 if not os.path.exists(changelog_path): 77 return None 78 changelog_list = None 79 for encoding_name in ('utf_8', 'utf_16'): 80 try: 81 with codecs.open(changelog_path, 'r', encoding=encoding_name) as changelog_file: 82 # Generate match object with fields from all matching lines 83 matches = re.findall( 84 r"^-\s?([0-9a-f]{40})\s(\d{4,4}-\d{2,2}-\d{2,2})\s(.*)\s\[(.*)\]\s*$", 85 changelog_file.read(), re.MULTILINE) 86 log.debug("Parsed {} changelog lines from {}".format(len(matches), changelog_path)) 87 changelog_list = [{ 88 "hash": entry[0], 89 "date": entry[1], 90 "subject": entry[2], 91 "author": entry[3], 92 } for entry in matches] 93 except UnicodeError: 94 log.debug('Failed to parse log file %s with encoding %s' % (changelog_path, encoding_name)) 95 continue 96 except Exception: 97 log.warning("Parse error reading {}".format(changelog_path), exc_info=1) 98 return None 99 return changelog_list 100 101 102class About(QDialog): 103 """ About Dialog """ 104 105 ui_path = os.path.join(info.PATH, 'windows', 'ui', 'about.ui') 106 107 def __init__(self): 108 # Create dialog class 109 QDialog.__init__(self) 110 111 # Load UI from designer & init 112 ui_util.load_ui(self, self.ui_path) 113 ui_util.init_ui(self) 114 115 # get translations 116 self.app = get_app() 117 _ = self.app._tr 118 119 # Hide chnagelog button by default 120 self.btnchangelog.setVisible(False) 121 122 projects = ['openshot-qt', 'libopenshot', 'libopenshot-audio'] 123 # Old paths 124 paths = [os.path.join(info.PATH, 'settings', '{}.log'.format(p)) for p in projects] 125 # New paths 126 paths.extend([os.path.join(info.PATH, 'resources', '{}.log'.format(p)) for p in projects]) 127 if any([os.path.exists(path) for path in paths]): 128 self.btnchangelog.setVisible(True) 129 else: 130 log.warn("No changelog files found, disabling button") 131 132 create_text = _('Create & Edit Amazing Videos and Movies') 133 description_text = _( 134 "OpenShot Video Editor 2.x is the next generation of the award-winning <br/>" 135 "OpenShot video editing platform.") 136 learnmore_text = _('Learn more') 137 copyright_text = _('Copyright © %(begin_year)s-%(current_year)s') % { 138 'begin_year': '2008', 139 'current_year': str(datetime.datetime.today().year) 140 } 141 about_html = ''' 142 <html><head/><body><hr/> 143 <p align="center"> 144 <span style=" font-size:10pt; font-weight:600;">%s</span> 145 </p> 146 <p align="center"> 147 <span style=" font-size:10pt;">%s </span> 148 <a href="https://www.openshot.org/%s?r=about-us"> 149 <span style=" font-size:10pt; text-decoration: none; color:#55aaff;">%s</span> 150 </a> 151 <span style=" font-size:10pt;">.</span> 152 </p> 153 </body></html> 154 ''' % ( 155 create_text, 156 description_text, 157 info.website_language(), 158 learnmore_text) 159 company_html = ''' 160 <html><head/> 161 <body style="font-size:11pt; font-weight:400; font-style:normal;"> 162 <hr /> 163 <p align="center" 164 style="margin:12px 12px 0 0; -qt-block-indent:0; text-indent:0;"> 165 <span style="font-size:10pt; font-weight:600;">%s </span> 166 <a href="http://www.openshotstudios.com?r=about-us"> 167 <span style="font-size:10pt; font-weight:600; text-decoration: none; color:#55aaff;"> 168 OpenShot Studios, LLC<br /></span></a> 169 </p> 170 </body></html> 171 ''' % (copyright_text) 172 173 # Set description and company labels 174 self.lblAboutDescription.setText(about_html) 175 self.lblAboutCompany.setText(company_html) 176 177 # set events handlers 178 self.btncredit.clicked.connect(self.load_credit) 179 self.btnlicense.clicked.connect(self.load_license) 180 self.btnchangelog.clicked.connect(self.load_changelog) 181 182 # Look for frozen version info 183 frozen_version_label = "" 184 version_path = os.path.join(info.PATH, "settings", "version.json") 185 if os.path.exists(version_path): 186 with open(version_path, "r", encoding="UTF-8") as f: 187 version_info = json.loads(f.read()) 188 if version_info: 189 frozen_version_label = "<br/><br/><b>%s</b><br/>Build Date: %s" % \ 190 (version_info.get('build_name'), version_info.get('date')) 191 192 # Init some variables 193 openshot_qt_version = _("Version: %s") % info.VERSION 194 libopenshot_version = "libopenshot: %s" % openshot.OPENSHOT_VERSION_FULL 195 self.txtversion.setText( 196 "<b>%s</b><br/>%s%s" % (openshot_qt_version, libopenshot_version, frozen_version_label)) 197 self.txtversion.setAlignment(Qt.AlignCenter) 198 199 # Track metrics 200 track_metric_screen("about-screen") 201 202 def load_credit(self): 203 """ Load Credits for everybody who has contributed in several domain for Openshot """ 204 log.debug('Credit screen has been opened') 205 windo = Credits() 206 windo.exec_() 207 208 def load_license(self): 209 """ Load License of the project """ 210 log.debug('License screen has been opened') 211 windo = License() 212 windo.exec_() 213 214 def load_changelog(self): 215 """ Load the changelog window """ 216 log.debug('Changelog screen has been opened') 217 windo = Changelog() 218 windo.exec_() 219 220 221class License(QDialog): 222 """ License Dialog """ 223 224 ui_path = os.path.join(info.PATH, 'windows', 'ui', 'license.ui') 225 226 def __init__(self): 227 # Create dialog class 228 QDialog.__init__(self) 229 230 # Load UI from designer 231 ui_util.load_ui(self, self.ui_path) 232 233 # Init Ui 234 ui_util.init_ui(self) 235 236 # get translations 237 self.app = get_app() 238 _ = self.app._tr 239 240 # Init license 241 with open(os.path.join(info.RESOURCES_PATH, 'license.txt'), 'r') as my_license: 242 text = my_license.read() 243 self.textBrowser.append(text) 244 245 # Scroll to top 246 cursor = self.textBrowser.textCursor() 247 cursor.setPosition(0) 248 self.textBrowser.setTextCursor(cursor) 249 250 251class Credits(QDialog): 252 """ Credits Dialog """ 253 254 ui_path = os.path.join(info.PATH, 'windows', 'ui', 'credits.ui') 255 256 def Filter_Triggered(self, textbox, treeview): 257 """Callback for filter being changed""" 258 # Update model for treeview 259 treeview.refresh_view(filter=textbox.text()) 260 261 def __init__(self): 262 263 # Create dialog class 264 QDialog.__init__(self) 265 266 # Load UI from designer 267 ui_util.load_ui(self, self.ui_path) 268 269 # Init Ui 270 ui_util.init_ui(self) 271 272 # get translations 273 self.app = get_app() 274 _ = self.app._tr 275 276 # Update supporter button 277 supporter_text = _("Become a Supporter") 278 supporter_html = ''' 279 <html><head/><body> 280 <p align="center"> 281 <a href="https://www.openshot.org/%sdonate/?app-about-us"> 282 <span style="text-decoration: underline; color:#55aaff;">%s</span> 283 </a> 284 </p> 285 </body></html> 286 ''' % (info.website_language(), supporter_text) 287 self.lblBecomeSupporter.setText(supporter_html) 288 289 # Get list of developers 290 developer_list = [] 291 with codecs.open( 292 os.path.join(info.RESOURCES_PATH, 'contributors.json'), 'r', 'utf_8' 293 ) as contributors_file: 294 developer_string = contributors_file.read() 295 developer_list = json.loads(developer_string) 296 297 self.developersListView = CreditsTreeView( 298 credits=developer_list, columns=["email", "website"]) 299 self.vboxDevelopers.addWidget(self.developersListView) 300 self.txtDeveloperFilter.textChanged.connect(partial( 301 self.Filter_Triggered, 302 self.txtDeveloperFilter, 303 self.developersListView)) 304 305 # Get string of translators for the current language 306 translator_credits = [] 307 translator_credits_string = _("translator-credits").replace( 308 "Launchpad Contributions:\n", "" 309 ).replace("translator-credits", "") 310 if translator_credits_string: 311 # Parse string into a list of dictionaries 312 translator_rows = translator_credits_string.split("\n") 313 for row in translator_rows: 314 # Split each row into 2 parts (name and username) 315 translator_parts = row.split("https://launchpad.net/") 316 if len(translator_parts) >= 2: 317 name = translator_parts[0].strip() 318 username = translator_parts[1].strip() 319 translator_credits.append({ 320 "name": name, 321 "website": "https://launchpad.net/%s" % username 322 }) 323 324 # Add translators listview 325 self.translatorsListView = CreditsTreeView( 326 translator_credits, columns=["website"]) 327 self.vboxTranslators.addWidget(self.translatorsListView) 328 self.txtTranslatorFilter.textChanged.connect(partial( 329 self.Filter_Triggered, 330 self.txtTranslatorFilter, 331 self.translatorsListView)) 332 else: 333 # No translations for this language, hide credits 334 self.tabCredits.removeTab(1) 335 336 # Get list of supporters 337 supporter_list = [] 338 with codecs.open( 339 os.path.join(info.RESOURCES_PATH, 'supporters.json'), 'r', 'utf_8' 340 ) as supporter_file: 341 supporter_string = supporter_file.read() 342 supporter_list = json.loads(supporter_string) 343 344 # Add supporters listview 345 self.supportersListView = CreditsTreeView( 346 supporter_list, columns=["website"]) 347 self.vboxSupporters.addWidget(self.supportersListView) 348 self.txtSupporterFilter.textChanged.connect(partial( 349 self.Filter_Triggered, 350 self.txtSupporterFilter, 351 self.supportersListView)) 352 353 354class Changelog(QDialog): 355 """ Changelog Dialog """ 356 357 ui_path = os.path.join(info.PATH, 'windows', 'ui', 'changelog.ui') 358 359 def Filter_Triggered(self, textbox, treeview): 360 """Callback for filter being changed""" 361 # Update model for treeview 362 treeview.refresh_view(filter=textbox.text()) 363 364 def __init__(self): 365 366 # Create dialog class 367 QDialog.__init__(self) 368 369 # Load UI from designer 370 ui_util.load_ui(self, self.ui_path) 371 372 # Init Ui 373 ui_util.init_ui(self) 374 375 # get translations 376 _ = get_app()._tr 377 378 # Connections to objects imported from .ui file 379 tab = { 380 "openshot-qt": self.tab_openshot_qt, 381 "libopenshot": self.tab_libopenshot, 382 "libopenshot-audio": self.tab_libopenshot_audio, 383 } 384 vbox = { 385 "openshot-qt": self.vbox_openshot_qt, 386 "libopenshot": self.vbox_libopenshot, 387 "libopenshot-audio": self.vbox_libopenshot_audio, 388 } 389 filter = { 390 "openshot-qt": self.txtChangeLogFilter_openshot_qt, 391 "libopenshot": self.txtChangeLogFilter_libopenshot, 392 "libopenshot-audio": self.txtChangeLogFilter_libopenshot_audio, 393 } 394 395 # Update github link button 396 github_text = _("OpenShot on GitHub") 397 github_html = ''' 398 <html><head/><body> 399 <p align="center"> 400 <a href="https://github.com/OpenShot/"> 401 <span style=" text-decoration: underline; color:#55aaff;">%s</span> 402 </a> 403 </p> 404 </body></html> 405 ''' % (github_text) 406 self.lblGitHubLink.setText(github_html) 407 408 # Read changelog file for each project 409 for project in ['openshot-qt', 'libopenshot', 'libopenshot-audio']: 410 new_changelog_path = os.path.join(info.PATH, 'resources', '{}.log'.format(project)) 411 old_changelog_path = os.path.join(info.PATH, 'settings', '{}.log'.format(project)) 412 if os.path.exists(new_changelog_path): 413 log.debug("Reading changelog file: {}".format(new_changelog_path)) 414 changelog_list = parse_new_changelog(new_changelog_path) 415 elif os.path.isfile(old_changelog_path): 416 log.debug("Reading legacy changelog file: {}".format(old_changelog_path)) 417 changelog_list = parse_changelog(old_changelog_path) 418 else: 419 changelog_list = None 420 # Hopefully we found ONE of the two 421 if changelog_list is None: 422 log.warn("Could not load changelog for {}".format(project)) 423 # Hide the tab for this changelog 424 tabindex = self.tabChangelog.indexOf(tab[project]) 425 if tabindex >= 0: 426 self.tabChangelog.removeTab(tabindex) 427 continue 428 # Populate listview widget with changelog data 429 cl_treeview = ChangelogTreeView( 430 commits=changelog_list, 431 commit_url="https://github.com/OpenShot/{}/commit/%s/".format(project)) 432 vbox[project].addWidget(cl_treeview) 433 filter[project].textChanged.connect( 434 partial(self.Filter_Triggered, filter[project], cl_treeview)) 435