From a5bc66e859ee117fa12ff3153176c912c3a6d1c7 Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:00:32 +0100 Subject: [PATCH 1/9] Implemented new Tribler GUI in PyQt --- TriblerGUI/__init__.py | 3 + TriblerGUI/core_manager.py | 145 + TriblerGUI/debug_window.py | 128 + TriblerGUI/defs.py | 81 + TriblerGUI/dialogs/__init__.py | 3 + TriblerGUI/dialogs/confirmationdialog.py | 76 + TriblerGUI/dialogs/dialogcontainer.py | 33 + TriblerGUI/dialogs/feedbackdialog.py | 107 + TriblerGUI/dialogs/startdownloaddialog.py | 117 + TriblerGUI/event_request_manager.py | 101 + TriblerGUI/images/add.png | Bin 0 -> 896 bytes TriblerGUI/images/buffering_animation.svg | 32 + TriblerGUI/images/debug.png | Bin 0 -> 4243 bytes TriblerGUI/images/default-placeholder.png | Bin 0 -> 12776 bytes TriblerGUI/images/delete.png | Bin 0 -> 1283 bytes TriblerGUI/images/dialog_close.png | Bin 0 -> 436 bytes TriblerGUI/images/discovered.png | Bin 0 -> 2967 bytes TriblerGUI/images/discovering_animation.svg | 1 + TriblerGUI/images/down_arrow.png | Bin 0 -> 942 bytes TriblerGUI/images/downloads.png | Bin 0 -> 1723 bytes TriblerGUI/images/edit.png | Bin 0 -> 1583 bytes TriblerGUI/images/edit_white.png | Bin 0 -> 1507 bytes TriblerGUI/images/full_screen.png | Bin 0 -> 1000 bytes TriblerGUI/images/gear.png | Bin 0 -> 2499 bytes TriblerGUI/images/home.png | Bin 0 -> 1519 bytes TriblerGUI/images/info.png | Bin 0 -> 2219 bytes TriblerGUI/images/loading_animation.svg | 1 + TriblerGUI/images/menu.png | Bin 0 -> 466 bytes TriblerGUI/images/menu_white.png | Bin 0 -> 407 bytes TriblerGUI/images/page_back.png | Bin 0 -> 610 bytes TriblerGUI/images/pause.png | Bin 0 -> 601 bytes TriblerGUI/images/play.png | Bin 0 -> 1565 bytes TriblerGUI/images/playlist_items.png | Bin 0 -> 885 bytes TriblerGUI/images/search.png | Bin 0 -> 2638 bytes TriblerGUI/images/share.png | Bin 0 -> 2412 bytes TriblerGUI/images/stop.png | Bin 0 -> 656 bytes TriblerGUI/images/subscribed.png | Bin 0 -> 1974 bytes TriblerGUI/images/subscribed_not.png | Bin 0 -> 2073 bytes TriblerGUI/images/subscribed_yes.png | Bin 0 -> 1968 bytes TriblerGUI/images/tribler.png | Bin 0 -> 14157 bytes TriblerGUI/images/volume_off.png | Bin 0 -> 1452 bytes TriblerGUI/images/volume_on.png | Bin 0 -> 2005 bytes TriblerGUI/qt_resources/buttonsdialog.ui | 215 + TriblerGUI/qt_resources/channel_list_item.ui | 386 + .../qt_resources/channel_torrent_list_item.ui | 592 ++ TriblerGUI/qt_resources/debugwindow.ui | 345 + TriblerGUI/qt_resources/feedback_dialog.ui | 411 + .../qt_resources/home_recommended_item.ui | 174 + TriblerGUI/qt_resources/loading_list_item.ui | 77 + TriblerGUI/qt_resources/mainwindow.ui | 8953 +++++++++++++++++ TriblerGUI/qt_resources/playlist_list_item.ui | 414 + .../qt_resources/startdownloaddialog.ui | 509 + TriblerGUI/qt_resources/text_list_item.ui | 77 + .../qt_resources/video_file_list_item.ui | 106 + TriblerGUI/qt_resources/video_info_popup.ui | 130 + TriblerGUI/scripts/__init__.py | 3 + TriblerGUI/scripts/start_fake_core.py | 26 + TriblerGUI/single_application.py | 117 + TriblerGUI/tribler_action_menu.py | 30 + TriblerGUI/tribler_app.py | 38 + TriblerGUI/tribler_request_manager.py | 117 + TriblerGUI/tribler_window.py | 558 + TriblerGUI/utilities.py | 200 + TriblerGUI/widgets/__init__.py | 3 + TriblerGUI/widgets/channel_list_item.py | 21 + .../widgets/channel_torrent_list_item.py | 171 + TriblerGUI/widgets/channelpage.py | 87 + TriblerGUI/widgets/circlebutton.py | 8 + TriblerGUI/widgets/createtorrentpage.py | 114 + TriblerGUI/widgets/discoveredpage.py | 46 + TriblerGUI/widgets/discoveringpage.py | 40 + TriblerGUI/widgets/downloadprogressbar.py | 60 + .../widgets/downloadsdetailstabwidget.py | 164 + TriblerGUI/widgets/downloadspage.py | 297 + TriblerGUI/widgets/downloadwidgetitem.py | 79 + TriblerGUI/widgets/editchannelpage.py | 549 + TriblerGUI/widgets/ellipsebutton.py | 8 + TriblerGUI/widgets/home_recommended_item.py | 129 + TriblerGUI/widgets/homepage.py | 89 + TriblerGUI/widgets/lazyloadlist.py | 81 + TriblerGUI/widgets/leftmenuplaylist.py | 48 + TriblerGUI/widgets/loading_list_item.py | 17 + TriblerGUI/widgets/loadingpage.py | 28 + TriblerGUI/widgets/manageplaylistpage.py | 150 + TriblerGUI/widgets/playlist_list_item.py | 45 + TriblerGUI/widgets/playlistpage.py | 26 + TriblerGUI/widgets/searchresultspage.py | 91 + TriblerGUI/widgets/settingspage.py | 189 + TriblerGUI/widgets/subscribedchannelspage.py | 64 + TriblerGUI/widgets/subscriptionswidget.py | 65 + TriblerGUI/widgets/tabbuttonpanel.py | 31 + TriblerGUI/widgets/text_list_item.py | 18 + TriblerGUI/widgets/thumbnailwidget.py | 23 + TriblerGUI/widgets/underlinetabbutton.py | 16 + TriblerGUI/widgets/videoplayerinfobutton.py | 20 + TriblerGUI/widgets/videoplayerinfopopup.py | 28 + TriblerGUI/widgets/videoplayerpage.py | 202 + .../widgets/videoplayerpositionslider.py | 24 + .../widgets/videoplayervolumecontainer.py | 18 + TriblerGUI/widgets/videoplayerwidget.py | 27 + run_tribler.py | 25 + tribler.sh | 8 +- 102 files changed, 17411 insertions(+), 4 deletions(-) create mode 100644 TriblerGUI/__init__.py create mode 100644 TriblerGUI/core_manager.py create mode 100644 TriblerGUI/debug_window.py create mode 100644 TriblerGUI/defs.py create mode 100644 TriblerGUI/dialogs/__init__.py create mode 100644 TriblerGUI/dialogs/confirmationdialog.py create mode 100644 TriblerGUI/dialogs/dialogcontainer.py create mode 100644 TriblerGUI/dialogs/feedbackdialog.py create mode 100644 TriblerGUI/dialogs/startdownloaddialog.py create mode 100644 TriblerGUI/event_request_manager.py create mode 100644 TriblerGUI/images/add.png create mode 100644 TriblerGUI/images/buffering_animation.svg create mode 100644 TriblerGUI/images/debug.png create mode 100644 TriblerGUI/images/default-placeholder.png create mode 100644 TriblerGUI/images/delete.png create mode 100644 TriblerGUI/images/dialog_close.png create mode 100644 TriblerGUI/images/discovered.png create mode 100644 TriblerGUI/images/discovering_animation.svg create mode 100644 TriblerGUI/images/down_arrow.png create mode 100644 TriblerGUI/images/downloads.png create mode 100644 TriblerGUI/images/edit.png create mode 100644 TriblerGUI/images/edit_white.png create mode 100644 TriblerGUI/images/full_screen.png create mode 100644 TriblerGUI/images/gear.png create mode 100644 TriblerGUI/images/home.png create mode 100644 TriblerGUI/images/info.png create mode 100644 TriblerGUI/images/loading_animation.svg create mode 100644 TriblerGUI/images/menu.png create mode 100644 TriblerGUI/images/menu_white.png create mode 100644 TriblerGUI/images/page_back.png create mode 100644 TriblerGUI/images/pause.png create mode 100644 TriblerGUI/images/play.png create mode 100644 TriblerGUI/images/playlist_items.png create mode 100644 TriblerGUI/images/search.png create mode 100644 TriblerGUI/images/share.png create mode 100644 TriblerGUI/images/stop.png create mode 100644 TriblerGUI/images/subscribed.png create mode 100644 TriblerGUI/images/subscribed_not.png create mode 100644 TriblerGUI/images/subscribed_yes.png create mode 100644 TriblerGUI/images/tribler.png create mode 100644 TriblerGUI/images/volume_off.png create mode 100644 TriblerGUI/images/volume_on.png create mode 100644 TriblerGUI/qt_resources/buttonsdialog.ui create mode 100644 TriblerGUI/qt_resources/channel_list_item.ui create mode 100644 TriblerGUI/qt_resources/channel_torrent_list_item.ui create mode 100644 TriblerGUI/qt_resources/debugwindow.ui create mode 100644 TriblerGUI/qt_resources/feedback_dialog.ui create mode 100644 TriblerGUI/qt_resources/home_recommended_item.ui create mode 100644 TriblerGUI/qt_resources/loading_list_item.ui create mode 100644 TriblerGUI/qt_resources/mainwindow.ui create mode 100644 TriblerGUI/qt_resources/playlist_list_item.ui create mode 100644 TriblerGUI/qt_resources/startdownloaddialog.ui create mode 100644 TriblerGUI/qt_resources/text_list_item.ui create mode 100644 TriblerGUI/qt_resources/video_file_list_item.ui create mode 100644 TriblerGUI/qt_resources/video_info_popup.ui create mode 100644 TriblerGUI/scripts/__init__.py create mode 100644 TriblerGUI/scripts/start_fake_core.py create mode 100644 TriblerGUI/single_application.py create mode 100644 TriblerGUI/tribler_action_menu.py create mode 100644 TriblerGUI/tribler_app.py create mode 100644 TriblerGUI/tribler_request_manager.py create mode 100644 TriblerGUI/tribler_window.py create mode 100644 TriblerGUI/utilities.py create mode 100644 TriblerGUI/widgets/__init__.py create mode 100644 TriblerGUI/widgets/channel_list_item.py create mode 100644 TriblerGUI/widgets/channel_torrent_list_item.py create mode 100644 TriblerGUI/widgets/channelpage.py create mode 100644 TriblerGUI/widgets/circlebutton.py create mode 100644 TriblerGUI/widgets/createtorrentpage.py create mode 100644 TriblerGUI/widgets/discoveredpage.py create mode 100644 TriblerGUI/widgets/discoveringpage.py create mode 100644 TriblerGUI/widgets/downloadprogressbar.py create mode 100644 TriblerGUI/widgets/downloadsdetailstabwidget.py create mode 100644 TriblerGUI/widgets/downloadspage.py create mode 100644 TriblerGUI/widgets/downloadwidgetitem.py create mode 100644 TriblerGUI/widgets/editchannelpage.py create mode 100644 TriblerGUI/widgets/ellipsebutton.py create mode 100644 TriblerGUI/widgets/home_recommended_item.py create mode 100644 TriblerGUI/widgets/homepage.py create mode 100644 TriblerGUI/widgets/lazyloadlist.py create mode 100644 TriblerGUI/widgets/leftmenuplaylist.py create mode 100644 TriblerGUI/widgets/loading_list_item.py create mode 100644 TriblerGUI/widgets/loadingpage.py create mode 100644 TriblerGUI/widgets/manageplaylistpage.py create mode 100644 TriblerGUI/widgets/playlist_list_item.py create mode 100644 TriblerGUI/widgets/playlistpage.py create mode 100644 TriblerGUI/widgets/searchresultspage.py create mode 100644 TriblerGUI/widgets/settingspage.py create mode 100644 TriblerGUI/widgets/subscribedchannelspage.py create mode 100644 TriblerGUI/widgets/subscriptionswidget.py create mode 100644 TriblerGUI/widgets/tabbuttonpanel.py create mode 100644 TriblerGUI/widgets/text_list_item.py create mode 100644 TriblerGUI/widgets/thumbnailwidget.py create mode 100644 TriblerGUI/widgets/underlinetabbutton.py create mode 100644 TriblerGUI/widgets/videoplayerinfobutton.py create mode 100644 TriblerGUI/widgets/videoplayerinfopopup.py create mode 100644 TriblerGUI/widgets/videoplayerpage.py create mode 100644 TriblerGUI/widgets/videoplayerpositionslider.py create mode 100644 TriblerGUI/widgets/videoplayervolumecontainer.py create mode 100644 TriblerGUI/widgets/videoplayerwidget.py create mode 100644 run_tribler.py diff --git a/TriblerGUI/__init__.py b/TriblerGUI/__init__.py new file mode 100644 index 00000000000..2f4c4ab6f5c --- /dev/null +++ b/TriblerGUI/__init__.py @@ -0,0 +1,3 @@ +""" +This package contains the code for the GUI, written in pyQt. +""" diff --git a/TriblerGUI/core_manager.py b/TriblerGUI/core_manager.py new file mode 100644 index 00000000000..d96334f475e --- /dev/null +++ b/TriblerGUI/core_manager.py @@ -0,0 +1,145 @@ +import logging +from twisted.internet.error import ReactorAlreadyInstalledError + +# We always use a selectreactor +try: + from twisted.internet import selectreactor + selectreactor.install() +except ReactorAlreadyInstalledError: + pass + +import multiprocessing +import os +import sys +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QApplication +import sqlite3 +import signal +from Tribler.Core.Modules.process_checker import ProcessChecker +from Tribler.Core.Session import Session +from Tribler.Core.SessionConfig import SessionStartupConfig + +from TriblerGUI.event_request_manager import EventRequestManager +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_base_path, is_frozen + +START_FAKE_API = False + + +def start_tribler_core(base_path): + """ + This method is invoked by multiprocessing when the Tribler core is started and will start a Tribler session. + Note that there is no direct communication between the GUI process and the core: all communication is performed + through the HTTP API. + """ + from twisted.internet import reactor + + def on_tribler_shutdown(_): + reactor.stop() + + def shutdown(session, *_): + logging.info("Stopping Tribler core") + session.shutdown().addCallback(on_tribler_shutdown) + + sys.path.insert(0, base_path) + + def start_tribler(): + config = SessionStartupConfig().load() + config.set_http_api_port(8085) + config.set_http_api_enabled(True) + + # Check if we are already running a Tribler instance + process_checker = ProcessChecker() + if process_checker.already_running: + return + + session = Session(config) + upgrader = session.prestart() + if upgrader.failed: + pass + + signal.signal(signal.SIGTERM, lambda signum, stack: shutdown(session, signum, stack)) + session.start() + + reactor.callWhenRunning(start_tribler) + reactor.run() + + +class CoreManager(object): + """ + The CoreManager is responsible for managing the Tribler core (starting/stopping). When we are running the GUI tests, + a fake API will be started. + """ + + def __init__(self, api_port): + self.base_path = get_base_path() + if not is_frozen(): + self.base_path = os.path.join(get_base_path(), "..") + + self.api_port = api_port + + self.request_mgr = None + self.core_process = None + self.events_manager = EventRequestManager(api_port) + + self.shutting_down = False + self.recorded_stderr = "" + self.use_existing_core = True + + self.stop_timer = QTimer() + self.stop_timer.timeout.connect(self.check_stopped) + + def check_stopped(self): + if not self.core_process.is_alive(): + self.stop_timer.stop() + self.on_finished() + + def start(self): + """ + First test whether we already have a Tribler process listening on port 8085. If so, use that one and don't + start a new, fresh session. + """ + def on_request_error(_): + self.use_existing_core = False + self.start_tribler_core() + + self.events_manager.connect(reschedule_on_err=False) + self.events_manager.reply.error.connect(on_request_error) + + def start_tribler_core(self): + if START_FAKE_API: + from TriblerGUI.scripts.start_fake_core import start_fake_core + self.core_process = multiprocessing.Process(target=start_fake_core, args=(self.api_port,)) + else: + # Workaround for MacOS + sqlite3.connect(':memory:').close() + + self.core_process = multiprocessing.Process(target=start_tribler_core, args=(self.base_path,)) + self.core_process.start() + self.check_core_ready() + + def check_core_ready(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("state", self.on_received_state, capture_errors=False) + + def on_received_state(self, state): + if not state: + self.check_core_ready() + elif state['state'] == 'STARTED': + self.events_manager.connect(reschedule_on_err=False) + elif state['state'] == 'ERROR': + raise RuntimeError(state['last_exception']) + else: + self.check_core_ready() + + def stop(self): + if self.core_process: + self.core_process.terminate() + self.stop_timer.start() + + def throw_core_exception(self): + raise RuntimeError(self.recorded_stderr) + + def on_finished(self): + if self.shutting_down: + QApplication.quit() diff --git a/TriblerGUI/debug_window.py b/TriblerGUI/debug_window.py new file mode 100644 index 00000000000..6c9f23f0efb --- /dev/null +++ b/TriblerGUI/debug_window.py @@ -0,0 +1,128 @@ +import json +from time import localtime, strftime + +from PyQt5 import uic +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow, QTreeWidgetItem + +from TriblerGUI.utilities import get_ui_file_path, format_size +from TriblerGUI.tribler_request_manager import performed_requests as tribler_performed_requests, TriblerRequestManager +from TriblerGUI.event_request_manager import received_events as tribler_received_events + + +class DebugWindow(QMainWindow): + """ + The debug window shows various statistics about Tribler such as performed requests, Dispersy statistics and + community information. + """ + + def __init__(self): + QMainWindow.__init__(self) + + self.request_mgr = None + + uic.loadUi(get_ui_file_path('debugwindow.ui'), self) + self.setWindowTitle("Tribler debug pane") + + self.window().debug_tab_widget.setCurrentIndex(0) + self.window().dispersy_tab_widget.setCurrentIndex(0) + self.window().debug_tab_widget.currentChanged.connect(self.tab_changed) + self.window().dispersy_tab_widget.currentChanged.connect(self.dispersy_tab_changed) + self.window().events_tree_widget.itemClicked.connect(self.on_event_clicked) + self.load_general_tab() + + def tab_changed(self, index): + if index == 0: + self.load_general_tab() + elif index == 1: + self.load_requests_tab() + elif index == 2: + self.load_multichain_tab() + elif index == 3: + self.dispersy_tab_changed(self.window().dispersy_tab_widget.currentIndex()) + elif index == 4: + self.load_events_tab() + + def dispersy_tab_changed(self, index): + if index == 0: + self.load_dispersy_general_tab() + elif index == 1: + self.load_dispersy_communities_tab() + + def create_and_add_widget_item(self, key, value, widget): + item = QTreeWidgetItem(widget) + item.setText(0, key) + item.setText(1, "%s" % value) + widget.addTopLevelItem(item) + + def load_general_tab(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("statistics/tribler", self.on_tribler_statistics) + + def on_tribler_statistics(self, data): + data = data["tribler_statistics"] + self.window().general_tree_widget.clear() + self.create_and_add_widget_item("Number of channels", data["num_channels"], self.window().general_tree_widget) + self.create_and_add_widget_item("Database size", format_size(data["database_size"]), + self.window().general_tree_widget) + self.create_and_add_widget_item("Number of collected torrents", data["torrents"]["num_collected"], + self.window().general_tree_widget) + self.create_and_add_widget_item("Number of torrent files", data["torrents"]["num_files"], + self.window().general_tree_widget) + self.create_and_add_widget_item("Total size of torrent files", format_size(data["torrents"]["total_size"]), + self.window().general_tree_widget) + + def load_requests_tab(self): + self.window().requests_tree_widget.clear() + for endpoint, method, data, timestamp, status_code in sorted(tribler_performed_requests.values(), + key=lambda x: x[3]): + item = QTreeWidgetItem(self.window().requests_tree_widget) + item.setText(0, "%s %s %s" % (method, endpoint, data)) + item.setText(1, ("%d" % status_code) if status_code else "unknown") + item.setText(2, "%s" % strftime("%H:%M:%S", localtime(timestamp))) + self.window().requests_tree_widget.addTopLevelItem(item) + + def load_multichain_tab(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("multichain/statistics", self.on_multichain_statistics) + + def on_multichain_statistics(self, data): + self.window().multichain_tree_widget.clear() + for key, value in data["statistics"].iteritems(): + self.create_and_add_widget_item(key, value, self.window().multichain_tree_widget) + + def load_dispersy_general_tab(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("statistics/dispersy", self.on_dispersy_general_stats) + + def on_dispersy_general_stats(self, data): + self.window().dispersy_general_tree_widget.clear() + for key, value in data["dispersy_statistics"].iteritems(): + self.create_and_add_widget_item(key, value, self.window().dispersy_general_tree_widget) + + def load_dispersy_communities_tab(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("statistics/communities", self.on_dispersy_community_stats) + + def on_dispersy_community_stats(self, data): + self.window().communities_tree_widget.clear() + for community in data["community_statistics"]: + item = QTreeWidgetItem(self.window().communities_tree_widget) + item.setText(0, community["classification"]) + item.setText(1, community["identifier"][:6]) + item.setText(2, community["member"][:6]) + item.setText(3, "%s" % community["candidates"]) + self.window().communities_tree_widget.addTopLevelItem(item) + + def on_event_clicked(self, item): + event_dict = item.data(0, Qt.UserRole) + self.window().event_text_box.setPlainText(json.dumps(event_dict)) + + def load_events_tab(self): + self.window().events_tree_widget.clear() + for event_dict, timestamp in tribler_received_events: + item = QTreeWidgetItem(self.window().events_tree_widget) + item.setData(0, Qt.UserRole, event_dict) + item.setText(0, "%s" % event_dict['type']) + item.setText(1, "%s" % strftime("%H:%M:%S", localtime(timestamp))) + self.window().events_tree_widget.addTopLevelItem(item) diff --git a/TriblerGUI/defs.py b/TriblerGUI/defs.py new file mode 100644 index 00000000000..41fa9952500 --- /dev/null +++ b/TriblerGUI/defs.py @@ -0,0 +1,81 @@ +""" +This file contains various definitions used by the Tribler GUI. +""" + +# Define stacked widget page indices +PAGE_HOME = 0 +PAGE_EDIT_CHANNEL = 1 +PAGE_SEARCH_RESULTS = 2 +PAGE_CHANNEL_DETAILS = 3 +PAGE_SETTINGS = 4 +PAGE_VIDEO_PLAYER = 5 +PAGE_SUBSCRIBED_CHANNELS = 6 +PAGE_DOWNLOADS = 7 +PAGE_PLAYLIST_DETAILS = 8 +PAGE_LOADING = 9 +PAGE_DISCOVERING = 10 +PAGE_DISCOVERED = 11 + +PAGE_CHANNEL_CONTENT = 0 +PAGE_CHANNEL_COMMENTS = 1 +PAGE_CHANNEL_ACTIVITY = 2 + +PAGE_EDIT_CHANNEL_OVERVIEW = 0 +PAGE_EDIT_CHANNEL_SETTINGS = 1 +PAGE_EDIT_CHANNEL_TORRENTS = 2 +PAGE_EDIT_CHANNEL_PLAYLISTS = 3 +PAGE_EDIT_CHANNEL_RSS_FEEDS = 4 +PAGE_EDIT_CHANNEL_PLAYLIST_EDIT = 5 +PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS = 6 +PAGE_EDIT_CHANNEL_PLAYLIST_MANAGE = 7 +PAGE_EDIT_CHANNEL_CREATE_TORRENT = 8 + +PAGE_SETTINGS_GENERAL = 0 +PAGE_SETTINGS_CONNECTION = 1 +PAGE_SETTINGS_BANDWIDTH = 2 +PAGE_SETTINGS_SEEDING = 3 +PAGE_SETTINGS_ANONYMITY = 4 + +# Definition of the download statuses and the corresponding strings +DLSTATUS_ALLOCATING_DISKSPACE = 0 +DLSTATUS_WAITING4HASHCHECK = 1 +DLSTATUS_HASHCHECKING = 2 +DLSTATUS_DOWNLOADING = 3 +DLSTATUS_SEEDING = 4 +DLSTATUS_STOPPED = 5 +DLSTATUS_STOPPED_ON_ERROR = 6 +DLSTATUS_METADATA = 7 +DLSTATUS_CIRCUITS = 8 + +DLSTATUS_STRINGS = ["Allocating disk space", "Waiting for check", "Checking", "Downloading", "Seeding", "Stopped", + "Stopped on error", "Waiting for metadata", "Building circuits"] + +# Definitions of the download filters. For each filter, it is specified which download statuses can be displayed. +DOWNLOADS_FILTER_ALL = 0 +DOWNLOADS_FILTER_DOWNLOADING = 1 +DOWNLOADS_FILTER_COMPLETED = 2 +DOWNLOADS_FILTER_ACTIVE = 3 +DOWNLOADS_FILTER_INACTIVE = 4 + +DOWNLOADS_FILTER_DEFINITION = { + DOWNLOADS_FILTER_ALL: [DLSTATUS_ALLOCATING_DISKSPACE, DLSTATUS_WAITING4HASHCHECK, DLSTATUS_HASHCHECKING, + DLSTATUS_DOWNLOADING, DLSTATUS_SEEDING, DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR, + DLSTATUS_METADATA, DLSTATUS_CIRCUITS], + DOWNLOADS_FILTER_DOWNLOADING: [DLSTATUS_DOWNLOADING], + DOWNLOADS_FILTER_COMPLETED: [DLSTATUS_SEEDING], + DOWNLOADS_FILTER_ACTIVE: [DLSTATUS_ALLOCATING_DISKSPACE, DLSTATUS_WAITING4HASHCHECK, DLSTATUS_HASHCHECKING, + DLSTATUS_DOWNLOADING, DLSTATUS_SEEDING, DLSTATUS_METADATA, DLSTATUS_CIRCUITS], + DOWNLOADS_FILTER_INACTIVE: [DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR] +} + +BUTTON_TYPE_NORMAL = 0 +BUTTON_TYPE_CONFIRM = 1 + +VIDEO_EXTS = ['aac', 'asf', 'avi', 'dv', 'divx', 'flac', 'flc', 'flv', 'mkv', 'mpeg', 'mpeg4', 'mpegts', + 'mpg4', 'mp3', 'mp4', 'mpg', 'mkv', 'mov', 'm4v', 'ogg', 'ogm', 'ogv', 'oga', 'ogx', 'qt', + 'rm', 'swf', 'ts', 'vob', 'wmv', 'wav', 'webm'] + +# Torrent health status +STATUS_GOOD = 0 +STATUS_UNKNOWN = 1 +STATUS_DEAD = 2 diff --git a/TriblerGUI/dialogs/__init__.py b/TriblerGUI/dialogs/__init__.py new file mode 100644 index 00000000000..297d719aefe --- /dev/null +++ b/TriblerGUI/dialogs/__init__.py @@ -0,0 +1,3 @@ +""" +This package contains dialogs and overlay windows that are used in Tribler. +""" diff --git a/TriblerGUI/dialogs/confirmationdialog.py b/TriblerGUI/dialogs/confirmationdialog.py new file mode 100644 index 00000000000..9993cd26d68 --- /dev/null +++ b/TriblerGUI/dialogs/confirmationdialog.py @@ -0,0 +1,76 @@ +from PyQt5 import uic +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QSizePolicy, QSpacerItem +from TriblerGUI.defs import BUTTON_TYPE_NORMAL +from TriblerGUI.dialogs.dialogcontainer import DialogContainer +from TriblerGUI.utilities import get_ui_file_path +from TriblerGUI.widgets.ellipsebutton import EllipseButton + + +class ConfirmationDialog(DialogContainer): + + button_clicked = pyqtSignal(int) + + def __init__(self, parent, title, main_text, buttons, show_input=False): + DialogContainer.__init__(self, parent) + + uic.loadUi(get_ui_file_path('buttonsdialog.ui'), self.dialog_widget) + + self.dialog_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + + self.dialog_widget.dialog_title_label.setText(title) + + self.dialog_widget.dialog_main_text_label.setText(main_text) + self.dialog_widget.dialog_main_text_label.adjustSize() + + if not show_input: + self.dialog_widget.dialog_input.setHidden(True) + + hspacer_left = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Fixed) + self.dialog_widget.dialog_button_container.layout().addSpacerItem(hspacer_left) + + self.buttons = [] + for index in range(len(buttons)): + self.create_button(index, *buttons[index]) + + hspacer_right = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Fixed) + self.dialog_widget.dialog_button_container.layout().addSpacerItem(hspacer_right) + + self.window().escape_pressed.connect(self.close_dialog) + self.on_main_window_resize() + + @classmethod + def show_error(cls, window, title, error_text): + error_dialog = ConfirmationDialog(window, title, error_text, [('close', BUTTON_TYPE_NORMAL)]) + + def on_close(): + error_dialog.setParent(None) + + error_dialog.button_clicked.connect(on_close) + error_dialog.show() + + def create_button(self, index, button_text, _): + button = EllipseButton(self.dialog_widget) + button.setText(button_text) + button.setFixedHeight(26) + button.setCursor(QCursor(Qt.PointingHandCursor)) + self.buttons.append(button) + + button.setStyleSheet(""" + EllipseButton { + border: 1px solid #B5B5B5; + border-radius: 13px; + color: white; + padding-left: 4px; + padding-right: 4px; + } + + EllipseButton::hover { + border: 1px solid white; + color: white; + } + """) + + self.dialog_widget.dialog_button_container.layout().addWidget(button) + button.clicked.connect(lambda: self.button_clicked.emit(index)) diff --git a/TriblerGUI/dialogs/dialogcontainer.py b/TriblerGUI/dialogs/dialogcontainer.py new file mode 100644 index 00000000000..357e021334d --- /dev/null +++ b/TriblerGUI/dialogs/dialogcontainer.py @@ -0,0 +1,33 @@ +from PyQt5.QtCore import QPoint +from PyQt5.QtGui import QPainter +from PyQt5.QtWidgets import QWidget, QStyleOption, QStyle + + +class DialogContainer(QWidget): + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.setStyleSheet("background-color: rgba(30, 30, 30, 0.75);") + + self.dialog_widget = QWidget(self) + + self.window().resize_event.connect(self.on_main_window_resize) + + def paintEvent(self, _): + opt = QStyleOption() + opt.initFrom(self) + painter = QPainter(self) + self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) + + def close_dialog(self): + self.setParent(None) + + def on_main_window_resize(self): + if not self.parentWidget(): + return + + self.setFixedSize(self.parentWidget().size()) + self.dialog_widget.setFixedWidth(self.width() - 100) + self.dialog_widget.move(QPoint(self.geometry().center().x() - self.dialog_widget.geometry().width() / 2, + self.geometry().center().y() - self.dialog_widget.geometry().height() / 2)) diff --git a/TriblerGUI/dialogs/feedbackdialog.py b/TriblerGUI/dialogs/feedbackdialog.py new file mode 100644 index 00000000000..e59d8b6865c --- /dev/null +++ b/TriblerGUI/dialogs/feedbackdialog.py @@ -0,0 +1,107 @@ +import os +from urllib import quote_plus +from PyQt5 import uic +from PyQt5.QtCore import QCoreApplication +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QAction +import sys +import platform +import time +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_ui_file_path + + +class FeedbackDialog(QDialog): + + def __init__(self, parent, exception_text, tribler_version): + QDialog.__init__(self, parent) + + uic.loadUi(get_ui_file_path('feedback_dialog.ui'), self) + + self.setWindowTitle("Unexpected error") + self.selected_item_index = 0 + self.tribler_version = tribler_version + self.request_mgr = None + + # Qt 5.2 does not have the setPlaceholderText property + if hasattr(self.comments_text_edit, "setPlaceholderText"): + self.comments_text_edit.setPlaceholderText("Comments (optional)") + + def add_item_to_info_widget(key, value): + item = QTreeWidgetItem(self.env_variables_list) + item.setText(0, key) + item.setText(1, value) + + self.error_text_edit.setPlainText(exception_text.rstrip()) + + self.cancel_button.clicked.connect(self.on_cancel_clicked) + self.send_report_button.clicked.connect(self.on_send_clicked) + + # Add machine information to the tree widget + add_item_to_info_widget('os.getcwd', '%s' % os.getcwd()) + add_item_to_info_widget('sys.executable', '%s' % sys.executable) + + add_item_to_info_widget('os', os.name) + add_item_to_info_widget('platform', sys.platform) + add_item_to_info_widget('platform.details', platform.platform()) + add_item_to_info_widget('platform.machine', platform.machine()) + add_item_to_info_widget('python.version', sys.version) + add_item_to_info_widget('indebug', str(__debug__)) + + for argv in sys.argv: + add_item_to_info_widget('sys.argv', '%s' % argv) + + for path in sys.path: + add_item_to_info_widget('sys.path', '%s' % path) + + for key in os.environ.keys(): + add_item_to_info_widget('os.environ', '%s: %s' % (key, os.environ[key])) + + # Users can remove specific lines in the report + self.env_variables_list.customContextMenuRequested.connect(self.on_right_click_item) + + def on_remove_entry(self): + self.env_variables_list.takeTopLevelItem(self.selected_item_index) + + def on_right_click_item(self, pos): + selected_item = self.env_variables_list.selectedItems()[0] + self.selected_item_index = self.env_variables_list.indexOfTopLevelItem(selected_item) + + menu = TriblerActionMenu(self) + + remove_action = QAction('Remove entry', self) + remove_action.triggered.connect(self.on_remove_entry) + menu.addAction(remove_action) + menu.exec_(self.env_variables_list.mapToGlobal(pos)) + + def on_cancel_clicked(self): + QCoreApplication.instance().quit() + + def on_report_sent(self, _): + QCoreApplication.instance().quit() + + def on_send_clicked(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.base_url = 'http://reporter.tribler.org/' + + sys_info = "" + for ind in xrange(self.env_variables_list.topLevelItemCount()): + item = self.env_variables_list.topLevelItem(ind) + sys_info += "%s\t%s\n" % (quote_plus(item.text(0)), quote_plus(item.text(1))) + + comments = self.comments_text_edit.toPlainText() + if len(comments) == 0: + comments = "Not provided" + comments = quote_plus(comments) + + stack = quote_plus(self.error_text_edit.toPlainText()) + + post_data = "version=%s&machine=%s&os=%s×tamp=%s&sysinfo=%s&comments=%s&stack=%s" % \ + (self.tribler_version, platform.machine(), platform.platform(), + int(time.time()), sys_info, comments, stack) + + self.request_mgr.perform_request("report", self.on_report_sent, data=str(post_data), method='POST') + + def closeEvent(self, close_event): + QCoreApplication.instance().quit() + close_event.ignore() diff --git a/TriblerGUI/dialogs/startdownloaddialog.py b/TriblerGUI/dialogs/startdownloaddialog.py new file mode 100644 index 00000000000..bb4e0a5f5f7 --- /dev/null +++ b/TriblerGUI/dialogs/startdownloaddialog.py @@ -0,0 +1,117 @@ +from PyQt5 import uic +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtWidgets import QSizePolicy, QFileDialog, QTreeWidgetItem +from TriblerGUI.dialogs.dialogcontainer import DialogContainer +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_ui_file_path, format_size, get_gui_setting + + +class DownloadFileTreeWidgetItem(QTreeWidgetItem): + + def __init__(self, parent): + QTreeWidgetItem.__init__(self, parent) + + def get_num_checked(self): + total_checked = 0 + for ind in xrange(self.treeWidget().topLevelItemCount()): + item = self.treeWidget().topLevelItem(ind) + if item.checkState(2) == Qt.Checked: + total_checked += 1 + return total_checked + + def setData(self, index, role, value): + if index == 2 and self.get_num_checked() == 1 and role == Qt.CheckStateRole and value == Qt.Unchecked: + return + + QTreeWidgetItem.setData(self, index, role, value) + + +class StartDownloadDialog(DialogContainer): + + button_clicked = pyqtSignal(int) + received_metainfo = pyqtSignal(dict) + + def __init__(self, parent, download_uri, torrent_name): + DialogContainer.__init__(self, parent) + + self.download_uri = download_uri + gui_settings = self.window().gui_settings + + uic.loadUi(get_ui_file_path('startdownloaddialog.ui'), self.dialog_widget) + + self.dialog_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + + self.dialog_widget.browse_dir_button.clicked.connect(self.on_browse_dir_clicked) + self.dialog_widget.cancel_button.clicked.connect(lambda: self.button_clicked.emit(0)) + self.dialog_widget.download_button.clicked.connect(lambda: self.button_clicked.emit(1)) + + if self.window().tribler_settings: + self.dialog_widget.destination_input.setText(self.window().tribler_settings['downloadconfig']['saveas']) + + self.dialog_widget.torrent_name_label.setText(torrent_name) + + self.dialog_widget.safe_seed_checkbox.setChecked(get_gui_setting(gui_settings, "default_safeseeding_enabled", + True, is_bool=True)) + self.dialog_widget.anon_download_checkbox.setChecked(get_gui_setting(gui_settings, "default_anonymity_enabled", + True, is_bool=True)) + + self.dialog_widget.safe_seed_checkbox.setEnabled(self.dialog_widget.anon_download_checkbox.isChecked()) + self.dialog_widget.anon_download_checkbox.stateChanged.connect(self.on_anon_download_state_changed) + + self.perform_files_request() + self.dialog_widget.files_list_view.setHidden(True) + self.dialog_widget.adjustSize() + + self.on_main_window_resize() + + def get_selected_files(self): + included_files = [] + for ind in xrange(self.dialog_widget.files_list_view.topLevelItemCount()): + item = self.dialog_widget.files_list_view.topLevelItem(ind) + if item.checkState(2) == Qt.Checked: + included_files.append(u'/'.join(item.data(0, Qt.UserRole)['path'])) + + return included_files + + def perform_files_request(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("torrentinfo?uri=%s" % self.download_uri, self.on_received_metainfo, + capture_errors=False) + + def on_received_metainfo(self, metainfo): + if 'error' in metainfo: + self.dialog_widget.loading_files_label.setText("Timeout when trying to fetch files.") + return + + metainfo = metainfo['metainfo'] + if 'files' in metainfo['info']: # Multi-file torrent + files = metainfo['info']['files'] + else: + files = [{'path': [metainfo['info']['name']], 'length': metainfo['info']['length']}] + + for filename in files: + item = DownloadFileTreeWidgetItem(self.dialog_widget.files_list_view) + item.setText(0, '/'.join(filename['path'])) + item.setText(1, format_size(float(filename['length']))) + item.setData(0, Qt.UserRole, filename) + item.setCheckState(2, Qt.Checked) + self.dialog_widget.files_list_view.addTopLevelItem(item) + + self.dialog_widget.loading_files_label.setHidden(True) + self.dialog_widget.files_list_view.setHidden(False) + self.dialog_widget.adjustSize() + self.on_main_window_resize() + + self.received_metainfo.emit(metainfo) + + def on_browse_dir_clicked(self): + chosen_dir = QFileDialog.getExistingDirectory(self, "Please select the destination directory of your download", + "", QFileDialog.ShowDirsOnly) + + if len(chosen_dir) != 0: + self.dialog_widget.destination_input.setText(chosen_dir) + + def on_anon_download_state_changed(self, _): + if self.dialog_widget.anon_download_checkbox.isChecked(): + self.dialog_widget.safe_seed_checkbox.setChecked(True) + self.dialog_widget.safe_seed_checkbox.setEnabled(not self.dialog_widget.anon_download_checkbox.isChecked()) diff --git a/TriblerGUI/event_request_manager.py b/TriblerGUI/event_request_manager.py new file mode 100644 index 00000000000..ffb6be06985 --- /dev/null +++ b/TriblerGUI/event_request_manager.py @@ -0,0 +1,101 @@ +import json +import logging +from PyQt5.QtCore import QUrl, pyqtSignal, QTimer +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +import time + + +received_events = [] + + +class EventRequestManager(QNetworkAccessManager): + """ + The EventRequestManager class handles the events connection over which important events in Tribler are pushed. + """ + + received_search_result_channel = pyqtSignal(object) + received_search_result_torrent = pyqtSignal(object) + tribler_started = pyqtSignal() + upgrader_tick = pyqtSignal(str) + new_version_available = pyqtSignal(str) + discovered_channel = pyqtSignal(object) + discovered_torrent = pyqtSignal(object) + torrent_finished = pyqtSignal(object) + + def __init__(self, api_port): + QNetworkAccessManager.__init__(self) + url = QUrl("http://localhost:%d/events" % api_port) + self.request = QNetworkRequest(url) + self.failed_attempts = 0 + self.connect_timer = QTimer() + self.current_event_string = "" + self.tribler_version = "Unknown" + self.reply = None + self._logger = logging.getLogger('TriblerGUI') + + def on_error(self, error, reschedule_on_err): + self._logger.error("Got Tribler core error: %s" % error) + if error == QNetworkReply.ConnectionRefusedError: + if self.failed_attempts == 40: + raise RuntimeError("Could not connect with the Tribler Core within 20 seconds") + + self.failed_attempts += 1 + + if reschedule_on_err: + # Reschedule an attempt + self.connect_timer = QTimer() + self.connect_timer.timeout.connect(self.connect) + self.connect_timer.start(500) + + def on_read_data(self): + self.connect_timer.stop() + data = self.reply.readAll() + self.current_event_string += data + if len(self.current_event_string) > 0 and self.current_event_string[-1] == '\n': + for event in self.current_event_string.split('\n'): + if len(event) == 0: + continue + json_dict = json.loads(str(event)) + + received_events.insert(0, (json_dict, time.time())) + if len(received_events) > 100: # Only buffer the last 100 events + received_events.pop() + + if json_dict["type"] == "search_result_channel": + self.received_search_result_channel.emit(json_dict["event"]["result"]) + elif json_dict["type"] == "search_result_torrent": + self.received_search_result_torrent.emit(json_dict["event"]["result"]) + elif json_dict["type"] == "tribler_started": + self.tribler_started.emit() + elif json_dict["type"] == "new_version_available": + self.new_version_available.emit(json_dict["event"]["version"]) + elif json_dict["type"] == "upgrader_tick": + self.upgrader_tick.emit(json_dict["event"]["text"]) + elif json_dict["type"] == "channel_discovered": + self.discovered_channel.emit(json_dict["event"]) + elif json_dict["type"] == "torrent_discovered": + self.discovered_torrent.emit(json_dict["event"]) + elif json_dict["type"] == "events_start": + self.tribler_version = json_dict["event"]["version"] + if json_dict["event"]["tribler_started"]: + self.tribler_started.emit() + elif json_dict["type"] == "torrent_finished": + self.torrent_finished.emit(json_dict["event"]) + elif json_dict["type"] == "tribler_exception": + raise RuntimeError(json_dict["event"]["text"]) + self.current_event_string = "" + + def on_finished(self): + """ + Somehow, the events connection dropped. Try to reconnect. + """ + self._logger.warning("Events connection dropped, attempting to reconnect") + self.failed_attempts = 0 + self.connect() + + def connect(self, reschedule_on_err=True): + self._logger.info("Will connect to events endpoint") + self.reply = self.get(self.request) + + self.reply.readyRead.connect(self.on_read_data) + self.reply.error.connect(lambda error: self.on_error(error, reschedule_on_err=reschedule_on_err)) diff --git a/TriblerGUI/images/add.png b/TriblerGUI/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..f014cc1845495e6f6da5dad1d911baff1a4fae90 GIT binary patch literal 896 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2_SGmUKs7M+SzC{oH>NS%G|oWRDy)vIg-2>Wx(>pOP8zc-fP(BLp1!W^4_G956;u|-#Q^1){5)M8Ln>~)ookpU*M%W+PY6I6(Q8TavOujl>4oA!9K>eR3LlJ+ww zH906mNq`qpZ%rE|5Uiwt_8geWnGCkf8V^` zZ+lx}x8tSjUZv^N^0s@7)&tedkyv6vb~{90 zL&}Mtcnp&BH@pkkI3JkUR7+eVN>UO_QmvAUQh^kMk%6I^u7QcJk!gsbv6Zorm9d$& zfsvJg!B)nWRVW&A^HVa@DsgK#5@RC<)F276Aviy+q&%@Gm7%=6TrV>(yEr+qAXP8F VD1G)j8!4b722WQ%mvv4FO#rPSN`C+V literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/buffering_animation.svg b/TriblerGUI/images/buffering_animation.svg new file mode 100644 index 00000000000..075a3990dc9 --- /dev/null +++ b/TriblerGUI/images/buffering_animation.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/TriblerGUI/images/debug.png b/TriblerGUI/images/debug.png new file mode 100644 index 0000000000000000000000000000000000000000..ab9362b0693d4ab0d84b30d5d78edb5fc23decb6 GIT binary patch literal 4243 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 X?H6$O$xn5z^ALd0H`A-rb&B~P*NvzX literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/default-placeholder.png b/TriblerGUI/images/default-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..65ffd5409c34e45df9ff7c95a0d6069784f1f503 GIT binary patch literal 12776 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 JKkN4A{{jY-@^=6L literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/delete.png b/TriblerGUI/images/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..b686753ebd2ac5a348badaadf21998912113222d GIT binary patch literal 1283 zcmZ`&eN>Wn6#ika0hxkXnwEjnFlQ!Wp>ruvc)eiFxZ zk2&dj=R`}&S*^*ag!4PP*PCdK>()F7CZ!i zoC?5*0D!0p0K6m@?rq(O4xGM<=db~|q4lSs6<)$k%|o5~<^!{7&G`bfiIv4A#$rv7 zkBd81+@FTB7yKH>W~CIow7iOz`ml(H`tKg~$SQXeb+aA_(;ja<-7h$PiF-DD471I` zTqo*sbjgaMXSE<%T!V0ykn^v)mzU2}GbVM8-tjt8hYT614U8Vr|5KKURX!ukHdo4Z zRoj%vu+7*@njXP44SZlrehDS7F^(6T=tbq#6a!W>V_&AZr^?q1+ha8^RT&za-B~lO z+O9F%Q=Q^t{M(d{#p;lY+Um7MCtJ@M+59R7OJLmVSKDdufBPn!*gH7KF?AX#dNV>S zu!iGDZF9saYdub7E5fs}io%6z#@s0ZF&en_o&%?D-%t{x)o#y9f$p|aS*BpUqDxQv zj((ljSh+HN!KW|p4|xuG&+UTkBXw2s72ozmYT&qsZhvwd@5{*yR?oqH7s(*vptr{p zBU9^(Bdz0z`)hw(Rjo~vW_vom$Vyya5s)n`wcEls+aN84OCs-=AN8Yse$6`|bK4Ce zZF|6svH?3deI{Y5Or+#*+4rb_Us{r~JIMLSDyk)@aV1&W{>W?B2C((OwU&XK`wxG& z6w?xHD7%cCJo1s(i_RTZJkpSlA1x{Qo}1C)3=-k{-V?E3E_TuWpo`BBp&$!exq_}n zC%t=x)dCWrQWXKXsDlw~E7R2V!D)|2lXVuHg^7Mx&#Cmc)z1vdW$zE}dR zS0`rSyW?KWLgeQbzmq&*n8vvhNdrWi+WsY5!0yTj=w*?cS#*!H%rT}YdS9t8e6iie?bZ|@MrFMox5f+~SwTz(I^+IG7=fonQP_^aAPg zr{Iyu=iv>87fYLMT8gkcm*%P{JW%r7`mpA;kx+H>u)Sw^x}9)Aue0leW;zJxCT+QL z`RE!Fj~X-g|WoF`Cdj9Y-roomurF|Mu=i z_pYpl6kh9z-8{Yi*>+z{e4%gKlZ;F=lwdPM&VN|S-SuXj`_i1Irli&iLlXavd4Pt= zbkJDlYjswr%VvcRWh-j%&#g81V=F=tkI557JkxXK%$aJPeW%mlz~7fH9auGa<#Nu` z`ftW|`o6z{_5D3&A|i_!C_Uyh%0d5qwJdgrY>!Zu9wm~dqX1~sFnS1eeF%-75*8K} z77?{RGMGw>qEeTCT>RUA1lhTJ#2NekUm#XQ=b!@bd56z(Gh_;(G#w}uign_g%si1$ ZlD;liy7&CpMjw;};$oB7w>Akh{{pcVDB1u3 literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/dialog_close.png b/TriblerGUI/images/dialog_close.png new file mode 100644 index 0000000000000000000000000000000000000000..7664b465bfbaf9263dccaef5fe79716cd4121db6 GIT binary patch literal 436 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|e=3*z$5DpHG+YkL80J)q69+AZi z3{2G^%y>DV?+#E(yEr+qAXP8FD1G)j8!4b7(Ey(i z*Z=?j1DR;Rw(U(JP>8K0$S;^dRCn*CSF2X8v;d0y_jGX#@#t)wc3Sk1f`F^1)(08Q z1HN1ZkMH?%ojAl6@=M>*!THCYk7>?EAN1xYrahW7d;PhJbJOqWte^aD{xx0G&&qec zGRWWK+}N@}@UqmQX^f7_HE%jPI4*RscyvT(i5z;~_k6vqV7x=fG;vN3r`{DMQh}#M z76od$XiEvsO>_A&>0fogm$t%l*ObEU&bqQMd$!HaTPx20+aPt%`pmTd<-4E$YudoT zn#jm?h~*83aDeifh%f&c*$**^w6H#4v|!a0+V08T>h62asr+*5L2dWxV(V>L`P$@I z_1o-K1$-Q*95LVxQaT#qb5iCsqnL<}XT+5aI!TF7FZCRit>d@rj}f}Hv;DL#D7-vf L{an^LB{Ts5RMep7 literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/discovered.png b/TriblerGUI/images/discovered.png new file mode 100644 index 0000000000000000000000000000000000000000..345b393e16f50d8473c4f11f9c4ecd70385ce7b2 GIT binary patch literal 2967 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 wW diff --git a/TriblerGUI/images/down_arrow.png b/TriblerGUI/images/down_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..c61722ed5dedf8fc91d8bf7e39085ae712a05587 GIT binary patch literal 942 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxDSqwD$ItVj5Y0R!* zU|@{Q42dX-@b$4u&d=3LOvz75)vL%Y0IFtSu&J;DGILW)5)~?PbMlIA_ld`MjvE3l4Bq~1&ac`YR6@x4-YmwE}ui{k-$LS@9E+g62bX)ns44A1A$iA zQ0bP64RZ= z#=q_H`?E*nTsw+?zn3hS!LYXU(tIn)zQ-Ado~}ID=Dw||tHMB{;%wUIq`h(3#+jSH z+nf*ds%_d^s5aS?;kjVeI*nuBUNbOr=w7TpQuyZpkBzxh@9U>Unh$RC*xZ$7Vwo|$ z`{*Oj#E|u2+Vfg@&-jMlELCDT^I$8>m%fRIMQY|TK0DEs`~6xHS>QH}>%n|0SCPj5LIKk|7JZn@)F&FqwlCZv zR50}+JL?qDK-PKM3s!Wj`ryR$$)&(9`e8)Mrtfn;x8K^HU6Ju{p0?CS$thvepKjHe zb6!mBz)?3Bi8kB&-`iU@Z7kZfZT6>At7iG!__2`Vo%QUqc>$*P6W;$=vGeJSnTzwc zKAfw*u>W}Se76~ro4L;A@fG~~IO*%=>@~kTtbP`3FVdQ&MBb@08v_LRIhuAp@I5Pb0`1+ z`oti=qZ%y!QhGX?o`&!A(SS~hZ-_4d{F@6?CxSKHf)aEz1ON*6YIJ1)u&G%UzW{*K z7ywuy1As>p0PM(W={w@3Ie?N52l@fP)mlrOhC#A|qH;7z@-L-z(55+Gv)q8qm_e!V2C8Lu_*=;Gk6JRWGXk>_| zM~4VQKU8MRYj@&rIzCRn@~%9(N5{w(Y3$MDa`9sB1IHQOzXW6O200jEGqxBzuzKcp ziBhynw*UEtSI$z)*jmx*6y%W<^cqm~yarEs9*Bljr2W!r?|E4L-ia}W9foh9Xb5IP z;9^%Cqbl|(xJygB^F46hH*ysI zE;~|WMtdoh@k_}a=?OB+nI!gD23=`y@aoR>EfaT>o#^*Dck{eHg4zfl$1_@?Yra0s z^V9KqsYhoNFfBgG*2l9ruN`zlJN5rCeh9c%{5|#FTNJyF!cS9ki6SirZB%FC%o zDQA)-V+j|QFP%A3^k_m#s@vFe_CY0nXP>7qa{P{alEv!1q4G~^N2ed?4X4h9jkvhR zHy&RF-x!JHEsb8EL6`i$R6$;mK?! z-Zo$5C~SnhkCfb^B0ESQ%}Js{v%L|Ebbey#^q+(iPGke)SNho&1kKukhi110x|BCN{)~b=q2q;Bs?Xd zB#L#HXG)GKNn_mw$1c)pZ7|#|JBn7p6YQ-3IR~Uv8RR#zfH7ssQ{j?n({Ke>#oL-S zzE+=8883<~YEO^FrLRJubm#fkQRHV2E?rJpth~L+nLyC9pBx6~t%a?v zj)-59xN_zGL*)j?7QT@pPHkB`xUJ(IFLghM>FT5JiwpJGN_DZIYwk>+{2Bf8h6?cu z@{iEv?YAS@G6bFB>)C0ip43NFwVn_*&fp@*C|r- z+6MMDbvQpM<@6q=Rl(o3yPamJtqPLQv+M3Dsv+Pz5 zwOaC{!ST!mP-{;WJ?SyIL~Eey7|R)EecA*$i0@(CkO8ExpIP_46P%YkU8#-z`8T7_ z+}t%TFOC=E8GCv5K@{0=_SO6{NShTAqv{}WpD&BA$uI*S7L|FX$YHS(V(i)bI~JyL zM&WDeDa8l*-U9-CR*bRLn4PeUt~_)+FN-H<4U6|5vI=vWH5_^q9%&Kqs-d!JR=3FzS@z@!}S z);?Qpruox*a{W)_QWJBNJt&N14FEU{*3ALq>VU;XVzC}rHxE~g9R}xt!JJsYhyKlw zL8qqC3jW!!Pr`kxF_?Wxh@jJQnTd>KfXQS!re&t*P!h9}9qEje<~LsE8WBMB5B2Lh HNG|;g1vdFn literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/edit.png b/TriblerGUI/images/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..db52487d0377dc85604d5d36d3c910ddea9db45a GIT binary patch literal 1583 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 literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/full_screen.png b/TriblerGUI/images/full_screen.png new file mode 100644 index 0000000000000000000000000000000000000000..631212a88ff7eb657dfb155ad93f761f1309f412 GIT binary patch literal 1000 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 literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/loading_animation.svg b/TriblerGUI/images/loading_animation.svg new file mode 100644 index 00000000000..98f85393521 --- /dev/null +++ b/TriblerGUI/images/loading_animation.svg @@ -0,0 +1 @@ + diff --git a/TriblerGUI/images/menu.png b/TriblerGUI/images/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..23a8c95c030b2a5eff5dddd921dbd33bb3875c1f GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^4j?SR0we=Iq>2G4mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5SkaJjr7Mr7 zC~0-2$ZFkkIdmpv+XvCQ{HFEV>qQtCu7&(AzkNyYCgU%OmmN#otrX5OylgOGUc!7S z>gSzuD<%epXRG%=FI=L$h`FLgEWM$`VHV>h#>}<*&Kt`zGBA9;`ft~|5|=WD{tLHw zCh&O}J6I{Kox1V;OAZEx8SnXj?3M`KGGp6+pqo@nTq8NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk<^gvcyzQMR$TW63Z;6wIEG~0dwb&|X9EKVs{?O) z&YD}RmTfY;=l+~Yj_cEg?VCzlelr7AUb!-p^-EiRVGYn>!TDkrgajEF4!ma!4i{4p zWMuHU8`3I$y#S9GcidXn2Dk4T^fNstY} s`DrEPiAAXl<>lpinR(g8$%zH2dih1^v)|cB0TnTLy85}Sb4q9e03@Mv5dZ)H literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/page_back.png b/TriblerGUI/images/page_back.png new file mode 100644 index 0000000000000000000000000000000000000000..b44d57416735e41c363b14e8c2aabe9d65bcac32 GIT binary patch literal 610 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk<^eldBsGVv(^DkVKne`aSX}0clPE+-a`f=Es5QN zOv)QXRM#ArF%eNYq7_M0x{M5gKnc>|H zD~9_v3^p5H^GZBtd~i$L;4Jfjeewx<^)J73o_U&o`aVnXJWh_Ml?@5=SUH|sHn_}V z^mzKYA;pf{<9RWIlO6MdCyyC|?6?*@O=d{4V_NX+Hsg~T?S^^z%p9L|89evNDV(~_ zl=9Q6!62S_!n5m)kN!wKSaD9DnQhON$|=5u`~UqE`E$_qMq|L{6Mvjl)UN26{Fmh9 z)a^cNuA=1hn(^7}AV$Ta2|o|E_;k#({(M+-q5%61a}AFcnFISgT@{`%e(OD@(#X6a zKTMs&rlB@LP^EzJ+rq2*PRBRim09@tL6Mh1Pu_8s%O7?vsH*wT=(~ceaqZDJe}KWM zTH+c}l9E`GYL#4+3Zxi}3=GY54UBb-3_}c!t&EJUj109646O_dK3ecbplHa=PsvQH z#I2zrb?XzL21$?&!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NC6cwc)I$ztaD0e F0s!-r)Cm9p literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/pause.png b/TriblerGUI/images/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..18826d22f11cfdd6ecc42f07ecb329d4001a78f2 GIT binary patch literal 601 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 V?T?S4bT0sVAX=Qn*O>Vw{SS2ur&a&} literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/share.png b/TriblerGUI/images/share.png new file mode 100644 index 0000000000000000000000000000000000000000..db6e23888bd97609a24f6e700422519950a38505 GIT binary patch literal 2412 zcmZ`*X*|>m8~xjc7^HHYA2IOqJ%bDr~izCN+n%}s;_p@IMa2xG4rT5**AF9diw z?4iZf=LoMG#tZ`hRf$3eu6!I1BV4sI1Ax%qIC~KQu*VtE=KvrG2>=Ut06?VyfP`Px zD@!yd;J%GBF$93pcv)4BJLY%QA%Jsf_+M}t$Y%y~hWvq8Gb8>9J}?*p3=~grG659U z5MvWOI-Pyj&xS42cDW;}*f3x0Y612-w_V9?CzMQ5asoosD?|r+FR=7d*g>Wl|8j30hwZRh;db;B9XS$5ixtxZm|ZB%0H3SOL;R`#9@e%=Ys*MzBYDQu1iLSsd!|-CbNHvQDwrNwrxF!A`oS7#7!FcWqci_6POFoYSTKmyqkF`!2DG zgfhKNDiWWTv7Vxg-QS$Al)7FRK8?7QlHh5qN_g48_lc-`Qt-QW2kFhX?dm~_nbL9B zE6wY!0%kUm19W!xR>Z7y24rl%bGvNxg|lsMP42@QS1F#(&+oNGcTwr>HwAj&esRU? z`G4+bU4AFVb!8-1+b<;I;*tQ4cZ`V9Q)E;^{2rekN(T!?bIGRpM$(SohTRWSjdi;Wo_bcn z@6Lc3=|@O8G=~^@Rzy;>BJvFNFAi7rPo@7_vR`VK18@YmXYUzCOZnHmd=%OdvQPC} z=b7=U!$fbFVBiJ;L8CmoQnn|+JPoD8!mnOi+`8dG$zEjJ<{gBWp{pc(4Qi}%ho$d3 zY(i}+cO*xBdq-%20gZ!26p0JrtSGsk(<`3!&-+$`^s9I?e2r>c7S-X(KL%<(=9R53 z>OzxhkJz4F)<;kytC8yP8KD|%akbS7+n-&3{IYGh>YRI-Zx57IxKZx)WBCX>BLpYGPk7J5Xg-ZTd-uu=`f@3N(dcLDgGDxHTixzQt<${koUZd+c2TIeOKEbj@*lS#Nrsolv8gbrS# z5`Avw-aq?oc=xUI>|)(Qx5HF~om4#RiRpURs)YQl6p<3U61zm`SWS?7Md`uGT+b#& zMo$+n#mgl@*#i4;J{e3fw#1D?kU81nZsRF^EmP%_+2?DXoW7s_>&vPr`t&FGn%UyI7F8>J+sPZHEmj$a zQ>^_Vs1W>-l0O255UA)Q1%(~f_<2@7IE@m2_*>X)U6ZpspSV3iv^(prCJ)G;sj=k| z)}^MBr@HFaIF`0hhd#;UU34zthf_{0m*!vRdhluE+yPQ}HSWBA${+Q@m2o1Cd^4Ib z&*Vd1M3_*wkA9ia*4yUBla*3j%^?dtx0^6=^)EH8rGHn0wQ71T<;MW-)vXO#UW^6> zrs4Ij*WymWsgKn`G^8Tmx2arlryX$5nrNJEBN_<|IrvVq&~4`AU4*90WNN|dbo+>* zqgKJ%J%o;=Id5Rq3;7C{P6cn_&x)8;@NSurgQVYDb;I0tD7_I!0F*2H7bX@_+;5zu zeTMj6-B<=K8ul!lZY&Va27j7h(&y){5 zYfJImRR?{ry!x=4-}#f&#Z_C@pH7l#g=D!zDuqjz*TmXHh7f8+bc{Vgl2D-$^xu5X zyudE$1nUjYB|kOn+K8EV&0gG7dt-mu7U$S-$EP=_{>0z?jmvswD_2}{3kzrGwLlE- z=&&MLs|C}huxdv;VL{4Q*v}O0fa;4GK;`*_+FQHasxnU} z2Fl+q8tec`V5>Xqmz5k)8fQKY8D%g%O6M(7P14IRLj!j!S4^=vZ>#cTcrwR|yS^w| zzi7!D@Y(w#YHM(f`aYt0 z>Q~J7)6e7Uq_W~ufuHs*Ek+*9$D!u>y$SuJ?Qr$VVgZCLHTtJX<@qKIbu0M{LD2^j z2k?I_pEVG7FfUS~&0rW1E-Dx=)`eJsC;BT1-sb&4%`jokYI-dWv#*KFz>{JijZd4Q z`)?+s0Q+tNIF{><`6<4%_9mF<1FCd&aF(gdnk?+yi1^4AzZ`rdyGxzd#B&sFdd!%A zoPMWl>74ep<>O!fn-&L5^vMkak+iOY}-Z06c+c4Q-crb=DE}F*|4d zu#~3ESjF9*A;r515p*@Q({O|lv47y?;HH6h5_`1|BRc>{vq-B%yV&Ge=Lp&E$;*5y z(xjUO_Y5>Z+RvPOX74X+Ek3Z(Yn=XWZMvEI<~yY?=EyrNuA}ZAR@Wsq+#;^O_|7ik zX_9|UM*N5H68QaFcbRbKac-vkl%P0LZmP2xd$TCE)%#@GKblRL2#@ZkPdrOY$oL-W zL(7{aRw^h}_!s{zABl5`zds7)JQnakBl|$2Yaj_l@F#HuP(`X}B9Iyg6;&G*Rg|ha z3W-!es&W`PGWGd?3O*E~2RY>b7cdb{L{35S-wW0hav;ssp9IioG$jvTuKNS%G|oWRDy)vIg-2>Wx(>pOP8zc-fP(BLp1!W^4_KH58AX&Erj#=&5)w z8&yjKB6t`@dDu1v^e74>X>K*N$d#CL=)1y>&nCg=i_Y4l%|8EgMzv+y{PmX8UmM>4 ze8u#{`#slg=2ich`8?<<^4Fz`$26 zag8WRNi0dVN-jzTQVd20hGx13Cb|a3A%^Bw28LEf2HFNjRt5(8yZ-M)(U6;;l9^VC zTSMG)cTj4P1lbUrpH@mmtT}V`<;yxP!WTttDnm{r-UW| DBEH(= literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/subscribed.png b/TriblerGUI/images/subscribed.png new file mode 100644 index 0000000000000000000000000000000000000000..5d733de598f995f8355796b0643e682ef679eac7 GIT binary patch literal 1974 zcmZ`)dpr|*AO6i{=GI(77EMVQW7{dQi#8D(*4#RBrj5C5gfJ$TY;;*kiFrvXxxIA| zE7iIXCzmc9mr963=9Dr;qK+=k`t$wc{p0(6KF{-gp3n2=_c(*@sjG$10s!bzz1;nk zDEv7vHRa}Qzd}}mS~SI%0)X_i_O~!~CAZ*s`TGKpzD4Of48V#~63zgSf&t(i8vuMM z044{@@Awgw2bFMdPj>)*&$q-WndSkny~)a^7e5E$W^*o0DQfVkz8)Hr>N?tMwaUZ{ zG62xSRCh{X+UVT*Q104a%{u!|Zt{!auD7Lbt$pra)0~f1+Yy`BKguxh&_;>juG6+~ z*R3a}DU6LMlz5sVX}!EPNbX7ID!l8YW$gN!)Pc%i-*9*T^>LfZI!IZ;g6J3L$IqW8 zd~Q7Z?WpcoEa7wcbwR52FM=N&y;oflV~buc7*wg&%mN%ztD^~WBT{|gr8EE48od0L zHH6zs(XZ`F2*8LpjO9%n>bBhNUGwrvX%O)NCF(UacJEaB$lF?jp^v-%W2|xTXpnvC z-TeCFcJ)IYh?N~kHWIi)DM7UY+g zThD2|2p$jr*NY~J9xI8Ad?K!D8kPa zfv)Smx$6pv-OL=eLmw|jnlHbu!DyuFQ1?x3rcYVrqlcokD|S__D^#$Ly*7?5G`C)L zWQd9|nfq_w*V6MI;F5fFCe4|h?z}9-7r2$oD5F5OGs4?Op=v1YOlL0%4Zb-|-ObjS z;xP07x{*$f<0@34F>=Ah&%(a>>&>X%%u_4_RhiIb@Wj?+{bbQFs>zwZEOH6nuinHA z$%j1=&7jy7>(vKi)MnZv`qdZ++E+4!R#iv7-E4q;Tm=dAn$@dP!33YYUy}m+c+>Rz zHXU?wZ^0o*m(4m|@XsK^{Sk5wQbn?uCbdl#ya~fuioy)PE9JM_No&vuTOw|O5t3^I z_skHch%F>zxB-U-iX_$0{HFz$1;=nFlb<{`iJ#-=uKvjaL;4D{3G%A$?)1>lEq0ois~leHn+{0cJ0aC zL+dW(m7*J{mT0NIzLF%=Y~xwIAJVEYwXO-`$v2mA1cn;4Kgk(3sRe7~sfJ4A&q zq^yv0#>>_h9+nXId_j+O#15{g)FPiIPNbJ+zMs!h6bzX-6Ab1o|Lz&klwm7xX=cnj zY_N7+m>Q9vmimQoD)#%_guhHaHY;@ti6(y&7CX8|nvfkFICq+dfb95g|I<0cMvSz> zM(ZSd>dGngO$1m3i&Ck(zIq82tvaf>Kxgfkd?rTh7#3SCJ66LdeVLRq&G8=maN`Zb z_a&|3$Ex*zG&Lt#>2QMZU(eP(EbrdJo1+aMUVO{yVeaf`!`c@OeJGNDftvb6*;N+i zUTh+d=EBPsJYE;Nj?_K8CyKh~IQPIZI~?+(mzYUl>zoD^H<&-VFxY`Wpb)`{LBnGcEDoWpFTNIfp}M zjNM%@0J7gkub}U?q%9Z&S-@zV7NNyu0Rd#8qlpGDTUO>bfMB~hJiS3+a!3W-q7kgA z@k{b{7MP6UeI8Tvi6+1&lL=aFSL2~oh_Kkq7`vl~w)$@TnGwCuKl8L%@9I3U9c7f8 z{!9&Ip4fHo>cAl+>HxhnCr}+^ijjVoAN>f;JhJ1NJuMWFv42jIbeC8ED3LY9grT0y zPIP59L*wroEj}OlH7iTB&)zMdV)GQ(6walT z^c=e~souC~R(syNzpQIfGTYc1aHV*Xb%Q19;OW3+*RqA2Pc~1Dz)Y0M?~UW-jtPKW zoICdbR>zMSTn}VU-fH?+pVxT_z*)=wc-AK2Cw20S-p+p0)(a9r2mDpUEQtoR5z~VeeOLGp)>)i2i^Uu8$0J;mC{|4 literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/subscribed_not.png b/TriblerGUI/images/subscribed_not.png new file mode 100644 index 0000000000000000000000000000000000000000..d72494ed795fbb58fc5c2eb9c0c47531a25753c7 GIT binary patch literal 2073 zcmZ`)c|6qn8vc%b7b#2BPqr&zX0m5wong!{#8AWxV;t4s4zh$sDSJ3$J%j9IDJh(A zCwpXYtw&-3S#L~y`}iztWy0L1NVt)02Z_*p`H z+#X=~(3}f={uX!(0G_0a?vwes9P+hw#sd(g&b^BVV3#W~z5oyg2jD9S0FyibWP=M@ zolLn49v^#*H2^hfDo8F94z|4%!gYH0vtXA0VnuL8!B9KAjo?QSAyFe4mv{kD00b88 ztSww3rdA6BuDiI(-FqNG%HSL7OedOqN~@_~R(`YAf3H*JheuFOJQtRSTR>>O`W~3w-L9OVJ+am+xVcX0IWOZ39lOs9 zx0=8SI+nX!>@`_XZI#1ejw3QN5`4YXyv@R882NulJ65=aBpJ(KRbNKPqYz{;t$ET=_CddMv-Cnm`0 z$e!gVwXQ1_#vJ2qV11&OQG0HzP%_q3imKAH)WkdA+vgQ^q`-t!=;DJha=ZM<&yt%= zmbm21fqIKFwD&e0RmoiHDPpWxIU4Ahjs7B#7apMtA;$K3Oy$2MhWL@+1DuYU&IOKs z%xN*^a2_lVV}!m*i-G-ZI7v6zEkC?zZh@Uh*peAVJuHN&v&yt%cQHwCRd!o^X2jeJ zkXy=kdc!X8ki005n`;>I(5yLEF0I!HzN(l221!xZmW2yGP=je8b(C1vWy=tQ%NJ7Sfry=X|BW)M#N{akm**$DhK5UzcQ+kzYtpZ#>!Wy$Ry%U3 zgG)#Hesa?uVVZbNf$bacn((RP0Ts*dZz)~k?vbF-I~IPoC19E0>+u~wbf6g}<UwNL7&F1t*K41un!Yqi1!^sXjYIopQd`Jadjt!B`7o!bh1Vb|h27L93m z7@csHXgyjl_1mL*O~TcgaRG>oedfxKPKsX>0eLnRcV7Q?DtoZzrn4^C$+o1?;t1}y zRJ^;#K8rekJjn?=K6+~k0y|80^Vti8A`eriMygyPL0<^y0y@>1BBc%eiLfP+y?JuY=Y3SY>_>mC*YDIEo zv)7w^O^lDy{WeTz^l<68WKRFv3EiIA6Q=i(mqq+iQ?{Mo12%F;n;moqyFTBz3d|+o z-X3ilgSosMZ(KVn7IFUBSQ`~Hb6D)IS_=*3=<-fad=(^nVLgX6mK+cp;1k2JC8|$% zllemWfloyu)~%x=rCC<6pzN{g0iIf7eJ=|UuQe5TJ>;(Pt5pvXz&5hlahuN6KJ=@0 zzq~_wVw=3;D|WmV=)-E{RQBo$uRh17UiYtQ)*ZlQuM!=boYXPIr!Ld^YB~K^mo=tq zkLt0SroR2!IQ{{py*Ix}JZCfQ%QOBbYfE9`k&IeUBX-))lQ`M527!#0OAlA|)V1bm zdufVeBFuoaARR+k&VZiAUy8@=nM?Q;tt`LKe^1l;#y(T1N6)l2SS!wUU!RX2#e1g( zTHT_pby`ODMf%FZIxA6S9-Pv~1D^cF%ICF!H1e)YOTPM-TaVh8AMr|J7xP}#g`Fr@ zIh+gIc}vqM8kInHPvE8*tFx!QvG2p@oH(i*JQeRx6(4A^`(t%Y*6X6OB9nC^;Lx6I z#HHA+N-fC&|NOv}1rAvG@e*n~)-yLpp7b>C-cVehIaDs%e%(<8V|Y@aw&A}}Duvrr0ejTy`L4nL&Oc$1 z0O@68(fG+Q;w!$LiBpAU#7sS_35WBNCar(eCVlNToCKYw3ID#LeJFie@L=Rih?3Jt z;T2PM>7A!yIn3BIn}OA;^MHWXqK&@n~FfOa<_EGtg4ilWti^HmcoDv4rV896U}xiC{m7DZ`yY`@p;G_= literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/subscribed_yes.png b/TriblerGUI/images/subscribed_yes.png new file mode 100644 index 0000000000000000000000000000000000000000..0b457b445a253932fd15135a7ae03a475e5adc6e GIT binary patch literal 1968 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 xH2EJ*{zsGlxzF;~<(EGfUH)~;<$rgd4SK=SeS z3_=z8T6%c2C$Mk3qk@m)_;Ud6SuzUQxJON7cEFDlsOz{pEqDkFf3$tlWK?XY175Vscbzk9h$j(dmQlHZgfJ=AY$jMfQ^{e zAv{C6vG`B&y9)J!y7(un*ZP8mEC0MR-5g2C@{~K%%n^9W8(P>xe8miXR4l;usCC5c zW!E2)mP&SSZX3l$5i=F#oj*yTeX6b(+dkvU0-Alcnbh10>}&HEA|(S}1a-tjG4|En znGSg0Gj}HD3yb%3E=X!^5%~sC(?!3KOWz{VBxbL~< z;B+POPfxeU7~i5K?PU6wA5DSVP@%`!mIaCl)yd=|wh>-}eIHmEfJ`*%2Z#jal| z6Y2n_Zcj)3Y1JU-Y^Y{AtSKu>%sNX89dg_614v@k0z*+pecC)J9JUy)8fw8sWmB!{ zJR!vJ#*+)gBK_>#*&qzh_9BRMlnu;zrMn|C`InIVD{iaZA!}>`ls31yQw@;t`Cz>K zFm2uivjYVKtt%vJ}V zk8a7Ww{&SRA0Mtd*SG#ni$HQfoL6hnYcpzxfb*)%%}N4n;pazn%bEcppU}+YNl2i0 zeCqX;BUdL(Ya2ZxJ4GFVpboSx33P1vR<65pqsx=s<8T=eTvcsBAe8{h2I07#5)ixn zsVC?*l<$07@+i`AOv9Uc5*L#)c)T8Z#2#(_^zcpn?SHh&yxCRGOzU=1QX45p=rJcR zuJ3Da8mnBx7ENjYPI`FFanUlGkiBG~x{uU^qo;YKS}SY~lSk6?WWW~HdjFQm7O8w& zh3p$VYi%{9v&`s259#*jH?b0S*pTn-5VygHG>4#_9orW80ql$_H0%e6@5%rA}&v~H(C_q#23V%0uJx9UIC-+NE|JQ0kK$2 fOGr)>@}pB?Xa@vw1>-Iz=n?pE13b$&MP>gBl5AXE literal 0 HcmV?d00001 diff --git a/TriblerGUI/images/volume_on.png b/TriblerGUI/images/volume_on.png new file mode 100644 index 0000000000000000000000000000000000000000..416c0166cede6655a4feab81f84ab171f9a34e11 GIT binary patch literal 2005 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 literal 0 HcmV?d00001 diff --git a/TriblerGUI/qt_resources/buttonsdialog.ui b/TriblerGUI/qt_resources/buttonsdialog.ui new file mode 100644 index 00000000000..a8e156276c3 --- /dev/null +++ b/TriblerGUI/qt_resources/buttonsdialog.ui @@ -0,0 +1,215 @@ + + + Form + + + + 0 + 0 + 538 + 180 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + ArrowCursor + + + Form + + + false + + + background-color: #333333; +border-radius: 2px; + + + + 0 + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + QLayout::SetMinimumSize + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + font-size: 14px; +color: white; + + + Title of the dialog + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + color: #B5B5B5; + + + Here comes the main text of the dialog. + + + Qt::AlignCenter + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + background-color: #444; +border: none; +color: #C0C0C0; +padding: 4px; + + + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + + + + diff --git a/TriblerGUI/qt_resources/channel_list_item.ui b/TriblerGUI/qt_resources/channel_list_item.ui new file mode 100644 index 00000000000..66850a54882 --- /dev/null +++ b/TriblerGUI/qt_resources/channel_list_item.ui @@ -0,0 +1,386 @@ + + + Form + + + + 0 + 0 + 585 + 60 + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + PointingHandCursor + + + Form + + + false + + + background-color: #666; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 14 + 20 + + + + + + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 10 + 7 + + + + + + + + 4 + + + + + + 0 + 0 + + + + + 58 + 18 + + + + + 58 + 18 + + + + border: 1px solid #B5B5B5; +border-radius: 9px; +color: #B5B5B5; +font-size: 12px; +background-color: transparent; + + + channel + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + QLabel { +color: #eee; +border: none; +background-color: transparent; +font-size: 15px; +} + + + TextLabel + + + + + + + + + -1 + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 16777215 + 16777215 + + + + QLabel { +color: #B5B5B5; +border: none; +background-color: transparent; +font-size: 15px; +} + + + active 6 days ago • 143 items + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + + 80 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 28 + 28 + + + + + 28 + 28 + + + + QPushButton { +border: none; +background-color: transparent; +} + + + + + + + ../images/subscribed_not.png../images/subscribed_not.png + + + + 18 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 2 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + color: #B5B5B5; +background-color: transparent; + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 14 + 20 + + + + + + + + + SubscriptionsWidget + QWidget +
TriblerGUI.widgets.subscriptionswidget.h
+ 1 +
+
+ + +
diff --git a/TriblerGUI/qt_resources/channel_torrent_list_item.ui b/TriblerGUI/qt_resources/channel_torrent_list_item.ui new file mode 100644 index 00000000000..040d83f7f41 --- /dev/null +++ b/TriblerGUI/qt_resources/channel_torrent_list_item.ui @@ -0,0 +1,592 @@ + + + Form + + + + 0 + 0 + 585 + 60 + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + ArrowCursor + + + Form + + + QWidget { +background-color: #666; +} + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 60 + 42 + + + + + 60 + 42 + + + + PO + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + 4 + + + + + + 0 + 0 + + + + + 0 + 18 + + + + + 200 + 18 + + + + border: 1px solid #B5B5B5; +border-radius: 9px; +color: #B5B5B5; +font-size: 12px; +background-color: transparent; +padding-left: 5px; +padding-right: 5px; + + + video + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + color: #eee; +border: none; +background-color: transparent; +font-size: 15px; + + + TextLabel + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + color: #b5b5b5; +border: none; +background-color: transparent; +font-size: 13px; + + + 384MB (3 files) + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 10 + 10 + + + + + 10 + 10 + + + + background-color: orange; +border-radius: 5px; + + + + + + + + + 0 + 0 + + + + + 0 + 20 + + + + + 16777215 + 20 + + + + color: #b5b5b5; +border: none; +background-color: transparent; +font-size: 13px; +padding-left: 2px; +padding-bottom: 1px; + + + unknown health + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + + + + 0 + 0 + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + background-color: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 28 + 28 + + + + + 28 + 28 + + + + border-radius: 14px; + + + + + + + + ../images/delete.png../images/delete.png + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + background-color: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 28 + 28 + + + + + 28 + 28 + + + + border-radius: 14px; +padding-left: 2px; + + + + + + + ../images/play.png../images/play.png + + + + 14 + 14 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + + 28 + 28 + + + + + 28 + 28 + + + + border-radius: 14px; + + + + + + + ../images/downloads.png../images/downloads.png + + + + 14 + 14 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 14 + 20 + + + + + + + + + CircleButton + QToolButton +
TriblerGUI.widgets.circlebutton.h
+
+ + ThumbnailWidget + QLabel +
TriblerGUI.widgets.thumbnailwidget.h
+
+
+ + +
diff --git a/TriblerGUI/qt_resources/debugwindow.ui b/TriblerGUI/qt_resources/debugwindow.ui new file mode 100644 index 00000000000..90f935a7a85 --- /dev/null +++ b/TriblerGUI/qt_resources/debugwindow.ui @@ -0,0 +1,345 @@ + + + MainWindow + + + + 0 + 0 + 873 + 652 + + + + MainWindow + + + + + 20 + + + + + 4 + + + + General + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 200 + + + + Key + + + + + Value + + + + + + + + + Requests + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 200 + + + + Path + + + + + Status code + + + + + Time + + + + + + + + + Multichain + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 2 + + + 200 + + + + Key + + + + + Value + + + + + + + + + Dispersy + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + General + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 200 + + + + Key + + + + + Value + + + + + + + + + Communities + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 200 + + + + Classification + + + + + Identifier + + + + + Member + + + + + Candidates + + + + + + + + + + + + + Events + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Type + + + + + Time + + + + + + + + + 0 + 0 + + + + + 0 + 160 + + + + + 16777215 + 160 + + + + true + + + + + + + + + + + + + diff --git a/TriblerGUI/qt_resources/feedback_dialog.ui b/TriblerGUI/qt_resources/feedback_dialog.ui new file mode 100644 index 00000000000..f0b91d3c70e --- /dev/null +++ b/TriblerGUI/qt_resources/feedback_dialog.ui @@ -0,0 +1,411 @@ + + + Form + + + + 0 + 0 + 600 + 470 + + + + + 0 + 0 + + + + + 600 + 470 + + + + + 16777215 + 16777215 + + + + ArrowCursor + + + Form + + + false + + + QWidget { +background-color: #202020; +border: none; +} + +QLabel { +color: #B5B5B5; +} + +EllipseButton { +border: 1px solid #B5B5B5; +border-radius: 13px; +color: white; +} + +EllipseButton::hover { +border: 1px solid white; +color: white; +} + +QLineEdit, QPlainTextEdit { +background-color: #303030; +border: none; +color: #C0C0C0; +padding: 4px; +} + +QTreeWidget::item { +color: #C0C0C0; +} + +QTreeWidget { +background-color: #303030; +} + + + + 0 + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 10 + + + QLayout::SetMinimumSize + + + 12 + + + 10 + + + 12 + + + 10 + + + + + + 0 + 0 + + + + + 0 + 18 + + + + + 16777215 + 18 + + + + + + + Tribler experienced an error. Please help us by sending the following report. + + + + + + + true + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 16777215 + + + + + + + true + + + + + + + The following information about your environment will be sent to us. You can remove entries by right-clicking them. + + + true + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 16777215 + + + + Qt::CustomContextMenu + + + + + + 0 + + + + key + + + + + value + + + + + + + + + 0 + 0 + + + + + 0 + 18 + + + + + 16777215 + 18 + + + + Comments (optional): + + + true + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 16777215 + + + + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 26 + + + + + 16777215 + 26 + + + + PointingHandCursor + + + + + + send report + + + + + + + + 0 + 26 + + + + + 16777215 + 26 + + + + PointingHandCursor + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + EllipseButton + QToolButton +
TriblerGUI.widgets.ellipsebutton.h
+
+
+ + +
diff --git a/TriblerGUI/qt_resources/home_recommended_item.ui b/TriblerGUI/qt_resources/home_recommended_item.ui new file mode 100644 index 00000000000..668e8c35312 --- /dev/null +++ b/TriblerGUI/qt_resources/home_recommended_item.ui @@ -0,0 +1,174 @@ + + + Form + + + + 0 + 0 + 323 + 235 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + ArrowCursor + + + Form + + + false + + + QWidget { +background-color: #393939; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 30000 + + + + background-color: red; +color: rgba(255, 255, 255, 0.7); +font-size: 44px; + + + PO + + + Qt::AlignCenter + + + + + + + + + + + 0 + 0 + + + + color: white; +font-size: 14px; +padding: 6px; + + + Pioneer.One-HD-2014.avi + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + color: #C0C0C0; +padding: 6px; +padding-top: 0; + + + Added 3 days ago + + + + + + + + ThumbnailWidget + QLabel +
TriblerGUI.widgets.thumbnailwidget.h
+
+
+ + +
diff --git a/TriblerGUI/qt_resources/loading_list_item.ui b/TriblerGUI/qt_resources/loading_list_item.ui new file mode 100644 index 00000000000..a2c5bbd6e7c --- /dev/null +++ b/TriblerGUI/qt_resources/loading_list_item.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 585 + 60 + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + ArrowCursor + + + Form + + + false + + + background-color: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + color: white; +font-size: 15px; + + + Loading... + + + Qt::AlignCenter + + + + + + + + diff --git a/TriblerGUI/qt_resources/mainwindow.ui b/TriblerGUI/qt_resources/mainwindow.ui new file mode 100644 index 00000000000..f7a5ce19b1f --- /dev/null +++ b/TriblerGUI/qt_resources/mainwindow.ui @@ -0,0 +1,8953 @@ + + + MainWindow + + + + 0 + 0 + 855 + 655 + + + + + 0 + 0 + + + + MainWindow + + + QScrollBar:vertical { +border: none; +width: 10px; +} +QScrollBar:horizontal { +border: none; +height: 10px; +} +QScrollBar::sub-line:vertical, QScrollBar::add-line:vertical, QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + border: none; + background: none; +} +QScrollBar::handle:vertical { +background: #bbb; +border-radius: 5px; +} +QScrollBar::handle:horizontal { +background: #bbb; +border-radius: 5px; +} +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; +} +Line { +background-color: red; +} +QStatusBar::item { +border: 0px solid black; +} + + + + QWidget { +background-color: #202020; +} + +TabButtonPanel QToolButton { +color: #B5B5B5; +border: none; +border-bottom: 3px solid transparent; +background: none; +font-size: 14px; +} + +TabButtonPanel QToolButton::hover { +color: white; +} + +TabButtonPanel QToolButton::checked { +border-bottom: 3px solid #e67300; +color: white; +} + +CircleButton { +border: 1px solid #B5B5B5; +border-radius: 16px; +} + +CircleButton::hover { +border: 1px solid white; +} + +EllipseButton { +border: 1px solid #B5B5B5; +border-radius: 12px; +color: white; +} + +EllipseButton::hover { +border: 1px solid white; +color: white; +} + +QLineEdit, QTextEdit { +background-color: #303030; +border: none; +color: #C0C0C0; +padding: 4px; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + QWidget { +background-color: #282828; +border-bottom: 1px solid #242424; +} + +CircleButton { +border: 1px solid white; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + color: #e67300; +font-size: 28px; +font-weight: bold; +font-family: "Arial"; + + + Tribler + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 12 + 20 + + + + + + + + + 0 + 0 + + + + + 350 + 28 + + + + + 350 + 28 + + + + QLineEdit { +background-color: #eee; +border: none; +padding-left: 5px; +border-radius: 14px; +color: black; +} + + + Search for your favorite content + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + 32 + 32 + + + + + 32 + 32 + + + + PointingHandCursor + + + + + + + + + + ../images/add.png../images/add.png + + + + 16 + 16 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 12 + 20 + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + false + + + QWidget { +background-color: #282828; +} + +QPushButton { +color: #B5B5B5; +border: none; +background: none; +text-align: left; +font-size: 14px; +padding-left: 20px; +border-left: 4px solid transparent; +} + +QPushButton::hover { +color: white; +} + +QPushButton::checked { +border-left: 4px solid #e67300; +color: white; +} + + + + + + + 10 + + + 0 + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Home + + + + ../images/home.png + ../images/home.png../images/home.png + + + + 16 + 16 + + + + true + + + true + + + false + + + true + + + + + + + + 0 + 0 + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Discovered + + + + ../images/discovered.png + ../images/discovered.png + ../images/discovered.png../images/discovered.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + My Channel + + + + ../images/share.png + ../images/share.png../images/share.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Subscriptions + + + + ../images/subscribed.png + ../images/subscribed.png../images/subscribed.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Downloads + + + + ../images/downloads.png + ../images/downloads.png../images/downloads.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Video Player + + + + ../images/play.png + ../images/play.png../images/play.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Settings + + + + ../images/gear.png + ../images/gear.png../images/gear.png + + + + 16 + 16 + + + + true + + + true + + + + + + + + 0 + 26 + + + + + 16777215 + 30 + + + + PointingHandCursor + + + Qt::NoFocus + + + + + + Debug + + + + ../images/debug.png + ../images/debug.png../images/debug.png + + + + 16 + 16 + + + + false + + + true + + + + + + + + 0 + 0 + + + + border: none; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 2 + + + + + 16777215 + 2 + + + + background-color: #666; +border-radius: 1px; + + + + + + + border: none; +color: #A0A0A0; +font-size: 14px; +padding-left: 16px; + + + VIDEO PLAYLIST + + + + + + + + 0 + 0 + + + + ArrowCursor + + + QListWidget { +color: white; +border: none; +} +QListWidget:item:selected:active { +background-color: #e67300; +} + + + + + + + + + + + + + 9 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + color: white; +font-weight: bold; +font-size: 20px; +margin: 10px; + + + Recommended for you + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + TORRENTS + + + true + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + CHANNELS + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + ArrowCursor + + + QTableWidget { +border: none; +border-top: 1px solid #555; +padding-top: 10px; +padding-left: 10px; +} +QTableWidget::item { +padding-right: 10px; +padding-bottom: 10px; +} + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + 3 + + + 3 + + + false + + + false + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 44 + + + + + 16777215 + 44 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; +margin: 10px; + + + My Channel + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 160 + + + + + 16777215 + 160 + + + + color: #B5B5B5; +font-size: 16px; + + + It appears that you currently have no channel. Do you want to create one? + + + false + + + Qt::AlignCenter + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 32 + + + + + 16777215 + 32 + + + + PointingHandCursor + + + border-radius: 14px; +font-size: 16px; +padding: 4px; +padding-top: 6px; +padding-bottom: 4px; + + + CREATE CHANNEL + + + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + color: #B5B5B5; + + + Channel name + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + + + Channel name + + + + + + + color: #B5B5B5; + + + Channel description + + + + + + + + + + Channel description + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 157 + 32 + + + + + 157 + 32 + + + + PointingHandCursor + + + border-radius: 14px; +font-size: 16px; +padding: 4px; +padding-top: 6px; +padding-bottom: 4px; + + + CREATE CHANNEL + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 2 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + OVERVIEW + + + true + + + true + + + + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + SETTINGS + + + true + + + + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + TORRENTS + + + true + + + + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + PLAYLISTS + + + true + + + + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + RSS FEEDS + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 10 + 20 + + + + + + + + + + + QStackedWidget { +border-top: 1px solid #555; +} +QLabel { +color: #B5B5B5; +font-size: 14px; +} + + + 1 + + + 8 + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + color: #B5B5B5; +font-size: 14px; + + + <html><head/><body><p>Welcome to the management interface of your channel!</p><p>Here, you can change settings of you channel, manage your shared torrents, manage your playlists and add rss feeds which are periodically polled.</p></body></html> + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + QLayout::SetMinAndMaxSize + + + QFormLayout::ExpandingFieldsGrow + + + QFormLayout::WrapAllRows + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 0 + + + 0 + + + + + font-weight: bold; +color: white; + + + Identifier + + + + + + + color: #B5B5B5; +font-size: 14px; + + + - + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + font-weight: bold; +color: white; + + + Name + + + + + + + color: #B5B5B5; +font-size: 14px; + + + - + + + + + + + font-weight: bold; +color: white; + + + Description + + + + + + + + 0 + 0 + + + + color: #B5B5B5; +font-size: 14px; + + + - + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 12 + + + 12 + + + 12 + + + 12 + + + + + color: #B5B5B5; + + + Channel name + + + + + + + + + + Channel name + + + + + + + color: #B5B5B5; + + + Channel description + + + + + + + + + + Channel description + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 54 + 26 + + + + + 54 + 26 + + + + PointingHandCursor + + + border-radius: 13px; +font-size: 14px; + + + SAVE + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 2 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +} + + + QAbstractItemView::ContiguousSelection + + + QAbstractItemView::ScrollPerPixel + + + false + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + REMOVE SELECTED + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + REMOVE ALL + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + ADD + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +} + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + NEW PLAYLIST + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QTreeWidget { +border: none; +font-size: 14px; +} +QTreeWidget::item { +color: white; +border-bottom: 1px solid #303030; +padding-left: 14px; +height: 40px; +} +QTreeWidget::item::selected { +background-color: #404040;; +} +QTreeWidget::item:hover { +background-color: #303030; +} +QHeaderView { +background-color: transparent; +} +QHeaderView::section { +background-color: transparent; +border: none; +color: #B5B5B5; +padding: 10px; +padding-left: 20px; +font-size: 14px; +border-bottom: 1px solid #303030; +} +QTableCornerButton::section { +background-color: transparent; +} + + + false + + + QAbstractItemView::ContiguousSelection + + + QAbstractItemView::ScrollPerPixel + + + 0 + + + + RSS FEED URL + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + REMOVE SELECTED + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + ADD + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + REFRESH ALL + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + margin: 10px; + + + Please enter the details of your playlist below. + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + color: #B5B5B5; + + + Playlist name + + + + + + + + + + Playlist name + + + + + + + color: #B5B5B5; + + + Playlist description + + + + + + + + + + Playlist description + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 26 + + + + + 16777215 + 26 + + + + PointingHandCursor + + + border-radius: 13px; +font-size: 14px; + + + CANCEL + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 26 + + + + + 6000 + 26 + + + + PointingHandCursor + + + border-radius: 13px; +font-size: 14px; +padding-left: 4px; +padding-right: 4px; + + + SAVE + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 2 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; +border-image: url(images/page_back.png) 0 0 0 0 stretch stretch; +background: none; + + + + + + + + + + margin: 10px; +font-size: 15px; +font-weight: bold; +margin-left: 5px; +color: white; + + + Torrents in playlist 'bla' + + + + + + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +} + + + QAbstractItemView::NoSelection + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 506 + 20 + + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + MANAGE TORRENTS + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; +border-image: url(images/page_back.png) 0 0 0 0 stretch stretch; +background: none; + + + + + + + + + + margin: 10px; +font-size: 15px; +font-weight: bold; +margin-left: 5px; +color: white; + + + Manage torrents in playlist 'bla' + + + + + + + + + + QListWidget { +background-color: #303030; +border: none; +} +QListWidget::item { +color: white; +} +QToolButton { +font-size: 16px; +border-radius: 13px; +color: white; +} + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 26 + 26 + + + + + 26 + 26 + + + + PointingHandCursor + + + + + + < + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 26 + 26 + + + + + 26 + 26 + + + + PointingHandCursor + + + + + + > + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + + + + + Torrents in playlist + + + + + + + Torrents in channel but not in playlist + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 581 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + SAVE + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; +border-image: url(images/page_back.png) 0 0 0 0 stretch stretch; +background: none; + + + + + + + + + + margin: 10px; +font-size: 15px; +font-weight: bold; +margin-left: 5px; +color: white; + + + Create a new torrent + + + + + + + + + + + 0 + 0 + + + + + QFormLayout::ExpandingFieldsGrow + + + + + + + + Name (optional) + + + + + + + + + + Torrent name + + + + + + + + + + Description (optional) + + + + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + + + + Torrent description + + + + + + + + + + Files in .torrent + + + + + + + + 0 + 0 + + + + + 0 + 120 + + + + + 16777215 + 120 + + + + Qt::CustomContextMenu + + + background-color: #303030; +border: none; +color: #C0C0C0; + + + + /a/b/c/d/e + + + + + /d/e/f/g/h + + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 26 + + + + + 16777215 + 26 + + + + PointingHandCursor + + + border-radius: 13px; +padding-left: 4px; +padding-right: 4px; + + + CHOOSE DIRECTORY + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 26 + + + + + 16666666 + 26 + + + + PointingHandCursor + + + border-radius: 13px; +padding-left: 4px; +padding-right: 4px; + + + CHOOSE FILES + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Horizontal + + + + 570 + 20 + + + + + + + + + 0 + 24 + + + + + 16777212 + 24 + + + + PointingHandCursor + + + border-radius: 12px; +padding-left: 4px; +padding-right: 4px; + + + CREATE + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; + + + Search results for 'test' + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + font-size: 14px; +color: #B5B5B5; + + + 34843 results + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + ALL + + + true + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + CHANNELS + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + TORRENTS + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +border-top: 1px solid #555; +} + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; + + + Some fancy channel + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + color: #eee; font-size: 15px; + + + Preview + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 28 + 28 + + + + + 28 + 28 + + + + PointingHandCursor + + + QToolButton { +border: none; +background-color: transparent; +} + + + Subscribe + + + + ../images/subscribed_not.png../images/subscribed_not.png + + + + 18 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 2 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + color: #B5B5B5; +background-color: transparent; + + + - + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 45 + 24 + + + + + 45 + 24 + + + + PointingHandCursor + + + + + + EDIT + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +border-top: 1px solid #555; +} + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + color: #ddd; + + + Write your comment below: + + + + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + border: 1px solid #555; +background-color: #ccc; +border-radius: 4px; + + + Comment + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 40 + 26 + + + + + 40 + 26 + + + + border: none; +border-radius: 2px; +background-color: #e67300; +color: white; +font-size: 14px; + + + + Post + + + + + + + + + + + + + QTreeWidget { +border: none; +} + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + 10 + + + false + + + + 1 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QListWidget { +border: none; +} +QListWidget::item { +border-bottom: 1px solid #555; +} + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; +margin: 10px; + + + Settings + + + + + + + + 42 + 24 + + + + + 42 + 24 + + + + PointingHandCursor + + + + + + SAVE + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + GENERAL + + + true + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + CONNECTION + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + BANDWIDTH + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + PointingHandCursor + + + + + + SEEDING + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + ANONYMITY + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QStackedWidget { +border-top: 1px solid #555; +} +QStackedWidget QLabel, QStackedWidget QRadioButton, QStackedWidget QCheckBox { +color: #B5B5B5; +} +QComboBox { +background-color: #333333; +border-radius: 4px; +color: white; +} +QComboBox::drop-down { +border: 0px; +} + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + font-weight: bold; +color: white; + + + Family filter + + + + + + + Family filter enabled? + + + + + + + + + + + + + + font-weight: bold; +color: white; + + + Download location + + + + + + + + + + Save files to: + + + + + + + + 0 + 0 + + + + + 250 + 28 + + + + + 250 + 28 + + + + + + + File location + + + + + + + + + + Always ask download +settings? + + + + + + + margin-top: 10px; + + + + + + + + + + font-weight: bold; +color: white; + + + Default download settings + + + + + + + Download anonymously +using proxies + + + + + + + margin-top: 10px; + + + + + + + + + + Encrypted anonymous +seeding using proxies + + + + + + + margin-top: 10px; + + + + + + + + + + font-weight: bold; +color: white; + + + Watch Folder + + + + + + + + + + Enable watch folder? + + + + + + + margin-top: 2px; + + + + + + + + + + + + + Location: + + + + + + + + 0 + 0 + + + + + 250 + 28 + + + + + 250 + 28 + + + + + + + Location + + + + + + + font-weight: bold; +color: white; + + + Developer mode + + + + + + + Enable developer mode? + + + + + + + margin-top: 2px; + + + + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + font-weight: bold; +color: white; + + + Firewall status + + + + + + + + + + Current port + + + + + + + false + + + + 0 + 0 + + + + + 250 + 28 + + + + + 250 + 28 + + + + + + + + + + + font-weight: bold; +color: white; + + + Torrent proxy settings + + + + + + + + + + Type + + + + + + + + 250 + 28 + + + + + 250 + 28 + + + + + None + + + + + Socks4 + + + + + Socks5 + + + + + Socks5 with authentication + + + + + HTTP + + + + + HTTP with authentication + + + + + + + + + + + Server + + + + + + + + 0 + 0 + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + + + + Server + + + + + + + + + + Port + + + + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + + + + Port + + + + + + + + + + Username + + + + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + + + + Username + + + + + + + + + + Password + + + + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + + + + Password + + + + + + + font-weight: bold; +color: white; + + + BitTorrent features + + + + + + + + + + Enabled bandwidth +Management (uTP) + + + + + + + margin-top: 2px; + + + + + + + + + + Max connections +per download + + + + + + + + 0 + 0 + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + Max connections per download + + + + + + + 0 = unlimited + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + font-weight: bold; +color: white; + + + Bandwidth limits + + + + + + + + + + Upload rate limit + + + + + + + + 0 + 0 + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + + + + Upload + + + + + + + + + + KB/s (0 = unlimited) + + + + + + + + + + + + + Download rate limit + + + + + + + + 0 + 0 + + + + + + + Note that these settings apply to anonymous and plain downloads. + + + + + + + + 0 + 0 + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + + + + Download + + + + + + + + + + KB/s (0 = unlimited) + + + + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + font-weight: bold; +color: white; + + + Seeding options + + + + + + + + 0 + 0 + + + + + 60 + 28 + + + + + 60 + 28 + + + + + + + hh:mm + + + + + + + + 0 + 0 + + + + + 70 + 24 + + + + + 70 + 24 + + + + + 0.5 + + + + + 0.75 + + + + + 1.0 + + + + + 1.5 + + + + + 2.0 + + + + + 3.0 + + + + + 5.0 + + + + + + + + + + + No seeding + + + + + + + + + + Seeding for (hours:minutes) + + + + + + + + + + Unlimited seeding + + + + + + + + + + Seed until up/down ratio is bigger than + + + + + + + + + 10 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + font-weight: bold; +color: white; + + + Anonymity + + + + + + + + + + Allow Tribler to be an exit node + + + + + + + + 0 + 0 + + + + + + + By allowing Tribler to be an exit node, your computer will act as a proxy for other Tribler users' bittorrent traffic, be it seeding or downloading. Check your local laws and make sure you are aware of the implications of enabling this checkbox. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 10 + + + + + + + + font-weight: bold; +color: white; + + + Proxy downloading + + + + + + + color: white; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + High speed +Minimum anonymity + + + + + + + Low speed +High anonymity + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + 2 + + + 2 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + font-weight: bold; +color: white; + + + Multichain + + + + + + + Enable multichain + + + + + + + + 0 + 0 + + + + Multichain is a blockchain-based way to keep track of sharing ratio, and eventually prevent freeriding. It is currently still in an early phase of development. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background-color: black; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + color: white; +margin: 8px; +font-weight: bold; +font-size: 14px; + + + + + + + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + background-color: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 4 + + + + + 16777215 + 4 + + + + PointingHandCursor + + + QSlider::groove:horizontal { + background: white; + } + +QSlider::add-page:horizontal { + background: #444; +} + +QSlider::sub-page:horizontal { + background: #e67300; +} + + + 1000 + + + Qt::Horizontal + + + + + + + + 0 + + + 8 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + + 24 + 24 + + + + PointingHandCursor + + + border: none; + + + + + + + 20 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + + 24 + 24 + + + + PointingHandCursor + + + border: none; + + + ... + + + + 16 + 16 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + + 150 + 0 + + + + + 150 + 15 + + + + + 16777215 + 15 + + + + false + + + QSlider::groove:horizontal { + background: white; + height: 6px; + } + +QSlider::add-page:horizontal { + background: #444; +} + +QSlider::sub-page:horizontal { + background: #e67300; +} + + + Qt::Horizontal + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + + 0 + 0 + + + + color: white; +font-size: 12px; + + + 0:00 / 0:00 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + border: none; + + + + + + + 20 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + PointingHandCursor + + + border: none; + + + ... + + + + 24 + 24 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 16 + 20 + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; +margin: 10px; + + + Subscriptions + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 42 + 24 + + + + + 42 + 24 + + + + PointingHandCursor + + + + + + ADD + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +border-top: 1px solid #555; +} + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 44 + + + + + 16777215 + 44 + + + + margin: 10px; +font-size: 20px; +font-weight: bold; +color: white; + + + Downloads + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 180 + 28 + + + + + 180 + 28 + + + + QLineEdit { +background-color: transparent; +border-radius: 3px; +} +QLineEdit:focus, QLineEdit::hover { +background-color: #404040; +color: white; +} + + + Filter + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 12 + 20 + + + + + + + + false + + + + 28 + 28 + + + + + 28 + 28 + + + + PointingHandCursor + + + border-radius: 14px; +padding-left: 2px; + + + + + + + ../images/play.png../images/play.png + + + + 14 + 14 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + false + + + + 28 + 28 + + + + + 28 + 28 + + + + PointingHandCursor + + + border-radius: 14px; + + + + + + + ../images/stop.png../images/stop.png + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + false + + + + 28 + 28 + + + + + 28 + 28 + + + + PointingHandCursor + + + border-radius: 14px; + + + + + + + ../images/delete.png../images/delete.png + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 12 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + ALL + + + true + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + DOWNLOADING + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + COMPLETED + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + ACTIVE + + + true + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + PointingHandCursor + + + + + + INACTIVE + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + -1 + + + + + Qt::Vertical + + + 5 + + + + Qt::CustomContextMenu + + + QTreeWidget { +border: none; +font-size: 13px; +} +QTreeWidget::item { +color: white; +height: 40px; +border-bottom: 1px solid #303030; +} +QTreeWidget::item:hover { +background-color: #303030; +} +QTreeWidget::item::selected { +background-color: #444; +} +QHeaderView { +background-color: transparent; +} +QHeaderView::section { +background-color: transparent; +border: none; +color: #B5B5B5; +padding: 10px; +font-size: 14px; +border-bottom: 1px solid #303030; +} +QHeaderView::section:hover { +color: white; +} +QTableCornerButton::section { +background-color: transparent; +} + + + QAbstractScrollArea::AdjustToContents + + + false + + + QAbstractItemView::ScrollPerPixel + + + 0 + + + true + + + true + + + + NAME + + + + + SIZE + + + + + PROGRESS + + + + + STATUS + + + + + SEEDS + + + + + PEERS + + + + + ↓ SPEED + + + + + ↑ SPEED + + + + + ANONYMOUS? + + + + + HOPS + + + + + ETA + + + + + + QLabel { +color: white; +} + + + + 1 + + + false + + + + Details + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border: none; + + + true + + + + + 0 + 0 + 127 + 212 + + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 12 + + + 12 + + + 12 + + + 12 + + + + + font-weight: bold; + + + Name + + + + + + + + + + + + + + font-weight: bold; + + + Status + + + + + + + + + + + + + + font-weight: bold; + + + Filesize + + + + + + + + + + + + + + font-weight: bold; + + + Health + + + + + + + + + + + + + + font-weight: bold; + + + Infohash + + + + + + + + + + + + + + font-weight: bold; + + + Availability + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 26 + + + + + 600000 + 26 + + + + background-color: white; +margin-right: 10px; + + + + + + + + font-weight: bold; + + + Progress + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + + + + + + Files + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QTreeWidget { +border: none; +font-size: 13px; +} +QTreeWidget::item { +color: white; +border-bottom: 1px solid #303030; +} +QTreeWidget::item:hover { +background-color: #303030; +} +QTreeWidget::item::selected { +background-color: #444; +} +QHeaderView { +background-color: transparent; +} +QHeaderView::section { +background-color: transparent; +border: none; +color: #B5B5B5; +padding: 10px; +font-size: 14px; +border-bottom: 1px solid #303030; +} +QHeaderView::drop-down { +color: red; +} +QHeaderView::section:hover { +color: white; +} +QTableCornerButton::section { +background-color: transparent; +} + + + false + + + QAbstractItemView::SingleSelection + + + 0 + + + true + + + false + + + false + + + + PATH + + + + + SIZE + + + + + % DONE + + + + + INCLUDED + + + + + + + + + Trackers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QTreeWidget { +border: none; +font-size: 13px; +} +QTreeWidget::item { +color: white; +border-bottom: 1px solid #303030; +} +QTreeWidget::item:hover { +background-color: #303030; +} +QTreeWidget::item::selected { +background-color: #444; +} +QHeaderView { +background-color: transparent; +} +QHeaderView::section { +background-color: transparent; +border: none; +color: #B5B5B5; +padding: 10px; +font-size: 14px; +border-bottom: 1px solid #303030; +} +QHeaderView::drop-down { +color: red; +} +QHeaderView::section:hover { +color: white; +} +QTableCornerButton::section { +background-color: transparent; +} + + + false + + + QAbstractItemView::NoSelection + + + 0 + + + + NAME + + + + + STATUS + + + + + PEERS + + + + + + + + + Peers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QTreeWidget { +border: none; +font-size: 13px; +} +QTreeWidget::item { +color: white; +border-bottom: 1px solid #303030; +} +QTreeWidget::item:hover { +background-color: #303030; +} + QTreeWidget::item::selected { + background-color: #444; + } + QHeaderView { + background-color: transparent; + } + QHeaderView::section { + background-color: transparent; + border: none; + color: #B5B5B5; + padding: 10px; + font-size: 14px; + border-bottom: 1px solid #303030; + } + QHeaderView::drop-down { + color: red; + } + QHeaderView::section:hover { + color: white; + } + QTableCornerButton::section { + background-color: transparent; + } + + + false + + + QAbstractItemView::NoSelection + + + 0 + + + true + + + + PEER (IP/PORT) + + + + + COMPLETED + + + + + ↓ SPEED + + + + + ↑ SPEED + + + + + FLAGS + + + + + CLIENT + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; +border-image: url(images/page_back.png) 0 0 0 0 stretch stretch; + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; + + + My Playlist + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + font-size: 14px; +color: #B5B5B5; + + + 23 items + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item:selected { +background-color: #404040; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +border-top: 1px solid #555; +} + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border: none; + + + QPainter::Antialiasing|QPainter::HighQualityAntialiasing|QPainter::TextAntialiasing + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + color: white; font-size: 18px; + + + Discovering your first content... + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 200 + + + + + 16777215 + 200 + + + + border: none; + + + + + + + + 0 + 0 + + + + color: white; font-size: 14px; + + + This process might take around a minute. + + + Qt::AlignCenter + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + color: #eee; +background-color: transparent; +font-size: 20px; +font-weight: bold; +margin: 10px; + + + Discovered channels + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + font-size: 14px; +color: #B5B5B5; + + + 0 items + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + QListWidget::item:hover { +background-color: #303030; +} +QListWidget::item { +border-bottom: 1px solid #303030; +} +QListWidget { +border: none; +border-top: 1px solid #555; +} + + + QAbstractItemView::ScrollPerPixel + + + + + + + + + + + + + + + + EllipseButton + QToolButton +
TriblerGUI.widgets.ellipsebutton.h
+
+ + VideoPlayerPage + QWidget +
TriblerGUI.widgets.videoplayerpage.h
+ 1 +
+ + VideoPlayerVolumeContainer + QWidget +
TriblerGUI.widgets.videoplayervolumecontainer.h
+ 1 +
+ + VideoPlayerPositionSlider + QSlider +
TriblerGUI.widgets.videoplayerpositionslider.h
+
+ + EditChannelPage + QWidget +
TriblerGUI.widgets.editchannelpage.h
+ 1 +
+ + UnderlineTabButton + QToolButton +
TriblerGUI.widgets.underlinetabbutton.h
+
+ + TabButtonPanel + QWidget +
TriblerGUI.widgets.tabbuttonpanel.h
+ 1 +
+ + SettingsPage + QWidget +
TriblerGUI.widgets.settingspage.h
+ 1 +
+ + SearchResultsPage + QWidget +
TriblerGUI.widgets.searchresultspage.h
+ 1 +
+ + DownloadsPage + QWidget +
TriblerGUI.widgets.downloadspage.h
+ 1 +
+ + LazyLoadList + QListWidget +
TriblerGUI.widgets.lazyloadlist.h
+
+ + SubscribedChannelsPage + QWidget +
TriblerGUI.widgets.subscribedchannelspage.h
+ 1 +
+ + ChannelPage + QWidget +
TriblerGUI.widgets.channelpage.h
+ 1 +
+ + LeftMenuPlaylist + QListWidget +
TriblerGUI.widgets.leftmenuplaylist.h
+
+ + HomePage + QWidget +
TriblerGUI.widgets.homepage.h
+ 1 +
+ + PlaylistPage + QWidget +
TriblerGUI.widgets.playlistpage.h
+ 1 +
+ + DownloadsDetailsTabWidget + QTabWidget +
TriblerGUI.widgets.downloadsdetailstabwidget.h
+ 1 +
+ + ManagePlaylistPage + QWidget +
TriblerGUI.widgets.manageplaylistpage.h
+ 1 +
+ + CreateTorrentPage + QWidget +
TriblerGUI.widgets.createtorrentpage.h
+ 1 +
+ + LoadingPage + QWidget +
TriblerGUI.widgets.loadingpage.h
+ 1 +
+ + SubscriptionsWidget + QWidget +
TriblerGUI.widgets.subscriptionswidget.h
+ 1 +
+ + DiscoveringPage + QWidget +
TriblerGUI.widgets.discoveringpage.h
+ 1 +
+ + DiscoveredPage + QWidget +
TriblerGUI.widgets.discoveredpage.h
+ 1 +
+ + CircleButton + QToolButton +
TriblerGUI.widgets.circlebutton.h
+
+ + DownloadProgressBar + QWidget +
TriblerGUI.widgets.downloadprogressbar.h
+ 1 +
+ + VideoPlayerWidget + QWidget +
TriblerGUI.widgets.videoplayerwidget.h
+ 1 +
+ + VideoPlayerInfoButton + QToolButton +
TriblerGUI.widgets.videoplayerinfobutton.h
+
+
+ + + + top_search_bar + returnPressed() + MainWindow + on_top_search_button_click() + + + 308 + 24 + + + 427 + 317 + + + + + add_torrent_button + clicked() + MainWindow + on_add_torrent_button_click() + + + 826 + 24 + + + 427 + 317 + + + + + top_menu_button + clicked() + MainWindow + on_top_menu_button_click() + + + 17 + 24 + + + 427 + 317 + + + + + search_results_list + itemClicked(QListWidgetItem*) + MainWindow + on_channel_item_click(QListWidgetItem*) + + + 250 + 65 + + + 427 + 317 + + + + + subscribed_channels_list + itemClicked(QListWidgetItem*) + MainWindow + on_channel_item_click(QListWidgetItem*) + + + 250 + 65 + + + 427 + 317 + + + + + left_menu_button_home + clicked() + MainWindow + clicked_menu_button_home() + + + 100 + 77 + + + 427 + 317 + + + + + left_menu_button_my_channel + clicked() + MainWindow + clicked_menu_button_my_channel() + + + 100 + 121 + + + 427 + 317 + + + + + left_menu_button_video_player + clicked() + MainWindow + clicked_menu_button_video_player() + + + 100 + 237 + + + 427 + 317 + + + + + left_menu_button_downloads + clicked() + MainWindow + clicked_menu_button_downloads() + + + 100 + 197 + + + 427 + 317 + + + + + left_menu_button_settings + clicked() + MainWindow + clicked_menu_button_settings() + + + 100 + 277 + + + 427 + 317 + + + + + left_menu_button_subscriptions + clicked() + MainWindow + clicked_menu_button_subscriptions() + + + 100 + 157 + + + 427 + 317 + + + + + top_search_bar + textChanged(QString) + MainWindow + on_search_text_change() + + + 308 + 24 + + + 427 + 317 + + + + + channel_back_button + clicked() + MainWindow + on_page_back_clicked() + + + 218 + 75 + + + 427 + 317 + + + + + channel_torrents_list + itemClicked(QListWidgetItem*) + MainWindow + on_playlist_item_click(QListWidgetItem*) + + + 527 + 357 + + + 427 + 317 + + + + + playlist_back_button + clicked() + MainWindow + on_page_back_clicked() + + + 221 + 75 + + + 427 + 317 + + + + + edit_channel_button + clicked() + MainWindow + on_edit_channel_clicked() + + + 834 + 75 + + + 427 + 317 + + + + + left_menu_button_discovered + clicked() + MainWindow + clicked_menu_button_discovered() + + + 100 + 117 + + + 427 + 327 + + + + + discovered_channels_list + itemClicked(QListWidgetItem*) + MainWindow + on_channel_item_click(QListWidgetItem*) + + + 527 + 366 + + + 427 + 327 + + + + + left_menu_button_debug + clicked() + MainWindow + clicked_menu_button_debug() + + + 94 + 327 + + + 427 + 327 + + + + + + on_top_search_button_click() + on_add_torrent_button_click() + on_top_menu_button_click() + on_channel_item_click(QListWidgetItem*) + clicked_menu_button_home() + clicked_menu_button_my_channel() + clicked_menu_button_video_player() + clicked_menu_button_downloads() + clicked_menu_button_settings() + clicked_menu_button_subscriptions() + on_search_text_change() + on_page_back_clicked() + on_playlist_item_click(QListWidgetItem*) + on_edit_channel_clicked() + clicked_menu_button_discovered() + clicked_menu_button_debug() + +
diff --git a/TriblerGUI/qt_resources/playlist_list_item.ui b/TriblerGUI/qt_resources/playlist_list_item.ui new file mode 100644 index 00000000000..fd77e4ae63b --- /dev/null +++ b/TriblerGUI/qt_resources/playlist_list_item.ui @@ -0,0 +1,414 @@ + + + Form + + + + 0 + 0 + 585 + 60 + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + PointingHandCursor + + + Form + + + QWidget { +background-color: #666; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 60 + 42 + + + + + 60 + 42 + + + + PO + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + 4 + + + + + + 0 + 0 + + + + + 54 + 18 + + + + + 54 + 18 + + + + border: 1px solid #B5B5B5; +border-radius: 9px; +color: #B5B5B5; +font-size: 12px; +background-color: transparent; + + + playlist + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + color: #eee; +border: none; +background-color: transparent; +font-size: 15px; + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + color: #b5b5b5; +border: none; +background-color: transparent; +font-size: 15px; + + + 34 items + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + background: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 28 + 28 + + + + + 28 + 28 + + + + border-radius: 14px; + + + + + + + ../images/edit_white.png../images/edit_white.png + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + + 28 + 28 + + + + + 28 + 28 + + + + PointingHandCursor + + + border-radius: 14px; + + + + + + + ../images/delete.png../images/delete.png + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 14 + 20 + + + + + + + + + + + + CircleButton + QToolButton +
TriblerGUI.widgets.circlebutton.h
+
+ + ThumbnailWidget + QLabel +
TriblerGUI.widgets.thumbnailwidget.h
+
+
+ + +
diff --git a/TriblerGUI/qt_resources/startdownloaddialog.ui b/TriblerGUI/qt_resources/startdownloaddialog.ui new file mode 100644 index 00000000000..0610ca39d6a --- /dev/null +++ b/TriblerGUI/qt_resources/startdownloaddialog.ui @@ -0,0 +1,509 @@ + + + Form + + + + 0 + 0 + 538 + 482 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + ArrowCursor + + + Form + + + false + + + QWidget { +background-color: #333333; +border-radius: 2px; +} +QToolButton { +border: 1px solid #B5B5B5; +border-radius: 12px; +color: white; +padding-left: 4px; +padding-right: 4px; +} +QToolButton::hover { +border: 1px solid white; +color: white; +} + + + + 0 + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + QLayout::SetMinimumSize + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 0 + 20 + + + + + 16777215 + 20 + + + + font-size: 14px; +color: white; + + + Download torrent + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 200 + + + + color: #B5B5B5; + + + my_fancy_torrent + + + true + + + + + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + background-color: #444; +border: none; +color: #C0C0C0; +padding: 4px; + + + Destination directory + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + Browse + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 0 + 240 + + + + + 16777215 + 240 + + + + QTreeWidget { +border: none; +font-size: 13px; +background-color: #444; +} +QTreeWidget::item { +color: white; +} +QHeaderView { +background-color: transparent; +} +QHeaderView::section { +background-color: transparent; +border: none; +padding-left: 10px; +height: 26px; +color: #B5B5B5; +font-size: 14px; +border-bottom: 1px solid #303030; +} +QTableCornerButton::section { +background-color: transparent; +} + + + QAbstractItemView::NoSelection + + + 0 + + + 150 + + + true + + + + Name + + + + + Size + + + + + Included? + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + 16777215 + 40 + + + + color: white; + + + Loading files... + + + Qt::AlignCenter + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + color: #B5B5B5; + + + Download anonymous using proxies + + + + + + + true + + + color: #B5B5B5; + + + Seed encrypted using proxies + + + false + + + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + Cancel + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + PointingHandCursor + + + + + + Download + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + diff --git a/TriblerGUI/qt_resources/text_list_item.ui b/TriblerGUI/qt_resources/text_list_item.ui new file mode 100644 index 00000000000..9a7cc679133 --- /dev/null +++ b/TriblerGUI/qt_resources/text_list_item.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 585 + 60 + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 16777215 + 60 + + + + ArrowCursor + + + Form + + + false + + + background-color: transparent; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + color: #eee; +font-size: 14px; + + + This is some fancy text + + + Qt::AlignCenter + + + + + + + + diff --git a/TriblerGUI/qt_resources/video_file_list_item.ui b/TriblerGUI/qt_resources/video_file_list_item.ui new file mode 100644 index 00000000000..527eb585088 --- /dev/null +++ b/TriblerGUI/qt_resources/video_file_list_item.ui @@ -0,0 +1,106 @@ + + + Form + + + + 0 + 0 + 717 + 60 + + + + + 0 + 0 + + + + + 0 + 21 + + + + + 16777215 + 16777215 + + + + PointingHandCursor + + + Form + + + QWidget { +background-color: #666; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 3 + 6 + + + + + + + + + 0 + 0 + + + + color: #eee; +background-color: transparent; +font-size: 16px; + + + TextLabel + + + true + + + + + + + + + + diff --git a/TriblerGUI/qt_resources/video_info_popup.ui b/TriblerGUI/qt_resources/video_info_popup.ui new file mode 100644 index 00000000000..dc585302b6d --- /dev/null +++ b/TriblerGUI/qt_resources/video_info_popup.ui @@ -0,0 +1,130 @@ + + + Form + + + + 0 + 0 + 244 + 88 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + ArrowCursor + + + Form + + + false + + + QWidget { +background-color: #444; +border-radius: 4px; +} +QLabel { +color: white; +border-radius: 0px; +} + + + + + + + 0 + 0 + + + + + 0 + 16 + + + + + 16777215 + 16 + + + + Speed: 341KB/s ... + + + + + + + + 0 + 0 + + + + + 0 + 16 + + + + + 16777215 + 16 + + + + Pre-buffering progress: + + + + + + + + 0 + 0 + + + + + 0 + 16 + + + + + 16777215 + 16 + + + + Peers: 3S 5L + + + + + + + + diff --git a/TriblerGUI/scripts/__init__.py b/TriblerGUI/scripts/__init__.py new file mode 100644 index 00000000000..ef5b65c1b06 --- /dev/null +++ b/TriblerGUI/scripts/__init__.py @@ -0,0 +1,3 @@ +""" +This package contains various utility scripts. +""" diff --git a/TriblerGUI/scripts/start_fake_core.py b/TriblerGUI/scripts/start_fake_core.py new file mode 100644 index 00000000000..43d032dcd82 --- /dev/null +++ b/TriblerGUI/scripts/start_fake_core.py @@ -0,0 +1,26 @@ +import logging + + +def start_fake_core(port): + from twisted.internet import reactor + from twisted.web.server import Site + + from FakeTriblerAPI.endpoints.root_endpoint import RootEndpoint + from FakeTriblerAPI.tribler_data import TriblerData + import FakeTriblerAPI.tribler_utils as tribler_utils + + def generate_tribler_data(): + tribler_utils.tribler_data = TriblerData() + tribler_utils.tribler_data.generate() + + logging.basicConfig() + logger = logging.getLogger(__file__) + logger.setLevel(logging.INFO) + + logger.info("Generating random Tribler data") + generate_tribler_data() + + site = Site(RootEndpoint()) + logger.info("Starting fake Tribler API on port %d", port) + reactor.listenTCP(port, site) + reactor.run() diff --git a/TriblerGUI/single_application.py b/TriblerGUI/single_application.py new file mode 100644 index 00000000000..f2cd3d8cc05 --- /dev/null +++ b/TriblerGUI/single_application.py @@ -0,0 +1,117 @@ +# Copied and modified from http://stackoverflow.com/a/12712362/605356 + +import logging, sys + +from PyQt5.QtCore import pyqtSignal, QTextStream, Qt +from PyQt5.QtNetwork import QLocalSocket, QLocalServer +from PyQt5.QtWidgets import QApplication + +LOGVARSTR = "%25s = '%s'" + + +class QtSingleApplication(QApplication): + """ + This class makes sure that we can only start one Tribler application. + When a user tries to open a second Tribler instance, the current active one will be brought to front. + """ + + messageReceived = pyqtSignal(unicode) + + def __init__(self, win_id, *argv): + + logfunc = logging.info + logfunc(sys._getframe().f_code.co_name + '()') + + QApplication.__init__(self, *argv) + + self._id = win_id + self._activation_window = None + self._activate_on_message = False + + # Is there another instance running? + self._outSocket = QLocalSocket() + self._outSocket.connectToServer(self._id) + self._isRunning = self._outSocket.waitForConnected() + + self._outStream = None + self._inSocket = None + self._inStream = None + self._server = None + + if self._isRunning: + # Yes, there is. + self._outStream = QTextStream(self._outSocket) + self._outStream.setCodec('UTF-8') + else: + # No, there isn't, at least not properly. + # Cleanup any past, crashed server. + error = self._outSocket.error() + logfunc(LOGVARSTR % ('self._outSocket.error()', error)) + if error == QLocalSocket.ConnectionRefusedError: + logfunc('received QLocalSocket.ConnectionRefusedError; ' + \ + 'removing server.') + self.close() + QLocalServer.removeServer(self._id) + self._outSocket = None + self._server = QLocalServer() + self._server.listen(self._id) + self._server.newConnection.connect(self._on_new_connection) + + logfunc(sys._getframe().f_code.co_name + '(): returning') + + def close(self): + logfunc = logging.info + logfunc(sys._getframe().f_code.co_name + '()') + if self._inSocket: + self._inSocket.disconnectFromServer() + if self._outSocket: + self._outSocket.disconnectFromServer() + if self._server: + self._server.close() + logfunc(sys._getframe().f_code.co_name + '(): returning') + + def is_running(self): + return self._isRunning + + def get_id(self): + return self._id + + def activation_window(self): + return self._activation_window + + def set_activation_window(self, activation_window, activate_on_message=True): + self._activation_window = activation_window + self._activate_on_message = activate_on_message + + def activate_window(self): + if not self._activation_window: + return + self._activation_window.setWindowState( + self._activation_window.windowState() & ~Qt.WindowMinimized) + self._activation_window.raise_() + + def send_message(self, msg): + if not self._outStream: + return False + self._outStream << msg << '\n' + self._outStream.flush() + return self._outSocket.waitForBytesWritten() + + def _on_new_connection(self): + if self._inSocket: + self._inSocket.readyRead.disconnect(self._on_ready_read) + self._inSocket = self._server.nextPendingConnection() + if not self._inSocket: + return + self._inStream = QTextStream(self._inSocket) + self._inStream.setCodec('UTF-8') + self._inSocket.readyRead.connect(self._on_ready_read) + if self._activate_on_message: + self.activate_window() + + def _on_ready_read(self): + while True: + msg = self._inStream.readLine() + if not msg: + break + self.messageReceived.emit(msg) diff --git a/TriblerGUI/tribler_action_menu.py b/TriblerGUI/tribler_action_menu.py new file mode 100644 index 00000000000..3c60b418da7 --- /dev/null +++ b/TriblerGUI/tribler_action_menu.py @@ -0,0 +1,30 @@ +from PyQt5.QtWidgets import QMenu + + +class TriblerActionMenu(QMenu): + """ + This menu is displayed when a user right-clicks some items in Tribler, i.e. a download widget. + Overrides QMenu to provide some custom CSS rules. + """ + + def __init__(self, parent): + QMenu.__init__(self, parent) + + self.setStyleSheet(""" + QMenu { + background-color: #404040; + } + + QMenu::item { + color: #D0D0D0; + padding: 5px; + } + + QMenu::item:selected { + background-color: #707070; + } + + QMenu::item:disabled { + color: #999999; + } + """) diff --git a/TriblerGUI/tribler_app.py b/TriblerGUI/tribler_app.py new file mode 100644 index 00000000000..19c9ef5cc7e --- /dev/null +++ b/TriblerGUI/tribler_app.py @@ -0,0 +1,38 @@ +import os + +from PyQt5.QtCore import QEvent +from TriblerGUI.single_application import QtSingleApplication + + +class TriblerApplication(QtSingleApplication): + """ + This class represents the main Tribler application. + """ + def __init__(self, app_name, args): + QtSingleApplication.__init__(self, app_name, args) + self.messageReceived.connect(self.on_app_message) + + def on_app_message(self, msg): + if msg.startswith('file') or msg.startswith('magnet'): + self.handle_uri(msg) + + def handle_uri(self, uri): + if not self.activation_window().tribler_started: + self.activation_window().pending_uri_requests.append(uri) + else: + if uri.startswith('file'): + self.activation_window().on_selected_torrent_file(uri[5:]) + elif uri.startswith('magnet'): + self.activation_window().on_added_magnetlink(uri) + + def parse_sys_args(self, args): + for arg in args[1:]: + if os.path.exists(arg): + self.handle_uri('file:%s' % arg) + elif arg.startswith('magnet'): + self.handle_uri(arg) + + def event(self, event): + if event.type() == QEvent.FileOpen and event.file().endswith(".torrent"): + self.handle_uri('file:%s' % event.file()) + return QtSingleApplication.event(self, event) diff --git a/TriblerGUI/tribler_request_manager.py b/TriblerGUI/tribler_request_manager.py new file mode 100644 index 00000000000..6843f0629c5 --- /dev/null +++ b/TriblerGUI/tribler_request_manager.py @@ -0,0 +1,117 @@ +import json +import logging +import random +import string +from time import time + +from PyQt5.QtCore import QUrl, pyqtSignal, QIODevice, QBuffer +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest +from TriblerGUI.defs import BUTTON_TYPE_NORMAL +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog + +API_PORT = 8085 +performed_requests = {} + + +class TriblerRequestManager(QNetworkAccessManager): + """ + This class is responsible for all the requests made to the Tribler REST API. + """ + window = None + + received_json = pyqtSignal(object, int) + received_file = pyqtSignal(str, object) + + def __init__(self): + QNetworkAccessManager.__init__(self) + self.request_id = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + self.base_url = "http://localhost:%d/" % API_PORT + self.reply = None + self.error_dialog = None + + def perform_request(self, endpoint, read_callback, data="", method='GET', capture_errors=True): + """ + Perform a HTTP request. + :param endpoint: the endpoint to call (i.e. "statistics") + :param read_callback: the callback to be called with result info when we have the data + :param data: optional POST data to be sent with the request + :param method: the HTTP verb (GET/POST/PUT/PATCH) + :param capture_errors: whether errors should be handled by this class (defaults to True) + """ + performed_requests[self.request_id] = [endpoint, method, data, time(), -1] + url = self.base_url + endpoint + + if method == 'GET': + buf = QBuffer() + buf.setData(data) + buf.open(QIODevice.ReadOnly) + get_request = QNetworkRequest(QUrl(url)) + self.reply = self.sendCustomRequest(get_request, "GET", buf) + buf.setParent(self.reply) + elif method == 'PATCH': + buf = QBuffer() + buf.setData(data) + buf.open(QIODevice.ReadOnly) + patch_request = QNetworkRequest(QUrl(url)) + self.reply = self.sendCustomRequest(patch_request, "PATCH", buf) + buf.setParent(self.reply) + elif method == 'PUT': + request = QNetworkRequest(QUrl(url)) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") + self.reply = self.put(request, data) + elif method == 'DELETE': + buf = QBuffer() + buf.setData(data) + buf.open(QIODevice.ReadOnly) + delete_request = QNetworkRequest(QUrl(url)) + self.reply = self.sendCustomRequest(delete_request, "DELETE", buf) + buf.setParent(self.reply) + elif method == 'POST': + request = QNetworkRequest(QUrl(url)) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") + self.reply = self.post(request, data) + + if read_callback: + self.received_json.connect(read_callback) + + self.finished.connect(lambda reply: self.on_finished(reply, capture_errors)) + + def on_finished(self, reply, capture_errors): + performed_requests[self.request_id][4] = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + data = reply.readAll() + try: + json_result = json.loads(str(data)) + + if 'error' in json_result and capture_errors: + if isinstance(json_result['error'], (str, unicode)): + self.show_error(json_result['error']) + elif 'message' in json_result['error']: + self.show_error(json_result['error']['message']) + else: + self.received_json.emit(json_result, reply.error()) + except ValueError as ex: + self.received_json.emit(None, reply.error()) + logging.exception(ex) + + def download_file(self, endpoint, read_callback): + url = self.base_url + endpoint + self.reply = self.get(QNetworkRequest(QUrl(url))) + self.received_file.connect(read_callback) + self.finished.connect(self.on_file_download_finished) + + def on_file_download_finished(self, reply): + content_header = str(reply.rawHeader("Content-Disposition")) + data = reply.readAll() + self.received_file.emit(content_header.split("=")[1], data) + + def show_error(self, error_text): + main_text = "An error occurred during the request:\n\n%s" % error_text + self.error_dialog = ConfirmationDialog(TriblerRequestManager.window, "Request error", + main_text, [('close', BUTTON_TYPE_NORMAL)]) + self.error_dialog.button_clicked.connect(self.on_error_dialog_cancel_clicked) + self.error_dialog.show() + + def on_error_dialog_cancel_clicked(self, _): + self.error_dialog.setParent(None) + self.error_dialog = None diff --git a/TriblerGUI/tribler_window.py b/TriblerGUI/tribler_window.py new file mode 100644 index 00000000000..be50455140b --- /dev/null +++ b/TriblerGUI/tribler_window.py @@ -0,0 +1,558 @@ +import glob +import logging +import sys +import traceback +from urllib import quote_plus +import signal + +from PyQt5 import uic +from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel, QSettings, QPoint, QCoreApplication, pyqtSlot, QUrl, QObject +from PyQt5.QtGui import QIcon, QDesktopServices +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QMainWindow, QLineEdit, QTreeWidget, QSystemTrayIcon, QAction, QFileDialog, \ + QCompleter, QApplication, QStyledItemDelegate, QListWidget + +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.core_manager import CoreManager +from TriblerGUI.debug_window import DebugWindow +from TriblerGUI.defs import PAGE_SEARCH_RESULTS, \ + PAGE_HOME, PAGE_EDIT_CHANNEL, PAGE_VIDEO_PLAYER, PAGE_DOWNLOADS, PAGE_SETTINGS, PAGE_SUBSCRIBED_CHANNELS, \ + PAGE_CHANNEL_DETAILS, PAGE_PLAYLIST_DETAILS, BUTTON_TYPE_NORMAL, BUTTON_TYPE_CONFIRM, PAGE_LOADING,\ + PAGE_DISCOVERING, PAGE_DISCOVERED +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.dialogs.feedbackdialog import FeedbackDialog +from TriblerGUI.dialogs.startdownloaddialog import StartDownloadDialog +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_ui_file_path, get_image_path, get_gui_setting + + + + +# Pre-load form UI classes +fc_channel_torrent_list_item, _ = uic.loadUiType(get_ui_file_path('channel_torrent_list_item.ui')) +fc_channel_list_item, _ = uic.loadUiType(get_ui_file_path('channel_list_item.ui')) +fc_playlist_list_item, _ = uic.loadUiType(get_ui_file_path('playlist_list_item.ui')) +fc_home_recommended_item, _ = uic.loadUiType(get_ui_file_path('home_recommended_item.ui')) +fc_loading_list_item, _ = uic.loadUiType(get_ui_file_path('loading_list_item.ui')) + + +class MagnetHandler(QObject): + + def __init__(self, window): + QObject.__init__(self) + self.window = window + + @pyqtSlot(QUrl) + def on_open_magnet_link(self, url): + self.window.on_added_magnetlink(url) + + +class TriblerWindow(QMainWindow): + + resize_event = pyqtSignal() + escape_pressed = pyqtSignal() + received_search_completions = pyqtSignal(object) + + def on_exception(self, *exc_info): + # Stop the download loop + self.downloads_page.stop_loading_downloads() + + if not self.core_manager.shutting_down: + self.core_manager.stop() + + self.setHidden(True) + + if self.debug_window: + self.debug_window.setHidden(True) + + exception_text = "".join(traceback.format_exception(*exc_info)) + logging.error(exception_text) + + if not self.feedback_dialog_is_open: + dialog = FeedbackDialog(self, exception_text, self.core_manager.events_manager.tribler_version) + self.feedback_dialog_is_open = True + _ = dialog.exec_() + + def __init__(self, api_port=8085): + QMainWindow.__init__(self) + + self.api_port = api_port + self.navigation_stack = [] + self.feedback_dialog_is_open = False + self.tribler_started = False + self.tribler_settings = None + self.debug_window = None + self.core_manager = CoreManager(self.api_port) + self.pending_requests = {} + self.pending_uri_requests = [] + self.download_uri = None + self.dialog = None + self.request_mgr = None + self.search_request_mgr = None + self.search_suggestion_mgr = None + self.selected_torrent_files = [] + + sys.excepthook = self.on_exception + + uic.loadUi(get_ui_file_path('mainwindow.ui'), self) + TriblerRequestManager.window = self + + self.magnet_handler = MagnetHandler(self.window) + QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link") + + QCoreApplication.setOrganizationDomain("nl") + QCoreApplication.setOrganizationName("TUDelft") + QCoreApplication.setApplicationName("Tribler") + QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + + self.read_settings() + + # Remove the focus rect on OS X + for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget): + widget.setAttribute(Qt.WA_MacShowFocusRect, 0) + + self.menu_buttons = [self.left_menu_button_home, self.left_menu_button_my_channel, + self.left_menu_button_subscriptions, self.left_menu_button_video_player, + self.left_menu_button_settings, self.left_menu_button_downloads, + self.left_menu_button_discovered] + + self.video_player_page.initialize_player() + self.search_results_page.initialize_search_results_page() + self.settings_page.initialize_settings_page() + self.subscribed_channels_page.initialize() + self.edit_channel_page.initialize_edit_channel_page() + self.downloads_page.initialize_downloads_page() + self.home_page.initialize_home_page() + self.loading_page.initialize_loading_page() + self.discovering_page.initialize_discovering_page() + self.discovered_page.initialize_discovered_page() + + self.stackedWidget.setCurrentIndex(PAGE_LOADING) + + # Create the system tray icon + if QSystemTrayIcon.isSystemTrayAvailable(): + self.tray_icon = QSystemTrayIcon() + self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png')))) + self.tray_icon.show() + + self.hide_left_menu_playlist() + self.left_menu_button_debug.setHidden(True) + self.top_menu_button.setHidden(True) + self.left_menu.setHidden(True) + self.add_torrent_button.setHidden(True) + self.top_search_bar.setHidden(True) + + # Set various icons + self.top_menu_button.setIcon(QIcon(get_image_path('menu.png'))) + + self.search_completion_model = QStringListModel() + completer = QCompleter() + completer.setModel(self.search_completion_model) + completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + self.item_delegate = QStyledItemDelegate() + completer.popup().setItemDelegate(self.item_delegate) + completer.popup().setStyleSheet(""" + QListView { + background-color: #404040; + } + + QListView::item { + color: #D0D0D0; + padding-top: 5px; + padding-bottom: 5px; + } + + QListView::item:hover { + background-color: #707070; + } + """) + self.top_search_bar.setCompleter(completer) + + # Toggle debug if developer mode is enabled + self.window().left_menu_button_debug.setHidden( + not get_gui_setting(self.gui_settings, "debug", False, is_bool=True)) + + self.core_manager.start() + + self.core_manager.events_manager.received_search_result_channel.connect( + self.search_results_page.received_search_result_channel) + self.core_manager.events_manager.received_search_result_torrent.connect( + self.search_results_page.received_search_result_torrent) + self.core_manager.events_manager.torrent_finished.connect(self.on_torrent_finished) + self.core_manager.events_manager.new_version_available.connect(self.on_new_version_available) + self.core_manager.events_manager.tribler_started.connect(self.on_tribler_started) + + # Install signal handler for ctrl+c events + def sigint_handler(*_): + self.close_tribler() + + signal.signal(signal.SIGINT, sigint_handler) + + self.installEventFilter(self.video_player_page) + + self.show() + + def on_torrent_finished(self, torrent_info): + self.window().tray_icon.showMessage("Download finished", "Download of %s has finished." % torrent_info["name"]) + + def show_loading_screen(self): + self.top_menu_button.setHidden(True) + self.left_menu.setHidden(True) + self.add_torrent_button.setHidden(True) + self.top_search_bar.setHidden(True) + self.stackedWidget.setCurrentIndex(PAGE_LOADING) + + def on_tribler_started(self): + self.tribler_started = True + + self.top_menu_button.setHidden(False) + self.left_menu.setHidden(False) + self.add_torrent_button.setHidden(False) + self.top_search_bar.setHidden(False) + + # fetch the variables, needed for the video player port + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("variables", self.received_variables) + + self.downloads_page.start_loading_downloads() + self.home_page.load_popular_torrents() + if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core: + self.window().gui_settings.setValue("first_discover", True) + self.discovering_page.is_discovering = True + self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING) + else: + self.clicked_menu_button_home() + + # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed) + for uri in self.pending_uri_requests: + if uri.startswith('file'): + self.on_selected_torrent_file(uri[5:]) + elif uri.startswith('magnet'): + self.on_added_magnetlink(uri) + + def perform_start_download_request(self, uri, anon_download, safe_seeding, selected_files, total_files=0): + selected_files_uri = "" + if len(selected_files) != total_files: # Not all files included + selected_files_uri = '&' + ''.join(u"selected_files[]=%s&" % file for + file in selected_files)[:-1].encode('utf-8') + + anon_hops = int(self.tribler_settings['Tribler']['default_number_hops']) if anon_download else 0 + safe_seeding = 1 if safe_seeding else 0 + post_data = str("uri=%s&anon_hops=%d&safe_seeding=%d%s" % (uri, anon_hops, safe_seeding, selected_files_uri)) + request_mgr = TriblerRequestManager() + self.pending_requests[request_mgr.request_id] = request_mgr + request_mgr.perform_request("downloads", self.on_download_added, method='PUT', data=post_data) + + def on_new_version_available(self, version): + if version == str(self.gui_settings.value('last_reported_version')): + return + + self.dialog = ConfirmationDialog(self, "New version available", + "Version %s of Tribler is available.Do you want to visit the website to " + "download the newest version?" % version, + [('ignore', BUTTON_TYPE_NORMAL), ('later', BUTTON_TYPE_NORMAL), + ('ok', BUTTON_TYPE_NORMAL)]) + self.dialog.button_clicked.connect(lambda action: self.on_new_version_dialog_done(version, action)) + self.dialog.show() + + def on_new_version_dialog_done(self, version, action): + if action == 0: # ignore + self.gui_settings.setValue("last_reported_version", version) + elif action == 2: # ok + import webbrowser + webbrowser.open("https://tribler.org") + + self.dialog.setParent(None) + self.dialog = None + + def read_settings(self): + self.gui_settings = QSettings() + center = QApplication.desktop().availableGeometry(self).center() + pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5)) + size = self.gui_settings.value("size", self.size()) + + self.move(pos) + self.resize(size) + + def on_search_text_change(self, text): + self.search_suggestion_mgr = TriblerRequestManager() + self.search_suggestion_mgr.perform_request( + "search/completions?q=%s" % text, self.on_received_search_completions) + + def on_received_search_completions(self, completions): + self.received_search_completions.emit(completions) + self.search_completion_model.setStringList(completions["completions"]) + + def received_variables(self, variables): + self.video_player_page.video_player_port = variables["variables"]["ports"]["video~port"] + self.fetch_settings() + + def fetch_settings(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("settings", self.received_settings) + + def received_settings(self, settings): + self.tribler_settings = settings['settings'] + + # Disable various components based on the settings + if not self.tribler_settings['search_community']['enabled']: + self.window().top_search_bar.setHidden(True) + if not self.tribler_settings['video']['enabled']: + self.left_menu_button_video_player.setHidden(True) + + def on_top_search_button_click(self): + self.deselect_all_menu_buttons() + self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS) + self.search_results_page.perform_search(self.top_search_bar.text()) + self.search_request_mgr = TriblerRequestManager() + self.search_request_mgr.perform_request("search?q=%s" % self.top_search_bar.text(), None) + + def on_add_torrent_button_click(self, pos): + menu = TriblerActionMenu(self) + + browse_files_action = QAction('Import torrent from file', self) + browse_directory_action = QAction('Import torrents from directory', self) + add_url_action = QAction('Import torrent from URL', self) + + browse_files_action.triggered.connect(self.on_add_torrent_browse_file) + browse_directory_action.triggered.connect(self.on_add_torrent_browse_dir) + add_url_action.triggered.connect(self.on_add_torrent_from_url) + + menu.addAction(browse_files_action) + menu.addAction(browse_directory_action) + menu.addAction(add_url_action) + + menu.exec_(self.mapToGlobal(self.add_torrent_button.pos())) + + def on_add_torrent_browse_file(self): + filename = QFileDialog.getOpenFileName(self, "Please select the .torrent file", "", "Torrent files (*.torrent)") + if len(filename[0]) > 0: + self.on_selected_torrent_file(filename[0]) + + def on_added_magnetlink(self, magnet_link): + self.download_uri = magnet_link + + self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri, self.download_uri) + self.dialog.button_clicked.connect(self.on_start_download_action) + self.dialog.show() + + def on_selected_torrent_file(self, filepath): + self.download_uri = u"file:%s" % filepath + + if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True): + self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri, filepath) + self.dialog.button_clicked.connect(self.on_start_download_action) + self.dialog.show() + else: + self.window().perform_start_download_request(self.download_uri, + get_gui_setting(self.gui_settings, + "default_anonymity_enabled", True, + is_bool=True), + get_gui_setting(self.gui_settings, + "default_safeseeding_enabled", True, + is_bool=True), [], 0) + + def on_start_download_action(self, action): + if action == 1: + self.window().perform_start_download_request(self.download_uri, + self.dialog.dialog_widget.anon_download_checkbox.isChecked(), + self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), + self.dialog.get_selected_files(), + self.dialog.dialog_widget.files_list_view.topLevelItemCount()) + + self.dialog.setParent(None) + self.dialog = None + + def on_add_torrent_browse_dir(self): + chosen_dir = QFileDialog.getExistingDirectory(self, "Please select the directory containing the .torrent files", + "", QFileDialog.ShowDirsOnly) + + if len(chosen_dir) != 0: + self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")] + self.dialog = ConfirmationDialog(self, "Add torrents from directory", + "Are you sure you want to add %d torrents to Tribler?" % + len(self.selected_torrent_files), + [('add', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(self.on_confirm_add_directory_dialog) + self.dialog.show() + + def on_confirm_add_directory_dialog(self, action): + if action == 0: + for torrent_file in self.selected_torrent_files: + escaped_uri = quote_plus((u"file:%s" % torrent_file).encode('utf-8')) + self.perform_start_download_request(escaped_uri, + get_gui_setting(self.gui_settings, + "default_anonymity_enabled", True, is_bool=True), + get_gui_setting(self.gui_settings, + "default_safeseeding_enabled", True, is_bool=True), + [], 0) + + self.dialog.setParent(None) + self.dialog = None + + def on_add_torrent_from_url(self): + self.dialog = ConfirmationDialog(self, "Add torrent from URL/magnet link", + "Please enter the URL/magnet link in the field below:", + [('add', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)], + show_input=True) + self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link') + self.dialog.button_clicked.connect(self.on_torrent_from_url_dialog_done) + self.dialog.show() + + def on_torrent_from_url_dialog_done(self, action): + self.download_uri = self.dialog.dialog_widget.dialog_input.text() + + # Remove first dialog + self.dialog.setParent(None) + self.dialog = None + + if action == 0: + self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri, self.download_uri) + self.dialog.button_clicked.connect(self.on_start_download_action) + self.dialog.show() + + def on_download_added(self, result): + self.window().left_menu_button_downloads.click() + + def on_top_menu_button_click(self): + if self.left_menu.isHidden(): + self.left_menu.show() + else: + self.left_menu.hide() + + def deselect_all_menu_buttons(self, except_select=None): + for button in self.menu_buttons: + if button == except_select: + button.setEnabled(False) + continue + button.setEnabled(True) + button.setChecked(False) + + def clicked_menu_button_home(self): + self.deselect_all_menu_buttons(self.left_menu_button_home) + self.stackedWidget.setCurrentIndex(PAGE_HOME) + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def clicked_menu_button_discovered(self): + self.deselect_all_menu_buttons(self.left_menu_button_discovered) + self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED) + self.discovered_page.load_discovered_channels() + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def clicked_menu_button_my_channel(self): + self.deselect_all_menu_buttons(self.left_menu_button_my_channel) + self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL) + self.edit_channel_page.load_my_channel_overview() + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def clicked_menu_button_video_player(self): + self.deselect_all_menu_buttons(self.left_menu_button_video_player) + self.stackedWidget.setCurrentIndex(PAGE_VIDEO_PLAYER) + self.navigation_stack = [] + self.show_left_menu_playlist() + + def clicked_menu_button_downloads(self): + self.deselect_all_menu_buttons(self.left_menu_button_downloads) + self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS) + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def clicked_menu_button_settings(self): + self.deselect_all_menu_buttons(self.left_menu_button_settings) + self.stackedWidget.setCurrentIndex(PAGE_SETTINGS) + self.settings_page.load_settings() + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def clicked_menu_button_debug(self): + self.debug_window = DebugWindow() + self.debug_window.show() + + def clicked_menu_button_subscriptions(self): + self.deselect_all_menu_buttons(self.left_menu_button_subscriptions) + self.subscribed_channels_page.load_subscribed_channels() + self.stackedWidget.setCurrentIndex(PAGE_SUBSCRIBED_CHANNELS) + self.navigation_stack = [] + self.hide_left_menu_playlist() + + def hide_left_menu_playlist(self): + self.left_menu_seperator.setHidden(True) + self.left_menu_playlist_label.setHidden(True) + self.left_menu_playlist.setHidden(True) + + def show_left_menu_playlist(self): + self.left_menu_seperator.setHidden(False) + self.left_menu_playlist_label.setHidden(False) + self.left_menu_playlist.setHidden(False) + + def on_channel_item_click(self, channel_list_item): + list_widget = channel_list_item.listWidget() + from TriblerGUI.widgets.channel_list_item import ChannelListItem + if isinstance(list_widget.itemWidget(channel_list_item), ChannelListItem): + channel_info = channel_list_item.data(Qt.UserRole) + self.channel_page.initialize_with_channel(channel_info) + self.navigation_stack.append(self.stackedWidget.currentIndex()) + self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_DETAILS) + + def on_playlist_item_click(self, playlist_list_item): + list_widget = playlist_list_item.listWidget() + from TriblerGUI.widgets.playlist_list_item import PlaylistListItem + if isinstance(list_widget.itemWidget(playlist_list_item), PlaylistListItem): + playlist_info = playlist_list_item.data(Qt.UserRole) + self.playlist_page.initialize_with_playlist(playlist_info) + self.navigation_stack.append(self.stackedWidget.currentIndex()) + self.stackedWidget.setCurrentIndex(PAGE_PLAYLIST_DETAILS) + + def on_page_back_clicked(self): + prev_page = self.navigation_stack.pop() + self.stackedWidget.setCurrentIndex(prev_page) + + def on_edit_channel_clicked(self): + self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL) + self.navigation_stack = [] + self.channel_page.on_edit_channel_clicked() + + def resizeEvent(self, _): + # Resize home page cells + cell_width = self.home_page_table_view.width() / 3 - 3 # We have some padding to the right + cell_height = cell_width / 2 + 60 + + for i in range(0, 3): + self.home_page_table_view.setColumnWidth(i, cell_width) + self.home_page_table_view.setRowHeight(i, cell_height) + + self.resize_event.emit() + + def exit_full_screen(self): + self.top_bar.show() + self.left_menu.show() + self.video_player_page.is_full_screen = False + self.showNormal() + + def close_tribler(self): + if not self.core_manager.shutting_down: + self.show_loading_screen() + + self.gui_settings.setValue("pos", self.pos()) + self.gui_settings.setValue("size", self.size()) + + if self.core_manager.use_existing_core: + # Don't close the core that we are using + QApplication.quit() + + self.core_manager.stop() + self.core_manager.shutting_down = True + self.downloads_page.stop_loading_downloads() + + def closeEvent(self, close_event): + self.close_tribler() + close_event.ignore() + + def keyReleaseEvent(self, event): + if event.key() == Qt.Key_Escape: + self.escape_pressed.emit() + if self.isFullScreen(): + self.exit_full_screen() diff --git a/TriblerGUI/utilities.py b/TriblerGUI/utilities.py new file mode 100644 index 00000000000..8edfa95beb7 --- /dev/null +++ b/TriblerGUI/utilities.py @@ -0,0 +1,200 @@ +from datetime import datetime +import hashlib +import os +import re +import sys + +import TriblerGUI +from TriblerGUI.defs import VIDEO_EXTS + + +def format_size(num, suffix='B'): + for unit in ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']: + if abs(num) < 1024.0: + return "%3.1f %s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f %s%s" % (num, 'Yi', suffix) + + +def format_speed(num): + return "%s/s" % format_size(num) + + +def seconds_to_string(seconds): + minutes = seconds / 60 + seconds_left = seconds % 60 + return "%d:%02d" % (minutes, seconds_left) + + +def string_to_minutes(time_str): + parts = time_str.split(":") + if len(parts) != 2: + raise ValueError("Invalid time string") + + hours = float(parts[0]) + minutes = float(parts[1]) + return hours * 60 + minutes + + +def timestamp_to_time(timestamp): + today = datetime.today() + discovered = datetime.fromtimestamp(timestamp) + + diff = today - discovered + if diff.days > 0 or today.day != discovered.day: + return discovered.strftime('%d-%m-%Y') + return discovered.strftime('Today %H:%M') + + +def get_color(name): + """ + This method deterministically determines a color of a given name. This is done by taking the MD5 hash of the text. + """ + md5_hash = hashlib.md5() + md5_hash.update(name.encode('utf-8')) + md5_str_hash = md5_hash.hexdigest() + + red = int(md5_str_hash[0:10], 16) % 128 + 100 + green = int(md5_str_hash[10:20], 16) % 128 + 100 + blue = int(md5_str_hash[20:30], 16) % 128 + 100 + + return '#%02x%02x%02x' % (red, green, blue) + + +def is_video_file(filename): + _, ext = os.path.splitext(filename) + if ext.startswith('.'): + ext = ext[1:] + return ext in VIDEO_EXTS + + +def pretty_date(time=False): + """ + Get a datetime object or a int() Epoch timestamp and return a + pretty string like 'an hour ago', 'Yesterday', '3 months ago', + 'just now', etc + """ + now = datetime.now() + if isinstance(time, int): + diff = now - datetime.fromtimestamp(time) + elif isinstance(time, datetime): + diff = now - time + elif not time: + diff = now - now + second_diff = diff.seconds + day_diff = diff.days + + if day_diff < 0: + return '' + + if day_diff == 0: + if second_diff < 10: + return "just now" + if second_diff < 60: + return str(second_diff) + " seconds ago" + if second_diff < 120: + return "a minute ago" + if second_diff < 3600: + return str(second_diff / 60) + " minutes ago" + if second_diff < 7200: + return "an hour ago" + if second_diff < 86400: + return str(second_diff / 3600) + " hours ago" + if day_diff == 1: + return "yesterday" + if day_diff < 7: + return str(day_diff) + " days ago" + if day_diff < 31: + return str(day_diff / 7) + " weeks ago" + if day_diff < 365: + return str(day_diff / 30) + " months ago" + return str(day_diff / 365) + " years ago" + + +def duration_to_string(seconds): + weeks = int(seconds / (60 * 60 * 24 * 7)) + seconds -= weeks * (60 * 60 * 24 * 7) + days = int(seconds / (60 * 60 * 24)) + seconds -= days * (60 * 60 * 24) + hours = int(seconds / (60 * 60)) + seconds -= hours * (60 * 60) + minutes = int(seconds / 60) + seconds -= minutes * 60 + seconds = int(seconds) + + if weeks > 0: + return "{}w {}d".format(weeks, days) + if days > 0: + return "{}d {}h".format(days, hours) + if hours > 0: + return "{}h {}m".format(hours, minutes) + if minutes > 0: + return "{}m {}s".format(minutes, seconds) + return "{}s".format(seconds) + + +def split_into_keywords(query): + RE_KEYWORD_SPLIT = re.compile(r"[\W_]", re.UNICODE) + return [kw for kw in RE_KEYWORD_SPLIT.split(query.lower()) if len(kw) > 0] + + +def get_base_path(): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.dirname(TriblerGUI.__file__) + return base_path + + +def is_frozen(): + """ + Return whether we are running in a frozen environment + """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + _ = sys._MEIPASS + except Exception: + return False + return True + + +def get_ui_file_path(filename): + return os.path.join(get_base_path(), 'qt_resources/%s' % filename) + + +def get_image_path(filename): + return os.path.join(get_base_path(), 'images/%s' % filename) + + +def bisect_right(item, item_list, is_torrent): + """ + This method inserts a channel/torrent in a sorted list. The sorting is based on relevance score. + The implementation is based on bisect_right. + """ + lo = 0 + hi = len(item_list) + while lo < hi: + mid = (lo+hi) // 2 + if item['relevance_score'] == item_list[mid]['relevance_score'] and is_torrent: + if len(split_into_keywords(item['name'])) < len(split_into_keywords(item_list[mid]['name'])): + hi = mid + else: + lo = mid + 1 + elif item['relevance_score'] > item_list[mid]['relevance_score']: + hi = mid + else: + lo = mid + 1 + return lo + + +def get_gui_setting(gui_settings, value, default, is_bool=False): + """ + Utility method to get a specific GUI setting. The is_bool flag defines whether we expect a boolean so we convert it + since on Windows, all values are saved as plain text. + """ + val = gui_settings.value(value, default) + if is_bool: + val = val == True or val == 'true' + return val diff --git a/TriblerGUI/widgets/__init__.py b/TriblerGUI/widgets/__init__.py new file mode 100644 index 00000000000..5ab1e96d6cf --- /dev/null +++ b/TriblerGUI/widgets/__init__.py @@ -0,0 +1,3 @@ +""" +This module contains the widgets used by the Qt GUI. +""" diff --git a/TriblerGUI/widgets/channel_list_item.py b/TriblerGUI/widgets/channel_list_item.py new file mode 100644 index 00000000000..a5f05f1beda --- /dev/null +++ b/TriblerGUI/widgets/channel_list_item.py @@ -0,0 +1,21 @@ +from PyQt5.QtWidgets import QWidget +from TriblerGUI.tribler_window import fc_channel_list_item + + +class ChannelListItem(QWidget, fc_channel_list_item): + """ + This class is responsible for managing the item in the list of channels. + The list item supports a fade-in effect, which can be enabled with the should_fade parameter in the constructor. + """ + + def __init__(self, parent, channel): + QWidget.__init__(self, parent) + fc_channel_list_item.__init__(self) + + self.setupUi(self) + + self.channel_info = channel + self.channel_name.setText(channel["name"]) + self.channel_description_label.setText("%d items" % channel["torrents"]) + + self.subscriptions_widget.initialize_with_channel(channel) diff --git a/TriblerGUI/widgets/channel_torrent_list_item.py b/TriblerGUI/widgets/channel_torrent_list_item.py new file mode 100644 index 00000000000..f1411c74a7a --- /dev/null +++ b/TriblerGUI/widgets/channel_torrent_list_item.py @@ -0,0 +1,171 @@ +from urllib import quote_plus +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget +from TriblerGUI.defs import STATUS_GOOD, STATUS_DEAD +from TriblerGUI.defs import STATUS_UNKNOWN + +from TriblerGUI.dialogs.startdownloaddialog import StartDownloadDialog +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.tribler_window import fc_channel_torrent_list_item +from TriblerGUI.utilities import format_size, get_image_path, get_gui_setting + + +class ChannelTorrentListItem(QWidget, fc_channel_torrent_list_item): + """ + This class is responsible for managing the item in the torrents list of a channel. + """ + + def __init__(self, parent, torrent, show_controls=False, on_remove_clicked=None): + QWidget.__init__(self, parent) + fc_channel_torrent_list_item.__init__(self) + + self.torrent_info = torrent + + self.setupUi(self) + self.show_controls = show_controls + self.remove_control_button_container.setHidden(True) + self.control_buttons_container.setHidden(True) + self.is_health_checking = False + self.has_health = False + self.health_request_mgr = None + self.request_mgr = None + self.download_uri = None + self.dialog = None + + self.channel_torrent_name.setText(torrent["name"]) + if torrent["size"] is None: + self.channel_torrent_description.setText("Size: -") + else: + self.channel_torrent_description.setText("Size: %s" % format_size(float(torrent["size"]))) + + if torrent["category"]: + self.channel_torrent_category.setText(torrent["category"]) + else: + self.channel_torrent_category.setText("Unknown") + self.thumbnail_widget.initialize(torrent["name"], 24) + + if torrent["last_tracker_check"] > 0: + self.update_health(int(torrent["num_seeders"]), int(torrent["num_leechers"])) + + self.torrent_play_button.clicked.connect(self.on_play_button_clicked) + self.torrent_download_button.clicked.connect(self.on_download_clicked) + + if on_remove_clicked is not None: + self.remove_torrent_button.clicked.connect(lambda: on_remove_clicked(self)) + + def on_download_clicked(self): + gui_settings = self.window().gui_settings + self.download_uri = quote_plus((u"magnet:?xt=urn:btih:%s&dn=%s" % + (self.torrent_info["infohash"], self.torrent_info['name'])).encode('utf-8')) + + if get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True): + self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri, self.torrent_info["name"]) + self.dialog.button_clicked.connect(self.on_start_download_action) + self.dialog.show() + else: + self.window().perform_start_download_request(self.download_uri, + get_gui_setting(gui_settings, "default_anonymity_enabled", + True, is_bool=True), + get_gui_setting(gui_settings, "default_safeseeding_enabled", + True, is_bool=True), [], 0) + + def on_start_download_action(self, action): + if action == 1: + self.window().perform_start_download_request(self.download_uri, + self.dialog.dialog_widget.anon_download_checkbox.isChecked(), + self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), + self.dialog.get_selected_files(), + self.dialog.dialog_widget.files_list_view.topLevelItemCount()) + self.dialog.setParent(None) + self.dialog = None + + def on_play_button_clicked(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % self.torrent_info["infohash"], + self.on_play_request_done, method='PUT') + + def on_play_request_done(self, result, response_code): + self.window().left_menu_button_video_player.click() + self.window().video_player_page.set_torrent_infohash(self.torrent_info["infohash"]) + self.window().left_menu_playlist.set_loading() + + def show_buttons(self): + if not self.show_controls: + self.remove_control_button_container.setHidden(True) + self.control_buttons_container.setHidden(False) + self.torrent_play_button.setIcon(QIcon(get_image_path('play.png'))) + self.torrent_download_button.setIcon(QIcon(get_image_path('downloads.png'))) + else: + self.control_buttons_container.setHidden(True) + self.remove_control_button_container.setHidden(False) + self.remove_torrent_button.setIcon(QIcon(get_image_path('delete.png'))) + + def hide_buttons(self): + self.remove_control_button_container.setHidden(True) + self.control_buttons_container.setHidden(True) + + def enterEvent(self, _): + self.show_buttons() + + def leaveEvent(self, _): + self.hide_buttons() + + def check_health(self): + """ + Perform a request to check the health of the torrent that is represented by this widget. + Don't do this if we are already checking the health or if we have the health info. + """ + if self.is_health_checking or self.has_health: # Don't check health again + return + + self.health_text.setText("checking health...") + self.set_health_indicator(STATUS_UNKNOWN) + self.is_health_checking = True + self.health_request_mgr = TriblerRequestManager() + self.health_request_mgr.perform_request("torrents/%s/health?timeout=15" % self.torrent_info["infohash"], + self.on_health_response, capture_errors=False) + + def on_health_response(self, response): + """ + When we receive a health response, update the health status. + """ + self.has_health = True + total_seeders = 0 + total_leechers = 0 + + if not response or 'error' in response: + self.update_health(0, 0) # Just set the health to 0 seeders, 0 leechers + return + + for _, status in response['health'].iteritems(): + if 'error' in status: + continue # Timeout or invalid status + + total_seeders += int(status['seeders']) + total_leechers += int(status['leechers']) + + self.is_health_checking = False + self.update_health(total_seeders, total_leechers) + + def update_health(self, seeders, leechers): + if seeders > 0: + self.health_text.setText("good health (S%d L%d)" % (seeders, leechers)) + self.set_health_indicator(STATUS_GOOD) + elif leechers > 0: + self.health_text.setText("unknown health (found peers)") + self.set_health_indicator(STATUS_UNKNOWN) + else: + self.health_text.setText("no peers found") + self.set_health_indicator(STATUS_DEAD) + + def set_health_indicator(self, status): + color = "orange" + if status == STATUS_GOOD: + color = "green" + elif status == STATUS_UNKNOWN: + color = "orange" + elif status == STATUS_DEAD: + color = "red" + + self.health_indicator.setStyleSheet("background-color: %s; border-radius: %dpx" + % (color, self.health_indicator.height() / 2)) diff --git a/TriblerGUI/widgets/channelpage.py b/TriblerGUI/widgets/channelpage.py new file mode 100644 index 00000000000..1b85550f9af --- /dev/null +++ b/TriblerGUI/widgets/channelpage.py @@ -0,0 +1,87 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem +from TriblerGUI.widgets.loading_list_item import LoadingListItem +from TriblerGUI.widgets.playlist_list_item import PlaylistListItem +from TriblerGUI.widgets.text_list_item import TextListItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_image_path + + +class ChannelPage(QWidget): + """ + The ChannelPage is the page with an overview of each channel and displays the list of torrents/playlist available. + """ + + def __init__(self): + QWidget.__init__(self) + + self.playlists = [] + self.torrents = [] + self.loaded_channels = False + self.loaded_playlists = False + self.channel_info = None + + self.get_torents_in_channel_manager = None + self.get_playlists_in_channel_manager = None + + def initialize_with_channel(self, channel_info): + self.playlists = [] + self.torrents = [] + self.loaded_channels = False + self.loaded_playlists = False + + self.get_torents_in_channel_manager = None + self.get_playlists_in_channel_manager = None + + self.channel_info = channel_info + + self.window().channel_torrents_list.set_data_items([(LoadingListItem, None)]) + self.window().channel_preview_label.setHidden(channel_info['subscribed']) + self.window().channel_back_button.setIcon(QIcon(get_image_path('page_back.png'))) + + self.get_torents_in_channel_manager = TriblerRequestManager() + self.get_torents_in_channel_manager.perform_request("channels/discovered/%s/torrents" % + channel_info['dispersy_cid'], + self.received_torrents_in_channel) + + self.get_playlists_in_channel_manager = TriblerRequestManager() + self.get_playlists_in_channel_manager.perform_request("channels/discovered/%s/playlists" % + channel_info['dispersy_cid'], + self.received_playlists_in_channel) + + # initialize the page about a channel + self.window().channel_name_label.setText(channel_info['name']) + self.window().num_subs_label.setText(str(channel_info['votes'])) + self.window().subscription_widget.initialize_with_channel(channel_info) + + self.window().edit_channel_button.setHidden(True) + + def update_result_list(self): + if self.loaded_channels and self.loaded_playlists: + self.window().channel_torrents_list.set_data_items(self.playlists + self.torrents) + + def received_torrents_in_channel(self, results): + for result in results['torrents']: + self.torrents.append((ChannelTorrentListItem, result)) + + if not self.channel_info['subscribed']: + self.torrents.append((TextListItem, "You're looking at a preview of this channel.\n" + "Subscribe to this channel to see the full content.")) + + self.loaded_channels = True + self.update_result_list() + + def received_playlists_in_channel(self, results): + for result in results['playlists']: + self.playlists.append((PlaylistListItem, result)) + self.loaded_playlists = True + self.update_result_list() + + def on_edit_channel_clicked(self): + self.window().edit_channel_page.initialize_with_channel_overview( + {"channel": + {"name": self.channel_info["name"], + "description": self.channel_info["description"], + "identifier": self.channel_info["dispersy_cid"]}}) diff --git a/TriblerGUI/widgets/circlebutton.py b/TriblerGUI/widgets/circlebutton.py new file mode 100644 index 00000000000..771e3016a53 --- /dev/null +++ b/TriblerGUI/widgets/circlebutton.py @@ -0,0 +1,8 @@ +from PyQt5.QtWidgets import QToolButton + + +class CircleButton(QToolButton): + """ + Represents a circular button in the GUI. + """ + pass diff --git a/TriblerGUI/widgets/createtorrentpage.py b/TriblerGUI/widgets/createtorrentpage.py new file mode 100644 index 00000000000..ad1d2b060a0 --- /dev/null +++ b/TriblerGUI/widgets/createtorrentpage.py @@ -0,0 +1,114 @@ +import os +from PyQt5.QtGui import QIcon + +from PyQt5.QtWidgets import QWidget, QFileDialog, QAction + +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.defs import PAGE_EDIT_CHANNEL_TORRENTS, BUTTON_TYPE_NORMAL +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_image_path + + +class CreateTorrentPage(QWidget): + """ + The CreateTorrentPage is the page where users can create torrent files so they can be added to their channel. + """ + + def __init__(self): + QWidget.__init__(self) + + self.channel_identifier = None + self.request_mgr = None + self.dialog = None + self.selected_item_index = -1 + + def initialize(self, identifier): + self.channel_identifier = identifier + self.window().manage_channel_create_torrent_back.setIcon(QIcon(get_image_path('page_back.png'))) + + self.window().create_torrent_name_field.setText('') + self.window().create_torrent_description_field.setText('') + self.window().create_torrent_files_list.clear() + self.window().create_torrent_files_list.customContextMenuRequested.connect(self.on_right_click_file_item) + + self.window().manage_channel_create_torrent_back.clicked.connect(self.on_create_torrent_manage_back_clicked) + self.window().create_torrent_choose_files_button.clicked.connect(self.on_choose_files_clicked) + self.window().create_torrent_choose_dir_button.clicked.connect(self.on_choose_dir_clicked) + self.window().edit_channel_create_torrent_button.clicked.connect(self.on_create_clicked) + + def on_create_torrent_manage_back_clicked(self): + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_TORRENTS) + + def on_choose_files_clicked(self): + filenames = QFileDialog.getOpenFileNames(self, "Please select the files", "") + + for filename in filenames[0]: + self.window().create_torrent_files_list.addItem(filename) + + def on_choose_dir_clicked(self): + chosen_dir = QFileDialog.getExistingDirectory(self, "Please select the directory containing the files", "", + QFileDialog.ShowDirsOnly) + + if len(chosen_dir) == 0: + return + + files = [] + for path, _, dir_files in os.walk(chosen_dir): + for filename in dir_files: + files.append(os.path.join(path, filename)) + + self.window().create_torrent_files_list.clear() + for filename in files: + self.window().create_torrent_files_list.addItem(filename) + + def on_create_clicked(self): + if self.window().create_torrent_files_list.count() == 0: + self.dialog = ConfirmationDialog(self, "Notice", "You should add at least one file to your torrent.", + [('CLOSE', BUTTON_TYPE_NORMAL)]) + self.dialog.button_clicked.connect(self.on_dialog_ok_clicked) + self.dialog.show() + return + + files_str = u"" + for ind in xrange(self.window().create_torrent_files_list.count()): + files_str += u"files[]=%s&" % self.window().create_torrent_files_list.item(ind).text() + + description = self.window().create_torrent_description_field.toPlainText() + post_data = (u"%s&description=%s" % (files_str[:-1], description)).encode('utf-8') + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("createtorrent", self.on_torrent_created, data=post_data, method='POST') + + def on_dialog_ok_clicked(self, _): + self.dialog.setParent(None) + self.dialog = None + + def on_torrent_created(self, result): + if 'torrent' in result: + self.add_torrent_to_channel(result['torrent']) + + def add_torrent_to_channel(self, torrent): + post_data = str("torrent=%s" % torrent) + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("channels/discovered/%s/torrents" % + self.channel_identifier, self.on_torrent_to_channel_added, + data=post_data, method='PUT') + + def on_torrent_to_channel_added(self, result): + if 'added' in result: + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_TORRENTS) + self.window().edit_channel_page.load_channel_torrents() + + def on_remove_entry(self): + self.window().create_torrent_files_list.takeItem(self.selected_item_index) + + def on_right_click_file_item(self, pos): + selected_item = self.window().create_torrent_files_list.selectedItems()[0] + self.selected_item_index = self.window().create_torrent_files_list.row(selected_item) + + menu = TriblerActionMenu(self) + + remove_action = QAction('Remove file', self) + remove_action.triggered.connect(self.on_remove_entry) + menu.addAction(remove_action) + menu.exec_(self.window().create_torrent_files_list.mapToGlobal(pos)) diff --git a/TriblerGUI/widgets/discoveredpage.py b/TriblerGUI/widgets/discoveredpage.py new file mode 100644 index 00000000000..729dc6ae123 --- /dev/null +++ b/TriblerGUI/widgets/discoveredpage.py @@ -0,0 +1,46 @@ +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.widgets.channel_list_item import ChannelListItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager + + +class DiscoveredPage(QWidget): + """ + The DiscoveredPage shows an overview of all discovered channels in Tribler. + """ + + def __init__(self): + QWidget.__init__(self) + self.discovered_channels = [] + self.request_mgr = None + + def initialize_discovered_page(self): + self.window().core_manager.events_manager.discovered_channel.connect(self.on_discovered_channel) + + def load_discovered_channels(self): + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("channels/discovered", self.received_discovered_channels) + + def received_discovered_channels(self, results): + self.discovered_channels = [] + self.window().discovered_channels_list.set_data_items([]) + items = [] + + results['channels'].sort(key=lambda x: x['torrents'], reverse=True) + + for result in results['channels']: + items.append((ChannelListItem, result)) + self.discovered_channels.append(result) + self.update_num_label() + self.window().discovered_channels_list.set_data_items(items) + + def on_discovered_channel(self, channel_info): + channel_info['torrents'] = 0 + channel_info['subscribed'] = False + channel_info['votes'] = 0 + self.window().discovered_channels_list.append_item((ChannelListItem, channel_info)) + self.discovered_channels.append(channel_info) + self.update_num_label() + + def update_num_label(self): + self.window().num_discovered_channels_label.setText("%d items" % len(self.discovered_channels)) diff --git a/TriblerGUI/widgets/discoveringpage.py b/TriblerGUI/widgets/discoveringpage.py new file mode 100644 index 00000000000..67fe57e753f --- /dev/null +++ b/TriblerGUI/widgets/discoveringpage.py @@ -0,0 +1,40 @@ +from PyQt5.QtSvg import QGraphicsSvgItem, QSvgRenderer +from PyQt5.QtWidgets import QWidget, QGraphicsScene + +from TriblerGUI.utilities import get_image_path + + +class DiscoveringPage(QWidget): + """ + The DiscoveringPage is shown when users are starting Tribler for the first time. It hides when there are at least + five discovered channels. + """ + + def __init__(self): + QWidget.__init__(self) + self.found_channels = 0 + self.is_discovering = False + + def initialize_discovering_page(self): + svg_container = QGraphicsScene(self.window().discovering_svg_view) + svg_item = QGraphicsSvgItem() + + svg = QSvgRenderer(get_image_path("loading_animation.svg")) + svg.repaintNeeded.connect(svg_item.update) + svg_item.setSharedRenderer(svg) + svg_container.addItem(svg_item) + + self.window().discovering_svg_view.setScene(svg_container) + + self.window().core_manager.events_manager.discovered_channel.connect(self.on_discovered_channel) + + def on_discovered_channel(self, _): + self.found_channels += 1 + + if self.found_channels >= 5 and self.is_discovering: + self.is_discovering = False + self.window().clicked_menu_button_discovered() + return + + self.window().discovering_top_label.setText("Discovering your first content...\n\nFound %d channels" + % self.found_channels) diff --git a/TriblerGUI/widgets/downloadprogressbar.py b/TriblerGUI/widgets/downloadprogressbar.py new file mode 100644 index 00000000000..4c0ee3ef6bd --- /dev/null +++ b/TriblerGUI/widgets/downloadprogressbar.py @@ -0,0 +1,60 @@ +import base64 +from PyQt5.QtCore import QRect +from PyQt5.QtGui import QPainter, QColor +from PyQt5.QtWidgets import QWidget, QStyleOption, QStyle +import math + + +class DownloadProgressBar(QWidget): + """ + The DownloadProgressBar is visible in the download details pane and displays the completed pieces (or the progress + of various actions such as file checking). + """ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.show_pieces = False + self.pieces = [] + self.fraction = 0 + self.download = None + + def update_with_download(self, download): + self.download = download + if download["status"] in ("DLSTATUS_SEEDING", "DLSTATUS_STOPPED", "DLSTATUS_STOPPED_ON_ERROR", + "DLSTATUS_CIRCUITS"): + self.set_fraction(download["progress"]) + elif download["status"] in ("DLSTATUS_HASHCHECKING", "DLSTATUS_DOWNLOADING"): + self.set_pieces() + else: + self.set_fraction(0.0) + + def set_fraction(self, fraction): + self.show_pieces = False + self.fraction = fraction + self.repaint() + + def set_pieces(self): + self.show_pieces = True + self.fraction = 0.0 + self.pieces = self.decode_pieces(self.download["pieces"])[:self.download["total_pieces"]] + self.repaint() + + def decode_pieces(self, pieces): + byte_array = map(ord, base64.b64decode(pieces)) + byte_string = ''.join(bin(num)[2:].zfill(8) for num in byte_array) + return [i == '1' for i in byte_string] + + def paintEvent(self, _): + opt = QStyleOption() + opt.initFrom(self) + painter = QPainter(self) + self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) + + if self.show_pieces: + piece_width = self.width() / float(len(self.pieces)) + for i in xrange(len(self.pieces)): + if self.pieces[i]: + painter.fillRect(QRect(float(i) * piece_width, 0, math.ceil(piece_width), self.height()), + QColor(230, 115, 0)) + else: + painter.fillRect(QRect(0, 0, self.width() * self.fraction, self.height()), QColor(230, 115, 0)) diff --git a/TriblerGUI/widgets/downloadsdetailstabwidget.py b/TriblerGUI/widgets/downloadsdetailstabwidget.py new file mode 100644 index 00000000000..221050147cb --- /dev/null +++ b/TriblerGUI/widgets/downloadsdetailstabwidget.py @@ -0,0 +1,164 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTabWidget, QTreeWidgetItem, QAction + +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.defs import * +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import format_size, format_speed + + +class DownloadsDetailsTabWidget(QTabWidget): + """ + The DownloadDetailsTab is the tab that provides details about a specific selected download. This information + includes the connected peers, tracker status and file information. + """ + + def __init__(self, parent): + QTabWidget.__init__(self, parent) + self.current_download = None + self.request_mgr = None + self.selected_item = None + + def initialize_details_widget(self): + self.window().download_files_list.customContextMenuRequested.connect(self.on_right_click_file_item) + + def update_with_download(self, download): + self.current_download = download + self.update_pages() + + def update_pages(self): + if self.current_download is None: + return + + self.window().download_progress_bar.update_with_download(self.current_download) + self.window().download_detail_name_label.setText(self.current_download['name']) + self.window().download_detail_status_label.setText(DLSTATUS_STRINGS[eval(self.current_download["status"])]) + self.window().download_detail_filesize_label.setText("%s in %d files" % + (format_size(float(self.current_download["size"])), + len(self.current_download["files"]))) + self.window().download_detail_health_label.setText("%d seeders, %d leechers" % + (self.current_download["num_seeds"], + self.current_download["num_peers"])) + self.window().download_detail_infohash_label.setText(self.current_download['infohash']) + self.window().download_detail_availability_label.setText("%.2f" % self.current_download['availability']) + + # Populate the files list + self.window().download_files_list.clear() + for filename in self.current_download["files"]: + item = QTreeWidgetItem(self.window().download_files_list) + item.setText(0, filename["name"]) + item.setText(1, format_size(float(filename["size"]))) + item.setText(2, '{percent:.1%}'.format(percent=filename["progress"])) + item.setText(3, "yes" if filename["included"] else "no") + item.setData(0, Qt.UserRole, filename) + self.window().download_files_list.addTopLevelItem(item) + + # Populate the trackers list + self.window().download_trackers_list.clear() + for tracker in self.current_download["trackers"]: + item = QTreeWidgetItem(self.window().download_trackers_list) + item.setText(0, tracker["url"]) + item.setText(1, tracker["status"]) + item.setText(2, str(tracker["peers"])) + + # Populate the peers list if the peer information is available + self.window().download_peers_list.clear() + if "peers" in self.current_download: + for peer in self.current_download["peers"]: + self.create_widget_with_peer_info(peer) + + def clear_data(self): + self.setCurrentIndex(0) + self.window().download_progress_bar.set_fraction(0.0) + self.window().download_detail_name_label.setText("") + self.window().download_detail_status_label.setText("") + self.window().download_detail_filesize_label.setText("") + self.window().download_detail_health_label.setText("") + self.window().download_detail_infohash_label.setText("") + self.window().download_detail_availability_label.setText("") + + self.window().download_files_list.clear() + self.window().download_trackers_list.clear() + self.window().download_peers_list.clear() + + def create_widget_with_peer_info(self, peer): + item = QTreeWidgetItem(self.window().download_peers_list) + + peer_name = "%s:%s" % (peer["ip"], peer["port"]) + if peer['connection_type'] == 1: + peer_name += ' [WebSeed]' + elif peer['connection_type'] == 2: + peer_name += ' [HTTP Seed]' + elif peer['connection_type'] == 3: + peer_name += ' [uTP]' + + state = "" + if peer['optimistic']: + state += "O," + if peer['uinterested']: + state += "UI," + if peer['uchoked']: + state += "UC," + if peer['uhasqueries']: + state += "UQ," + if not peer['uflushed']: + state += "UBL," + if peer['dinterested']: + state += "DI," + if peer['dchoked']: + state += "DC," + if peer['snubbed']: + state += "S," + state += peer['direction'] + + item.setText(0, peer_name) + item.setText(1, '%d%%' % (peer['completed'] * 100.0)) + item.setText(2, format_speed(peer['downrate'])) + item.setText(3, format_speed(peer['uprate'])) + item.setText(4, state) + item.setText(5, peer['extended_version']) + + def on_right_click_file_item(self, pos): + self.selected_item = self.window().download_files_list.selectedItems()[0] + file_data = self.selected_item.data(0, Qt.UserRole) + + menu = TriblerActionMenu(self) + + include_action = QAction('Include', self) + exclude_action = QAction('Exclude', self) + + include_action.triggered.connect(lambda: self.on_file_included(file_data)) + include_action.setEnabled(not file_data["included"]) + exclude_action.triggered.connect(lambda: self.on_file_excluded(file_data)) + exclude_action.setEnabled(file_data["included"]) + + menu.addAction(include_action) + menu.addAction(exclude_action) + + menu.exec_(self.window().download_files_list.mapToGlobal(pos)) + + def get_included_file_list(self): + return [unicode(file_info["name"]) for file_info in self.current_download["files"] if file_info["included"]] + + def on_file_included(self, file_data): + included_list = self.get_included_file_list() + if not file_data["name"] in included_list: + included_list.append(file_data["name"]) + + self.set_included_files(included_list) + + def on_file_excluded(self, file_data): + included_list = self.get_included_file_list() + if file_data["name"] in included_list: + included_list.remove(file_data["name"]) + + self.set_included_files(included_list) + + def set_included_files(self, files): + data_str = ''.join(u"selected_files[]=%s&" % file for file in files)[:-1].encode('utf-8') + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % self.current_download['infohash'], self.on_files_included, + method='PATCH', data=data_str) + + def on_files_included(self, response): + pass diff --git a/TriblerGUI/widgets/downloadspage.py b/TriblerGUI/widgets/downloadspage.py new file mode 100644 index 00000000000..43f1f7b9237 --- /dev/null +++ b/TriblerGUI/widgets/downloadspage.py @@ -0,0 +1,297 @@ +import os + +from PyQt5.QtCore import QTimer, QUrl, pyqtSignal +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QWidget, QAction, QFileDialog, QSystemTrayIcon + +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.defs import DOWNLOADS_FILTER_ALL, DOWNLOADS_FILTER_DOWNLOADING, DOWNLOADS_FILTER_COMPLETED, \ + DOWNLOADS_FILTER_ACTIVE, DOWNLOADS_FILTER_INACTIVE, DOWNLOADS_FILTER_DEFINITION, DLSTATUS_STOPPED, \ + DLSTATUS_STOPPED_ON_ERROR, BUTTON_TYPE_NORMAL, BUTTON_TYPE_CONFIRM, DLSTATUS_METADATA, DLSTATUS_HASHCHECKING, \ + DLSTATUS_WAITING4HASHCHECK +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.widgets.downloadwidgetitem import DownloadWidgetItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import format_speed + + +class DownloadsPage(QWidget): + """ + This class is responsible for managing all items on the downloads page. + The downloads page shows all downloads and specific details about a download. + """ + received_downloads = pyqtSignal(object) + + def __init__(self): + QWidget.__init__(self) + self.export_dir = None + self.filter = DOWNLOADS_FILTER_ALL + self.download_widgets = {} # key: infohash, value: QTreeWidgetItem + self.downloads = None + self.downloads_timer = QTimer() + self.selected_item = None + self.dialog = None + self.request_mgr = None + + def initialize_downloads_page(self): + self.window().downloads_tab.initialize() + self.window().downloads_tab.clicked_tab_button.connect(self.on_downloads_tab_button_clicked) + + self.window().start_download_button.clicked.connect(self.on_start_download_clicked) + self.window().stop_download_button.clicked.connect(self.on_stop_download_clicked) + self.window().remove_download_button.clicked.connect(self.on_remove_download_clicked) + + self.window().downloads_list.itemSelectionChanged.connect(self.on_download_item_clicked) + + self.window().downloads_list.customContextMenuRequested.connect(self.on_right_click_item) + + self.window().download_details_widget.initialize_details_widget() + self.window().download_details_widget.hide() + + self.window().downloads_filter_input.textChanged.connect(self.on_filter_text_changed) + + def on_filter_text_changed(self, text): + self.update_download_visibility() + + def start_loading_downloads(self): + self.load_downloads() + self.downloads_timer = QTimer() + self.downloads_timer.timeout.connect(self.load_downloads) + self.downloads_timer.start(1000) + + def stop_loading_downloads(self): + self.downloads_timer.stop() + + def load_downloads(self): + url = "downloads?get_pieces=1" + if self.window().download_details_widget.currentIndex() == 3: + url = "downloads?get_peers=1&get_pieces=1" + + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request(url, self.on_received_downloads) + + def on_received_downloads(self, downloads): + total_download = 0 + total_upload = 0 + self.received_downloads.emit(downloads) + self.downloads = downloads + + download_infohashes = set() + for download in downloads["downloads"]: + if download["infohash"] in self.download_widgets: + item = self.download_widgets[download["infohash"]] + else: + item = DownloadWidgetItem(self.window().downloads_list) + self.download_widgets[download["infohash"]] = item + + item.update_with_download(download) + + # Update video player with download info + video_infohash = self.window().video_player_page.active_infohash + if video_infohash != "" and download["infohash"] == video_infohash: + self.window().video_player_page.update_with_download_info(download) + + total_download += download["speed_down"] + total_upload += download["speed_up"] + + download_infohashes.add(download["infohash"]) + + if self.window().download_details_widget.current_download is not None and \ + self.window().download_details_widget.current_download["infohash"] == download["infohash"]: + self.window().download_details_widget.current_download = download + self.window().download_details_widget.update_pages() + + # Check whether there are download that should be removed + toremove = set() + for infohash, item in self.download_widgets.iteritems(): + if infohash not in download_infohashes: + index = self.window().downloads_list.indexOfTopLevelItem(item) + toremove.add((infohash, index)) + + for infohash, index in toremove: + self.window().downloads_list.takeTopLevelItem(index) + del self.download_widgets[infohash] + + if QSystemTrayIcon.isSystemTrayAvailable(): + self.window().tray_icon.setToolTip( + "Down: %s, Up: %s" % (format_speed(total_download), format_speed(total_upload))) + self.update_download_visibility() + + def update_download_visibility(self): + for i in range(self.window().downloads_list.topLevelItemCount()): + item = self.window().downloads_list.topLevelItem(i) + filter_match = self.window().downloads_filter_input.text().lower() in item.download_info["name"].lower() + item.setHidden( + not item.get_raw_download_status() in DOWNLOADS_FILTER_DEFINITION[self.filter] or not filter_match) + + def on_downloads_tab_button_clicked(self, button_name): + if button_name == "downloads_all_button": + self.filter = DOWNLOADS_FILTER_ALL + elif button_name == "downloads_downloading_button": + self.filter = DOWNLOADS_FILTER_DOWNLOADING + elif button_name == "downloads_completed_button": + self.filter = DOWNLOADS_FILTER_COMPLETED + elif button_name == "downloads_active_button": + self.filter = DOWNLOADS_FILTER_ACTIVE + elif button_name == "downloads_inactive_button": + self.filter = DOWNLOADS_FILTER_INACTIVE + + self.window().download_details_widget.clear_data() + self.update_download_visibility() + + @staticmethod + def start_download_enabled(download_widget): + return download_widget.get_raw_download_status() == DLSTATUS_STOPPED + + @staticmethod + def stop_download_enabled(download_widget): + status = download_widget.get_raw_download_status() + return status != DLSTATUS_STOPPED and status != DLSTATUS_STOPPED_ON_ERROR + + @staticmethod + def force_recheck_download_enabled(download_widget): + status = download_widget.get_raw_download_status() + return status != DLSTATUS_METADATA and status != DLSTATUS_HASHCHECKING and status != DLSTATUS_WAITING4HASHCHECK + + def on_download_item_clicked(self): + self.window().download_details_widget.show() + if len(self.window().downloads_list.selectedItems()) == 0: + self.window().remove_download_button.setEnabled(False) + self.window().start_download_button.setEnabled(False) + self.window().stop_download_button.setEnabled(False) + return + + self.selected_item = self.window().downloads_list.selectedItems()[0] + self.window().remove_download_button.setEnabled(True) + self.window().start_download_button.setEnabled(DownloadsPage.start_download_enabled(self.selected_item)) + self.window().stop_download_button.setEnabled(DownloadsPage.stop_download_enabled(self.selected_item)) + + self.window().download_details_widget.update_with_download(self.selected_item.download_info) + + def on_start_download_clicked(self): + infohash = self.selected_item.download_info["infohash"] + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_resumed, + method='PATCH', data="state=resume") + + def on_download_resumed(self, json_result): + if json_result["modified"]: + self.selected_item.download_info['status'] = "DLSTATUS_DOWNLOADING" + self.selected_item.update_item() + self.on_download_item_clicked() + + def on_stop_download_clicked(self): + infohash = self.selected_item.download_info["infohash"] + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_stopped, + method='PATCH', data="state=stop") + + def on_play_download_clicked(self): + self.window().left_menu_button_video_player.click() + self.window().video_player_page.set_torrent_infohash(self.selected_item.download_info["infohash"]) + self.window().left_menu_playlist.set_loading() + + def on_download_stopped(self, json_result): + if json_result["modified"]: + self.selected_item.download_info['status'] = "DLSTATUS_STOPPED" + self.selected_item.update_item() + self.on_download_item_clicked() + + def on_remove_download_clicked(self): + self.dialog = ConfirmationDialog(self, "Remove download", "Are you sure you want to remove this download?", + [('remove download', BUTTON_TYPE_NORMAL), + ('remove download + data', BUTTON_TYPE_NORMAL), + ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(self.on_remove_download_dialog) + self.dialog.show() + + def on_remove_download_dialog(self, action): + if action != 2: + infohash = self.selected_item.download_info["infohash"] + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_removed, + method='DELETE', data="remove_data=%d" % action) + + self.dialog.setParent(None) + self.dialog = None + + def on_download_removed(self, json_result): + if json_result["removed"]: + infohash = self.selected_item.download_info["infohash"] + index = self.window().downloads_list.indexOfTopLevelItem(self.selected_item) + self.window().downloads_list.takeTopLevelItem(index) + del self.download_widgets[infohash] + if self.window().downloads_list.topLevelItemCount() == 0: + self.window().download_details_widget.clear_data() + + # Reset video player if necessary + if self.window().video_player_page.active_infohash == infohash: + self.window().video_player_page.reset_player() + + def on_force_recheck_download(self): + infohash = self.selected_item.download_info["infohash"] + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("downloads/%s" % infohash, self.on_forced_recheck, + method='PATCH', data='state=recheck') + + def on_forced_recheck(self, result): + if result['modified']: + self.selected_item.download_info['status'] = "DLSTATUS_HASHCHECKING" + self.selected_item.update_item() + self.on_download_item_clicked() + + def on_explore_files(self): + QDesktopServices.openUrl(QUrl.fromLocalFile(self.selected_item.download_info["destination"])) + + def on_export_download(self): + self.export_dir = QFileDialog.getExistingDirectory(self, "Please select the destination directory", "", + QFileDialog.ShowDirsOnly) + + self.request_mgr = TriblerRequestManager() + self.request_mgr.download_file("downloads/%s/torrent" % self.selected_item.download_info['infohash'], + self.on_export_download_request_done) + + def on_export_download_request_done(self, filename, data): + dest_path = os.path.join(self.export_dir, filename) + with open(dest_path, "wb") as torrent_file: + torrent_file.write(data) + + self.window().tray_icon.showMessage("Torrent file exported", "Torrent file exported to %s" % dest_path) + + def on_right_click_item(self, pos): + self.selected_item = self.window().downloads_list.selectedItems()[0] + + menu = TriblerActionMenu(self) + + start_action = QAction('Start', self) + stop_action = QAction('Stop', self) + play_action = QAction('Play', self) + remove_download_action = QAction('Remove download', self) + force_recheck_action = QAction('Force recheck', self) + export_download_action = QAction('Export .torrent file', self) + explore_files_action = QAction('Explore files', self) + + start_action.triggered.connect(self.on_start_download_clicked) + start_action.setEnabled(DownloadsPage.start_download_enabled(self.selected_item)) + stop_action.triggered.connect(self.on_stop_download_clicked) + stop_action.setEnabled(DownloadsPage.stop_download_enabled(self.selected_item)) + play_action.triggered.connect(self.on_play_download_clicked) + remove_download_action.triggered.connect(self.on_remove_download_clicked) + force_recheck_action.triggered.connect(self.on_force_recheck_download) + force_recheck_action.setEnabled(DownloadsPage.force_recheck_download_enabled(self.selected_item)) + export_download_action.triggered.connect(self.on_export_download) + explore_files_action.triggered.connect(self.on_explore_files) + + menu.addAction(start_action) + menu.addAction(stop_action) + menu.addAction(play_action) + menu.addSeparator() + menu.addAction(remove_download_action) + menu.addSeparator() + menu.addAction(force_recheck_action) + menu.addSeparator() + menu.addAction(export_download_action) + menu.addSeparator() + menu.addAction(explore_files_action) + + menu.exec_(self.window().downloads_list.mapToGlobal(pos)) diff --git a/TriblerGUI/widgets/downloadwidgetitem.py b/TriblerGUI/widgets/downloadwidgetitem.py new file mode 100644 index 00000000000..e5684a4ffaf --- /dev/null +++ b/TriblerGUI/widgets/downloadwidgetitem.py @@ -0,0 +1,79 @@ +from PyQt5.QtCore import QSize +from PyQt5.QtWidgets import QTreeWidgetItem, QProgressBar +from TriblerGUI.defs import * +from TriblerGUI.utilities import format_size, format_speed, duration_to_string + + +class DownloadWidgetItem(QTreeWidgetItem): + """ + This class is responsible for managing the item in the downloads list and fills the item with the relevant data. + """ + + def __init__(self, parent): + QTreeWidgetItem.__init__(self, parent) + self.download_info = None + self.progress_slider = QProgressBar() + self.progress_slider.setStyleSheet(""" + QProgressBar { + margin: 8px; + margin-left: 0; + background-color: white; + color: #ddd; + font-size: 12px; + text-align: center; + } + + QProgressBar::chunk { + background-color: #e67300; + } + """) + + parent.setItemWidget(self, 2, self.progress_slider) + self.setSizeHint(0, QSize(-1, 24)) + + def update_with_download(self, download): + self.download_info = download + self.update_item() + + def get_raw_download_status(self): + return eval(self.download_info["status"]) + + def update_item(self): + self.setText(0, self.download_info["name"]) + self.setText(1, format_size(float(self.download_info["size"]))) + + try: + self.progress_slider.setValue(int(self.download_info["progress"] * 100)) + except RuntimeError: + pass + + self.setText(3, DLSTATUS_STRINGS[eval(self.download_info["status"])]) + self.setText(4, str(self.download_info["num_seeds"])) + self.setText(5, str(self.download_info["num_peers"])) + self.setText(6, format_speed(self.download_info["speed_down"])) + self.setText(7, format_speed(self.download_info["speed_up"])) + self.setText(8, "yes" if self.download_info["anon_download"] else "no") + self.setText(9, str(self.download_info["hops"]) if self.download_info["anon_download"] else "-") + + eta_text = "-" + if self.get_raw_download_status() == DLSTATUS_DOWNLOADING: + eta_text = duration_to_string(self.download_info["eta"]) + self.setText(10, eta_text) + + def __lt__(self, other): + column = self.treeWidget().sortColumn() + if column == 1: + return float(self.download_info["size"]) > float(other.download_info["size"]) + elif column == 2: + return int(self.download_info["progress"] * 100) > int(other.download_info["progress"] * 100) + elif column == 4: + return self.download_info["num_seeds"] > other.download_info["num_seeds"] + elif column == 5: + return self.download_info["num_peers"] > other.download_info["num_peers"] + elif column == 6: + return float(self.download_info["speed_down"]) > float(other.download_info["speed_down"]) + elif column == 7: + return float(self.download_info["speed_up"]) > float(other.download_info["speed_up"]) + elif column == 10: + return float(self.download_info["eta"]) > float(other.download_info["eta"]) + return self.text(column) > other.text(column) diff --git a/TriblerGUI/widgets/editchannelpage.py b/TriblerGUI/widgets/editchannelpage.py new file mode 100644 index 00000000000..38eb7361d1f --- /dev/null +++ b/TriblerGUI/widgets/editchannelpage.py @@ -0,0 +1,549 @@ +import base64 +import urllib + +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QIcon, QCursor + +from PyQt5.QtWidgets import QWidget, QAction, QTreeWidgetItem, QFileDialog + +from TriblerGUI.tribler_action_menu import TriblerActionMenu +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem +from TriblerGUI.defs import PAGE_EDIT_CHANNEL_OVERVIEW, BUTTON_TYPE_NORMAL, BUTTON_TYPE_CONFIRM, \ + PAGE_EDIT_CHANNEL_PLAYLISTS, PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS, PAGE_EDIT_CHANNEL_PLAYLIST_MANAGE, \ + PAGE_EDIT_CHANNEL_PLAYLIST_EDIT, PAGE_EDIT_CHANNEL_SETTINGS, PAGE_EDIT_CHANNEL_TORRENTS,\ + PAGE_EDIT_CHANNEL_RSS_FEEDS, PAGE_EDIT_CHANNEL_CREATE_TORRENT +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.widgets.loading_list_item import LoadingListItem +from TriblerGUI.widgets.playlist_list_item import PlaylistListItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_image_path + + +class EditChannelPage(QWidget): + """ + This class is responsible for managing lists and data on the your channel page, including torrents, playlists + and rss feeds. + """ + playlists_loaded = pyqtSignal(object) + + def __init__(self): + QWidget.__init__(self) + + self.remove_torrent_requests = [] + self.channel_overview = None + self.playlists = None + self.editing_playlist = None + self.viewing_playlist = None + self.editing_own_channel = False + self.dialog = None + self.editchannel_request_mgr = None + + def initialize_edit_channel_page(self): + self.window().create_channel_intro_button.clicked.connect(self.on_create_channel_intro_button_clicked) + + self.window().create_channel_form.hide() + + self.window().edit_channel_stacked_widget.setCurrentIndex(1) + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_OVERVIEW) + + self.window().create_channel_button.clicked.connect(self.on_create_channel_button_pressed) + self.window().edit_channel_save_button.clicked.connect(self.on_edit_channel_save_button_pressed) + + self.window().edit_channel_torrents_remove_selected_button.clicked.connect( + self.on_torrents_remove_selected_clicked) + self.window().edit_channel_torrents_remove_all_button.clicked.connect(self.on_torrents_remove_all_clicked) + self.window().edit_channel_torrents_add_button.clicked.connect(self.on_torrents_add_clicked) + + self.window().edit_channel_details_playlist_manage.playlist_saved.connect(self.load_channel_playlists) + + self.window().edit_channel_playlist_torrents_back.clicked.connect(self.on_playlist_torrents_back_clicked) + self.window().edit_channel_playlists_list.itemClicked.connect(self.on_playlist_item_clicked) + self.window().edit_channel_playlist_manage_torrents_button.clicked.connect(self.on_playlist_manage_clicked) + self.window().edit_channel_create_playlist_button.clicked.connect(self.on_playlist_created_clicked) + + self.window().playlist_edit_save_button.clicked.connect(self.on_playlist_edit_save_clicked) + self.window().playlist_edit_cancel_button.clicked.connect(self.on_playlist_edit_cancel_clicked) + + self.window().edit_channel_details_rss_feeds_remove_selected_button.clicked.connect( + self.on_rss_feeds_remove_selected_clicked) + self.window().edit_channel_details_rss_add_button.clicked.connect(self.on_rss_feed_add_clicked) + self.window().edit_channel_details_rss_refresh_button.clicked.connect(self.on_rss_feeds_refresh_clicked) + + # Tab bar buttons + self.window().channel_settings_tab.initialize() + self.window().channel_settings_tab.clicked_tab_button.connect(self.clicked_tab_button) + + def load_my_channel_overview(self): + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("mychannel", self.initialize_with_channel_overview, + capture_errors=False) + + def initialize_with_channel_overview(self, overview): + if 'error' in overview: + self.window().edit_channel_stacked_widget.setCurrentIndex(0) + else: + if "mychannel" in overview: + self.channel_overview = overview["mychannel"] + self.set_editing_own_channel(True) + self.window().edit_channel_name_label.setText("My channel") + else: + self.channel_overview = overview["channel"] + self.set_editing_own_channel(False) + self.window().edit_channel_name_label.setText(self.channel_overview["name"]) + + self.window().edit_channel_overview_name_label.setText(self.channel_overview["name"]) + self.window().edit_channel_description_label.setText(self.channel_overview["description"]) + self.window().edit_channel_identifier_label.setText(self.channel_overview["identifier"]) + + self.window().edit_channel_name_edit.setText(self.channel_overview["name"]) + self.window().edit_channel_description_edit.setText(self.channel_overview["description"]) + + self.window().edit_channel_stacked_widget.setCurrentIndex(1) + + def set_editing_own_channel(self, edit_own): + self.editing_own_channel = edit_own + + self.window().edit_channel_settings_button.setHidden(not edit_own) + self.window().edit_channel_rss_feeds_button.setHidden(not edit_own) + self.window().edit_channel_playlists_button.setHidden(not edit_own) + + self.window().edit_channel_torrents_remove_all_button.setHidden(not edit_own) + self.window().edit_channel_torrents_remove_selected_button.setHidden(not edit_own) + + def load_channel_torrents(self): + self.window().edit_channel_torrents_list.set_data_items([(LoadingListItem, None)]) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/torrents?disable_filter=1" % + self.channel_overview["identifier"], self.initialize_with_torrents) + + def initialize_with_torrents(self, torrents): + self.window().edit_channel_torrents_list.set_data_items([]) + + items = [] + for result in torrents['torrents']: + items.append((ChannelTorrentListItem, result, + {"show_controls": True, "on_remove_clicked": self.on_torrent_remove_clicked})) + self.window().edit_channel_torrents_list.set_data_items(items) + + def load_channel_playlists(self): + self.window().edit_channel_playlists_list.set_data_items([(LoadingListItem, None)]) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/playlists" % + self.channel_overview["identifier"], + self.initialize_with_playlists) + + def initialize_with_playlists(self, playlists): + self.playlists_loaded.emit(playlists) + self.playlists = playlists + self.window().edit_channel_playlists_list.set_data_items([]) + + self.update_playlist_list() + + viewing_playlist_index = self.get_index_of_viewing_playlist() + if viewing_playlist_index != -1: + self.viewing_playlist = self.playlists['playlists'][viewing_playlist_index] + self.update_playlist_torrent_list() + + def load_channel_rss_feeds(self): + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/rssfeeds" % + self.channel_overview["identifier"], + self.initialize_with_rss_feeds) + + def initialize_with_rss_feeds(self, rss_feeds): + self.window().edit_channel_rss_feeds_list.clear() + for feed in rss_feeds["rssfeeds"]: + item = QTreeWidgetItem(self.window().edit_channel_rss_feeds_list) + item.setText(0, feed["url"]) + + self.window().edit_channel_rss_feeds_list.addTopLevelItem(item) + + def on_torrent_remove_clicked(self, item): + self.dialog = ConfirmationDialog(self, "Remove selected torrent", + "Are you sure that you want to remove the selected torrent from this channel?", + [('confirm', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(lambda action: self.on_torrents_remove_selected_action(action, item)) + self.dialog.show() + + def on_create_channel_button_pressed(self): + channel_name = self.window().new_channel_name_edit.text() + channel_description = self.window().new_channel_description_edit.toPlainText() + if len(channel_name) == 0: + self.window().new_channel_name_label.setStyleSheet("color: red;") + return + + self.window().create_channel_button.setEnabled(False) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered", self.on_channel_created, + data=unicode('name=%s&description=%s' % + (channel_name, channel_description)).encode('utf-8'), + method='PUT') + + def on_channel_created(self, result): + if u'added' in result: + self.window().create_channel_button.setEnabled(True) + self.load_my_channel_overview() + + def on_edit_channel_save_button_pressed(self): + channel_name = self.window().edit_channel_name_edit.text() + channel_description = self.window().edit_channel_description_edit.toPlainText() + self.window().edit_channel_save_button.setEnabled(False) + + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("mychannel", self.on_channel_edited, + data=unicode('name=%s&description=%s' % + (channel_name, channel_description)).encode('utf-8'), + method='POST') + + def on_channel_edited(self, result): + if 'modified' in result: + self.window().edit_channel_name_label.setText(self.window().edit_channel_name_edit.text()) + self.window().edit_channel_description_label.setText( + self.window().edit_channel_description_edit.toPlainText()) + self.window().edit_channel_save_button.setEnabled(True) + + def on_torrents_remove_selected_clicked(self): + num_selected = len(self.window().edit_channel_torrents_list.selectedItems()) + if num_selected == 0: + return + + item = self.window().edit_channel_torrents_list.itemWidget( + self.window().edit_channel_torrents_list.selectedItems()[0]) + + self.dialog = ConfirmationDialog(self, "Remove %s selected torrents" % num_selected, + "Are you sure that you want to remove %s selected torrents " + "from your channel?" % num_selected, + [('confirm', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(lambda action: self.on_torrents_remove_selected_action(action, item)) + self.dialog.show() + + def on_torrents_remove_all_clicked(self): + self.dialog = ConfirmationDialog(self.window(), "Remove all torrents", + "Are you sure that you want to remove all torrents from your channel? " + "You cannot undo this action.", + [('confirm', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(self.on_torrents_remove_all_action) + self.dialog.show() + + def on_torrents_add_clicked(self): + menu = TriblerActionMenu(self) + + browse_files_action = QAction('Import torrent from file', self) + add_url_action = QAction('Add URL', self) + create_torrent_action = QAction('Create torrent from file(s)', self) + + browse_files_action.triggered.connect(self.on_add_torrent_browse_file) + add_url_action.triggered.connect(self.on_add_torrent_from_url) + create_torrent_action.triggered.connect(self.on_create_torrent_from_files) + + menu.addAction(browse_files_action) + menu.addAction(add_url_action) + menu.addAction(create_torrent_action) + + menu.exec_(QCursor.pos()) + + def on_add_torrent_browse_file(self): + filename = QFileDialog.getOpenFileName(self, "Please select the .torrent file", "", "Torrent files (*.torrent)") + + with open(filename[0], "rb") as torrent_file: + torrent_content = base64.b64encode(torrent_file.read()) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/torrents" % + self.channel_overview['identifier'], + self.on_torrent_to_channel_added, method='PUT', + data='torrent=%s' % torrent_content) + + def on_add_torrent_from_url(self): + self.dialog = ConfirmationDialog(self, "Add torrent from URL/magnet link", + "Please enter the URL/magnet link in the field below:", + [('add', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)], + show_input=True) + self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link') + self.dialog.button_clicked.connect(self.on_torrent_from_url_dialog_done) + self.dialog.show() + + def on_torrent_from_url_dialog_done(self, action): + if action == 0: + url = urllib.quote_plus(self.dialog.dialog_widget.dialog_input.text()) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/torrents/%s" % + (self.channel_overview['identifier'], url), + self.on_torrent_to_channel_added, method='PUT') + + self.dialog.setParent(None) + self.dialog = None + + def on_torrent_to_channel_added(self, result): + if 'added' in result: + self.load_channel_torrents() + + def on_create_torrent_from_files(self): + self.window().edit_channel_details_create_torrent.initialize(self.channel_overview['identifier']) + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_CREATE_TORRENT) + + def on_playlist_torrents_back_clicked(self): + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLISTS) + + def on_playlist_item_clicked(self, item): + playlist_info = item.data(Qt.UserRole) + self.window().edit_channel_playlist_torrents_list.set_data_items([]) + self.window().edit_channel_details_playlist_torrents_header.setText("Torrents in '%s'" % playlist_info['name']) + self.window().edit_channel_playlist_torrents_back.setIcon(QIcon(get_image_path('page_back.png'))) + + self.viewing_playlist = playlist_info + self.update_playlist_torrent_list() + + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS) + + def update_playlist_list(self): + self.playlists['playlists'].sort(key=lambda torrent: len(torrent['torrents']), reverse=True) + + items = [] + for result in self.playlists['playlists']: + items.append((PlaylistListItem, result, + {"show_controls": True, "on_remove_clicked": self.on_playlist_remove_clicked, + "on_edit_clicked": self.on_playlist_edit_clicked})) + self.window().edit_channel_playlists_list.set_data_items(items) + + def update_playlist_torrent_list(self): + items = [] + for torrent in self.viewing_playlist["torrents"]: + items.append((ChannelTorrentListItem, torrent, + {"show_controls": True, "on_remove_clicked": self.on_playlist_torrent_remove_clicked})) + self.window().edit_channel_playlist_torrents_list.set_data_items(items) + + def on_playlist_manage_clicked(self): + self.window().edit_channel_details_playlist_manage.initialize(self.channel_overview, self.viewing_playlist) + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_MANAGE) + + def on_playlist_torrent_remove_clicked(self, item): + self.dialog = ConfirmationDialog(self, + "Remove selected torrent from playlist", + "Are you sure that you want to remove the selected torrent " + "from this playlist?", + [('confirm', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(lambda action: self.on_playlist_torrent_remove_selected_action(item, action)) + self.dialog.show() + + def on_playlist_torrent_remove_selected_action(self, item, action): + if action == 0: + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/playlists/%s/%s" % + (self.channel_overview["identifier"], + self.viewing_playlist['id'], item.torrent_info['infohash']), + lambda result: self.on_playlist_torrent_removed( + result, item.torrent_info), + method='DELETE') + + self.dialog.setParent(None) + self.dialog = None + + def on_playlist_torrent_removed(self, result, torrent): + self.remove_torrent_from_playlist(torrent) + + def get_index_of_viewing_playlist(self): + if self.viewing_playlist is None: + return -1 + + for index in xrange(len(self.playlists['playlists'])): + if self.playlists['playlists'][index]['id'] == self.viewing_playlist['id']: + return index + + return -1 + + def remove_torrent_from_playlist(self, torrent): + playlist_index = self.get_index_of_viewing_playlist() + + torrent_index = -1 + for index in xrange(len(self.viewing_playlist['torrents'])): + if self.viewing_playlist['torrents'][index]['infohash'] == torrent['infohash']: + torrent_index = index + break + + if torrent_index != -1: + del self.playlists['playlists'][playlist_index]['torrents'][torrent_index] + self.viewing_playlist = self.playlists['playlists'][playlist_index] + self.update_playlist_list() + self.update_playlist_torrent_list() + + def on_playlist_edit_save_clicked(self): + if len(self.window().playlist_edit_name.text()) == 0: + return + + name = self.window().playlist_edit_name.text() + description = self.window().playlist_edit_description.toPlainText() + + self.editchannel_request_mgr = TriblerRequestManager() + if self.editing_playlist is None: + self.editchannel_request_mgr.perform_request("channels/discovered/%s/playlists" % + self.channel_overview["identifier"], self.on_playlist_created, + data=unicode('name=%s&description=%s' % + (name, description)).encode('utf-8'), + method='PUT') + else: + self.editchannel_request_mgr.perform_request("channels/discovered/%s/playlists/%s" % + (self.channel_overview["identifier"], + self.editing_playlist["id"]), self.on_playlist_edited, + data=unicode('name=%s&description=%s' % + (name, description)).encode('utf-8'), + method='POST') + + def on_playlist_created(self, json_result): + if 'created' in json_result and json_result['created']: + self.on_playlist_edited_done() + + def on_playlist_edited(self, json_result): + if 'modified' in json_result and json_result['modified']: + self.on_playlist_edited_done() + + def on_playlist_edited_done(self): + self.window().playlist_edit_name.setText('') + self.window().playlist_edit_description.setText('') + self.load_channel_playlists() + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLISTS) + + def on_playlist_edit_cancel_clicked(self): + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLISTS) + + def on_playlist_created_clicked(self): + self.editing_playlist = None + self.window().playlist_edit_save_button.setText("CREATE") + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_EDIT) + + def on_playlist_remove_clicked(self, item): + self.dialog = ConfirmationDialog(self, "Remove selected playlist", + "Are you sure that you want to remove the selected playlist " + "from your channel?", + [('confirm', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(lambda action: self.on_playlist_remove_selected_action(item, action)) + self.dialog.show() + + def on_playlist_remove_selected_action(self, item, action): + if action == 0: + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/playlists/%s" % + (self.channel_overview["identifier"], + item.playlist_info['id']), + self.on_playlist_removed, method='DELETE') + + self.dialog.setParent(None) + self.dialog = None + + def on_playlist_removed(self, json_result): + if 'removed' in json_result and json_result['removed']: + self.load_channel_playlists() + + def on_playlist_edit_clicked(self, item): + self.editing_playlist = item.playlist_info + self.window().playlist_edit_save_button.setText("CREATE") + self.window().playlist_edit_name.setText(item.playlist_info["name"]) + self.window().playlist_edit_description.setText(item.playlist_info["description"]) + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_EDIT) + + def on_torrents_remove_selected_action(self, action, item): + if action == 0: + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/torrents/%s" % + (self.channel_overview["identifier"], + item.torrent_info['infohash']), + self.on_torrent_removed, method='DELETE') + + self.dialog.setParent(None) + self.dialog = None + + def on_torrent_removed(self, json_result): + if 'removed' in json_result and json_result['removed']: + selected_item = self.window().edit_channel_torrents_list.selectedItems()[0] + self.window().edit_channel_torrents_list.takeItem( + self.window().edit_channel_torrents_list.row(selected_item)) + + def on_torrents_remove_all_action(self, action): + if action == 0: + for torrent_ind in xrange(self.window().edit_channel_torrents_list.count()): + torrent_data = self.window().edit_channel_torrents_list.item(torrent_ind).data(Qt.UserRole) + request_mgr = TriblerRequestManager() + request_mgr.perform_request("channels/discovered/%s/torrents/%s" % + (self.channel_overview["identifier"], torrent_data['infohash']), + None, method='DELETE') + self.remove_torrent_requests.append(request_mgr) + + self.window().edit_channel_torrents_list.set_data_items([]) + + self.dialog.setParent(None) + self.dialog = None + + def clicked_tab_button(self, tab_button_name): + if tab_button_name == "edit_channel_overview_button": + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_OVERVIEW) + elif tab_button_name == "edit_channel_settings_button": + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_SETTINGS) + elif tab_button_name == "edit_channel_torrents_button": + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_TORRENTS) + self.load_channel_torrents() + elif tab_button_name == "edit_channel_playlists_button": + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLISTS) + self.load_channel_playlists() + elif tab_button_name == "edit_channel_rss_feeds_button": + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_RSS_FEEDS) + self.load_channel_rss_feeds() + + def on_create_channel_intro_button_clicked(self): + self.window().create_channel_form.show() + self.window().create_channel_intro_button_container.hide() + self.window().create_new_channel_intro_label.setText("Please enter your channel details below.") + + def on_rss_feed_add_clicked(self): + self.dialog = ConfirmationDialog(self, "Add RSS feed", "Please enter the RSS feed URL in the field below:", + [('add', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)], + show_input=True) + self.dialog.dialog_widget.dialog_input.setPlaceholderText('RSS feed URL') + self.dialog.button_clicked.connect(self.on_rss_feed_dialog_added) + self.dialog.show() + + def on_rss_feed_dialog_added(self, action): + if action == 0: + url = urllib.quote_plus(self.dialog.dialog_widget.dialog_input.text()) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/rssfeeds/%s" % + (self.channel_overview["identifier"], url), + self.on_rss_feed_added, method='PUT') + + self.dialog.setParent(None) + self.dialog = None + + def on_rss_feed_added(self, json_result): + if json_result['added']: + self.load_channel_rss_feeds() + + def on_rss_feeds_remove_selected_clicked(self): + self.dialog = ConfirmationDialog(self, "Remove RSS feed", + "Are you sure you want to remove the selected RSS feed?", + [('remove', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) + self.dialog.button_clicked.connect(self.on_rss_feed_dialog_removed) + self.dialog.show() + + def on_rss_feed_dialog_removed(self, action): + if action == 0: + url = urllib.quote_plus(self.window().edit_channel_rss_feeds_list.selectedItems()[0].text(0)) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request("channels/discovered/%s/rssfeeds/%s" % + (self.channel_overview["identifier"], url), + self.on_rss_feed_removed, method='DELETE') + + self.dialog.setParent(None) + self.dialog = None + + def on_rss_feed_removed(self, json_result): + if json_result['removed']: + self.load_channel_rss_feeds() + + def on_rss_feeds_refresh_clicked(self): + self.window().edit_channel_details_rss_refresh_button.setEnabled(False) + self.editchannel_request_mgr = TriblerRequestManager() + self.editchannel_request_mgr.perform_request('channels/discovered/%s/recheckfeeds' % + self.channel_overview["identifier"], self.on_rss_feeds_refreshed,\ + method='POST') + + def on_rss_feeds_refreshed(self, json_result): + if json_result["rechecked"]: + self.window().edit_channel_details_rss_refresh_button.setEnabled(True) diff --git a/TriblerGUI/widgets/ellipsebutton.py b/TriblerGUI/widgets/ellipsebutton.py new file mode 100644 index 00000000000..6222589fb1d --- /dev/null +++ b/TriblerGUI/widgets/ellipsebutton.py @@ -0,0 +1,8 @@ +from PyQt5.QtWidgets import QToolButton + + +class EllipseButton(QToolButton): + """ + Represents an ellipsoid button in the GUI. + """ + pass diff --git a/TriblerGUI/widgets/home_recommended_item.py b/TriblerGUI/widgets/home_recommended_item.py new file mode 100644 index 00000000000..942f861b5e5 --- /dev/null +++ b/TriblerGUI/widgets/home_recommended_item.py @@ -0,0 +1,129 @@ +from urllib import quote_plus + +from PyQt5.QtCore import QPoint, QSize, Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget, QLabel, QSizePolicy, QToolButton + +from TriblerGUI.dialogs.startdownloaddialog import StartDownloadDialog +from TriblerGUI.tribler_window import fc_home_recommended_item +from TriblerGUI.utilities import pretty_date, get_image_path, format_size, get_gui_setting + +HOME_ITEM_FONT_SIZE = 44 + + +class HomeRecommendedItem(QWidget, fc_home_recommended_item): + """ + This class represents a HomeRecommendedItem widget which is shown on the home page. This widget can either show + a channel or a torrent. + """ + + def __init__(self, parent): + QWidget.__init__(self, parent) + fc_home_recommended_item.__init__(self) + + self.setupUi(self) + + self.show_torrent = True + self.torrent_info = None + self.channel_info = None + self.download_uri = None + self.dialog = None + + # Create the category label, shown on cells that display a torrent on the home page + self.category_label = QLabel(self) + self.category_label.setFixedHeight(24) + self.category_label.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)) + self.category_label.setStyleSheet(""" + border: 2px solid white; + border-radius: 12px; + background-color: transparent; + color: white; + padding-left: 4px; + padding-right: 4px; + font-weight: bold; + """) + self.category_label.move(QPoint(6, 6)) + self.category_label.show() + + # Create the dark overlay and download button over the thumbnail on hover + self.dark_overlay = QWidget(self) + self.dark_overlay.setStyleSheet("background-color: rgba(0, 0, 0, 0.65);") + self.dark_overlay.hide() + + self.download_button = QToolButton(self) + self.download_button.setFixedSize(QSize(40, 40)) + self.download_button.setStyleSheet(""" + QToolButton { + background-color: transparent; + border: 2px solid white; + border-radius: 20px; + } + + QToolButton::hover { + border: 2px solid #B5B5B5; + } + """) + self.download_button.setIcon(QIcon(get_image_path('downloads.png'))) + self.download_button.setIconSize(QSize(18, 18)) + self.download_button.clicked.connect(self.on_download_button_clicked) + self.download_button.hide() + + def on_download_button_clicked(self): + gui_settings = self.window().gui_settings + self.download_uri = quote_plus((u"magnet:?xt=urn:btih:%s&dn=%s" % + (self.torrent_info["infohash"], self.torrent_info['name'])).encode('utf-8')) + + if get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True): + self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri, self.torrent_info["name"]) + self.dialog.button_clicked.connect(self.on_start_download_action) + self.dialog.show() + else: + self.window().perform_start_download_request(self.download_uri, + get_gui_setting(gui_settings, "default_anonymity_enabled", + True, is_bool=True), + get_gui_setting(gui_settings, "default_safeseeding_enabled", + True, is_bool=True), [], 0) + + def on_start_download_action(self, action): + if action == 1: + self.window().perform_start_download_request(self.download_uri, + self.dialog.dialog_widget.anon_download_checkbox.isChecked(), + self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), + self.dialog.get_selected_files(), + self.dialog.dialog_widget.files_list_view.topLevelItemCount()) + + self.dialog.setParent(None) + self.dialog = None + + def update_with_torrent(self, torrent): + self.show_torrent = True + self.torrent_info = torrent + self.thumbnail_widget.initialize(torrent["name"], HOME_ITEM_FONT_SIZE) + self.main_label.setText(torrent["name"]) + self.category_label.setText(torrent["category"]) + self.category_label.adjustSize() + self.category_label.setHidden(False) + self.setCursor(Qt.ArrowCursor) + self.detail_label.setText("Size: " + format_size(torrent["size"])) + + def update_with_channel(self, channel): + self.show_torrent = False + self.channel_info = channel + self.thumbnail_widget.initialize(channel["name"], HOME_ITEM_FONT_SIZE) + + self.main_label.setText(channel["name"]) + self.detail_label.setText("Updated " + pretty_date(channel["modified"])) + self.category_label.setHidden(True) + self.setCursor(Qt.PointingHandCursor) + + def enterEvent(self, _): + if self.show_torrent: + self.dark_overlay.resize(self.thumbnail_widget.size()) + self.dark_overlay.show() + self.download_button.move((self.thumbnail_widget.width() - self.download_button.width()) / 2, + (self.thumbnail_widget.height() - self.download_button.height()) / 2) + self.download_button.show() + + def leaveEvent(self, _): + self.dark_overlay.hide() + self.download_button.hide() diff --git a/TriblerGUI/widgets/homepage.py b/TriblerGUI/widgets/homepage.py new file mode 100644 index 00000000000..6a7b31d75e4 --- /dev/null +++ b/TriblerGUI/widgets/homepage.py @@ -0,0 +1,89 @@ +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.defs import PAGE_CHANNEL_DETAILS +from TriblerGUI.widgets.home_recommended_item import HomeRecommendedItem +from TriblerGUI.widgets.loading_list_item import LoadingListItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager + + +class HomePage(QWidget): + """ + The HomePage is usually the first page that Tribler users are seeing. It shows some recommended torrents and + channels in a grid view. + """ + + def __init__(self): + QWidget.__init__(self) + self.has_loaded_cells = False + self.recommended_request_mgr = None + self.show_channels = False + + def initialize_home_page(self): + self.window().home_page_table_view.cellClicked.connect(self.on_home_page_item_clicked) + + self.window().home_tab.initialize() + self.window().home_tab.clicked_tab_button.connect(self.clicked_tab_button) + + def load_cells(self): + self.window().home_page_table_view.clear() + for x in xrange(0, 3): + for y in xrange(0, 3): + widget_item = HomeRecommendedItem(self) + self.window().home_page_table_view.setCellWidget(x, y, widget_item) + self.has_loaded_cells = True + + def load_popular_torrents(self): + self.recommended_request_mgr = TriblerRequestManager() + self.recommended_request_mgr.perform_request("torrents/random?limit=50", self.received_popular_torrents) + + def clicked_tab_button(self, tab_button_name): + if tab_button_name == "home_tab_channels_button": + self.recommended_request_mgr = TriblerRequestManager() + self.recommended_request_mgr.perform_request("channels/popular?limit=50", self.received_popular_channels) + elif tab_button_name == "home_tab_torrents_button": + self.load_popular_torrents() + + def received_popular_channels(self, result): + self.show_channels = True + if not self.has_loaded_cells: + self.load_cells() + + if len(result["channels"]) == 0: + self.has_loaded_cells = False + self.window().home_page_table_view.clear() + self.window().home_page_table_view.setCellWidget( + 0, 1, LoadingListItem(self, label_text="No recommended channels")) + return + + cur_ind = 0 + for channel in result["channels"][:9]: + self.window().home_page_table_view.cellWidget(cur_ind % 3, cur_ind / 3).update_with_channel(channel) + cur_ind += 1 + + self.window().resizeEvent(None) + + def received_popular_torrents(self, result): + self.show_channels = False + if not self.has_loaded_cells: + self.load_cells() + + if len(result["torrents"]) == 0: + self.has_loaded_cells = False + self.window().home_page_table_view.clear() + self.window().home_page_table_view.setCellWidget( + 0, 1, LoadingListItem(self, label_text="No recommended torrents")) + return + + cur_ind = 0 + for torrent in result["torrents"][:9]: + self.window().home_page_table_view.cellWidget(cur_ind % 3, cur_ind / 3).update_with_torrent(torrent) + cur_ind += 1 + + self.window().resizeEvent(None) + + def on_home_page_item_clicked(self, row, col): + if self.show_channels: + channel_info = self.window().home_page_table_view.cellWidget(row, col).channel_info + self.window().channel_page.initialize_with_channel(channel_info) + self.window().navigation_stack.append(self.window().stackedWidget.currentIndex()) + self.window().stackedWidget.setCurrentIndex(PAGE_CHANNEL_DETAILS) diff --git a/TriblerGUI/widgets/lazyloadlist.py b/TriblerGUI/widgets/lazyloadlist.py new file mode 100644 index 00000000000..1d255df7dc3 --- /dev/null +++ b/TriblerGUI/widgets/lazyloadlist.py @@ -0,0 +1,81 @@ +from PyQt5.QtCore import QSize, Qt +from PyQt5.QtWidgets import QListWidget, QListWidgetItem + +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem + +ITEM_LOAD_BATCH = 30 + + +class LazyLoadList(QListWidget): + """ + This class implements a list where widget items are lazy-loaded. When the user has reached the end of the list + when scrolling, the next items are created and displayed. + """ + + def __init__(self, parent): + QListWidget.__init__(self, parent) + self.verticalScrollBar().valueChanged.connect(self.on_list_scroll) + self.itemSelectionChanged.connect(self.on_item_clicked) + self.data_items = [] # Tuple of (ListWidgetClass, json data) + self.items_loaded = 0 + + def load_next_items(self): + for i in range(self.items_loaded, min(self.items_loaded + ITEM_LOAD_BATCH, len(self.data_items))): + self.load_item(i) + + def load_item(self, index): + item = QListWidgetItem() + item.setSizeHint(QSize(-1, 60)) + data_item = self.data_items[index] + item.setData(Qt.UserRole, data_item[1]) + if len(data_item) > 2: + widget_item = data_item[0](self, data_item[1], **data_item[2]) + else: + widget_item = data_item[0](self, data_item[1]) + self.insertItem(index, item) + self.setItemWidget(item, widget_item) + self.items_loaded += 1 + + def insert_item(self, index, item): + self.data_items.insert(index, item) + if index < ITEM_LOAD_BATCH: + self.load_item(index) + + def set_data_items(self, items): + self.clear() + self.items_loaded = 0 + self.data_items = items + self.load_next_items() + + def append_item(self, item): + self.data_items.append(item) + if self.items_loaded < ITEM_LOAD_BATCH: + self.load_item(self.items_loaded) + + def on_list_scroll(self, event): + if self.verticalScrollBar().value() == self.verticalScrollBar().maximum(): + self.load_next_items() + + def get_first_items(self, num, cls=None): + """ + Return the first num widget items with type cls. + This can be useful when for instance you need the first five search results. + """ + result = [] + for i in xrange(self.count()): + widget_item = self.itemWidget(self.item(i)) + if not cls or (cls and isinstance(widget_item, cls)): + result.append(widget_item) + + if len(result) >= num: + break + + return result + + def on_item_clicked(self): + if len(self.selectedItems()) == 0: + return + + item_widget = self.itemWidget(self.selectedItems()[0]) + if isinstance(item_widget, ChannelTorrentListItem): + item_widget.check_health() diff --git a/TriblerGUI/widgets/leftmenuplaylist.py b/TriblerGUI/widgets/leftmenuplaylist.py new file mode 100644 index 00000000000..6c61dfcd2e9 --- /dev/null +++ b/TriblerGUI/widgets/leftmenuplaylist.py @@ -0,0 +1,48 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QListWidget +from TriblerGUI.utilities import is_video_file + + +class LeftMenuPlaylist(QListWidget): + """ + This class represents the menu with video files that is visible in the left menu. + Only shows when a video is playing. + """ + + playing_item_change = pyqtSignal(int, str) # file index, name of file + + def __init__(self, parent): + QListWidget.__init__(self, parent) + + self.files_data = [] + self.loaded_list = False + self.active_index = -1 + self.itemClicked.connect(self.on_item_clicked) + + def set_loading(self): + self.clear() + self.addItem("Loading...") + self.loaded_list = False + + def set_files(self, files): + self.clear() + self.files_data = [] + + for file_info in files: + if is_video_file(file_info['name']): + self.addItem(file_info['name']) + self.files_data.append((file_info['index'], file_info['name'])) + self.loaded_list = True + + def set_active_index(self, file_index): + cur_ind = 0 + for index, _ in self.files_data: + if index == file_index: + self.item(cur_ind).setSelected(True) + self.setFocus() + break + cur_ind += 1 + + def on_item_clicked(self, item): + item_ind = self.row(item) + self.playing_item_change.emit(*self.files_data[item_ind]) diff --git a/TriblerGUI/widgets/loading_list_item.py b/TriblerGUI/widgets/loading_list_item.py new file mode 100644 index 00000000000..e7d36ea0065 --- /dev/null +++ b/TriblerGUI/widgets/loading_list_item.py @@ -0,0 +1,17 @@ +from PyQt5.QtWidgets import QWidget +from TriblerGUI.tribler_window import fc_loading_list_item + + +class LoadingListItem(QWidget, fc_loading_list_item): + """ + When data is loading, we show a list widget with some text. + """ + + def __init__(self, parent, label_text=None): + QWidget.__init__(self, parent) + fc_loading_list_item.__init__(self) + + self.setupUi(self) + + if label_text is not None: + self.textlabel.setText(label_text) diff --git a/TriblerGUI/widgets/loadingpage.py b/TriblerGUI/widgets/loadingpage.py new file mode 100644 index 00000000000..cea455c83b1 --- /dev/null +++ b/TriblerGUI/widgets/loadingpage.py @@ -0,0 +1,28 @@ +from PyQt5.QtSvg import QGraphicsSvgItem, QSvgRenderer +from PyQt5.QtWidgets import QWidget, QGraphicsScene + +from TriblerGUI.utilities import get_image_path + + +class LoadingPage(QWidget): + """ + This page is presented when Tribler is starting. + """ + + def __init__(self): + QWidget.__init__(self) + + def initialize_loading_page(self): + svg_container = QGraphicsScene(self.window().loading_svg_view) + svg_item = QGraphicsSvgItem() + + svg = QSvgRenderer(get_image_path("loading_animation.svg")) + svg.repaintNeeded.connect(svg_item.update) + svg_item.setSharedRenderer(svg) + svg_container.addItem(svg_item) + + self.window().loading_svg_view.setScene(svg_container) + self.window().core_manager.events_manager.upgrader_tick.connect(self.set_loading_text) + + def set_loading_text(self, text): + pass diff --git a/TriblerGUI/widgets/manageplaylistpage.py b/TriblerGUI/widgets/manageplaylistpage.py new file mode 100644 index 00000000000..dceb926def7 --- /dev/null +++ b/TriblerGUI/widgets/manageplaylistpage.py @@ -0,0 +1,150 @@ +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget, QListWidgetItem +from TriblerGUI.defs import PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_image_path + + +class ManagePlaylistPage(QWidget): + """ + On this page, users can add or remove torrents from/to a playlist. + """ + + playlist_saved = pyqtSignal() + + def __init__(self): + QWidget.__init__(self) + + self.channel_info = None + self.playlist_info = None + self.request_mgr = None + + self.torrents_in_playlist = [] + self.torrents_in_channel = [] + + self.torrents_to_create = [] + self.torrents_to_remove = [] + + self.pending_requests = [] + self.requests_done = 0 + + def initialize(self, channel_info, playlist_info): + self.channel_info = channel_info + self.playlist_info = playlist_info + self.window().edit_channel_details_manage_playlist_header.setText("Manage torrents in playlist '%s'" % + playlist_info['name']) + self.window().manage_channel_playlist_torrents_back.setIcon(QIcon(get_image_path('page_back.png'))) + + self.window().playlist_manage_add_to_playlist.clicked.connect(self.on_add_clicked) + self.window().playlist_manage_remove_from_playlist.clicked.connect(self.on_remove_clicked) + self.window().edit_channel_manage_playlist_save_button.clicked.connect(self.on_save_clicked) + self.window().manage_channel_playlist_torrents_back.clicked.connect(self.on_playlist_manage_back_clicked) + + # Load torrents in your channel + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("channels/discovered/%s/torrents" % + channel_info["identifier"], self.on_received_channel_torrents) + + self.torrents_in_playlist = [] + self.torrents_in_channel = [] + + self.torrents_to_create = [] + self.torrents_to_remove = [] + + self.pending_requests = [] + self.requests_done = 0 + + def on_playlist_manage_back_clicked(self): + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS) + + def update_lists(self): + self.window().playlist_manage_in_channel_list.clear() + self.window().playlist_manage_in_playlist_list.clear() + + for torrent in self.torrents_in_channel: + item = QListWidgetItem(torrent["name"], self.window().playlist_manage_in_channel_list) + item.setData(Qt.UserRole, torrent) + self.window().playlist_manage_in_channel_list.addItem(item) + + for torrent in self.torrents_in_playlist: + item = QListWidgetItem(torrent["name"], self.window().playlist_manage_in_playlist_list) + item.setData(Qt.UserRole, torrent) + self.window().playlist_manage_in_playlist_list.addItem(item) + + @staticmethod + def remove_torrent_from_list(torrent, remove_from_list): + index = -1 + for torrent_index in xrange(len(remove_from_list)): + if remove_from_list[torrent_index]['infohash'] == torrent['infohash']: + index = torrent_index + break + + if index != -1: + del remove_from_list[index] + + def on_received_channel_torrents(self, result): + self.torrents_in_playlist = self.playlist_info['torrents'] + + self.torrents_in_channel = [] + for torrent in result['torrents']: + if not ManagePlaylistPage.list_contains_torrent(self.torrents_in_playlist, torrent): + self.torrents_in_channel.append(torrent) + + self.update_lists() + + @staticmethod + def list_contains_torrent(torrent_list, torrent): + for playlist_torrent in torrent_list: + if torrent['infohash'] == playlist_torrent['infohash']: + return True + return False + + def on_add_clicked(self): + for item in self.window().playlist_manage_in_channel_list.selectedItems(): + torrent = item.data(Qt.UserRole) + ManagePlaylistPage.remove_torrent_from_list(torrent, self.torrents_in_channel) + self.torrents_in_playlist.append(torrent) + + if ManagePlaylistPage.list_contains_torrent(self.torrents_to_remove, torrent): + ManagePlaylistPage.remove_torrent_from_list(torrent, self.torrents_to_remove) + self.torrents_to_create.append(torrent) + + self.update_lists() + + def on_remove_clicked(self): + for item in self.window().playlist_manage_in_playlist_list.selectedItems(): + torrent = item.data(Qt.UserRole) + ManagePlaylistPage.remove_torrent_from_list(torrent, self.torrents_in_playlist) + self.torrents_in_channel.append(torrent) + + if ManagePlaylistPage.list_contains_torrent(self.torrents_to_create, torrent): + ManagePlaylistPage.remove_torrent_from_list(torrent, self.torrents_to_create) + self.torrents_to_remove.append(torrent) + + self.update_lists() + + def on_save_clicked(self): + self.requests_done = 0 + self.pending_requests = [] + for torrent in self.torrents_to_create: + request = TriblerRequestManager() + request.perform_request("channels/discovered/%s/playlists/%s/%s" % + (self.channel_info["identifier"], self.playlist_info['id'], + torrent['infohash']), self.on_request_done, method="PUT") + self.pending_requests.append(request) + for torrent in self.torrents_to_remove: + request = TriblerRequestManager() + request.perform_request("channels/discovered/%s/playlists/%s/%s" % + (self.channel_info["identifier"], self.playlist_info['id'], torrent['infohash']), + self.on_request_done, method="DELETE") + self.pending_requests.append(request) + + def on_request_done(self, _): + self.requests_done += 1 + if self.requests_done == len(self.pending_requests): + self.on_requests_done() + + def on_requests_done(self): + self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_PLAYLIST_TORRENTS) + self.playlist_saved.emit() diff --git a/TriblerGUI/widgets/playlist_list_item.py b/TriblerGUI/widgets/playlist_list_item.py new file mode 100644 index 00000000000..4cce3950e82 --- /dev/null +++ b/TriblerGUI/widgets/playlist_list_item.py @@ -0,0 +1,45 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.tribler_window import fc_playlist_list_item +from TriblerGUI.utilities import get_image_path + + +class PlaylistListItem(QWidget, fc_playlist_list_item): + """ + This class is responsible for managing the playlist item widget. + """ + + def __init__(self, parent, playlist, show_controls=False, on_remove_clicked=None, on_edit_clicked=None): + QWidget.__init__(self, parent) + fc_playlist_list_item.__init__(self) + + self.setupUi(self) + + self.playlist_info = playlist + + self.edit_playlist_button.setIcon(QIcon(get_image_path("edit_white.png"))) + self.remove_playlist_button.setIcon(QIcon(get_image_path("delete.png"))) + + self.playlist_name.setText(playlist["name"]) + self.playlist_num_items.setText("%d items" % len(playlist["torrents"])) + + self.thumbnail_widget.initialize(playlist["name"], 24) + + self.controls_container.setHidden(True) + self.show_controls = show_controls + + if on_remove_clicked is not None: + self.remove_playlist_button.clicked.connect(lambda: on_remove_clicked(self)) + + if on_edit_clicked is not None: + self.edit_playlist_button.clicked.connect(lambda: on_edit_clicked(self)) + + def enterEvent(self, _): + if self.show_controls: + self.controls_container.setHidden(False) + self.edit_playlist_button.setIcon(QIcon(get_image_path('edit_white.png'))) + self.remove_playlist_button.setIcon(QIcon(get_image_path('delete.png'))) + + def leaveEvent(self, _): + self.controls_container.setHidden(True) diff --git a/TriblerGUI/widgets/playlistpage.py b/TriblerGUI/widgets/playlistpage.py new file mode 100644 index 00000000000..4d2eaafa946 --- /dev/null +++ b/TriblerGUI/widgets/playlistpage.py @@ -0,0 +1,26 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem +from TriblerGUI.utilities import get_image_path + + +class PlaylistPage(QWidget): + """ + This page shows torrents inside a specific playlist. + """ + + def __init__(self): + QWidget.__init__(self) + self.playlist = None + + def initialize_with_playlist(self, playlist): + self.playlist = playlist + self.window().playlist_name_label.setText(playlist["name"]) + self.window().playlist_num_items_label.setText("%d items" % len(playlist["torrents"])) + self.window().playlist_back_button.setIcon(QIcon(get_image_path('page_back.png'))) + + items = [] + for result in playlist['torrents']: + items.append((ChannelTorrentListItem, result)) + self.window().playlist_torrents_list.set_data_items(items) diff --git a/TriblerGUI/widgets/searchresultspage.py b/TriblerGUI/widgets/searchresultspage.py new file mode 100644 index 00000000000..d749a076624 --- /dev/null +++ b/TriblerGUI/widgets/searchresultspage.py @@ -0,0 +1,91 @@ +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.widgets.channel_list_item import ChannelListItem +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem +from TriblerGUI.utilities import bisect_right + + +class SearchResultsPage(QWidget): + """ + This class is responsible for displaying the search results. + """ + + def __init__(self): + QWidget.__init__(self) + self.search_results = {'channels': [], 'torrents': []} + self.health_timer = None + + def initialize_search_results_page(self): + self.window().search_results_tab.initialize() + self.window().search_results_tab.clicked_tab_button.connect(self.clicked_tab_button) + + def perform_search(self, query): + self.search_results = {'channels': [], 'torrents': []} + self.window().num_search_results_label.setText("") + self.window().search_results_header_label.setText("Search results for '%s'" % query) + self.window().search_results_list.set_data_items([]) # To clean the list + self.window().search_results_tab.on_tab_button_click(self.window().search_results_all_button) + + # Start the health timer that checks the health of the first five results + if self.health_timer: + self.health_timer.stop() + + self.health_timer = QTimer() + self.health_timer.setSingleShot(True) + self.health_timer.timeout.connect(self.check_health_of_results) + self.health_timer.start(2000) + + def check_health_of_results(self): + first_torrents = self.window().search_results_list.get_first_items(5, cls=ChannelTorrentListItem) + for torrent_item in first_torrents: + torrent_item.check_health() + + def clicked_tab_button(self, tab_button_name): + if tab_button_name == "search_results_all_button": + self.load_search_results_in_list() + elif tab_button_name == "search_results_channels_button": + self.load_search_results_in_list(show_torrents=False) + elif tab_button_name == "search_results_torrents_button": + self.load_search_results_in_list(show_channels=False) + + def update_num_search_results(self): + self.window().num_search_results_label.setText("%d results" % + (len(self.search_results['channels']) + + len(self.search_results['torrents']))) + + def load_search_results_in_list(self, show_channels=True, show_torrents=True): + if show_channels and show_torrents: + torrents_list = [(ChannelTorrentListItem, torrent) for torrent in self.search_results['torrents']] + channels_list = [(ChannelListItem, channel) for channel in self.search_results['channels']] + + self.window().search_results_list.set_data_items(channels_list + torrents_list) + return + + all_items = [] + if show_channels: + for channel_item in self.search_results['channels']: + all_items.append((ChannelListItem, channel_item)) + + if show_torrents: + for torrent_item in self.search_results['torrents']: + all_items.append((ChannelTorrentListItem, torrent_item)) + + self.window().search_results_list.set_data_items(all_items) + + def received_search_result_channel(self, result): + # Ignore channels that have a small amount of torrents or have no votes + if result['torrents'] <= 2 or result['votes'] == 0: + return + + channel_index = bisect_right(result, self.search_results['channels'], is_torrent=False) + self.window().search_results_list.insert_item(channel_index, (ChannelListItem, result)) + self.search_results['channels'].insert(channel_index, result) + self.update_num_search_results() + + def received_search_result_torrent(self, result): + torrent_index = bisect_right(result, self.search_results['torrents'], is_torrent=True) + self.search_results['torrents'].insert(torrent_index, result) + self.window().search_results_list.insert_item( + torrent_index + len(self.search_results['channels']), (ChannelTorrentListItem, result)) + self.update_num_search_results() diff --git a/TriblerGUI/widgets/settingspage.py b/TriblerGUI/widgets/settingspage.py new file mode 100644 index 00000000000..c2e60ceb1f7 --- /dev/null +++ b/TriblerGUI/widgets/settingspage.py @@ -0,0 +1,189 @@ +import json +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.defs import PAGE_SETTINGS_GENERAL, PAGE_SETTINGS_CONNECTION, PAGE_SETTINGS_BANDWIDTH, \ + PAGE_SETTINGS_SEEDING, PAGE_SETTINGS_ANONYMITY, BUTTON_TYPE_NORMAL +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import seconds_to_string, string_to_minutes, get_gui_setting + + +class SettingsPage(QWidget): + """ + This class is responsible for displaying and adjusting the settings present in Tribler. + """ + + def __init__(self): + QWidget.__init__(self) + self.settings = None + self.settings_request_mgr = None + self.saved_dialog = None + + def initialize_settings_page(self): + self.window().settings_tab.initialize() + self.window().settings_tab.clicked_tab_button.connect(self.clicked_tab_button) + self.window().settings_save_button.clicked.connect(self.save_settings) + + self.window().developer_mode_enabled_checkbox.stateChanged.connect(self.on_developer_mode_checkbox_changed) + self.window().download_settings_anon_checkbox.stateChanged.connect(self.on_anon_download_state_changed) + + def on_developer_mode_checkbox_changed(self, _): + self.window().gui_settings.setValue("debug", self.window().developer_mode_enabled_checkbox.isChecked()) + self.window().left_menu_button_debug.setHidden(not self.window().developer_mode_enabled_checkbox.isChecked()) + + def on_anon_download_state_changed(self, _): + if self.window().download_settings_anon_checkbox.isChecked(): + self.window().download_settings_anon_seeding_checkbox.setChecked(True) + self.window().download_settings_anon_seeding_checkbox.setEnabled( + not self.window().download_settings_anon_checkbox.isChecked()) + + def initialize_with_settings(self, settings): + self.settings = settings + settings = settings["settings"] + gui_settings = self.window().gui_settings + + # General settings + self.window().developer_mode_enabled_checkbox.setChecked(get_gui_setting(gui_settings, "debug", + False, is_bool=True)) + self.window().family_filter_checkbox.setChecked(settings['general']['family_filter']) + self.window().download_location_input.setText(settings['downloadconfig']['saveas']) + self.window().always_ask_location_checkbox.setChecked( + get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True)) + self.window().download_settings_anon_checkbox.setChecked(get_gui_setting( + gui_settings, "default_anonymity_enabled", True, is_bool=True)) + self.window().download_settings_anon_seeding_checkbox.setChecked( + get_gui_setting(gui_settings, "default_safeseeding_enabled", True, is_bool=True)) + self.window().watchfolder_enabled_checkbox.setChecked(settings['watch_folder']['enabled']) + self.window().watchfolder_location_input.setText(settings['watch_folder']['watch_folder_dir']) + + # Connection settings + self.window().firewall_current_port_input.setText(str(settings['general']['minport'])) + self.window().lt_proxy_type_combobox.setCurrentIndex(settings['libtorrent']['lt_proxytype']) + if settings['libtorrent']['lt_proxyserver']: + self.window().lt_proxy_server_input.setText(settings['libtorrent']['lt_proxyserver'][0]) + self.window().lt_proxy_port_input.setText(settings['libtorrent']['lt_proxyserver'][1]) + if settings['libtorrent']['lt_proxyauth']: + self.window().lt_proxy_username_input.setText(settings['libtorrent']['lt_proxyauth'][0]) + self.window().lt_proxy_password_input.setText(settings['libtorrent']['lt_proxyauth'][1]) + self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp']) + + max_conn_download = settings['libtorrent']['max_connections_download'] + if max_conn_download == -1: + max_conn_download = 0 + self.window().max_connections_download_input.setText(str(max_conn_download)) + + # Bandwidth settings + self.window().upload_rate_limit_input.setText(str(settings['Tribler']['maxuploadrate'])) + self.window().download_rate_limit_input.setText(str(settings['Tribler']['maxdownloadrate'])) + + # Seeding settings + getattr(self.window(), "seeding_" + settings['downloadconfig']['seeding_mode'] + "_radio").setChecked(True) + self.window().seeding_time_input.setText(seconds_to_string(settings['downloadconfig']['seeding_time'])) + ind = self.window().seeding_ratio_combobox.findText(str(settings['downloadconfig']['seeding_ratio'])) + if ind != -1: + self.window().seeding_ratio_combobox.setCurrentIndex(ind) + + # Anonymity settings + self.window().allow_exit_node_checkbox.setChecked(settings['tunnel_community']['exitnode_enabled']) + self.window().number_hops_slider.setValue(int(settings['Tribler']['default_number_hops']) - 1) + self.window().multichain_enabled_checkbox.setChecked(settings['multichain']['enabled']) + + def load_settings(self): + self.settings_request_mgr = TriblerRequestManager() + self.settings_request_mgr.perform_request("settings", self.initialize_with_settings) + + def clicked_tab_button(self, tab_button_name): + if tab_button_name == "settings_general_button": + self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_GENERAL) + elif tab_button_name == "settings_connection_button": + self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_CONNECTION) + elif tab_button_name == "settings_bandwidth_button": + self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_BANDWIDTH) + elif tab_button_name == "settings_seeding_button": + self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_SEEDING) + elif tab_button_name == "settings_anonymity_button": + self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_ANONYMITY) + + def save_settings(self): + # Create a dictionary with all available settings + settings_data = {'general': {}, 'Tribler': {}, 'downloadconfig': {}, 'libtorrent': {}, 'watch_folder': {}, + 'tunnel_community': {}, 'multichain': {}} + settings_data['general']['family_filter'] = self.window().family_filter_checkbox.isChecked() + settings_data['downloadconfig']['saveas'] = self.window().download_location_input.text() + + settings_data['watch_folder']['enabled'] = self.window().watchfolder_enabled_checkbox.isChecked() + if settings_data['watch_folder']['enabled']: + settings_data['watch_folder']['watch_folder_dir'] = self.window().watchfolder_location_input.text() + + settings_data['general']['minport'] = self.window().firewall_current_port_input.text() + settings_data['libtorrent']['lt_proxytype'] = self.window().lt_proxy_type_combobox.currentIndex() + + if len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0: + settings_data['libtorrent']['lt_proxyserver'] = [None, None] + settings_data['libtorrent']['lt_proxyserver'][0] = self.window().lt_proxy_server_input.text() + settings_data['libtorrent']['lt_proxyserver'][1] = self.window().lt_proxy_port_input.text() + + if len(self.window().lt_proxy_username_input.text()) > 0 and \ + len(self.window().lt_proxy_password_input.text()) > 0: + settings_data['libtorrent']['lt_proxyauth'] = [None, None] + settings_data['libtorrent']['lt_proxyauth'][0] = self.window().lt_proxy_username_input.text() + settings_data['libtorrent']['lt_proxyauth'][1] = self.window().lt_proxy_password_input.text() + settings_data['libtorrent']['utp'] = self.window().lt_utp_checkbox.isChecked() + + try: + max_conn_download = int(self.window().max_connections_download_input.text()) + except ValueError: + ConfirmationDialog.show_error(self.window(), "Invalid number of connections", + "You've entered an invalid format for the maximum number of connections.") + return + if max_conn_download == 0: + max_conn_download = -1 + settings_data['libtorrent']['max_connections_download'] = max_conn_download + + if self.window().upload_rate_limit_input.text(): + settings_data['Tribler']['maxuploadrate'] = self.window().upload_rate_limit_input.text() + if self.window().download_rate_limit_input.text(): + settings_data['Tribler']['maxdownloadrate'] = self.window().download_rate_limit_input.text() + + seeding_modes = ['forever', 'time', 'never', 'ratio'] + selected_mode = 'forever' + for seeding_mode in seeding_modes: + if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked(): + selected_mode = seeding_mode + break + settings_data['downloadconfig']['seeding_mode'] = selected_mode + settings_data['downloadconfig']['seeding_ratio'] = self.window().seeding_ratio_combobox.currentText() + + try: + settings_data['downloadconfig']['seeding_time'] = string_to_minutes(self.window().seeding_time_input.text()) + except ValueError: + ConfirmationDialog.show_error(self.window(), "Invalid seeding time", + "You've entered an invalid format for the seeding time (expected HH:MM)") + return + + settings_data['tunnel_community']['exitnode_enabled'] = self.window().allow_exit_node_checkbox.isChecked() + settings_data['Tribler']['default_number_hops'] = self.window().number_hops_slider.value() + 1 + settings_data['multichain']['enabled'] = self.window().multichain_enabled_checkbox.isChecked() + + self.settings_request_mgr = TriblerRequestManager() + self.settings_request_mgr.perform_request("settings", self.on_settings_saved, + method='POST', data=json.dumps(settings_data)) + + def on_settings_saved(self, _): + # Now save the GUI settings + self.window().gui_settings.setValue("ask_download_settings", + self.window().always_ask_location_checkbox.isChecked()) + self.window().gui_settings.setValue("default_anonymity_enabled", + self.window().download_settings_anon_checkbox.isChecked()) + self.window().gui_settings.setValue("default_safeseeding_enabled", + self.window().download_settings_anon_seeding_checkbox.isChecked()) + + self.saved_dialog = ConfirmationDialog(TriblerRequestManager.window, "Settings saved", + "Your settings have been saved.", [('close', BUTTON_TYPE_NORMAL)]) + self.saved_dialog.button_clicked.connect(self.on_dialog_cancel_clicked) + self.saved_dialog.show() + self.window().fetch_settings() + + def on_dialog_cancel_clicked(self, _): + self.saved_dialog.setParent(None) + self.saved_dialog = None diff --git a/TriblerGUI/widgets/subscribedchannelspage.py b/TriblerGUI/widgets/subscribedchannelspage.py new file mode 100644 index 00000000000..e2ab5421f50 --- /dev/null +++ b/TriblerGUI/widgets/subscribedchannelspage.py @@ -0,0 +1,64 @@ +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.widgets.channel_list_item import ChannelListItem +from TriblerGUI.defs import BUTTON_TYPE_NORMAL, BUTTON_TYPE_CONFIRM +from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog +from TriblerGUI.widgets.loading_list_item import LoadingListItem +from TriblerGUI.tribler_request_manager import TriblerRequestManager + + +class SubscribedChannelsPage(QWidget): + """ + This page shows all the channels that the user has subscribed to. + """ + + def __init__(self): + QWidget.__init__(self) + + self.dialog = None + self.request_mgr = None + + def initialize(self): + self.window().add_subscription_button.clicked.connect(self.on_add_subscription_clicked) + + def load_subscribed_channels(self): + self.window().subscribed_channels_list.set_data_items([(LoadingListItem, None)]) + + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("channels/subscribed", self.received_subscribed_channels) + + def received_subscribed_channels(self, results): + self.window().subscribed_channels_list.set_data_items([]) + items = [] + + if len(results['subscribed']) == 0: + self.window().subscribed_channels_list.set_data_items( + [(LoadingListItem, "You are not subscribed to any channel.")]) + return + + for result in results['subscribed']: + items.append((ChannelListItem, result)) + self.window().subscribed_channels_list.set_data_items(items) + + def on_add_subscription_clicked(self): + self.dialog = ConfirmationDialog(self, "Add subscribed channel", + "Please enter the identifier of the channel you want to subscribe to below. " + "It can take up to a minute before the channel is visible in your list of " + "subscribed channels.", + [('add', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)], + show_input=True) + self.dialog.dialog_widget.dialog_input.setPlaceholderText('Channel identifier') + self.dialog.button_clicked.connect(self.on_subscription_added) + self.dialog.show() + + def on_subscription_added(self, action): + if action == 0: + self.request_mgr = TriblerRequestManager() + self.request_mgr.perform_request("channels/subscribed/%s" % self.dialog.dialog_widget.dialog_input.text(), + self.on_channel_subscribed, method='PUT') + + self.dialog.setParent(None) + self.dialog = None + + def on_channel_subscribed(self, _): + pass diff --git a/TriblerGUI/widgets/subscriptionswidget.py b/TriblerGUI/widgets/subscriptionswidget.py new file mode 100644 index 00000000000..3db2744d3e7 --- /dev/null +++ b/TriblerGUI/widgets/subscriptionswidget.py @@ -0,0 +1,65 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.tribler_request_manager import TriblerRequestManager +from TriblerGUI.utilities import get_image_path + + +class SubscriptionsWidget(QWidget): + """ + This widget shows a favorite button and the number of subscriptions that a specific channel has. + """ + + unsubscribed_channel = pyqtSignal(object) + subscribed_channel = pyqtSignal(object) + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.subscribe_button = None + self.channel_info = None + self.num_subs_label = None + self.request_mgr = None + + def initialize_with_channel(self, channel): + self.channel_info = channel + + self.subscribe_button = self.findChild(QWidget, "subscribe_button") + self.num_subs_label = self.findChild(QWidget, "num_subs_label") + + self.subscribe_button.clicked.connect(self.on_subscribe_button_click) + self.update_subscribe_button() + + def update_subscribe_button(self): + if self.channel_info["subscribed"]: + self.subscribe_button.setIcon(QIcon(QPixmap(get_image_path('subscribed_yes.png')))) + else: + self.subscribe_button.setIcon(QIcon(QPixmap(get_image_path('subscribed_not.png')))) + + self.num_subs_label.setText(str(self.channel_info["votes"])) + + def on_subscribe_button_click(self): + self.request_mgr = TriblerRequestManager() + if self.channel_info["subscribed"]: + self.request_mgr.perform_request("channels/subscribed/%s" % + self.channel_info['dispersy_cid'], + self.on_channel_unsubscribed, method='DELETE') + else: + self.request_mgr.perform_request("channels/subscribed/%s" % + self.channel_info['dispersy_cid'], + self.on_channel_subscribed, method='PUT') + + def on_channel_unsubscribed(self, json_result): + if json_result["unsubscribed"]: + self.unsubscribed_channel.emit(self.channel_info) + self.channel_info["subscribed"] = False + self.channel_info["votes"] -= 1 + self.update_subscribe_button() + + def on_channel_subscribed(self, json_result): + if json_result["subscribed"]: + self.subscribed_channel.emit(self.channel_info) + self.channel_info["subscribed"] = True + self.channel_info["votes"] += 1 + self.update_subscribe_button() diff --git a/TriblerGUI/widgets/tabbuttonpanel.py b/TriblerGUI/widgets/tabbuttonpanel.py new file mode 100644 index 00000000000..c9280c84f70 --- /dev/null +++ b/TriblerGUI/widgets/tabbuttonpanel.py @@ -0,0 +1,31 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QWidget + + +class TabButtonPanel(QWidget): + """ + This class manages the tab button panels that can often be found above pages. + """ + clicked_tab_button = pyqtSignal(str) + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.buttons = [] + + def initialize(self): + for button in self.findChildren(QWidget): + self.buttons.append(button) + button.clicked_tab_button.connect(self.on_tab_button_click) + + def on_tab_button_click(self, clicked_button): + self.deselect_all_buttons(except_select=clicked_button) + self.clicked_tab_button.emit(clicked_button.objectName()) + + def deselect_all_buttons(self, except_select=None): + for button in self.buttons: + if button == except_select: + button.setEnabled(False) + continue + button.setEnabled(True) + button.setChecked(False) + except_select.setChecked(True) diff --git a/TriblerGUI/widgets/text_list_item.py b/TriblerGUI/widgets/text_list_item.py new file mode 100644 index 00000000000..8f21898d558 --- /dev/null +++ b/TriblerGUI/widgets/text_list_item.py @@ -0,0 +1,18 @@ +from PyQt5 import uic +from PyQt5.QtWidgets import QWidget + +from TriblerGUI.utilities import get_ui_file_path + + +class TextListItem(QWidget): + """ + This widget represents a list item with only some text. + """ + + def __init__(self, parent, label_text=None): + QWidget.__init__(self, parent) + + uic.loadUi(get_ui_file_path('text_list_item.ui'), self) + + if label_text is not None: + self.textlabel.setText(label_text) diff --git a/TriblerGUI/widgets/thumbnailwidget.py b/TriblerGUI/widgets/thumbnailwidget.py new file mode 100644 index 00000000000..247f7f25b42 --- /dev/null +++ b/TriblerGUI/widgets/thumbnailwidget.py @@ -0,0 +1,23 @@ +from PyQt5.QtWidgets import QLabel +from TriblerGUI.utilities import get_color + + +class ThumbnailWidget(QLabel): + """ + This widget represents a thumbnail that is associated with a torrent. For now, it only acts as a placeholder. + """ + + #def __init__(self): + # QLabel.__init__(self) + + def initialize(self, torrent_name, font_size): + stripped_torrent_name = ''.join([i for i in torrent_name if i.isalpha() or i == ' ']) + + parts = stripped_torrent_name.split(" ") + if len(parts) == 1: + self.setText(parts[0][:1].upper()) + else: + self.setText(parts[0][:1].upper() + parts[1][:1].upper()) + + self.setStyleSheet("font-size: " + str(font_size) + + "px; color: rgba(255, 255, 255, 0.7); background-color: %s" % get_color(torrent_name)) diff --git a/TriblerGUI/widgets/underlinetabbutton.py b/TriblerGUI/widgets/underlinetabbutton.py new file mode 100644 index 00000000000..8c5b6a51eb9 --- /dev/null +++ b/TriblerGUI/widgets/underlinetabbutton.py @@ -0,0 +1,16 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QToolButton + + +class UnderlineTabButton(QToolButton): + """ + This class is responsible for the buttons in the tab panels that can often be found at the top of the page. + """ + + clicked_tab_button = pyqtSignal(object) + + def __init__(self, parent): + QToolButton.__init__(self, parent) + + def mouseReleaseEvent(self, _): + self.clicked_tab_button.emit(self) diff --git a/TriblerGUI/widgets/videoplayerinfobutton.py b/TriblerGUI/widgets/videoplayerinfobutton.py new file mode 100644 index 00000000000..22cc62abd9f --- /dev/null +++ b/TriblerGUI/widgets/videoplayerinfobutton.py @@ -0,0 +1,20 @@ +from PyQt5.QtCore import QPoint +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QToolButton +from TriblerGUI.widgets.videoplayerinfopopup import VideoPlayerInfoPopup + + +class VideoPlayerInfoButton(QToolButton): + + def __init__(self, parent): + QToolButton.__init__(self, parent) + self.popup = VideoPlayerInfoPopup(self.window()) + self.popup.hide() + + def enterEvent(self, _): + self.popup.show() + self.popup.raise_() + self.popup.move(QPoint(QCursor.pos().x() - self.popup.width(), QCursor.pos().y() - self.popup.height())) + + def leaveEvent(self, _): + self.popup.hide() diff --git a/TriblerGUI/widgets/videoplayerinfopopup.py b/TriblerGUI/widgets/videoplayerinfopopup.py new file mode 100644 index 00000000000..2433218abea --- /dev/null +++ b/TriblerGUI/widgets/videoplayerinfopopup.py @@ -0,0 +1,28 @@ +from PyQt5 import uic +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPainter +from PyQt5.QtWidgets import QWidget, QStyleOption, QStyle +from TriblerGUI.utilities import get_ui_file_path, format_speed + + +class VideoPlayerInfoPopup(QWidget): + + def __init__(self, parent): + QWidget.__init__(self, parent) + + uic.loadUi(get_ui_file_path('video_info_popup.ui'), self) + + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + + def update(self, download_info): + self.download_speed_label.setText("Speed: d %s u %s" % (format_speed(download_info["speed_down"]), + format_speed(download_info["speed_up"]))) + self.prebuf_label.setText("Pre-buffering progress: %s" % download_info["vod_prebuffering_progress_consec"]) + self.peers_label.setText("Peers: S%d L%d" % (download_info["num_seeds"], download_info["num_peers"])) + + def paintEvent(self, _): + opt = QStyleOption() + opt.initFrom(self) + painter = QPainter(self) + self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) diff --git a/TriblerGUI/widgets/videoplayerpage.py b/TriblerGUI/widgets/videoplayerpage.py new file mode 100644 index 00000000000..c04923fc62a --- /dev/null +++ b/TriblerGUI/widgets/videoplayerpage.py @@ -0,0 +1,202 @@ +import os +import sys +from PyQt5.QtCore import QTimer, QEvent, Qt + +from PyQt5.QtGui import QPixmap, QIcon +from PyQt5.QtWidgets import QWidget +from Tribler import vlc +from TriblerGUI.defs import * +from TriblerGUI.utilities import is_video_file, seconds_to_string, get_image_path + + +class VideoPlayerPage(QWidget): + """ + This class manages the video player and all controls on the page. + """ + + def __init__(self): + QWidget.__init__(self) + + self.video_player_port = None + self.active_infohash = "" + self.active_index = -1 + self.is_full_screen = False + self.media = None + self.mediaplayer = None + self.instance = None + self.manager = None + self.play_icon = None + self.pause_icon = None + self.volume_on_icon = None + self.volume_off_icon = None + self.update_timer = None + + def initialize_player(self): + if vlc.plugin_path: + os.environ['VLC_PLUGIN_PATH'] = vlc.plugin_path + + self.instance = vlc.Instance() + self.mediaplayer = self.instance.media_player_new() + self.window().video_player_widget.should_hide_video_widgets.connect(self.hide_video_widgets) + self.window().video_player_widget.should_show_video_widgets.connect(self.show_video_widgets) + self.window().video_player_position_slider.should_change_video_position.connect( + self.on_should_change_video_time) + self.window().video_player_volume_slider.valueChanged.connect(self.on_volume_change) + self.window().video_player_volume_slider.setValue(self.mediaplayer.audio_get_volume()) + self.window().video_player_volume_slider.setFixedWidth(0) + + self.window().video_player_play_pause_button.clicked.connect(self.on_play_pause_button_click) + self.window().video_player_volume_button.clicked.connect(self.on_volume_button_click) + self.window().video_player_full_screen_button.clicked.connect(self.on_full_screen_button_click) + + # Create play/pause and volume button images + self.play_icon = QIcon(QPixmap(get_image_path("play.png"))) + self.pause_icon = QIcon(QPixmap(get_image_path("pause.png"))) + self.volume_on_icon = QIcon(QPixmap(get_image_path("volume_on.png"))) + self.volume_off_icon = QIcon(QPixmap(get_image_path("volume_off.png"))) + self.window().video_player_play_pause_button.setIcon(self.play_icon) + self.window().video_player_volume_button.setIcon(self.volume_on_icon) + self.window().video_player_full_screen_button.setIcon(QIcon(QPixmap(get_image_path("full_screen.png")))) + self.window().video_player_info_button.setIcon(QIcon(QPixmap(get_image_path("info.png")))) + self.window().video_player_info_button.hide() + + if sys.platform.startswith('linux'): + self.mediaplayer.set_xwindow(self.window().video_player_widget.winId()) + elif sys.platform == "win32": + self.mediaplayer.set_hwnd(self.window().video_player_widget.winId()) + elif sys.platform == "darwin": + self.mediaplayer.set_nsobject(int(self.window().video_player_widget.winId())) + + self.manager = self.mediaplayer.event_manager() + self.manager.event_attach(vlc.EventType.MediaPlayerBuffering, self.on_vlc_player_buffering) + self.manager.event_attach(vlc.EventType.MediaPlayerPlaying, self.on_vlc_player_playing) + + self.update_timer = QTimer() + self.update_timer.timeout.connect(self.on_update_timer_tick) + self.update_timer.start(500) + + self.window().left_menu_playlist.playing_item_change.connect(self.change_playing_index) + self.window().video_player_play_pause_button.setEnabled(False) + + def hide_video_widgets(self): + if self.is_full_screen: + self.window().video_player_header_label.setHidden(True) + self.window().video_player_controls_container.setHidden(True) + + def show_video_widgets(self): + self.window().video_player_header_label.setHidden(False) + self.window().video_player_controls_container.setHidden(False) + + def on_update_timer_tick(self): + total_duration_str = "-:--" + if self.media and self.media.get_duration() != 0: + total_duration_str = seconds_to_string(self.media.get_duration() / 1000) + + if self.active_infohash == "" or self.active_index == -1: + self.window().video_player_position_slider.setValue(0) + self.window().video_player_time_label.setText("0:00 / -:--") + else: + video_time = self.mediaplayer.get_time() + if video_time == -1: + video_time = 0 + + self.window().video_player_position_slider.setValue(self.mediaplayer.get_position() * 1000) + self.window().video_player_time_label.setText("%s / %s" % + (seconds_to_string(video_time / 1000), total_duration_str)) + + def update_with_download_info(self, download): + if len(download["files"]) > 0 and not self.window().left_menu_playlist.loaded_list: + self.window().left_menu_playlist.set_files(download["files"]) + + # Play the video with the largest file index + largest_file = None + + for file_info in download["files"]: + if is_video_file(file_info["name"]) and (largest_file is None or + file_info["size"] > largest_file["size"]): + largest_file = file_info + + self.window().left_menu_playlist.set_active_index(largest_file["index"]) + self.change_playing_index(largest_file["index"], largest_file["name"]) + + self.window().video_player_info_button.popup.update(download) + + def on_vlc_player_buffering(self, event): + pass + + def on_vlc_player_playing(self, event): + pass + + def on_should_change_video_time(self, position): + self.mediaplayer.set_position(position) + + def on_play_pause_button_click(self): + if not self.active_infohash or self.active_index == -1: + return + + if not self.mediaplayer.is_playing(): + self.window().video_player_play_pause_button.setIcon(self.pause_icon) + self.mediaplayer.play() + else: + self.window().video_player_play_pause_button.setIcon(self.play_icon) + self.mediaplayer.pause() + + def on_volume_button_click(self): + if not self.mediaplayer.audio_get_mute(): + self.window().video_player_volume_button.setIcon(self.volume_off_icon) + else: + self.window().video_player_volume_button.setIcon(self.volume_on_icon) + self.mediaplayer.audio_toggle_mute() + + def on_volume_change(self): + self.mediaplayer.audio_set_volume(self.window().video_player_volume_slider.value()) + + def on_full_screen_button_click(self): + if not self.is_full_screen: + self.window().top_bar.hide() + self.window().left_menu.hide() + self.window().showFullScreen() + else: + self.window().exit_full_screen() + self.is_full_screen = not self.is_full_screen + + def set_torrent_infohash(self, infohash): + self.active_infohash = infohash + + def change_playing_index(self, index, filename): + self.active_index = index + self.window().video_player_header_label.setText(filename) + + # reset video player controls + self.mediaplayer.stop() + self.window().video_player_play_pause_button.setIcon(self.play_icon) + self.window().video_player_position_slider.setValue(0) + + media_filename = u"http://127.0.0.1:" + unicode(self.video_player_port) + "/" + \ + self.active_infohash + "/" + unicode(index) + self.media = self.instance.media_new(media_filename) + self.mediaplayer.set_media(self.media) + self.media.parse() + + self.window().video_player_play_pause_button.setIcon(self.pause_icon) + self.mediaplayer.play() + + self.window().video_player_play_pause_button.setEnabled(True) + self.window().video_player_info_button.show() + + def reset_player(self): + """ + Reset the video player, i.e. when a download is removed that was being played. + """ + self.window().left_menu_playlist.clear() + self.window().video_player_header_label.setText("") + self.mediaplayer.stop() + self.mediaplayer.set_media(None) + self.window().video_player_play_pause_button.setIcon(self.play_icon) + self.window().video_player_position_slider.setValue(0) + + def eventFilter(self, source, event): + if event.type() == QEvent.KeyRelease and self.isVisible() and not self.window().top_search_bar.hasFocus() and\ + event.key() == Qt.Key_Space: + self.on_play_pause_button_click() + return QWidget.eventFilter(self, source, event) diff --git a/TriblerGUI/widgets/videoplayerpositionslider.py b/TriblerGUI/widgets/videoplayerpositionslider.py new file mode 100644 index 00000000000..cfc63f8ea5a --- /dev/null +++ b/TriblerGUI/widgets/videoplayerpositionslider.py @@ -0,0 +1,24 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QSlider, QStyle + + +class VideoPlayerPositionSlider(QSlider): + """ + The position slider can be used to seek in a playing video. + """ + + should_change_video_position = pyqtSignal(float) + + def __init__(self, parent): + QSlider.__init__(self, parent) + + def enterEvent(self, _): + self.setFixedHeight(10) + + def leaveEvent(self, _): + self.setFixedHeight(4) + + def mousePressEvent(self, event): + progress = QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), event.x(), self.width()) + self.setValue(progress) + self.should_change_video_position.emit(float(progress) / 1000.0) diff --git a/TriblerGUI/widgets/videoplayervolumecontainer.py b/TriblerGUI/widgets/videoplayervolumecontainer.py new file mode 100644 index 00000000000..d22d7cd36cb --- /dev/null +++ b/TriblerGUI/widgets/videoplayervolumecontainer.py @@ -0,0 +1,18 @@ +from PyQt5.QtWidgets import QWidget, QSlider + + +class VideoPlayerVolumeContainer(QWidget): + """ + This class is responsible for the expanding volume slider when hovering over it. + """ + + def __init__(self, parent): + QWidget.__init__(self, parent) + + def enterEvent(self, _): + slider = self.findChild(QSlider, "video_player_volume_slider") + slider.setFixedWidth(150) + + def leaveEvent(self, _): + slider = self.findChild(QSlider, "video_player_volume_slider") + slider.setFixedWidth(0) diff --git a/TriblerGUI/widgets/videoplayerwidget.py b/TriblerGUI/widgets/videoplayerwidget.py new file mode 100644 index 00000000000..814f68ec71e --- /dev/null +++ b/TriblerGUI/widgets/videoplayerwidget.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import QTimer, Qt, pyqtSignal +from PyQt5.QtWidgets import QWidget + + +class VideoPlayerWidget(QWidget): + + should_hide_video_widgets = pyqtSignal() + should_show_video_widgets = pyqtSignal() + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.mouse_move_timer = QTimer() + + def should_hide_widgets(self): + self.setCursor(Qt.BlankCursor) + self.should_hide_video_widgets.emit() + + def should_show_widgets(self): + self.setCursor(Qt.ArrowCursor) + self.should_show_video_widgets.emit() + + def mouseMoveEvent(self, _): + self.should_show_widgets() + self.mouse_move_timer.stop() + self.mouse_move_timer.setSingleShot(True) + self.mouse_move_timer.timeout.connect(self.should_hide_widgets) + self.mouse_move_timer.start(2000) diff --git a/run_tribler.py b/run_tribler.py new file mode 100644 index 00000000000..687e70e6fe8 --- /dev/null +++ b/run_tribler.py @@ -0,0 +1,25 @@ +import os +import sys +import multiprocessing + +if __name__ == "__main__": + multiprocessing.freeze_support() + + from TriblerGUI.tribler_app import TriblerApplication + from TriblerGUI.tribler_window import TriblerWindow + + app = TriblerApplication("triblerapp", sys.argv) + + if app.is_running(): + for arg in sys.argv[1:]: + if os.path.exists(arg): + app.send_message("file:%s" % arg) + elif arg.startswith('magnet'): + app.send_message(arg) + sys.exit(1) + + window = TriblerWindow() + window.setWindowTitle("Tribler") + app.set_activation_window(window) + app.parse_sys_args(sys.argv) + sys.exit(app.exec_()) diff --git a/tribler.sh b/tribler.sh index c6237d0d7e5..74715ff3f44 100755 --- a/tribler.sh +++ b/tribler.sh @@ -5,7 +5,7 @@ UNAME="$(uname -s)" if [ -z "$PROFILE_TRIBLER" ]; then - TRIBLER_SCRIPT=Tribler/Main/tribler.py + TRIBLER_SCRIPT=run_tribler.py else TRIBLER_SCRIPT=Tribler/Main/tribler_profiler.py fi @@ -26,9 +26,9 @@ if [ "$UNAME" = "Linux" ]; then exit 1 } - python2.7 $TRIBLER_SCRIPT + python2.7 $TRIBLER_SCRIPT "$@" elif [ ! -z `uname -s | grep CYGWIN_NT` ]; then - python $TRIBLER_SCRIPT + python $TRIBLER_SCRIPT "$@" else if [ "$UNAME" = "Darwin" ]; then @@ -43,6 +43,6 @@ else PYTHONVER=2.7 PYTHON="/usr/bin/python$PYTHONVER" - $PYTHON $TRIBLER_SCRIPT + $PYTHON $TRIBLER_SCRIPT "$@" fi fi From 5ecbbf8b1cd7b7625013644efaceaa236114c12a Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:02:29 +0100 Subject: [PATCH 2/9] Fixed paths in frozen environments --- Tribler/Core/CacheDB/sqlitecachedb.py | 2 - Tribler/Core/Category/Category.py | 11 ++-- Tribler/Core/Category/FamilyFilter.py | 6 +- Tribler/Core/Config/tribler_config.py | 3 +- Tribler/Core/Session.py | 8 ++- Tribler/Core/Utilities/install_dir.py | 57 +++++++++++-------- Tribler/Test/Core/Category/test_category.py | 31 +++++----- .../Test/Core/Category/test_family_filter.py | 36 ++++++------ .../Test/Core/test_sqlitecachedbhandler.py | 8 +-- 9 files changed, 81 insertions(+), 81 deletions(-) diff --git a/Tribler/Core/CacheDB/sqlitecachedb.py b/Tribler/Core/CacheDB/sqlitecachedb.py index d6ea5b67696..f2aba76f674 100644 --- a/Tribler/Core/CacheDB/sqlitecachedb.py +++ b/Tribler/Core/CacheDB/sqlitecachedb.py @@ -12,12 +12,10 @@ from Tribler.dispersy.taskmanager import TaskManager from Tribler.dispersy.util import blocking_call_on_reactor_thread, call_on_reactor_thread -from Tribler import LIBRARYNAME from Tribler.Core.CacheDB.db_versions import LATEST_DB_VERSION DB_SCRIPT_NAME = u"schema_sdb_v%s.sql" % str(LATEST_DB_VERSION) -DB_SCRIPT_RELATIVE_PATH = os.path.join(LIBRARYNAME, DB_SCRIPT_NAME) DB_FILE_NAME = u"tribler.sdb" DB_DIR_NAME = u"sqlite" diff --git a/Tribler/Core/Category/Category.py b/Tribler/Core/Category/Category.py index fdadd287969..de32f1cc6bd 100644 --- a/Tribler/Core/Category/Category.py +++ b/Tribler/Core/Category/Category.py @@ -6,10 +6,9 @@ import logging from ConfigParser import MissingSectionHeaderError, ParsingError -from Tribler import LIBRARYNAME from Tribler.Core.Category.init_category import getCategoryInfo from Tribler.Core.Category.FamilyFilter import XXXFilter -from Tribler.Core.Utilities.install_dir import determine_install_dir +from Tribler.Core.Utilities.install_dir import get_lib_path CATEGORY_CONFIG_FILE = "category.conf" @@ -18,12 +17,10 @@ class Category(object): __size_change = 1024 * 1024 - def __init__(self, install_dir=determine_install_dir(), ffEnabled=False): + def __init__(self, ffEnabled=False): self._logger = logging.getLogger(self.__class__.__name__) - self.install_dir = install_dir - - filename = os.path.join(self.install_dir, LIBRARYNAME, u'Core', u'Category', CATEGORY_CONFIG_FILE) + filename = os.path.join(get_lib_path(), u'Core', u'Category', CATEGORY_CONFIG_FILE) try: self.category_info = getCategoryInfo(filename) self.category_info.sort(cmp_rank) @@ -31,7 +28,7 @@ def __init__(self, install_dir=determine_install_dir(), ffEnabled=False): self.category_info = [] self._logger.critical('', exc_info=True) - self.xxx_filter = XXXFilter(self.install_dir) + self.xxx_filter = XXXFilter() self._logger.debug("category: Categories defined by user: %s", self.getCategoryNames()) diff --git a/Tribler/Core/Category/FamilyFilter.py b/Tribler/Core/Category/FamilyFilter.py index 5a8e8064dd4..46a10a5b148 100644 --- a/Tribler/Core/Category/FamilyFilter.py +++ b/Tribler/Core/Category/FamilyFilter.py @@ -5,18 +5,18 @@ import os import logging -from Tribler import LIBRARYNAME +from Tribler.Core.Utilities.install_dir import get_lib_path WORDS_REGEXP = re.compile('[a-zA-Z0-9]+') class XXXFilter(object): - def __init__(self, install_dir): + def __init__(self): super(XXXFilter, self).__init__() self._logger = logging.getLogger(self.__class__.__name__) - termfilename = os.path.join(install_dir, LIBRARYNAME, 'Core', 'Category', 'filter_terms.filter') + termfilename = os.path.join(get_lib_path(), 'Core', 'Category', 'filter_terms.filter') self.xxx_terms, self.xxx_searchterms = self.initTerms(termfilename) def initTerms(self, filename): diff --git a/Tribler/Core/Config/tribler_config.py b/Tribler/Core/Config/tribler_config.py index 5899c786401..0afd92c1c63 100644 --- a/Tribler/Core/Config/tribler_config.py +++ b/Tribler/Core/Config/tribler_config.py @@ -1,10 +1,11 @@ import os from configobj import ConfigObj from validate import Validator +from Tribler.Core.Utilities.install_dir import get_lib_path from Tribler.Core.simpledefs import STATEDIR_CONFIG -CONFIGSPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.spec') +CONFIGSPEC_PATH = os.path.join(get_lib_path(), 'Core', 'Config', 'config.spec') class TriblerConfig(object): diff --git a/Tribler/Core/Session.py b/Tribler/Core/Session.py index c11dda0d6e8..c20d880062a 100644 --- a/Tribler/Core/Session.py +++ b/Tribler/Core/Session.py @@ -19,12 +19,14 @@ from Tribler.Core import NoDispersyRLock from Tribler.Core.APIImplementation.LaunchManyCore import TriblerLaunchMany from Tribler.Core.CacheDB.Notifier import Notifier -from Tribler.Core.CacheDB.sqlitecachedb import SQLiteCacheDB, DB_FILE_RELATIVE_PATH, DB_SCRIPT_RELATIVE_PATH +from Tribler.Core.CacheDB.sqlitecachedb import SQLiteCacheDB, DB_FILE_RELATIVE_PATH, DB_SCRIPT_NAME from Tribler.Core.Config.tribler_config import TriblerConfig from Tribler.Core.Modules.restapi.rest_manager import RESTManager from Tribler.Core.SessionConfig import SessionConfigInterface, SessionStartupConfig from Tribler.Core.Upgrade.upgrade import TriblerUpgrader from Tribler.Core.Utilities.configparser import CallbackConfigParser +from Tribler.Core.Utilities.crypto_patcher import patch_crypto_be_discovery +from Tribler.Core.Utilities.install_dir import get_lib_path from Tribler.Core.defaults import tribler_defaults from Tribler.Core.exceptions import NotYetImplementedException, OperationNotEnabledByConfigurationException, \ DuplicateTorrentFileError @@ -73,6 +75,8 @@ def __init__(self, scfg=None, ignore_singleton=False, autoload_discovery=True): """ addObserver(self.unhandled_error_observer) + patch_crypto_be_discovery() + if not ignore_singleton: if Session.__single: raise RuntimeError("Session is singleton") @@ -199,7 +203,7 @@ def prestart(self): self.lm.api_manager.start() db_path = os.path.join(self.get_state_dir(), DB_FILE_RELATIVE_PATH) - db_script_path = os.path.join(self.get_install_dir(), DB_SCRIPT_RELATIVE_PATH) + db_script_path = os.path.join(get_lib_path(), DB_SCRIPT_NAME) self.sqlite_db = SQLiteCacheDB(db_path, db_script_path) self.sqlite_db.initialize() diff --git a/Tribler/Core/Utilities/install_dir.py b/Tribler/Core/Utilities/install_dir.py index d7a3519847b..4d85a101db0 100644 --- a/Tribler/Core/Utilities/install_dir.py +++ b/Tribler/Core/Utilities/install_dir.py @@ -35,12 +35,40 @@ # Code: import os.path import sys +import Tribler from Tribler.Core.osutils import is_android, get_home_dir -# This function is used from tribler.py too, but can't be there as tribler.py -# gets frozen into an exe on windows. +def is_frozen(): + """ + Return whether we are running in a frozen environment + """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + return False + return True + + +def get_base_path(): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.join(os.path.dirname(Tribler.__file__), '..') + return base_path + + +def get_lib_path(): + if is_frozen(): + return os.path.join(get_base_path(), 'tribler_source', 'Tribler') + return os.path.join(get_base_path(), 'Tribler') + + +# This function is used from tribler.py too, but can't be there as tribler.py gets frozen into an exe on windows. def determine_install_dir(): # Niels, 2011-03-03: Working dir sometimes set to a browsers working dir # only seen on windows @@ -52,30 +80,9 @@ def determine_install_dir(): # TODO(emilon): tribler_main.py is not frozen, so I think the special # treatment for windows could be removed (Needs to be tested) if sys.platform == 'win32': - def we_are_frozen(): - """Returns whether we are frozen via py2exe. - This will affect how we find out where we are located.""" - return hasattr(sys, "frozen") - - def module_path(): - """ This will get us the program's directory, - even if we are frozen using py2exe""" - if we_are_frozen(): - return os.path.dirname(unicode(sys.executable, sys.getfilesystemencoding())) - - filedir = os.path.dirname(unicode(__file__, sys.getfilesystemencoding())) - return os.path.abspath(os.path.join(filedir, '..', '..', '..')) - - return module_path() - + return get_base_path() elif sys.platform == 'darwin': - # On a packaged app, this file will be at: - # Tribler.app/Contents/Resources/lib/Python2.7/site-packages.zip/Tribler/Core/Utilities/install_dir.py - cur_file = os.path.dirname(__file__) - if "site-packages.zip" in cur_file: - return os.path.abspath(os.path.join(cur_file, '..', '..', '..', '..', '..', '..')) - # Otherwise do the same than on Unix/Linux - + return get_base_path() elif is_android(): return os.path.abspath(os.path.join(unicode(os.environ['ANDROID_PRIVATE']), u'lib/python2.7/site-packages')) diff --git a/Tribler/Test/Core/Category/test_category.py b/Tribler/Test/Core/Category/test_category.py index 51b1e75fc67..6bf56f01600 100644 --- a/Tribler/Test/Core/Category/test_category.py +++ b/Tribler/Test/Core/Category/test_category.py @@ -8,41 +8,37 @@ class TriblerCategoryTest(AbstractServer): - FILE_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) - CATEGORY_TEST_DATA_DIR = os.path.abspath(os.path.join(FILE_DIR, u"data/")) + def setUp(self, annotate=True): + super(TriblerCategoryTest, self).setUp(annotate=annotate) + self.category = Category() + self.category.xxx_filter.xxx_terms.add("term1") def test_category_names_none_names(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) - cat.category_info = None - self.assertFalse(cat.getCategoryNames()) + self.category.category_info = None + self.assertFalse(self.category.getCategoryNames()) def test_get_category_names(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) - self.assertEquals(len(cat.category_info), 9) + self.assertEquals(len(self.category.category_info), 9) def test_calculate_category_multi_file(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) torrent_info = {"info": {"files": [{"path": "/my/path/video.avi", "length": 1234}]}, "announce": "http://tracker.org", "comment": "lorem ipsum"} - self.assertEquals(cat.calculateCategory(torrent_info, "my torrent"), 'other') + self.assertEquals(self.category.calculateCategory(torrent_info, "my torrent"), 'other') def test_calculate_category_single_file(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) torrent_info = {"info": {"name": "my_torrent", "length": 1234}, "announce-list": ["http://tracker.org"], "comment": "lorem ipsum"} - self.assertEquals(cat.calculateCategory(torrent_info, "my torrent"), 'other') + self.assertEquals(self.category.calculateCategory(torrent_info, "my torrent"), 'other') def test_calculate_category_xxx(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) torrent_info = {"info": {"name": "term1", "length": 1234}, "announce-list": ["http://tracker.org"], "comment": "lorem ipsum"} - self.assertEquals(cat.calculateCategory(torrent_info, "my torrent"), 'xxx') + self.assertEquals(self.category.calculateCategory(torrent_info, "my torrent"), 'xxx') def test_get_family_filter_sql(self): - cat = Category(install_dir=self.CATEGORY_TEST_DATA_DIR) - self.assertFalse(cat.get_family_filter_sql()) - cat.set_family_filter(b=True) - self.assertTrue(cat.get_family_filter_sql()) + self.assertFalse(self.category.get_family_filter_sql()) + self.category.set_family_filter(b=True) + self.assertTrue(self.category.get_family_filter_sql()) def test_cmp_rank(self): self.assertEquals(cmp_rank({'bla': 3}, {'bla': 4}), 1) @@ -53,4 +49,3 @@ def test_non_existent_conf_file(self): category_file.CATEGORY_CONFIG_FILE = "thisfiledoesnotexist.conf" test_category = Category() self.assertEqual(test_category.category_info, []) - diff --git a/Tribler/Test/Core/Category/test_family_filter.py b/Tribler/Test/Core/Category/test_family_filter.py index b8e31222259..74396b54543 100644 --- a/Tribler/Test/Core/Category/test_family_filter.py +++ b/Tribler/Test/Core/Category/test_family_filter.py @@ -1,36 +1,34 @@ -import os - from Tribler.Core.Category.FamilyFilter import XXXFilter from Tribler.Test.test_as_server import AbstractServer class TriblerCategoryTestFamilyFilter(AbstractServer): - FILE_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) - CATEGORY_TEST_DATA_DIR = os.path.abspath(os.path.join(FILE_DIR, u"data/")) + def setUp(self, annotate=True): + super(TriblerCategoryTestFamilyFilter, self).setUp(annotate=annotate) + self.family_filter = XXXFilter() + self.family_filter.xxx_terms.add("term1") + self.family_filter.xxx_terms.add("term2") + self.family_filter.xxx_searchterms.add("term3") def test_filter_torrent(self): - family_filter = XXXFilter(self.CATEGORY_TEST_DATA_DIR) - self.assertFalse(family_filter.isXXXTorrent(["file1.txt"], "mytorrent", "http://tracker.org")) - self.assertFalse(family_filter.isXXXTorrent(["file1.txt"], "mytorrent", "")) - self.assertTrue(family_filter.isXXXTorrent(["term1.txt"], "term2", "")) + self.assertFalse(self.family_filter.isXXXTorrent(["file1.txt"], "mytorrent", "http://tracker.org")) + self.assertFalse(self.family_filter.isXXXTorrent(["file1.txt"], "mytorrent", "")) + self.assertTrue(self.family_filter.isXXXTorrent(["term1.txt"], "term2", "")) def test_is_xxx(self): - family_filter = XXXFilter(self.CATEGORY_TEST_DATA_DIR) - self.assertTrue(family_filter.isXXX("term1")) - self.assertFalse(family_filter.isXXX("term0")) - self.assertTrue(family_filter.isXXX("term3")) + self.assertTrue(self.family_filter.isXXX("term1")) + self.assertFalse(self.family_filter.isXXX("term0")) + self.assertTrue(self.family_filter.isXXX("term3")) def test_is_xxx_term(self): - family_filter = XXXFilter(self.CATEGORY_TEST_DATA_DIR) - self.assertTrue(family_filter.isXXXTerm("term1es")) - self.assertFalse(family_filter.isXXXTerm("term0es")) - self.assertTrue(family_filter.isXXXTerm("term1s")) - self.assertFalse(family_filter.isXXXTerm("term0n")) + self.assertTrue(self.family_filter.isXXXTerm("term1es")) + self.assertFalse(self.family_filter.isXXXTerm("term0es")) + self.assertTrue(self.family_filter.isXXXTerm("term1s")) + self.assertFalse(self.family_filter.isXXXTerm("term0n")) def test_invalid_filename_exception(self): - family_filter = XXXFilter(self.CATEGORY_TEST_DATA_DIR) - terms, searchterms = family_filter.initTerms("thisfiledoesnotexist.txt") + terms, searchterms = self.family_filter.initTerms("thisfiledoesnotexist.txt") self.assertEqual(len(terms), 0) self.assertEqual(len(searchterms), 0) diff --git a/Tribler/Test/Core/test_sqlitecachedbhandler.py b/Tribler/Test/Core/test_sqlitecachedbhandler.py index fce21ba29d6..6f42b5e0a0a 100644 --- a/Tribler/Test/Core/test_sqlitecachedbhandler.py +++ b/Tribler/Test/Core/test_sqlitecachedbhandler.py @@ -3,11 +3,11 @@ import tarfile from twisted.internet.defer import inlineCallbacks -from Tribler.Core.CacheDB.SqliteCacheDBHandler import (BasicDBHandler, - PeerDBHandler, LimitedOrderedDict) -from Tribler.Core.CacheDB.sqlitecachedb import str2bin, SQLiteCacheDB, DB_SCRIPT_RELATIVE_PATH +from Tribler.Core.CacheDB.SqliteCacheDBHandler import (BasicDBHandler, LimitedOrderedDict) +from Tribler.Core.CacheDB.sqlitecachedb import SQLiteCacheDB, DB_SCRIPT_NAME from Tribler.Core.Session import Session from Tribler.Core.SessionConfig import SessionStartupConfig +from Tribler.Core.Utilities.install_dir import get_lib_path from Tribler.Test.Core.base_test import TriblerCoreTest from Tribler.Test.test_as_server import TESTS_DATA_DIR from Tribler.dispersy.util import blocking_call_on_reactor_thread @@ -56,7 +56,7 @@ def setUp(self): tar.extractall(self.session_base_dir) db_path = os.path.join(self.session_base_dir, 'bak_new_tribler.sdb') - db_script_path = os.path.join(self.session.get_install_dir(), DB_SCRIPT_RELATIVE_PATH) + db_script_path = os.path.join(get_lib_path(), DB_SCRIPT_NAME) self.sqlitedb = SQLiteCacheDB(db_path, db_script_path, busytimeout=BUSYTIMEOUT) self.sqlitedb.initialize() From c422956b8f5d3e23602b39f01d23feef21cf2e6c Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:03:14 +0100 Subject: [PATCH 3/9] Fixed Tribler builders --- Tribler/Main/Build/Mac/Info.plist | 103 ++++++------ Tribler/Main/Build/Ubuntu/tribler.desktop | 2 +- Tribler/Main/Build/Win/tribler.ico | Bin 0 -> 24190 bytes Tribler/Main/Build/Win/tribler.nsi | 71 ++------- Tribler/Main/Build/update_version_from_git.py | 1 + debian/bin/tribler | 2 +- debian/control | 4 +- debian/rules | 16 -- debian/tribler.install | 3 + mac/makedistmac_64bit.sh | 19 +-- tribler.spec | 81 ++++++++++ win/makedist_win.bat | 148 +++--------------- 12 files changed, 174 insertions(+), 276 deletions(-) create mode 100644 Tribler/Main/Build/Win/tribler.ico create mode 100644 tribler.spec diff --git a/Tribler/Main/Build/Mac/Info.plist b/Tribler/Main/Build/Mac/Info.plist index 2830b1015c3..bde201aa6d7 100644 --- a/Tribler/Main/Build/Mac/Info.plist +++ b/Tribler/Main/Build/Mac/Info.plist @@ -1,57 +1,54 @@ - - CFBundleDevelopmentRegion - English - CFBundleDocumentTypes - - - CFBundleTypeExtensions - - torrent - - CFBundleTypeIconFile - TriblerDoc - CFBundleTypeMIMETypes - - application/x-bittorrent - - CFBundleTypeName - BitTorrent Meta-Info - CFBundleTypeOSTypes - - BTMF - - CFBundleTypeRole - Viewer - NSDocumentClass - DownloadDocument - - - CFBundleTypeOSTypes - - **** - fold - disk - - CFBundleTypeRole - Viewer - - - CFBundleExecutable - Tribler - CFBundleIconFile - tribler.icns - CFBundleIdentifier - Tribler - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Tribler - CFBundlePackageType - APPL - CFBundleSignature - ???? - + + CFBundleVersion + __VERSION__ + CFBundleShortVersionString + __VERSION__ + CFBundleInfoDictionaryVersion + 1.0 + CFBundleDisplayName + tribler + CFBundleIconFile + tribler.icns + CFBundleIdentifier + nl.tudelft.tribler + CFBundleURLTypes + + + CFBundleURLName + magnet URL + CFBundleURLSchemes + + magnet + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + torrent + + CFBundleTypeIconFile + tribler.icns + CFBundleTypeName + BitTorrent Document + LSHandlerRank + Owner + + + CFBundleExecutable + MacOS/tribler + CFBundleName + tribler + CFBundlePackageType + APPL + NSHighResolutionCapable + True + LSBackgroundOnly + 0 + diff --git a/Tribler/Main/Build/Ubuntu/tribler.desktop b/Tribler/Main/Build/Ubuntu/tribler.desktop index f275c1691fa..7d8f78154a6 100644 --- a/Tribler/Main/Build/Ubuntu/tribler.desktop +++ b/Tribler/Main/Build/Ubuntu/tribler.desktop @@ -6,4 +6,4 @@ Icon=tribler Terminal=false Type=Application Categories=Application;Network;P2P -MimeType=x-scheme-handler/ppsp;x-scheme-handler/tswift;x-scheme-handler/magnet \ No newline at end of file +MimeType=x-scheme-handler/ppsp;x-scheme-handler/tswift;x-scheme-handler/magnet;application/x-bittorrent diff --git a/Tribler/Main/Build/Win/tribler.ico b/Tribler/Main/Build/Win/tribler.ico new file mode 100644 index 0000000000000000000000000000000000000000..aaea20ee79ffd3be2643be5eb710de444014fa21 GIT binary patch literal 24190 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 F{15Pbq-6jA literal 0 HcmV?d00001 diff --git a/Tribler/Main/Build/Win/tribler.nsi b/Tribler/Main/Build/Win/tribler.nsi index 1cffbb9c151..a51616af2c5 100644 --- a/Tribler/Main/Build/Win/tribler.nsi +++ b/Tribler/Main/Build/Win/tribler.nsi @@ -1,9 +1,10 @@ !define PRODUCT "Tribler" -; Laurens, 2016-03-14: The __GIT__ string will be replaced by update_version_from_git.py +; Laurens, 2016-03-14: The __GIT__ string will be replaced by update_version_from_git.py ; with the current version of the build. !define VERSION "__GIT__" ; Laurens, 2016-03-14: The _x86 will be replaced by _x64 if needed in update_version_from_git.py !define BITVERSION "x86" +!define VLCBITVERSION "32" !include "MUI2.nsh" !include "FileFunc.nsh" @@ -21,7 +22,7 @@ Name "${PRODUCT} ${VERSION}" OutFile "${PRODUCT}_${VERSION}_${BITVERSION}.exe" ;Folder selection page. -; Laurens, 2016-03-14: Note that $PROGRAMFILES will be replaced by $PROGRAMFILES64 +; Laurens, 2016-03-14: Note that $PROGRAMFILES will be replaced by $PROGRAMFILES64 ; if the 64 bit argument is passed to update_version_from_git.py. InstallDir "$PROGRAMFILES\${PRODUCT}" @@ -68,7 +69,7 @@ BrandingText "${PRODUCT}" ;-------------------------------- ;Modern UI Configuration -!define MUI_ICON "Tribler\Main\vwxGUI\images\tribler.ico" +!define MUI_ICON "tribler_source\Tribler\Main\Build\Win\tribler.ico" !define MUI_COMPONENTSPAGE_SMALLDESC !define MUI_ABORTWARNING @@ -88,8 +89,6 @@ BrandingText "${PRODUCT}" !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES -;!insertmacro MUI_DEFAULT UMUI_HEADERIMAGE_BMP heading.bmp" - ;-------------------------------- ;Languages @@ -119,63 +118,9 @@ Section "!Main EXE" SecMain ; Install Tribler stuff SetOutPath "$INSTDIR" - File /r Microsoft.VC90.CRT File /r * - File *.txt - File logger.conf - File tribler.exe - File ffmpeg.exe - File /r vlc - File tools\*.bat - Delete "$INSTDIR\*.pyd" - File *.pyd - Delete "$INSTDIR\python*.dll" - Delete "$INSTDIR\wx*.dll" - File *.dll - CreateDirectory "$INSTDIR\Tribler" - SetOutPath "$INSTDIR\Tribler" - File Tribler\*.sql - CreateDirectory "$INSTDIR\Tribler\Core" - - ; Main client specific - CreateDirectory "$INSTDIR\Tribler" - CreateDirectory "$INSTDIR\Tribler\Main\vwxGUI" - CreateDirectory "$INSTDIR\Tribler\Main\vwxGUI\images" - CreateDirectory "$INSTDIR\Tribler\Main\vwxGUI\images\default" - CreateDirectory "$INSTDIR\Tribler\Main\vwxGUI\images\flags" - SetOutPath "$INSTDIR\Tribler\Main\vwxGUI\images" - File Tribler\Main\vwxGUI\images\* - SetOutPath "$INSTDIR\Tribler\Main\vwxGUI\images\default" - File Tribler\Main\vwxGUI\images\default\* - SetOutPath "$INSTDIR\Tribler\Main\vwxGUI\images\flags" - File Tribler\Main\vwxGUI\images\flags\* - - CreateDirectory "$INSTDIR\Tribler\Main\webUI" - CreateDirectory "$INSTDIR\Tribler\Main\webUI\static" - CreateDirectory "$INSTDIR\Tribler\Main\webUI\static\images" - CreateDirectory "$INSTDIR\Tribler\Main\webUI\static\lang" - SetOutPath "$INSTDIR\Tribler\Main\webUI\static" - File Tribler\Main\webUI\static\*.* - SetOutPath "$INSTDIR\Tribler\Main\webUI\static\images" - File Tribler\Main\webUI\static\images\*.* - SetOutPath "$INSTDIR\Tribler\Main\webUI\static\lang" - File Tribler\Main\webUI\static\lang\*.* - - ; Categories - CreateDirectory "$INSTDIR\Tribler\Core\Category" - SetOutPath "$INSTDIR\Tribler\Core\Category" - File Tribler\Core\Category\*.conf - File Tribler\Core\Category\*.filter - - ; Arno, 2012-05-25: data files for pymdht - CreateDirectory "$INSTDIR\Tribler\Core\DecentralizedTracking" - CreateDirectory "$INSTDIR\Tribler\Core\DecentralizedTracking\pymdht" - CreateDirectory "$INSTDIR\Tribler\Core\DecentralizedTracking\pymdht\core" - SetOutPath "$INSTDIR\Tribler\Core\DecentralizedTracking\pymdht\core" - File Tribler\Core\DecentralizedTracking\pymdht\core\bootstrap_stable - File Tribler\Core\DecentralizedTracking\pymdht\core\bootstrap_unstable - - ; Install MSVCR 2008, 2010, 2012 + + ; Install MSVCR 2008 and 2012 SetOutPath "$INSTDIR" ; Libraries dependant on 2008 are: Python, APSW @@ -186,6 +131,10 @@ Section "!Main EXE" SecMain File vc_redist_110.exe ExecWait "$INSTDIR\vc_redist_110.exe /q /norestart" + ; Install VLC + File "vlc-2.2.4-win${VLCBITVERSION}.exe" + ExecWait "$INSTDIR\vlc-2.2.4-win${VLCBITVERSION}.exe /language=en_GB /S" + FileOpen $9 "$INSTDIR\tribler.exe.log" w FileWrite $9 "" FileClose $9 diff --git a/Tribler/Main/Build/update_version_from_git.py b/Tribler/Main/Build/update_version_from_git.py index 9f0a9c4324c..c1d7ead3d92 100755 --- a/Tribler/Main/Build/update_version_from_git.py +++ b/Tribler/Main/Build/update_version_from_git.py @@ -52,6 +52,7 @@ def runCommand(cmd): # Check if we are building 64 bit, replace the install dir and bit version accordingly. if len(sys.argv) > 1 and sys.argv[1] == "64": content = content.replace('x86', 'x64') + content = content.replace('"32"', '"64"') content = content.replace('$PROGRAMFILES', '$PROGRAMFILES64') f.seek(0) diff --git a/debian/bin/tribler b/debian/bin/tribler index ec92e18088d..1fedb50b32a 100755 --- a/debian/bin/tribler +++ b/debian/bin/tribler @@ -4,4 +4,4 @@ echo "Starting Tribler..." cd "/usr/share/tribler" -exec /usr/bin/python2.7 -m Tribler.Main.tribler "$@" > `mktemp /tmp/$USER-tribler-XXXXXXXX.log` 2>&1 +exec /usr/bin/python2.7 run_tribler.py "$@" > `mktemp /tmp/$USER-tribler-XXXXXXXX.log` 2>&1 diff --git a/debian/control b/debian/control index 68c1249615a..d01e2d372e0 100644 --- a/debian/control +++ b/debian/control @@ -27,13 +27,13 @@ Depends: ${misc:Depends}, python-decorator, python-feedparser, python-leveldb, - python-libnacl, python-libtorrent (>= 0.16.18), python-m2crypto, python-netifaces, python-pil, python-twisted, - python-wxgtk3.0 | python-wxgtk2.8, + python-pyqt5, + python-pyqt5.qtsvg, vlc (>= 1.1.0), Description: Python based Bittorrent/Internet TV application It allows you to watch videos and download content. Tribler aims to combine diff --git a/debian/rules b/debian/rules index ab2687b6a25..bb5a2d62d35 100755 --- a/debian/rules +++ b/debian/rules @@ -20,32 +20,16 @@ GIT_URL = https://github.com/Tribler/tribler.git override_dh_auto_configure: dh_auto_configure -# TODO: The "find [..] -delete" line is a hack to work around the fact that some -# .py files get copied both in dist-packages and /usr/share/tribler, which should -# really be fixed. - override_dh_install: dh_install --exclude=.git - find $(CURDIR)/debian/tribler/usr/share/tribler -name "*.py" -delete - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Core/DecentralizedTracking/pymdht/LICENSE.txt rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/LICENSE.txt - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Main/vwxGUI/images/license - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Main/webUI/static/LICENSE - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/ns-LICENSE.txt rm -rf $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Test rm -rf $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Main/Build - rm -rf $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Player/Build - rm -rf $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Transport/Build rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Core/DecentralizedTracking/pymdht/LGPL-2.1.txt - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Main/webUI/static/mootools.js - rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/Main/webUI/static/excanvas.js rm -f $(CURDIR)/debian/tribler/usr/share/tribler/Tribler/dispersy/twisted/plugins/dropin.cache - dh_link -ptribler usr/share/javascript/mootools/mootools.js usr/share/tribler/Tribler/Main/webUI/static/mootools.js - dh_link -ptribler usr/share/javascript/excanvas/excanvas.js usr/share/tribler/Tribler/Main/webUI/static/excanvas.js - get-orig-source: set -e; if echo $(DEB_VERSION) | grep -c "git"; \ then \ diff --git a/debian/tribler.install b/debian/tribler.install index e367f6fb443..cdb565044f0 100644 --- a/debian/tribler.install +++ b/debian/tribler.install @@ -1,7 +1,10 @@ Tribler usr/share/tribler +TriblerGUI usr/share/tribler Tribler/schema_sdb_v*.sql usr/share/tribler/Tribler Tribler/Main/Build/Ubuntu/tribler.desktop usr/share/applications Tribler/Main/Build/Ubuntu/tribler.xpm usr/share/pixmaps Tribler/Main/Build/Ubuntu/tribler_big.xpm usr/share/pixmaps debian/bin/* usr/bin logger.conf usr/share/tribler +run_tribler.py /usr/share/tribler +twisted usr/share/tribler diff --git a/mac/makedistmac_64bit.sh b/mac/makedistmac_64bit.sh index 150be60e111..fce09a5e189 100755 --- a/mac/makedistmac_64bit.sh +++ b/mac/makedistmac_64bit.sh @@ -14,21 +14,11 @@ fi export LIBRARYNAME=Tribler -PYTHON_VERSION=2.7 -PYTHON="/System/Library/Frameworks/Python.framework/Versions/$PYTHON_VERSION/bin/python$PYTHON_VERSION" - -# ----- Set python paths TODO dynamic checkout - -PYTHONPATH="$PWD:$PYTHONPATH" -PYTHONPATH="$HOME/Workspace_new/install/lib/python2.7/site-packages:$PYTHONPATH" -PYTHONPATH="/Users/tribler/Library/Python/2.7/lib/python/site-packages/:$PYTHONPATH" # user-defined libraries -export PYTHONPATH - # ----- Clean up /bin/rm -rf dist build # ----- Build -${PYTHON} -OO - < ${LIBRARYNAME}/Main/Build/Mac/setuptriblermac_64bit.py py2app +pyinstaller tribler.spec mkdir -p dist/installdir mv dist/$APPNAME.app dist/installdir @@ -46,13 +36,6 @@ ln -s /Applications dist/installdir/Applications touch dist/installdir -#Copy logger.conf -cp logger.conf dist/installdir/Tribler.app/Contents/Resources/ - -# Copy family filter -cp Tribler/Core/Category/category.conf dist/installdir/Tribler.app/Contents/Resources/Tribler/Core/Category/ -cp Tribler/Core/Category/filter_terms.filter dist/installdir/Tribler.app/Contents/Resources/Tribler/Core/Category/ - mkdir -p dist/temp # create image diff --git a/tribler.spec b/tribler.spec new file mode 100644 index 00000000000..d4d8bd9687e --- /dev/null +++ b/tribler.spec @@ -0,0 +1,81 @@ +# -*- mode: python -*- + +block_cipher = None + +import os +import sys +import shutil + +sys.path.insert(0, os.getcwdu()) + +from Tribler.Core.version import version_id + +version_str = version_id.split('-')[0] + +widget_files = [] +for file in os.listdir("TriblerGUI/widgets"): + if file.endswith(".py"): + widget_files.append('TriblerGUI.widgets.%s' % file[:-3]) + +data_to_copy = [('Tribler/dispersy/libnacl/libnacl', 'libnacl'), ('TriblerGUI/qt_resources', 'qt_resources'), ('TriblerGUI/images', 'images'), ('TriblerGUI/scripts', 'scripts'), ('twisted', 'twisted'), ('Tribler', 'tribler_source/Tribler')] +if sys.platform.startswith('darwin'): + data_to_copy += [('/Applications/VLC.app/Contents/MacOS/lib', 'vlc/lib'), ('/Applications/VLC.app/Contents/MacOS/plugins', 'vlc/plugins')] + + # Create the right version info in the Info.plist file + with open('Tribler/Main/Build/Mac/Info.plist', 'r') as f: + content = f.read() + content = content.replace('__VERSION__', version_str) + + os.unlink('Tribler/Main/Build/Mac/Info.plist') + with open('Tribler/Main/Build/Mac/Info.plist', 'w') as f: + f.write(content) + +a = Analysis(['run_tribler.py'], + pathex=['/Users/martijndevos/Documents/tribler'], + binaries=None, + datas=data_to_copy, + hiddenimports=['csv'] + widget_files, + hookspath=[], + runtime_hooks=[], + excludes=['wx'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +# Remove libsodium.so - workaround for MacOS (https://github.com/pyinstaller/pyinstaller/issues/1770) +if sys.platform == 'darwin': + a.binaries = a.binaries - TOC([('/usr/local/lib/libsodium.so', None, None),]) + +exe = EXE(pyz, + a.scripts, + exclude_binaries=True, + name='tribler', + debug=False, + strip=False, + upx=True, + console=True, + icon='Tribler/Main/Build/Win/tribler.ico') +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name='tribler') +app = BUNDLE(coll, + name='tribler.app', + icon='Tribler/Main/Build/Mac/tribler.icns', + bundle_identifier='nl.tudelft.tribler', + info_plist={'NSHighResolutionCapable': 'True', 'CFBundleInfoDictionaryVersion': 1.0, 'CFBundleVersion': version_str, 'CFBundleShortVersionString': version_str}, + console=True) + +# Remove libvlc - conflicts on Windows +if sys.platform == 'win32': + os.remove(os.path.join(DISTPATH, 'tribler', 'libvlc.dll')) + os.remove(os.path.join(DISTPATH, 'tribler', 'libvlccore.dll')) + +# Replace the Info.plist file on MacOS +if sys.platform == 'darwin': + shutil.copy('Tribler/Main/Build/Mac/Info.plist', 'dist/Tribler.app/Contents/Info.plist') diff --git a/win/makedist_win.bat b/win/makedist_win.bat index f5d261ee204..10e59d5b4c5 100644 --- a/win/makedist_win.bat +++ b/win/makedist_win.bat @@ -1,4 +1,4 @@ -@echo off +REM @echo off REM No LIBRARYNAME here as this is not distributed with Tribler as BaseLib REM Check that we are running from the expected directory @@ -16,26 +16,12 @@ REM Arno: Add . to find our core SET PYTHONPATH=.;%PYTHONHOME% ECHO PYTHONPATH SET TO %PYTHONPATH% -REM ----- Check for Python and essential site-packages +REM ----- Check for PyInstaller -IF NOT EXIST %PYTHONHOME%\python.exe ( +IF NOT EXIST %PYTHONHOME%\Scripts\pyinstaller.exe ( ECHO . - ECHO Could not locate Python in %PYTHONHOME%. - ECHO Please modify this script or install python [www.python.org] - exit /b -) - -IF NOT EXIST %PYTHONHOME%\Lib\site-packages\wx-*-msw ( - ECHO . - ECHO Could not locate wxPython in %PYTHONHOME%\Lib\site-packages. - ECHO Please modify this script or install wxPython [www.wxpython.org] - EXIT /b -) - -IF NOT EXIST %PYTHONHOME%\Lib\site-packages\py2exe ( - ECHO . - ECHO Could not locate py2exe in %PYTHONHOME%\Lib\site-packages. - ECHO Please modify this script or install wxPython [www.py2exe.org] + ECHO Could not locate pyinstaller in %PYTHONHOME%\Scripts. + ECHO Please modify this script or install PyInstaller [www.pyinstaller.org] EXIT /b ) @@ -59,115 +45,35 @@ REM ----- Build REM Arno: When adding files here, make sure tribler.nsi actually REM packs them in the installer .EXE -mkdir dist\installdir - -REM Arno, 2011-02-22: Python 2.7 requires Microsoft.VC90.CRT version 9.0.21022.8 -REM http://www.py2exe.org/index.cgi/Tutorial -REM This version is available in the vcredist_x86.exe for Visual Studio 2008 (NOT SP1) -REM http://www.microsoft.com/downloads/en/details.aspx?FamilyID=9b2da534-3e03-4391-8a4d-074b9f2bc1bf&displaylang=en -REM Date published: 29-11-2007 -REM Joyfully the paths for this CRT are different on XP and Win7 and the WinSxS -REM dir appears to be special when using wildcards.... - -IF EXIST C:\WINDOWS\WinSxS\amd64_microsoft.vc90.crt_1fc8b3b9a1e18e3b_9.0.30729.4148_none_08e3747fa83e48bc ( -set CRTFULLNAME=amd64_microsoft.vc90.crt_1fc8b3b9a1e18e3b_9.0.30729.4148_none_08e3747fa83e48bc -) ELSE ( -ECHO . -ECHO Could not find microsoft visual c++ runtime -EXIT /b -) +%PYTHONHOME%\Scripts\pyinstaller.exe tribler.spec -xcopy C:\WINDOWS\WinSxS\%CRTFULLNAME% dist\installdir\Microsoft.VC90.CRT /S /I -copy C:\WINDOWS\WinSxS\Manifests\%CRTFULLNAME%.manifest dist\installdir\Microsoft.VC90.CRT\Microsoft.VC90.CRT.manifest +copy Tribler\Main\Build\Win\tribler*.nsi dist\tribler -REM Arno: py2exe for Python 2.7 needs msvcp90.dll to be in topdir -copy C:\WINDOWS\WinSxS\%CRTFULLNAME%\msvcp90.dll . +REM Martijn 2016-11-05: causing problems with PyInstaller +REM copy Tribler\Main\Build\Win\tribler.exe.manifest dist\tribler +type Tribler\LICENSE.txt Tribler\binary-LICENSE-postfix.txt > Tribler\binary-LICENSE.txt +copy Tribler\binary-LICENSE.txt dist\tribler -%PYTHONHOME%\python.exe -O Tribler\Main\Build\Win\setuptribler.py py2exe - -REM Arno: Move py2exe results to installdir -move dist\* dist\installdir - -copy Tribler\Main\Build\Win\tribler*.nsi dist\installdir -copy Tribler\Main\Build\Win\tribler.exe.manifest dist\installdir -REM copy %PYTHONHOME%\msvcr71.dll dist\installdir -REM For Vista. This works only when building on XP -REM as Vista doesn't have this DLL by default. -REM JD: My XP SP2 doesn't have it. It /is/ shipped with wxPython though +REM copy C:\Build\ffmpeg\bin\ffmpeg.exe dist\tribler -REM Laurens: commented this because wx 3.0 no longer has this dll -REM copy %PYTHONHOME%\Lib\site-packages\wx-3.0-msw\wx\msvcp71.dll dist\installdir +mkdir dist\tribler\tools +copy win\tools\reset*.bat dist\tribler\tools -REM Laurens: commented since this file is not even present on the (old) win 2008 builder -REM copy %SystemRoot%\msvcp71.dll dist\installdir +REM Laurens, 2016-04-20: Copy the redistributables of 2008 and 2012 and the VLC installer to the install dir +copy C:\build\vc_redist_90.exe dist\tribler +copy C:\build\vc_redist_110.exe dist\tribler -REM Laurens: commented since this file is not even present on the (old) 2008 builder -REM copy %PYTHONHOME%\msvcp60.dll dist\installdir +REM Copy various libraries required on runtime (libsodium and openssl) +copy C:\build\libsodium.dll dist\tribler +copy C:\build\openssl\*.dll dist\tribler -REM py2exe does this: copy SSLEAY32.dll dist\installdir -REM copy LIBEAY32.dll dist\installdir - -type Tribler\LICENSE.txt Tribler\binary-LICENSE-postfix.txt > Tribler\binary-LICENSE.txt -copy Tribler\binary-LICENSE.txt dist\installdir -mkdir dist\installdir\Tribler -copy Tribler\schema_sdb_v*.sql dist\installdir\Tribler -mkdir dist\installdir\Tribler\Core -copy Tribler\Core\superpeer.txt dist\installdir\Tribler\Core -mkdir dist\installdir\Tribler\Core\Statistics -copy Tribler\Core\Statistics\*.txt dist\installdir\Tribler\Core\Statistics -copy Tribler\Core\Statistics\*.sql dist\installdir\Tribler\Core\Statistics - -copy Tribler\Main\Build\Win\heading.bmp dist\installdir - -REM Arno, 2012-05-25: data files for pymdht -mkdir dist\installdir\Tribler\Core\DecentralizedTracking -mkdir dist\installdir\Tribler\Core\DecentralizedTracking\pymdht -mkdir dist\installdir\Tribler\Core\DecentralizedTracking\pymdht\core -copy Tribler\Core\DecentralizedTracking\pymdht\core\bootstrap_stable dist\installdir\Tribler\Core\DecentralizedTracking\pymdht\core -copy Tribler\Core\DecentralizedTracking\pymdht\core\bootstrap_unstable dist\installdir\Tribler\Core\DecentralizedTracking\pymdht\core - -mkdir dist\installdir\Tribler\community -mkdir dist\installdir\Tribler\community\tunnel -mkdir dist\installdir\Tribler\community\tunnel\crypto -copy Tribler\community\tunnel\crypto\curves.ec dist\installdir\Tribler\community\tunnel\crypto - -copy logger.conf dist\installdir -copy C:\Build\ffmpeg\bin\ffmpeg.exe dist\installdir -xcopy vlc dist\installdir\vlc /E /I -copy vlc.py dist\installdir\vlc.py - -mkdir dist\installdir\tools -copy win\tools\reset*.bat dist\installdir\tools - -REM Laurens, 2016-04-20: Copy the redistributables of 2008, 2010 and 2012 to the install dir -copy C:\build\vc_redist_90.exe dist\installdir -copy C:\build\vc_redist_100.exe dist\installdir -copy C:\build\vc_redist_110.exe dist\installdir - -REM MainClient specific - -mkdir dist\installdir\Tribler\Main -mkdir dist\installdir\Tribler\Main\vwxGUI -mkdir dist\installdir\Tribler\Main\vwxGUI\images -mkdir dist\installdir\Tribler\Main\vwxGUI\images\default -mkdir dist\installdir\Tribler\Main\vwxGUI\images\flags -mkdir dist\installdir\Tribler\Main\webUI -mkdir dist\installdir\Tribler\Main\webUI\static -mkdir dist\installdir\Tribler\Main\webUI\static\images -mkdir dist\installdir\Tribler\Main\webUI\static\lang -copy Tribler\Main\vwxGUI\images\*.* dist\installdir\Tribler\Main\vwxGUI\images -copy Tribler\Main\vwxGUI\images\default\*.* dist\installdir\Tribler\Main\vwxGUI\images\default -copy Tribler\Main\vwxGUI\images\flags\*.* dist\installdir\Tribler\Main\vwxGUI\images\flags -copy Tribler\Main\webUI\static\*.* dist\installdir\Tribler\Main\webUI\static -copy Tribler\Main\webUI\static\images\*.* dist\installdir\Tribler\Main\webUI\static\images -copy Tribler\Main\webUI\static\lang\*.* dist\installdir\Tribler\Main\webUI\static\lang -mkdir dist\installdir\Tribler\Core\Category -copy Tribler\Core\Category\category.conf dist\installdir\Tribler\Core\Category -copy Tribler\Core\Category\filter_terms.filter dist\installdir\Tribler\Core\Category +REM Copy VLC, different files based on 32-bit or 64-bit +if %1==32 copy C:\build\vlc-2.2.4-win32.exe dist\tribler +if %1==64 copy C:\build\vlc-2.2.4-win64.exe dist\tribler @echo Running NSIS -cd dist\installdir +cd dist\tribler REM get password for swarmplayerprivatekey.pfx set /p PASSWORD="Enter the PFX password:" @@ -178,13 +84,7 @@ SET PATH=%PATH%;C:\Program Files\Microsoft Platform SDK for Windows Server 2003 signtool.exe sign /f c:\build\certs\swarmplayerprivatekey.pfx /p "%PASSWORD%" /d "Tribler" /du "http://www.pds.ewi.tudelft.nl/code.html" /t "http://timestamp.verisign.com/scripts/timestamp.dll" tribler.exe -REM Arno: Sign swift.exe so MS "Block / Unblock" dialog has publisher info. -REM signtool.exe sign /f c:\build\certs\swarmplayerprivatekey.pfx /p "%PASSWORD%" /d "Tribler" /du "http://www.pds.ewi.tudelft.nl/code.html" /t "http://timestamp.verisign.com/scripts/timestamp.dll" swift.exe - - :makeinstaller -REM %NSIS% tribler_novlc.nsi -REM move Tribler_*.exe .. %NSIS% tribler.nsi move Tribler_*.exe .. cd .. From 6acb4f60b0e8a38026669625666f9a9b40de9fe9 Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:04:51 +0100 Subject: [PATCH 4/9] Added GUI tests --- Tribler/Test/GUI/__init__.py | 3 + Tribler/Test/GUI/test_gui.py | 514 +++++++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 Tribler/Test/GUI/__init__.py create mode 100644 Tribler/Test/GUI/test_gui.py diff --git a/Tribler/Test/GUI/__init__.py b/Tribler/Test/GUI/__init__.py new file mode 100644 index 00000000000..4deb1863f70 --- /dev/null +++ b/Tribler/Test/GUI/__init__.py @@ -0,0 +1,3 @@ +""" +This package contains various GUI tests. +""" diff --git a/Tribler/Test/GUI/test_gui.py b/Tribler/Test/GUI/test_gui.py new file mode 100644 index 00000000000..bf70a7b8611 --- /dev/null +++ b/Tribler/Test/GUI/test_gui.py @@ -0,0 +1,514 @@ +import os +import sys +import unittest +from random import randint +from unittest import skipUnless + +from PyQt5.QtCore import QPoint, Qt, QTimer +from PyQt5.QtGui import QPixmap, QRegion +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QApplication, QListWidget, QTreeWidget + +from Tribler.Core.Utilities.network_utils import get_random_port +import TriblerGUI.core_manager as core_manager +from TriblerGUI.dialogs.feedbackdialog import FeedbackDialog +from TriblerGUI.widgets.channel_torrent_list_item import ChannelTorrentListItem +from TriblerGUI.widgets.home_recommended_item import HomeRecommendedItem + +rand_port = get_random_port() +core_manager.START_FAKE_API = True +core_manager.FAKE_API_PORT = rand_port + +import TriblerGUI.tribler_request_manager as request_mgr +request_mgr.API_PORT = rand_port + +import TriblerGUI + +from TriblerGUI.widgets.loading_list_item import LoadingListItem +from TriblerGUI.tribler_window import TriblerWindow + +if os.environ.get("TEST_GUI") == "yes": + app = QApplication(sys.argv) + window = TriblerWindow(api_port=rand_port) + QTest.qWaitForWindowExposed(window) +else: + window = None + +sys.excepthook = sys.__excepthook__ + + +class TimeoutException(Exception): + pass + + +class AbstractTriblerGUITest(unittest.TestCase): + """ + This class contains various utility methods that are used during the GUI test, i.e. methods that wait until + some data in a list is loaded or for taking a screenshot of the current window. + """ + + def setUp(self): + self.signal_received = None + + # To fix the Windows forking system it's necessary to point __main__ to + # the module we want to execute in the forked process + self.old_main = sys.modules["__main__"] + self.old_main_file = sys.modules["__main__"].__file__ + + from TriblerGUI.scripts import start_fake_core # So the module is loaded + sys.modules["__main__"] = sys.modules["TriblerGUI.scripts.start_fake_core"] + sys.modules["__main__"].__file__ = sys.modules["TriblerGUI.scripts.start_fake_core"].__file__ + + QTest.qWait(100) + self.screenshots_taken = 0 + window.downloads_page.can_update_items = True + + if not window.tribler_started: + self.screenshot(window, name="tribler_loading") + self.wait_for_signal(window.core_manager.events_manager.tribler_started, no_args=True) + + def tearDown(self): + sys.modules["__main__"] = self.old_main + sys.modules["__main__"].__file__ = self.old_main_file + + window.downloads_page.can_update_items = False + + @classmethod + def tearDownClass(cls): + if window: + window.core_manager.stop() + + def go_to_and_wait_for_downloads(self): + QTest.mouseClick(window.left_menu_button_downloads, Qt.LeftButton) + QTest.mouseClick(window.downloads_all_button, Qt.LeftButton) + self.wait_for_variable("downloads_page.downloads") + + def screenshot(self, widget, name=None): + """ + Take a screenshot of the widget. You can optionally append a string to the name of the screenshot. The + screenshot itself is saved as a JPEG file. + """ + pixmap = QPixmap(widget.rect().size()) + widget.render(pixmap, QPoint(), QRegion(widget.rect())) + + self.screenshots_taken += 1 + img_name = 'screenshot_%d.jpg' % self.screenshots_taken + if name is not None: + img_name = 'screenshot_%s.jpg' % name + + screenshots_dir = os.path.join(os.path.dirname(TriblerGUI.__file__), 'screenshots') + if not os.path.exists(screenshots_dir): + os.mkdir(screenshots_dir) + + pixmap.save(os.path.join(screenshots_dir, img_name)) + + def wait_for_list_populated(self, llist, num_items=1, timeout=10): + for _ in range(0, timeout * 1000, 100): + QTest.qWait(100) + if isinstance(llist, QListWidget) and llist.count() >= num_items: + if not isinstance(llist.itemWidget(llist.item(0)), LoadingListItem): + return + elif isinstance(llist, QTreeWidget) and llist.topLevelItemCount() > num_items: + if not isinstance(llist.topLevelItem(0), LoadingListItem): + return + + # List was not populated in time, fail the test + raise TimeoutException("The list was not populated within 10 seconds") + + def wait_for_home_page_table_populated(self, timeout=10): + for _ in range(0, timeout * 1000, 100): + QTest.qWait(100) + if isinstance(window.home_page_table_view.cellWidget(0, 0), HomeRecommendedItem): + return + + # List was not populated in time, fail the test + raise TimeoutException("The list was not populated within 10 seconds") + + def wait_for_settings(self, timeout=10): + for _ in range(0, timeout * 1000, 100): + QTest.qWait(100) + if window.settings_page.settings is not None: + return + + raise TimeoutException("Did not receive settings within 10 seconds") + + def get_attr_recursive(self, attr_name): + parts = attr_name.split(".") + cur_attr = window + for part in parts: + cur_attr = getattr(cur_attr, part) + return cur_attr + + def wait_for_variable(self, var, timeout=10): + for _ in range(0, timeout * 1000, 100): + QTest.qWait(100) + if self.get_attr_recursive(var) is not None: + return + + raise TimeoutException("Variable %s within 10 seconds" % var) + + def wait_for_signal(self, signal, timeout=10, no_args=False): + self.signal_received = False + + def on_signal(_): + self.signal_received = True + + if no_args: + signal.connect(lambda: on_signal(None)) + else: + signal.connect(on_signal) + + for _ in range(0, timeout * 1000, 100): + QTest.qWait(100) + if self.signal_received: + return + + raise TimeoutException("Signal %s not raised within 10 seconds" % signal) + + +@skipUnless(os.environ.get("TEST_GUI") == "yes", "Not testing the GUI by default") +class TriblerGUITest(AbstractTriblerGUITest): + """ + GUI tests for the GUI written in PyQt. These methods are using the QTest framework to simulate mouse clicks. + """ + + def test_home_page_torrents(self): + QTest.mouseClick(window.left_menu_button_home, Qt.LeftButton) + QTest.mouseClick(window.home_tab_torrents_button, Qt.LeftButton) + self.screenshot(window, name="home_page_torrents_loading") + self.wait_for_home_page_table_populated() + self.screenshot(window, name="home_page_torrents") + + def test_home_page_channels(self): + QTest.mouseClick(window.left_menu_button_home, Qt.LeftButton) + QTest.mouseClick(window.home_tab_channels_button, Qt.LeftButton) + self.screenshot(window, name="home_page_channels_loading") + self.wait_for_home_page_table_populated() + self.screenshot(window, name="home_page_channels") + + def test_subscriptions(self): + QTest.mouseClick(window.left_menu_button_subscriptions, Qt.LeftButton) + self.screenshot(window, name="subscriptions_loading") + self.wait_for_list_populated(window.subscribed_channels_list) + self.screenshot(window, name="subscriptions") + + first_widget = window.subscribed_channels_list.itemWidget(window.subscribed_channels_list.item(0)) + QTest.mouseClick(first_widget.subscribe_button, Qt.LeftButton) + self.wait_for_signal(first_widget.subscriptions_widget.unsubscribed_channel) + self.screenshot(window, name="unsubscribed") + QTest.mouseClick(first_widget.subscribe_button, Qt.LeftButton) + self.wait_for_signal(first_widget.subscriptions_widget.subscribed_channel) + + def test_edit_channel_overview(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + QTest.mouseClick(window.edit_channel_overview_button, Qt.LeftButton) + self.screenshot(window, name="channel_loading") + self.wait_for_variable("edit_channel_page.channel_overview") + self.screenshot(window, name="channel_overview") + + def test_edit_channel_settings(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_settings_button, Qt.LeftButton) + self.screenshot(window, name="channel_settings") + + def test_edit_channel_torrents(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_torrents_button, Qt.LeftButton) + self.screenshot(window, name="edit_channel_torrents_loading") + self.wait_for_list_populated(window.edit_channel_torrents_list) + self.screenshot(window, name="edit_channel_torrents") + + first_widget = window.edit_channel_torrents_list.itemWidget(window.edit_channel_torrents_list.item(0)) + QTest.mouseClick(first_widget, Qt.LeftButton) + self.screenshot(window, name="edit_channel_torrents_selected") + QTest.mouseClick(window.edit_channel_torrents_remove_selected_button, Qt.LeftButton) + self.screenshot(window, name="remove_channel_torrent_dialog") + QTest.mouseClick(window.edit_channel_page.dialog.buttons[1], Qt.LeftButton) + + QTest.mouseClick(window.edit_channel_torrents_remove_all_button, Qt.LeftButton) + self.screenshot(window, name="remove_all_channel_torrent_dialog") + QTest.mouseClick(window.edit_channel_page.dialog.buttons[1], Qt.LeftButton) + + def test_edit_channel_playlists(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_playlists_button, Qt.LeftButton) + self.screenshot(window, name="edit_channel_playlists_loading") + self.wait_for_list_populated(window.edit_channel_playlists_list) + self.screenshot(window, name="edit_channel_playlists") + + def test_edit_channel_rssfeeds(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_rss_feeds_button, Qt.LeftButton) + self.screenshot(window, name="edit_channel_rssfeeds_loading") + self.wait_for_list_populated(window.edit_channel_rss_feeds_list) + self.screenshot(window, name="edit_channel_rssfeeds") + + def test_add_remove_refresh_rssfeed(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_rss_feeds_button, Qt.LeftButton) + self.wait_for_list_populated(window.edit_channel_rss_feeds_list) + QTest.mouseClick(window.edit_channel_details_rss_add_button, Qt.LeftButton) + self.screenshot(window, name="edit_channel_add_rssfeeds_dialog") + window.edit_channel_page.dialog.dialog_widget.dialog_input.setText("http://test.com/rss.xml") + QTest.mouseClick(window.edit_channel_page.dialog.buttons[0], Qt.LeftButton) + + # Remove item + window.edit_channel_rss_feeds_list.topLevelItem(0).setSelected(True) + QTest.mouseClick(window.edit_channel_details_rss_feeds_remove_selected_button, Qt.LeftButton) + self.screenshot(window, name="edit_channel_remove_rssfeeds_dialog") + QTest.mouseClick(window.edit_channel_page.dialog.buttons[0], Qt.LeftButton) + + QTest.mouseClick(window.edit_channel_details_rss_refresh_button, Qt.LeftButton) + + def test_settings(self): + QTest.mouseClick(window.left_menu_button_settings, Qt.LeftButton) + QTest.mouseClick(window.settings_general_button, Qt.LeftButton) + self.screenshot(window, name="settings_not_loaded") + self.wait_for_settings() + self.screenshot(window, name="settings_general") + QTest.mouseClick(window.settings_connection_button, Qt.LeftButton) + self.screenshot(window, name="settings_connection") + QTest.mouseClick(window.settings_bandwidth_button, Qt.LeftButton) + self.screenshot(window, name="settings_bandwidth") + QTest.mouseClick(window.settings_seeding_button, Qt.LeftButton) + self.screenshot(window, name="settings_seeding") + QTest.mouseClick(window.settings_anonymity_button, Qt.LeftButton) + self.screenshot(window, name="settings_anonymity") + + def test_downloads(self): + self.go_to_and_wait_for_downloads() + self.screenshot(window, name="downloads_all") + QTest.mouseClick(window.downloads_downloading_button, Qt.LeftButton) + self.screenshot(window, name="downloads_downloading") + QTest.mouseClick(window.downloads_completed_button, Qt.LeftButton) + self.screenshot(window, name="downloads_completed") + QTest.mouseClick(window.downloads_active_button, Qt.LeftButton) + self.screenshot(window, name="downloads_active") + QTest.mouseClick(window.downloads_inactive_button, Qt.LeftButton) + self.screenshot(window, name="downloads_inactive") + + def test_download_start_stop_remove_recheck(self): + self.go_to_and_wait_for_downloads() + QTest.mouseClick(window.downloads_list.topLevelItem(0).progress_slider, Qt.LeftButton) + QTest.mouseClick(window.stop_download_button, Qt.LeftButton) + QTest.mouseClick(window.start_download_button, Qt.LeftButton) + QTest.mouseClick(window.remove_download_button, Qt.LeftButton) + self.screenshot(window, name="remove_download_dialog") + QTest.mouseClick(window.downloads_page.dialog.buttons[2], Qt.LeftButton) + + def test_download_details(self): + self.go_to_and_wait_for_downloads() + QTest.mouseClick(window.downloads_list.topLevelItem(0).progress_slider, Qt.LeftButton) + window.download_details_widget.setCurrentIndex(0) + self.screenshot(window, name="download_detail") + window.download_details_widget.setCurrentIndex(1) + self.screenshot(window, name="download_files") + window.download_details_widget.setCurrentIndex(2) + self.screenshot(window, name="download_trackers") + + def test_search_suggestions(self): + QTest.keyClick(window.top_search_bar, 't') + QTest.keyClick(window.top_search_bar, 'r') + self.wait_for_signal(window.received_search_completions) + self.screenshot(window, name="search_suggestions") + + def test_search(self): + window.top_search_bar.setText("trib") + QTest.keyClick(window.top_search_bar, Qt.Key_Enter) + self.wait_for_list_populated(window.search_results_list, num_items=20) + self.screenshot(window, name="search_results_all") + + QTest.mouseClick(window.search_results_channels_button, Qt.LeftButton) + self.wait_for_list_populated(window.search_results_list) + self.screenshot(window, name="search_results_channels") + QTest.mouseClick(window.search_results_torrents_button, Qt.LeftButton) + self.wait_for_list_populated(window.search_results_list) + self.screenshot(window, name="search_results_torrents") + + def test_channel_playlist(self): + QTest.mouseClick(window.left_menu_button_subscriptions, Qt.LeftButton) + self.wait_for_list_populated(window.subscribed_channels_list) + first_widget = window.subscribed_channels_list.itemWidget(window.subscribed_channels_list.item(0)) + QTest.mouseClick(first_widget, Qt.LeftButton) + self.screenshot(window, name="channel_loading") + self.wait_for_list_populated(window.channel_torrents_list) + self.screenshot(window, name="channel") + + first_widget = window.channel_torrents_list.itemWidget(window.channel_torrents_list.item(0)) + QTest.mouseClick(first_widget, Qt.LeftButton) + self.screenshot(window, name="channel_playlist") + + def test_start_download(self): + QTest.mouseClick(window.left_menu_button_subscriptions, Qt.LeftButton) + self.wait_for_list_populated(window.subscribed_channels_list) + first_widget = window.subscribed_channels_list.itemWidget(window.subscribed_channels_list.item(0)) + QTest.mouseClick(first_widget, Qt.LeftButton) + self.wait_for_list_populated(window.channel_torrents_list) + + torrent_widget = None + for ind in xrange(window.channel_torrents_list.count()): + cur_widget = window.channel_torrents_list.itemWidget(window.channel_torrents_list.item(ind)) + if isinstance(cur_widget, ChannelTorrentListItem): + torrent_widget = cur_widget + break + + QTest.mouseClick(torrent_widget.torrent_download_button, Qt.LeftButton) + self.screenshot(window, name="start_download_dialog") + QTest.mouseClick(torrent_widget.dialog.dialog_widget.cancel_button, Qt.LeftButton) + + def test_create_remove_playlist(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_playlists_button, Qt.LeftButton) + self.wait_for_list_populated(window.edit_channel_playlists_list) + old_count = window.edit_channel_playlists_list.count() + QTest.mouseClick(window.edit_channel_create_playlist_button, Qt.LeftButton) + self.screenshot(window, "create_playlist") + + # Create playlist + window.playlist_edit_name.setText("Unit test playlist") + window.playlist_edit_description.setText("Unit test playlist description") + QTest.mouseClick(window.playlist_edit_save_button, Qt.LeftButton) + self.wait_for_signal(window.edit_channel_page.playlists_loaded) + self.assertEqual(old_count + 1, window.edit_channel_playlists_list.count()) + + # Remove playlist + last_widget = window.edit_channel_playlists_list.itemWidget(window.edit_channel_playlists_list.item(old_count)) + QTest.mouseClick(last_widget.remove_playlist_button, Qt.LeftButton) + self.screenshot(window, name="remove_playlist_dialog") + QTest.mouseClick(window.edit_channel_page.dialog.buttons[0], Qt.LeftButton) + self.wait_for_signal(window.edit_channel_page.playlists_loaded) + self.assertEqual(old_count, window.edit_channel_playlists_list.count()) + + def test_edit_playlist(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_playlists_button, Qt.LeftButton) + self.wait_for_list_populated(window.edit_channel_playlists_list) + + first_widget = window.edit_channel_playlists_list.itemWidget(window.edit_channel_playlists_list.item(0)) + QTest.mouseClick(first_widget.edit_playlist_button, Qt.LeftButton) + self.screenshot(window, name="edit_playlist") + + rand_name = "Random name %d" % randint(1, 1000) + rand_desc = "Random description %d" % randint(1, 1000) + + window.playlist_edit_name.setText(rand_name) + window.playlist_edit_description.setText(rand_desc) + QTest.mouseClick(window.playlist_edit_save_button, Qt.LeftButton) + self.wait_for_signal(window.edit_channel_page.playlists_loaded) + + first_widget = window.edit_channel_playlists_list.itemWidget(window.edit_channel_playlists_list.item(0)) + self.assertEqual(first_widget.playlist_name.text(), rand_name) + + def test_add_download_url(self): + window.on_add_torrent_from_url() + self.go_to_and_wait_for_downloads() + old_count = window.downloads_list.topLevelItemCount() + self.screenshot(window, name="add_torrent_url_dialog") + window.dialog.dialog_widget.dialog_input.setText("http://test.url/test.torrent") + QTest.mouseClick(window.dialog.buttons[0], Qt.LeftButton) + self.screenshot(window, name="add_torrent_url_startdownload_dialog") + self.wait_for_signal(window.dialog.received_metainfo) + self.screenshot(window, name="add_torrent_url_startdownload_dialog_files") + QTest.mouseClick(window.dialog.dialog_widget.download_button, Qt.LeftButton) + self.wait_for_signal(window.downloads_page.received_downloads) + self.wait_for_signal(window.downloads_page.received_downloads) + self.assertEqual(window.downloads_list.topLevelItemCount(), old_count + 1) + + def test_video_player_page(self): + QTest.mouseClick(window.left_menu_button_video_player, Qt.LeftButton) + self.screenshot(window, name="video_player_page") + + # Some actions for the left menu playlist + window.left_menu_playlist.set_loading() + self.screenshot(window, name="video_player_page_playlist_loading") + window.left_menu_playlist.set_files([{'name': 'video.avi', 'index': 0}, + {'name': 'test.txt', 'index': 1}]) + self.screenshot(window, name="video_player_page_playlist_items") + window.left_menu_playlist.set_active_index(0) + self.screenshot(window, name="video_player_page_playlist_focus") + + def test_feedback_dialog(self): + def screenshot_dialog(): + self.screenshot(dialog, name="feedback_dialog") + dialog.close() + + dialog = FeedbackDialog(window, "test", "1.2.3") + dialog.closeEvent = lambda _: None # Otherwise, the application will stop + QTimer.singleShot(1000, screenshot_dialog) + dialog.exec_() + + def test_discovered_page(self): + QTest.mouseClick(window.left_menu_button_discovered, Qt.LeftButton) + self.wait_for_list_populated(window.discovered_channels_list) + self.screenshot(window, name="discovered_page") + + def test_debug_pane(self): + QTest.mouseClick(window.left_menu_button_settings, Qt.LeftButton) + QTest.mouseClick(window.settings_general_button, Qt.LeftButton) + self.wait_for_settings() + if not window.developer_mode_enabled_checkbox.isChecked(): + QTest.mouseClick(window.developer_mode_enabled_checkbox, Qt.LeftButton) + + QTest.mouseClick(window.left_menu_button_debug, Qt.LeftButton) + self.screenshot(window.debug_window, name="debug_panel_just_opened") + self.wait_for_list_populated(window.debug_window.general_tree_widget) + self.screenshot(window.debug_window, name="debug_panel_general_tab") + + window.debug_window.debug_tab_widget.setCurrentIndex(1) + self.wait_for_list_populated(window.debug_window.requests_tree_widget) + self.screenshot(window.debug_window, name="requests_panel_general_tab") + + window.debug_window.debug_tab_widget.setCurrentIndex(2) + self.wait_for_list_populated(window.debug_window.multichain_tree_widget) + self.screenshot(window.debug_window, name="requests_panel_multichain_tab") + + window.debug_window.debug_tab_widget.setCurrentIndex(3) + self.wait_for_list_populated(window.debug_window.dispersy_general_tree_widget) + self.screenshot(window.debug_window, name="requests_panel_dispersy_tab") + + window.debug_window.dispersy_tab_widget.setCurrentIndex(1) + self.wait_for_list_populated(window.debug_window.communities_tree_widget) + self.screenshot(window.debug_window, name="requests_panel_communities_tab") + + window.debug_window.close() + + def test_create_torrent(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_torrents_button, Qt.LeftButton) + self.wait_for_list_populated(window.edit_channel_torrents_list) + window.edit_channel_page.on_create_torrent_from_files() + self.screenshot(window, name="create_torrent_page") + QTest.mouseClick(window.manage_channel_create_torrent_back, Qt.LeftButton) + + def test_manage_playlist(self): + QTest.mouseClick(window.left_menu_button_my_channel, Qt.LeftButton) + self.wait_for_variable("edit_channel_page.channel_overview") + QTest.mouseClick(window.edit_channel_playlists_button, Qt.LeftButton) + self.wait_for_list_populated(window.edit_channel_playlists_list) + first_widget = window.edit_channel_playlists_list.itemWidget(window.edit_channel_playlists_list.item(0)) + QTest.mouseClick(first_widget, Qt.LeftButton) + QTest.mouseClick(window.edit_channel_playlist_manage_torrents_button, Qt.LeftButton) + self.wait_for_list_populated(window.playlist_manage_in_playlist_list) + self.screenshot(window, name="manage_playlist_before") + + # Swap the first item of the lists around + window.playlist_manage_in_playlist_list.setCurrentRow(0) + QTest.mouseClick(window.playlist_manage_remove_from_playlist, Qt.LeftButton) + + window.playlist_manage_in_channel_list.setCurrentRow(0) + QTest.mouseClick(window.playlist_manage_add_to_playlist, Qt.LeftButton) + + self.screenshot(window, name="manage_playlist_after") + + QTest.mouseClick(window.edit_channel_manage_playlist_save_button, Qt.LeftButton) + +if __name__ == "__main__": + unittest.main() From 0dd2660997f8a3df02bad3fceaa0e10a39a46d7d Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:05:06 +0100 Subject: [PATCH 5/9] Updated documentation Updated documentation of the builders and development environment due to changed requirements. --- doc/building/building.rst | 2 ++ doc/building/building_on_osx.rst | 21 +++--------- doc/building/building_on_windows.rst | 24 ++++++-------- doc/development/development_on_linux.rst | 14 ++------ doc/development/development_on_osx.rst | 37 ++++++++++++++++++---- doc/development/development_on_windows.rst | 23 +++++++++++--- 6 files changed, 66 insertions(+), 55 deletions(-) diff --git a/doc/building/building.rst b/doc/building/building.rst index 18c9ebfb9a3..7eb21a19b01 100644 --- a/doc/building/building.rst +++ b/doc/building/building.rst @@ -26,6 +26,8 @@ Run the following commands in your terminal: Tribler/Main/Build/update_version_from_git.py debuild -i -us -uc -b +This will build the ``.deb`` file that can be used to install Tribler on other systems. + Other Unixes ============ diff --git a/doc/building/building_on_osx.rst b/doc/building/building_on_osx.rst index 7d288c9a45b..4afed1e6cc3 100644 --- a/doc/building/building_on_osx.rst +++ b/doc/building/building_on_osx.rst @@ -1,11 +1,12 @@ -This section contains information about building Tribler on OS X. The final result you should have is a ``.dmg`` file which, when opened, allows ``Tribler.app`` to be copied to the Applications directory. This guide has been tested on OS X 10.11 (El Capitan). It is recommended to run this builder on a system that is already able to run Tribler without problems (it means that all the required packages required by Tribler are installed already). Information about setting up a developer environment on OS X can be found `here <(https://github.com/Tribler/tribler/blob/devel/doc/development/development_on_osx.rst>`_. +This section contains information about building Tribler on macOS. The final result you should have is a ``.dmg`` file which, when opened, allows ``Tribler.app`` to be copied to the Applications directory. This guide has been tested on macOS 10.11 (El Capitan). It is recommended to run this builder on a system that is already able to run Tribler without problems (it means that all the required packages required by Tribler are installed already). Information about setting up a developer environment on macOS can be found `here <(https://github.com/Tribler/tribler/blob/devel/doc/development/development_on_osx.rst>`_. Required packages ----------------- To build and distribute Tribler, there are some required scripts and packages: * The git command tools are required to fetch the latest release information. They are installed when you start Xcode for the first time but you can also install it using ``brew`` or another package library. -* Py2app. The built-in version of py2app does not function correctly when System Integrity Protection (SIP) is turned on. You can either turn SIP off (instructions on how to do this can be found `here `_) or you can install a more recent version of py2app using PIP in your user-defined ``site-packages``. Note that you should place the ``site-packages`` directory with py2app in it higher in the ``PYTHONPATH`` environment variable than the ``site-packages`` directory managed by the system. Otherwise, the builder will chooose the py2app package installed by the system. +* PyInstaller: this library creates an executable binary and can be installed using pip (``pip install pyinstaller``). +* vlc: PyInstaller automatically searches for the vlc library in the system and bundles it. * The builder needs to find all packages that are required by Tribler so make sure you can run Tribler on your machine and that there are no missing dependencies. * In order to attach the EULA to the ``.dmg`` file, we make use of the ``eulagise`` script. This script is written in PERL and is based on a more fully-featured script. The script can be dowloaded from `GitHub `_. The builder expects the script to be executable and added to the ``PATH`` environment variable. This can be done with the following commands: @@ -15,28 +16,16 @@ To build and distribute Tribler, there are some required scripts and packages: chmod +x /usr/local/bin/eulagise eulagise # to test it - it should show that you should add some flags -Building Tribler on OS X ------------------------- +Building Tribler on macOS +------------------------- Start by checking out the directory you want to clone (using ``git clone --recursive``). Open a terminal and ``cd`` to this new cloned directory (referenced to as ``tribler_source`` in this guide). -First we need to copy the ffmpeg library to ``tribler_source``. You can download this file from `here `_. Next, create a directory named ``vlc`` in ``tribler_source`` and copy the ``ffmpeg`` file to that directory. Make sure to name the file ``ffmpeg``, otherwise the builder cannot find it. - Next, we should inject version information into the files about the latest release. This is done by the ``update_version_from_git.py`` script found in ``Tribler/Main/Build``. Invoke it from the ``tribler_source`` directory by executing: .. code-block:: none Tribler/Main/Build/update_version_from_git.py -Before we can build the ``.dmg`` file, some environment variables need to be set: - -.. code-block:: none - - export MACOSX_DEPLOYMENT_TARGET=10.11 - export CFLAGS=' -mmacosx-version-min=10.6 -O -g ' - export CXXFLAGS=' -mmacosx-version-min=10.6 -O -g ' - -If you are building on another environment, you should change ``MACOSX_DEPLOYMENT_TARGET`` to match your version of OS X. The ``-mmacosx-version-min`` is required so the builder can optimize the build depending on the minimum supported version. - Now execute the builder with the following command: .. code-block:: none diff --git a/doc/building/building_on_windows.rst b/doc/building/building_on_windows.rst index d70b2a99f22..f7092487650 100644 --- a/doc/building/building_on_windows.rst +++ b/doc/building/building_on_windows.rst @@ -9,7 +9,7 @@ Required packages To build a Tribler installer, you'll need some additional scripts and packages. The versions used as of writing this guide are mentioned next to the package or script. * The git command tools (version 2.7.0) are required to fetch the latest release information. These can be downloaded from `here `_. -* Py2Exe (0.6.9), a tool to create an executeable from python files. Grab the latest version `here `_. +* PyInstaller, a tool to create an executeable from python files. Install the latest version from pip: ``pip install pyinstaller``. * The builder needs to find all packages that are required by Tribler so make sure you can run Tribler on your machine and that there are no missing dependencies. * Nullsoft Scriptable Install System (NSIS) (version 2.5.0) is a script-driven Installer authoring tool for Microsoft Windows with minimal overhead. It can be downloaded `here `_. We selected version 2.5 as the uninstall functions were not called properly in 3.03b. * Three plugins are required.The UAC plugin is the first. This can be downloaded from `here `_ (version 0.2.4c). How to install a plugin can be found `here `_. @@ -25,12 +25,12 @@ Start by cloning Tribler if you haven't done already (using the ``git clone --re Next, create a ``build`` folder directly on your ``C:\`` drive. Inside the ``build`` folder, put the following items: -1. A static version (64 bit, git-1d8f9b7) of ffmpeg, available `here `_. Place it in a folder called ``ffmpeg`` in the ``build`` folder. -2. A folder ``certs`` containing a ``.pfx`` key. In our case it's named ``swarmplayerprivatekey.pfx``. Make sure to rename paths in ``makedist_win.bat`` to match your file name. -3. A ``vlc`` folder containing a full instalation of vlc (Version 2.2.1). -4. ``vc_redist_90.exe`` (Microsoft Visual C++ 2008 Redistributable Package), which is available `here `_. In case you build 32 bit, get the x86 version `here `_. Don't forget to rename the file. -5. ``vc_redist_110.exe`` (Visual C++ Redistributable for Visual Studio 2012), which is available `here `_. In case you build 32 bit, get the x86 version. Once more, don't forget to rename the file. -6. `libsodium.dll` which can be downloaded from `libsodium.org `_ (as of writing version 1.0.8). +1. A folder ``certs`` containing a ``.pfx`` key. In our case it's named ``swarmplayerprivatekey.pfx``. Make sure to rename paths in ``makedist_win.bat`` to match your file name. +2. The VLC 2.2.4 installer (either 32 or 64-bit). This file must be named ``vlc-2.2.4-win64.exe`` (or ``vlc-2.2.4-win32.exe`` when building 32-bit). +3. ``vc_redist_90.exe`` (Microsoft Visual C++ 2008 Redistributable Package), which is available `here `_. In case you build 32 bit, get the x86 version `here `_. Don't forget to rename the file. +4. ``vc_redist_110.exe`` (Visual C++ Redistributable for Visual Studio 2012), which is available `here `_. In case you build 32 bit, get the x86 version. Once more, don't forget to rename the file. +5. ``libsodium.dll`` which can be downloaded from `libsodium.org `_ (as of writing version 1.0.8). +6. The openssl dll files ``libeay32.dll``, ``libssl32.dll`` and ``ssleay32.dll`` (place them in a directory named ``openssl``). Then, set a ``PASSWORD`` `environment variable `_ with its value set to the password matching the one set in your ``.pfx`` file. @@ -39,14 +39,8 @@ Note that for building 32 bit you need to pass anything but 64, i.e. 32 or 86 to .. code-block:: none - setlocal enabledelayedexpansion - call "C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" - SET PATH=%PATH%;C:\Windows\system32;C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin - dir cd tribler python Tribler/Main/Build/update_version_from_git.py 64 - xcopy C:\build\vlc vlc /E /I - win\makedist_win.bat - -This builds an ``.exe`` installer which installs Tribler when ran. + win\makedist_win.bat 64 +This builds an ``.exe`` installer which installs Tribler. diff --git a/doc/development/development_on_linux.rst b/doc/development/development_on_linux.rst index 1cee3ab30c4..bcbbf9111a7 100644 --- a/doc/development/development_on_linux.rst +++ b/doc/development/development_on_linux.rst @@ -3,23 +3,13 @@ This section contains information about setting up a Tribler development environ Debian/Ubuntu/Mint ------------------ -Execute the following command in your terminal: +First, install the required dependencies by executing the following command in your terminal: .. code-block:: none - sudo apt-get install libav-tools libjs-excanvas libjs-mootools libsodium13 libx11-6 python-apsw python-cherrypy3 python-crypto python-cryptography python-feedparser python-leveldb python-libtorrent python-m2crypto python-netifaces python-pil python-pyasn1 python-twisted python-wxgtk2.8 python2.7 vlc python-pip python-chardet python-configobj + sudo apt-get install libav-tools libsodium13 libx11-6 python-apsw python-cherrypy3 python-crypto python-cryptography python-feedparser python-leveldb python-libtorrent python-m2crypto python-netifaces python-pil python-pyasn1 python-twisted python2.7 vlc python-pip python-chardet python-configobj python-pyqt5, python-pyqt5.qtsvg sudo pip install decorator libnacl -Experimental support for Ubuntu 16.04 -------------------------------------- - -Please try if the latest experimental build works for you. - -.. code-block:: none - - bash - sudo apt-get install libsodium-dev python-nacl - Next, download the latest .deb file from `here `_. Installing libsodium13 and python-cryptography on Ubuntu 14.04 diff --git a/doc/development/development_on_osx.rst b/doc/development/development_on_osx.rst index ca5c40c04a1..b1e85819ba4 100644 --- a/doc/development/development_on_osx.rst +++ b/doc/development/development_on_osx.rst @@ -1,11 +1,11 @@ -This section contains information about setting up a Tribler development environment on OS X. Unlike Linux based systems where installing third-party libraries is often a single ``apt-get`` command, installing and configuring the necessary libraries requires more attention on OS X. This guide has been tested with OS X 10.10.5 (Yosemite) but should also work for OS X 10.11 (El Capitan). +This section contains information about setting up a Tribler development environment on macOS. Unlike Linux based systems where installing third-party libraries is often a single ``apt-get`` command, installing and configuring the necessary libraries requires more attention on macOS. This guide has been tested with macOS 10.10.5 (Yosemite) but should also work for macOS 10.11 (El Capitan). -Note that the guide below assumes that Python is installed in the default location of Python (shipped with OS X). This location is normally in ``/Library/Python/2.7``. Writing to this location requires root acccess when using easy_install or pip. To avoid root commands, you can install Python in a virtualenv. More information about setting up Python in a virtualenv can be found `here `_. +Note that the guide below assumes that Python is installed in the default location of Python (shipped with macOS). This location is normally in ``/Library/Python/2.7``. Writing to this location requires root acccess when using easy_install or pip. To avoid root commands, you can install Python in a virtualenv. More information about setting up Python in a virtualenv can be found `here `_. Introduction ------------ -Compilation of C/C++ libraries should be performed using Clang which is part of the Xcode Command Line Tools. The Python version shipped with OS X can be used and this guide has been tested using Python 2.7. The current installed version and binary of Python can be found by executing: +Compilation of C/C++ libraries should be performed using Clang which is part of the Xcode Command Line Tools. The Python version shipped with macOS can be used and this guide has been tested using Python 2.7. The current installed version and binary of Python can be found by executing: .. code-block:: none @@ -32,12 +32,35 @@ The installation of Xcode is required in order to compile some C/C++ libraries. xcode-select --install -WxPython --------- +PyQt5 +----- + +If you wish to run the Tribler Graphical User Interface, PyQt5 should be available on the system. While PyQt5 is available in the pip repository, this is only compatible with Python 3. To install PyQt5, we first need to install Qt5, a C++ library which can be installed with brew: + +.. code-block:: none + + brew install qt5 + brew cask install qt-creator # if you want the visual designer + qmake --version # test whether qt is installed correctly + +After the installation completed, PyQt5 should be compiled. This library depends on SIP, another library to automatically generate Python bindings from C++ code. Download the latest SIP version `here `_, extract it, navigate to the directory where it has been extracted and compile/install it: + +.. code-block:: none -WxPython is the Graphical User Interface manager and an installer can be downloaded from `their website `_. Note that at this point, Wx 2.8 should still be used but support for 2.8 will be dropped soon and the Wx 2.8 library should be replaced by Wx 3.0. You probably need the Cocoa version of Wx. + python configure.py + make + sudo make install + +Next, download PyQt5 from `here `_ and make sure that you download the version that matches with the version of Qt you installed in the previous steps. Extract the binary and compile it: + +.. code-block:: none + + python configure.py + make + sudo make install + python -c "import PyQt5" # this should work without any error -Note: there is a bug on OS X 10.11 (El Capitan) where the installer gives an error that there is no software available to install. A workaround for this is to install the required files manually. This can be done by opening the ``.pkg`` file. First, you should run the ``preflight.sh`` script as root to clean up any old installation of wx. Next, unzip the ``wxPython3.0-osx-cocoa-py2.7.pax.gz`` file. This will create a ``usr`` directory which should be copied to ``/usr`` on the system. Note that you need root permissions to write to this directory (you can open a finder window with the needed permissions by running ``sudo open /usr`` in terminal). To link wx so Python can find it, you should run the ``postflight.sh`` as root. +Note that the installation can take a while. After it has finished, the PyQt5 library is installed correctly. M2Crypto -------- diff --git a/doc/development/development_on_windows.rst b/doc/development/development_on_windows.rst index 10efff7d5cb..c1c4361bbd4 100644 --- a/doc/development/development_on_windows.rst +++ b/doc/development/development_on_windows.rst @@ -35,16 +35,29 @@ The first package to be installed is M2Crypto which can be installed using pip ( If the second statement does not raise an error, M2Crypto is successfully installed. -wxPython --------- +PyQt5 +----- + +If you wish to run the Tribler Graphical User Interface, PyQt5 should be available on the system. While PyQt5 is available in the pip repository, this is only compatible with Python 3. Start by downloading the Qt library from `here `_. You can either compile it from source or use a Qt installer which automatically installs the pre-compiled libraries. + +After the Qt installation is completed, PyQt5 should be compiled. This library depends on SIP, another library to automatically generate Python bindings from C++ code. Download the latest SIP version `here `_, extract it, navigate to the directory where it has been extracted and compile/install it (don't forget to execute these commands in the Visual Studio command line): + +.. code-block:: none + + python configure.py + nmake + nmake install -The graphical interface of Tribler is built using wxPython. wxPython can be installed by using the official win64 installer for Python 2.7 from `Sourceforge `_. **At the time of writing, wx3 is not supported yet so you should install wx2.8** (make sure to install the unicode version). You can test whether wx can be successfully imported by running: +Next, download PyQt5 from `here `_ and make sure that you download the version that matches with the version of Qt you installed in the previous steps. Extract the binary and compile it: .. code-block:: none - python -c "import wx" + python configure.py + nmake + nmake install + python -c "import PyQt5" # this should work without any error -This statement should proceed without error. +Note that the installation can take a while. After it has finished, the PyQt5 library is installed correctly. pyWin32 Tools ------------- From ae4ce942e6e52bd86c8516bf4b1298e795b3a713 Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:06:06 +0100 Subject: [PATCH 6/9] Moved cryptography patch to Tribler core --- Tribler/Core/Utilities/crypto_patcher.py | 25 ++++++++++++++++++++++ Tribler/Main/hacks.py | 27 ------------------------ 2 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 Tribler/Core/Utilities/crypto_patcher.py diff --git a/Tribler/Core/Utilities/crypto_patcher.py b/Tribler/Core/Utilities/crypto_patcher.py new file mode 100644 index 00000000000..25ac6abf52a --- /dev/null +++ b/Tribler/Core/Utilities/crypto_patcher.py @@ -0,0 +1,25 @@ +import sys +from Tribler.Core.Utilities.install_dir import is_frozen + + +def patch_crypto_be_discovery(): + """ + Monkey patches cryptography's backend detection. + Objective: support pyinstaller freezing. + """ + if (sys.platform == 'win32' or sys.platform == 'darwin') and is_frozen(): + from cryptography.hazmat import backends + + try: + from cryptography.hazmat.backends.commoncrypto.backend import backend as be_cc + except ImportError: + be_cc = None + + try: + from cryptography.hazmat.backends.openssl.backend import backend as be_ossl + except ImportError: + be_ossl = None + + backends._available_backends_list = [ + be for be in (be_cc, be_ossl) if be is not None + ] diff --git a/Tribler/Main/hacks.py b/Tribler/Main/hacks.py index 4796101f602..4ea5bed5973 100644 --- a/Tribler/Main/hacks.py +++ b/Tribler/Main/hacks.py @@ -42,33 +42,6 @@ import sys - -def patch_crypto_be_discovery(): - - """ - Monkey patches cryptography's backend detection. - Objective: support pyinstaller freezing. - """ - - # Just do the monkeypatching if running on a windows installer version. - if (sys.platform == 'win32' and sys.argv[0].lower().endswith("tribler.exe")) \ - or (sys.platform == 'darwin' and hasattr(sys, 'frozen')): - from cryptography.hazmat import backends - - try: - from cryptography.hazmat.backends.commoncrypto.backend import backend as be_cc - except ImportError: - be_cc = None - - try: - from cryptography.hazmat.backends.openssl.backend import backend as be_ossl - except ImportError: - be_ossl = None - - backends._available_backends_list = [ - be for be in (be_cc, be_ossl) if be is not None - ] - # Python2 sys.argv uses the non-unicode windows API, so non-ascii chars will be mangled. # TODO(emilon): This should be removed once we move to Python3 # Based on code from: From 2cadaf98cf3b64edd2fd72611de8833a241bcd87 Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:09:05 +0100 Subject: [PATCH 7/9] Updated VLC for pyinstaller --- Tribler/vlc.py | 113 +++++++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/Tribler/vlc.py b/Tribler/vlc.py index d0483fa0b8c..9d976546304 100755 --- a/Tribler/vlc.py +++ b/Tribler/vlc.py @@ -103,9 +103,21 @@ def bytes_to_str(b): # instanciated. _internal_guard = object() +def is_frozen(): + """ + Return whether we are running in a frozen environment + """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + return False + return True + def find_lib(): dll = None plugin_path = None + vlc_dir = None if sys.platform.startswith('linux'): p = find_library('vlc') try: @@ -114,61 +126,70 @@ def find_lib(): dll = ctypes.CDLL('libvlc.so.5') elif sys.platform.startswith('win'): # emilon: WORKAROUND! don't try to look for libvlc.dll in silly places, it breaks. - p = None - if p is None: - try: # some registry settings - # leaner than win32api, win32con - if PYTHON3: - import winreg as w - else: - import _winreg as w - for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER: - try: - r = w.OpenKey(r, 'Software\\VideoLAN\\VLC') - plugin_path, _ = w.QueryValueEx(r, 'InstallDir') - w.CloseKey(r) - break - except w.error: - pass - except ImportError: # no PyWin32 - pass - if plugin_path is None: - # try some standard locations. - for p in ('Program Files\\VideoLan\\', 'VideoLan\\', - 'Program Files\\', ''): - p = 'C:\\' + p + 'VLC\\libvlc.dll' - if os.path.exists(p): - plugin_path = os.path.dirname(p) - break - if plugin_path is not None: # try loading - p = os.getcwd() - os.chdir(plugin_path) - # if chdir failed, this will raise an exception - dll = ctypes.CDLL('libvlc.dll') - # restore cwd after dll has been loaded - os.chdir(p) - else: # may fail - dll = ctypes.CDLL('libvlc.dll') + # Martijn: On Windows we need to blank SetDllDirectoryW to allow loading of the VLC DLL. + # We restore this settings after we're done for security reasons. + buffer_size = 1024 + dll_directory = ctypes.create_unicode_buffer(buffer_size) + new_buffer_size = ctypes.windll.kernel32.GetDllDirectoryW(buffer_size, dll_directory) + dll_directory = ''.join(dll_directory[:new_buffer_size]).replace('\0', '') + ctypes.windll.kernel32.SetDllDirectoryW(None) + + try: # some registry settings + # leaner than win32api, win32con + if PYTHON3: + import winreg as w + else: + import _winreg as w + for r in w.HKEY_LOCAL_MACHINE, w.HKEY_CURRENT_USER: + try: + r = w.OpenKey(r, 'Software\\VideoLAN\\VLC') + vlc_dir, _ = w.QueryValueEx(r, 'InstallDir') + w.CloseKey(r) + break + except w.error: + pass + except ImportError: # no PyWin32 + pass + if vlc_dir is None: + # try some standard locations. + for p in ('Program Files\\VideoLAN\\', 'VideoLAN\\', + 'Program Files\\', 'Program Files (x86)\\VideoLAN\\', ''): + p = 'C:\\' + p + 'VLC\\libvlc.dll' + if os.path.exists(p): + vlc_dir = os.path.dirname(p) + break + if vlc_dir is not None: # try loading + p = os.getcwd() + os.chdir(vlc_dir) + # if chdir failed, this will raise an exception + dll = ctypes.CDLL('libvlc.dll') + # restore cwd after dll has been loaded + os.chdir(p) + else: # may fail + dll = ctypes.CDLL('libvlc.dll') + plugin_path = os.path.join(vlc_dir, 'plugins') + ctypes.windll.kernel32.SetDllDirectoryW(dll_directory) + elif sys.platform.startswith('darwin'): + # If we are frozen with PyInstaller, find the VLC libraries inside the .app + if is_frozen(): + vlc_dir = os.path.join(sys._MEIPASS, 'vlc') else: - plugin_path = os.path.dirname(p) - dll = ctypes.CDLL(p) + vlc_dir = '/Applications/VLC.app/Contents/MacOS/' - elif sys.platform.startswith('darwin'): # FIXME: should find a means to configure path - d = '/Applications/VLC.app/Contents/MacOS/' - p = d + 'lib/libvlc.dylib' - if os.path.exists(p): - dll = ctypes.CDLL(p) - d += 'modules' - if os.path.isdir(d): - plugin_path = d + libvlc_path = os.path.join(vlc_dir, 'lib', 'libvlc.dylib') + if os.path.exists(libvlc_path): + dll = ctypes.CDLL(libvlc_path) + vlc_dir = os.path.join(vlc_dir, 'plugins') + if os.path.isdir(vlc_dir): + plugin_path = vlc_dir else: # hope, some PATH is set... dll = ctypes.CDLL('libvlc.dylib') else: raise NotImplementedError('%s: %s not supported' % (sys.argv[0], sys.platform)) - return (dll, plugin_path) + return dll, plugin_path # plugin_path used on win32 and MacOS in override.py dll, plugin_path = find_lib() From d5de7f755c9a2f54b112452bcbdee3b2a5df6cd7 Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:09:27 +0100 Subject: [PATCH 8/9] Updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2ef6e97fe24..e64ea78eef5 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ doc/_build/ # Twisted twisted/plugins/dropin.cache +twistd.pid From 5c6b7c2db2e559b06a02716296a3a5ca39a2264d Mon Sep 17 00:00:00 2001 From: Martijn de Vos Date: Sun, 20 Nov 2016 21:09:35 +0100 Subject: [PATCH 9/9] Updated pylintrc --- .pylintrc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pylintrc b/.pylintrc index 0440a0d1b33..b745e1c0213 100644 --- a/.pylintrc +++ b/.pylintrc @@ -64,7 +64,7 @@ confidence= # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" #disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating -disable=C0321,W0142,invalid-name,missing-docstring +disable=C0321,W0142,invalid-name,missing-docstring,no-member,no-name-in-module,no-self-use,too-few-public-methods #missing-type-doc @@ -330,7 +330,7 @@ int-import-graph= [DESIGN] # Maximum number of arguments for function / method -max-args=5 +max-args=6 # Argument names that match this expression will be ignored. Default to name # with leading underscore @@ -340,7 +340,7 @@ ignored-argument-names=_.* max-locals=15 # Maximum number of return / yield for function / method body -max-returns=6 +max-returns=8 # Maximum number of branch for function / method body max-branches=12 @@ -352,7 +352,7 @@ max-statements=50 max-parents=7 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=10 # Minimum number of public methods for a class (see R0903). min-public-methods=2