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 &amp; 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 &copy; %(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