Merge branch 'development' into experimental-mac

# Conflicts:
#	DemonEditor.desktop
#	README.md
#	app/connections.py
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	app/ui/uicommons.py
#	build-deb.sh
#	deb/DEBIAN/control
#	deb/DEBIAN/copyright
#	deb/usr/bin/demon-editor
#	deb/usr/share/applications/DemonEditor.desktop
#	start.py
This commit is contained in:
DYefremov
2020-01-18 23:08:45 +03:00
29 changed files with 3725 additions and 1435 deletions

View File

@@ -1,18 +1,20 @@
import json
import os
import re
import socket
import time
import urllib
import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, error_perm
from http.client import RemoteDisconnected
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener
from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, \
build_opener, install_opener, Request
from app.commons import log
from app.settings import Profile
from app.commons import log, run_task
from app.settings import SettingsType
_BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
@@ -35,21 +37,28 @@ class DownloadType(Enum):
class HttpRequestType(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "tunersignal"
STREAM = "streamcurrentm3u"
STATUS = "statusinfo"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
PLAY = "mediaplayerplay?file=4097:0:1:0:0:0:0:0:0:0:"
TEST = None
TOKEN = "session"
class TestException(Exception):
pass
class HttpApiException(Exception):
pass
def download_data(*, settings, download_type=DownloadType.ALL, callback=print):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = settings.data_dir_path
save_path = settings.data_local_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
files = []
# bouquets
@@ -94,15 +103,16 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print):
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=print, done_callback=None, use_http=False):
profile = settings.profile
data_path = settings.data_dir_path
s_type = settings.setting_type
data_path = settings.data_local_path
host = settings.host
base_url = "http://{}:{}/api/".format(host, settings.http_port)
base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port)
url = "{}/web/".format(base_url)
tn, ht = None, None # telnet, http
try:
if profile is Profile.ENIGMA_2 and use_http:
ht = http(settings.http_user, settings.http_password, base_url, callback)
if s_type is SettingsType.ENIGMA_2 and use_http:
ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
@@ -113,12 +123,11 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
message = "Satellites.xml file will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5})
url = base_url + "message?{}".format(params)
ht.send(url)
ht.send((url + "message?{}".format(params), "Sending info message... "))
if download_type is DownloadType.ALL:
time.sleep(5)
ht.send(base_url + "/powerstate?newstate=0")
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
time.sleep(2)
else:
# telnet
@@ -139,7 +148,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if download_type is DownloadType.SATELLITES:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback)
if profile is Profile.NEUTRINO_MP and download_type is DownloadType.WEBTV:
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback)
if download_type is DownloadType.BOUQUETS:
@@ -148,7 +157,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if download_type is DownloadType.ALL:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback)
if profile is Profile.NEUTRINO_MP:
if s_type is SettingsType.NEUTRINO_MP:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback)
ftp.cwd(services_path)
@@ -156,17 +165,17 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
upload_files(ftp, data_path, _DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
upload_picons(ftp, settings.picons_dir_path, settings.picons_path, callback)
upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback)
if tn and not use_http:
# resume enigma or restart neutrino
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send(base_url + "/servicelistreload?mode=2")
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
elif download_type is DownloadType.ALL:
ht.send(base_url + "/servicelistreload?mode=0")
ht.send(base_url + "/powerstate?newstate=4")
ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
ht.send((url + "powerstate?newstate=4", "Wakeup from Standby."))
if done_callback is not None:
done_callback()
@@ -238,14 +247,14 @@ def send_file(file_name, path, ftp, callback):
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
def http(user, password, url, callback):
init_auth(user, password, url)
def http(user, password, url, callback, use_ssl=False):
init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url)
while True:
url = yield
with urlopen(url, timeout=5) as f:
msg = json.loads(f.read().decode("utf-8")).get("message", None)
if msg:
callback("HTTP: {}\n".format(msg))
url, message = yield
resp = get_response(HttpRequestType.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
def telnet(host, port=23, user="", password="", timeout=5):
@@ -276,38 +285,102 @@ def telnet(host, port=23, user="", password="", timeout=5):
# ***************** HTTP API *******************#
class HttpAPI:
__MAX_WORKERS = 4
def __init__(self, host, port, user, password):
self._base_url = "http://{}:{}/api/".format(host, port)
init_auth(user, password, self._base_url)
def __init__(self, settings):
self._settings = settings
self._shutdown = False
self._session_id = 0
self._base_url = None
self._data = None
self.init()
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=2)
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
def send(self, req_type, ref, callback=print):
if self._shutdown:
return
url = self._base_url + req_type.value
if req_type is HttpRequestType.ZAP:
if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM:
url += urllib.parse.quote(ref)
elif req_type is HttpRequestType.PLAY:
url += urllib.parse.quote(ref).replace("%3A", "%253A")
future = self._executor.submit(get_json, req_type, url)
future = self._executor.submit(get_response, req_type, url, self._data)
future.add_done_callback(lambda f: callback(f.result()))
@run_task
def init(self):
user, password = self._settings.http_user, self._settings.http_password
use_ssl = self._settings.http_use_ssl
url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(url)
init_auth(user, password, url, use_ssl)
url = "{}/web/{}".format(url, HttpRequestType.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
@run_task
def close(self):
self._executor.shutdown(False)
self._shutdown = True
self._executor.shutdown()
def get_json(req_type, url):
def get_response(req_type, url, data=None):
try:
with urlopen(url, timeout=10) as f:
if req_type is HttpRequestType.STREAM:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT:
return f.read().decode("utf-8")
elif req_type is HttpRequestType.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
else:
return json.loads(f.read().decode("utf-8"))
except (URLError, HTTPError):
pass
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpRequestType.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpRequestType.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
return {"error_code": -1}
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
install_opener(opener)
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpRequestType.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpRequestType.TOKEN.value))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
# ***************** Connections testing *******************#
@@ -320,36 +393,30 @@ def test_ftp(host, port, user, password, timeout=5):
raise TestException(e)
def test_http(host, port, user, password, timeout=5, skip_message=False):
try:
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
url = "http://{}:{}/api/{}".format(host, port, params)
# authentication
init_auth(user, password, url)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False):
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port)
# authentication
init_auth(user, password, base_url, use_ssl)
data = get_post_data(base_url, password, user)
with urlopen(url, timeout=5) as f:
return json.loads(f.read().decode("utf-8")).get("message", "")
try:
return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)
def init_auth(user, password, url):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
opener = build_opener(auth_handler)
install_opener(opener)
def test_telnet(host, port, user, password, timeout=5):
try:
gen = telnet_test(host, port, user, password, timeout)
res = next(gen)
print(res)
res = next(gen)
return res
msg = str(res, encoding="utf8").strip()
log(msg)
next(gen)
if re.search("password", msg, re.IGNORECASE):
raise TestException(msg)
return msg
except (socket.timeout, OSError) as e:
raise TestException(e)
@@ -358,14 +425,14 @@ def telnet_test(host, port, user, password, timeout):
tn = Telnet(host=host, port=port, timeout=timeout)
time.sleep(1)
tn.read_until(b"login: ", timeout=2)
tn.write(user.encode("utf-8") + b"\n")
tn.write(user.encode("utf-8") + b"\r")
time.sleep(timeout)
tn.read_until(b"Password: ", timeout=2)
tn.write(password.encode("utf-8") + b"\n")
tn.write(password.encode("utf-8") + b"\r")
time.sleep(timeout)
yield tn.read_very_eager()
tn.close()
yield "Done!"
yield
if __name__ == "__main__":

View File

@@ -1,5 +1,5 @@
from app.commons import run_task
from app.settings import Profile
from app.settings import SettingsType
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
from .enigma.blacklist import get_blacklist, write_blacklist
from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id
@@ -10,33 +10,33 @@ from .neutrino.services import get_services as get_neutrino_services, write_serv
from .satxml import get_satellites, write_satellites
def get_services(data_path, profile, format_version):
if profile is Profile.ENIGMA_2:
def get_services(data_path, s_type, format_version):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_services(data_path, format_version)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_services(data_path)
@run_task
def write_services(path, channels, profile, format_version):
if profile is Profile.ENIGMA_2:
def write_services(path, channels, s_type, format_version):
if s_type is SettingsType.ENIGMA_2:
write_enigma_services(path, channels, format_version)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_services(path, channels)
def get_bouquets(path, profile):
if profile is Profile.ENIGMA_2:
def get_bouquets(path, s_type):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_bouquets(path)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_bouquets(path)
@run_task
def write_bouquets(path, bouquets, profile):
if profile is Profile.ENIGMA_2:
def write_bouquets(path, bouquets, s_type):
if s_type is SettingsType.ENIGMA_2:
write_enigma_bouquets(path, bouquets)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

View File

@@ -3,7 +3,7 @@ import re
import urllib.request
from enum import Enum
from app.settings import Profile
from app.settings import SettingsType
from app.ui.uicommons import IPTV_ICON
from .ecommons import BqServiceType, Service
@@ -20,18 +20,18 @@ class StreamType(Enum):
NONE_REC_2 = "5002"
def parse_m3u(path, profile):
def parse_m3u(path, s_type):
with open(path) as file:
aggr = [None] * 10
services = []
groups = set()
counter = 0
name = None
for line in file.readlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
elif line.startswith("#EXTGRP") and profile is Profile.ENIGMA_2:
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
@@ -41,7 +41,7 @@ def parse_m3u(path, profile):
services.append(mr)
elif not line.startswith("#"):
url = line.strip()
fav_id = get_fav_id(url, name, profile)
fav_id = get_fav_id(url, name, s_type)
if name and url:
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
services.append(srv)
@@ -49,8 +49,8 @@ def parse_m3u(path, profile):
return services
def export_to_m3u(path, bouquet, profile):
pattern = re.compile(".*:(http.*):.*") if profile is Profile.ENIGMA_2 else re.compile("(http.*?)::::.*")
def export_to_m3u(path, bouquet, s_type):
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
@@ -72,13 +72,13 @@ def export_to_m3u(path, bouquet, profile):
file.writelines(lines)
def get_fav_id(url, service_name, profile):
def get_fav_id(url, service_name, s_type):
""" Returns fav id depending on the profile. """
if profile is Profile.ENIGMA_2:
if s_type is SettingsType.ENIGMA_2:
url = urllib.request.quote(url)
stream_type = StreamType.NONE_TS.value
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, service_name, service_name, None)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)

View File

@@ -1,9 +1,11 @@
import copy
import json
import locale
import os
from enum import Enum, IntEnum
from pathlib import Path
from pprint import pformat
from textwrap import dedent
from enum import Enum
from pathlib import Path
HOME_PATH = str(Path.home())
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
@@ -11,19 +13,94 @@ CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "/data/"
class Profile(Enum):
class Defaults(Enum):
""" Default program settings """
DEFAULT_PROFILE = "default"
BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False
HTTP_API_SUPPORT = False
ENABLE_YT_DL = False
ENABLE_SEND_TO = False
USE_COLORS = True
NEW_COLOR = "rgb(255,230,204)"
EXTRA_COLOR = "rgb(179,230,204)"
FAV_CLICK_MODE = 0
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
set_local_paths(def_settings, profile_name)
return {
"version": 1,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value
}
def set_local_paths(settings, profile_name):
settings["data_local_path"] = "{}{}/".format(settings["data_local_path"], profile_name)
settings["picons_local_path"] = "{}{}/".format(settings["picons_local_path"], profile_name)
settings["backup_local_path"] = "{}{}/".format(settings["backup_local_path"], profile_name)
class SettingsType(IntEnum):
""" Profiles for settings """
ENIGMA_2 = "0"
NEUTRINO_MP = "1"
ENIGMA_2 = 0
NEUTRINO_MP = 1
def get_default_settings(self):
""" Returns default settings for current type """
if self is self.ENIGMA_2:
return {"setting_type": self.value,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "root", "http_password": "", "http_port": "80",
"http_timeout": 5, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon/",
"picons_local_path": DATA_PATH + "enigma2/picons/",
"backup_local_path": DATA_PATH + "enigma2/backup/"}
elif self is self.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
"picons_local_path": DATA_PATH + "neutrino/picons/",
"backup_local_path": DATA_PATH + "neutrino/backup/"}
class SettingsException(Exception):
pass
class Settings:
__INSTANCE = None
__VERSION = 1
def __init__(self):
self._config = get_config()
self._current_profile = Profile(self._config.get("profile"))
self._current_profile_options = self._config.get(self._current_profile.value)
def __init__(self, ext_settings=None):
settings = ext_settings or get_settings()
if self.__VERSION > settings.get("version", 0):
raise SettingsException("Outdated version of the settings format!")
self._settings = settings
self._current_profile = self._settings.get("default_profile", "default")
self._profiles = self._settings.get("profiles", {"default": SettingsType.ENIGMA_2.get_default_settings()})
self._cp_settings = self._profiles.get(self._current_profile) # Current profile settings
def __str__(self):
return dedent(""" Current profile: {}
@@ -32,8 +109,8 @@ class Settings:
Full config:
{}
""").format(self._current_profile,
pformat(self._current_profile_options),
pformat(self._config))
pformat(self._cp_settings),
pformat(self._settings))
@classmethod
def get_instance(cls):
@@ -42,325 +119,350 @@ class Settings:
return cls.__INSTANCE
def save(self):
write_config(self._config)
write_settings(self._settings)
def reset(self, force_write=False):
def_settings = get_default_settings()
for p in Profile:
current = self._config.get(p.value)
default = def_settings.get(p.value)
for k in default:
current[k] = default.get(k)
for k, v in self.setting_type.get_default_settings().items():
self._cp_settings[k] = v
set_local_paths(self._cp_settings, self._current_profile)
if force_write:
write_config(get_default_settings())
self.save()
@staticmethod
def reset_to_default():
write_settings(get_default_settings())
def get_default(self, p_name):
""" Returns default value for current settings type """
return self.setting_type.get_default_settings().get(p_name)
def add(self, name, value):
""" Adds extra options """
self._config[name] = value
self._settings[name] = value
def get(self, name):
""" Returns extra options """
return self._config.get(name, None)
def get_default(self, name):
""" Returns default value of the option """
return get_default_settings().get(self._current_profile.value).get(name)
""" Returns extra options or None """
return self._settings.get(name, None)
@property
def presets(self):
raise NotImplementedError
def settings(self):
""" Returns copy of the current settings! """
return copy.deepcopy(self._settings)
@presets.setter
def presets(self, name):
raise NotImplementedError
@settings.setter
def settings(self, value):
""" Sets copy of the settings! """
self._settings = copy.deepcopy(value)
@property
def profile(self):
def current_profile(self):
return self._current_profile
@profile.setter
def profile(self, prf):
self._current_profile = prf
self._config["profile"] = prf.value
self._current_profile_options = self._config.get(prf.value)
@current_profile.setter
def current_profile(self, value):
self._current_profile = value
self._cp_settings = self._profiles.get(self._current_profile)
@property
def default_profile(self):
return self._settings.get("default_profile", "default")
@default_profile.setter
def default_profile(self, value):
self._settings["default_profile"] = value
@property
def profiles(self):
return self._profiles
@profiles.setter
def profiles(self, ps):
self._profiles = ps
self._settings["profiles"] = self._profiles
@property
def setting_type(self):
return SettingsType(self._cp_settings.get("setting_type", SettingsType.ENIGMA_2.value))
@setting_type.setter
def setting_type(self, s_type):
self._cp_settings["setting_type"] = s_type.value
@property
def language(self):
return self._settings.get("language", locale.getlocale()[0] or "en_US")
@language.setter
def language(self, value):
self._settings["language"] = value
@property
def load_last_config(self):
return self._settings.get("load_last_config", False)
@load_last_config.setter
def load_last_config(self, value):
self._settings["load_last_config"] = value
@property
def host(self):
return self._current_profile_options.get("host", self.get_default("host"))
return self._cp_settings.get("host", self.get_default("host"))
@host.setter
def host(self, value):
self._current_profile_options["host"] = value
self._cp_settings["host"] = value
@property
def port(self):
return self._current_profile_options.get("port", self.get_default("port"))
return self._cp_settings.get("port", self.get_default("port"))
@port.setter
def port(self, value):
self._current_profile_options["port"] = value
self._cp_settings["port"] = value
@property
def user(self):
return self._current_profile_options.get("user", self.get_default("user"))
return self._cp_settings.get("user", self.get_default("user"))
@user.setter
def user(self, value):
self._current_profile_options["user"] = value
self._cp_settings["user"] = value
@property
def password(self):
return self._current_profile_options.get("password", self.get_default("password"))
return self._cp_settings.get("password", self.get_default("password"))
@password.setter
def password(self, value):
self._current_profile_options["password"] = value
self._cp_settings["password"] = value
@property
def http_user(self):
return self._current_profile_options.get("http_user", self.get_default("http_user"))
return self._cp_settings.get("http_user", self.get_default("http_user"))
@http_user.setter
def http_user(self, value):
self._current_profile_options["http_user"] = value
self._cp_settings["http_user"] = value
@property
def http_password(self):
return self._current_profile_options.get("http_password", self.get_default("http_password"))
return self._cp_settings.get("http_password", self.get_default("http_password"))
@http_password.setter
def http_password(self, value):
self._current_profile_options["http_password"] = value
self._cp_settings["http_password"] = value
@property
def http_port(self):
return self._current_profile_options.get("http_port", self.get_default("http_port"))
return self._cp_settings.get("http_port", self.get_default("http_port"))
@http_port.setter
def http_port(self, value):
self._current_profile_options["http_port"] = value
self._cp_settings["http_port"] = value
@property
def http_timeout(self):
return self._current_profile_options.get("http_timeout", self.get_default("http_timeout"))
return self._cp_settings.get("http_timeout", self.get_default("http_timeout"))
@http_timeout.setter
def http_timeout(self, value):
self._current_profile_options["http_timeout"] = value
self._cp_settings["http_timeout"] = value
@property
def http_use_ssl(self):
return self._cp_settings.get("http_use_ssl", self.get_default("http_use_ssl"))
@http_use_ssl.setter
def http_use_ssl(self, value):
self._cp_settings["http_use_ssl"] = value
@property
def telnet_user(self):
return self._current_profile_options.get("telnet_user", self.get_default("telnet_user"))
return self._cp_settings.get("telnet_user", self.get_default("telnet_user"))
@telnet_user.setter
def telnet_user(self, value):
self._current_profile_options["telnet_user"] = value
self._cp_settings["telnet_user"] = value
@property
def telnet_password(self):
return self._current_profile_options.get("telnet_password", self.get_default("telnet_password"))
return self._cp_settings.get("telnet_password", self.get_default("telnet_password"))
@telnet_password.setter
def telnet_password(self, value):
self._current_profile_options["telnet_password"] = value
self._cp_settings["telnet_password"] = value
@property
def telnet_port(self):
return self._current_profile_options.get("telnet_port", self.get_default("telnet_port"))
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
@telnet_port.setter
def telnet_port(self, value):
self._current_profile_options["telnet_port"] = value
self._cp_settings["telnet_port"] = value
@property
def telnet_timeout(self):
return self._current_profile_options.get("telnet_timeout", self.get_default("telnet_timeout"))
return self._cp_settings.get("telnet_timeout", self.get_default("telnet_timeout"))
@telnet_timeout.setter
def telnet_timeout(self, value):
self._current_profile_options["telnet_timeout"] = value
self._cp_settings["telnet_timeout"] = value
@property
def services_path(self):
return self._current_profile_options.get("services_path", self.get_default("services_path"))
return self._cp_settings.get("services_path", self.get_default("services_path"))
@services_path.setter
def services_path(self, value):
self._current_profile_options["services_path"] = value
self._cp_settings["services_path"] = value
@property
def user_bouquet_path(self):
return self._current_profile_options.get("user_bouquet_path", self.get_default("user_bouquet_path"))
return self._cp_settings.get("user_bouquet_path", self.get_default("user_bouquet_path"))
@user_bouquet_path.setter
def user_bouquet_path(self, value):
self._current_profile_options["user_bouquet_path"] = value
self._cp_settings["user_bouquet_path"] = value
@property
def satellites_xml_path(self):
return self._current_profile_options.get("satellites_xml_path", self.get_default("satellites_xml_path"))
return self._cp_settings.get("satellites_xml_path", self.get_default("satellites_xml_path"))
@satellites_xml_path.setter
def satellites_xml_path(self, value):
self._current_profile_options["satellites_xml_path"] = value
self._cp_settings["satellites_xml_path"] = value
@property
def data_dir_path(self):
return self._current_profile_options.get("data_dir_path", self.get_default("data_dir_path"))
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
@data_dir_path.setter
def data_dir_path(self, value):
self._current_profile_options["data_dir_path"] = value
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@property
def picons_path(self):
return self._current_profile_options.get("picons_path", self.get_default("picons_path"))
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
@picons_path.setter
def picons_path(self, value):
self._current_profile_options["picons_path"] = value
self._cp_settings["picons_path"] = value
@property
def picons_dir_path(self):
return self._current_profile_options.get("picons_dir_path", self.get_default("picons_dir_path"))
def picons_local_path(self):
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
@picons_dir_path.setter
def picons_dir_path(self, value):
self._current_profile_options["picons_dir_path"] = value
@picons_local_path.setter
def picons_local_path(self, value):
self._cp_settings["picons_local_path"] = value
@property
def backup_dir_path(self):
return self._current_profile_options.get("backup_dir_path", self.get_default("backup_dir_path"))
def backup_local_path(self):
return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path"))
@backup_dir_path.setter
def backup_dir_path(self, value):
self._current_profile_options["backup_dir_path"] = value
@backup_local_path.setter
def backup_local_path(self, value):
self._cp_settings["backup_local_path"] = value
# ***** Program settings *****
@property
def backup_before_save(self):
return self._current_profile_options.get("backup_before_save", self.get_default("backup_before_save"))
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value)
@backup_before_save.setter
def backup_before_save(self, value):
self._current_profile_options["backup_before_save"] = value
self._settings["backup_before_save"] = value
@property
def backup_before_downloading(self):
return self._current_profile_options.get("backup_before_downloading",
self.get_default("backup_before_downloading"))
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value)
@backup_before_downloading.setter
def backup_before_downloading(self, value):
self._current_profile_options["backup_before_downloading"] = value
self._settings["backup_before_downloading"] = value
@property
def v5_support(self):
return self._current_profile_options.get("v5_support", self.get_default("v5_support"))
return self._settings.get("v5_support", Defaults.V5_SUPPORT.value)
@v5_support.setter
def v5_support(self, value):
self._current_profile_options["v5_support"] = value
self._settings["v5_support"] = value
@property
def http_api_support(self):
return self._current_profile_options.get("http_api_support", self.get_default("http_api_support"))
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
@http_api_support.setter
def http_api_support(self, value):
self._current_profile_options["http_api_support"] = value
self._settings["http_api_support"] = value
@property
def enable_yt_dl(self):
return self._current_profile_options.get("enable_yt_dl", self.get_default("enable_yt_dl"))
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value)
@enable_yt_dl.setter
def enable_yt_dl(self, value):
self._current_profile_options["enable_yt_dl"] = value
self._settings["enable_yt_dl"] = value
@property
def enable_send_to(self):
return self._current_profile_options.get("enable_send_to", self.get_default("enable_send_to"))
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
@enable_send_to.setter
def enable_send_to(self, value):
self._current_profile_options["enable_send_to"] = value
self._settings["enable_send_to"] = value
@property
def use_colors(self):
return self._current_profile_options.get("use_colors", self.get_default("use_colors"))
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
@use_colors.setter
def use_colors(self, value):
self._current_profile_options["use_colors"] = value
self._settings["use_colors"] = value
@property
def new_color(self):
return self._current_profile_options.get("new_color", self.get_default("new_color"))
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
@new_color.setter
def new_color(self, value):
self._current_profile_options["new_color"] = value
self._settings["new_color"] = value
@property
def extra_color(self):
return self._current_profile_options.get("extra_color", self.get_default("extra_color"))
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
@extra_color.setter
def extra_color(self, value):
self._current_profile_options["extra_color"] = value
self._settings["extra_color"] = value
@property
def fav_click_mode(self):
return self._current_profile_options.get("fav_click_mode", self.get_default("fav_click_mode"))
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
@fav_click_mode.setter
def fav_click_mode(self, value):
self._current_profile_options["fav_click_mode"] = value
self._settings["fav_click_mode"] = value
def get_config():
def get_settings():
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True)
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
write_config(get_default_settings())
write_settings(get_default_settings())
with open(CONFIG_FILE, "r") as config_file:
return json.load(config_file)
def write_config(config):
def write_settings(config):
with open(CONFIG_FILE, "w") as config_file:
json.dump(config, config_file, indent=" ")
def get_default_settings():
return {
Profile.ENIGMA_2.value: {
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root",
"http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_dir_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon", "picons_dir_path": DATA_PATH + "enigma2/picons/",
"backup_dir_path": DATA_PATH + "enigma2/backup/",
"backup_before_save": True, "backup_before_downloading": True,
"v5_support": False, "http_api_support": False, "enable_yt_dl": False, "enable_send_to": False,
"use_colors": True, "new_color": "rgb(255,230,204)", "extra_color": "rgb(179,230,204)",
"fav_click_mode": 0},
Profile.NEUTRINO_MP.value: {
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root",
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_dir_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/", "picons_dir_path": DATA_PATH + "neutrino/picons/",
"backup_dir_path": DATA_PATH + "neutrino/backup/",
"backup_before_save": True, "backup_before_downloading": True,
"fav_click_mode": 0},
"profile": Profile.ENIGMA_2.value}
if __name__ == "__main__":
pass

View File

@@ -1,11 +1,12 @@
import sys
from app.commons import run_task, log
class Player:
__VLC_INSTANCE = None
def __init__(self, rewind_callback, position_callback):
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
try:
from app.tools import vlc
from app.tools.vlc import EventType
@@ -28,10 +29,19 @@ class Player:
lambda et, p: position_callback(p.get_time()),
self._player)
if error_callback:
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
lambda et, p: error_callback(),
self._player)
if playing_callback:
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
lambda et, p: playing_callback(),
self._player)
@classmethod
def get_instance(cls, rewind_callback=None, position_callback=None):
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback)
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
return cls.__VLC_INSTANCE
@run_task

View File

@@ -7,7 +7,7 @@ from collections import namedtuple
from html.parser import HTMLParser
from app.commons import run_task
from app.settings import Profile
from app.settings import SettingsType
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
@@ -79,7 +79,7 @@ class PiconsParser(HTMLParser):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, profile=Profile.ENIGMA_2):
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
@@ -100,7 +100,7 @@ class PiconsParser(HTMLParser):
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, profile)
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e:
@@ -109,10 +109,10 @@ class PiconsParser(HTMLParser):
print(msg)
@staticmethod
def format(ssid, on_id, namespace, picon_ids, profile: Profile):
if profile is Profile.ENIGMA_2:
def format(ssid, on_id, namespace, picon_ids, s_type):
if s_type is SettingsType.ENIGMA_2:
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), namespace), None)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid))
else:
@@ -249,12 +249,12 @@ def parse_providers(open_path):
@run_task
def convert_to(src_path, dest_path, profile, callback, done_callback):
def convert_to(src_path, dest_path, s_type, callback, done_callback):
""" Converts names format of picons.
Copies resulting files from src to dest and writes state to callback.
"""
pattern = "/*_0_0_0.png" if profile is Profile.ENIGMA_2 else "/*.png"
pattern = "/*_0_0_0.png" if s_type is SettingsType.ENIGMA_2 else "/*.png"
for file in glob.glob(src_path + pattern):
base_name = os.path.basename(file)
pic_data = base_name.rstrip(".png").split("_")

View File

@@ -7,7 +7,7 @@ from datetime import datetime
from enum import Enum
from app.commons import run_idle
from app.settings import Profile
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType
from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey
@@ -36,9 +36,9 @@ class BackupDialog:
builder.connect_signals(handlers)
self._settings = settings
self._profile = settings.profile
self._data_path = self._settings.data_dir_path
self._backup_path = self._settings.backup_dir_path or self._data_path + "backup/"
self._s_type = settings.setting_type
self._data_path = self._settings.data_local_path
self._backup_path = self._settings.backup_local_path or self._data_path + "backup/"
self._open_data_callback = callback
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
@@ -152,7 +152,7 @@ class BackupDialog:
shutil.unpack_archive(full_file_name, self._data_path)
elif restore_type is RestoreType.BOUQUETS:
tmp_dir = tempfile.gettempdir() + "/" + file_name
cond = (".tv", ".radio") if self._profile is Profile.ENIGMA_2 else "bouquets.xml"
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
shutil.unpack_archive(full_file_name, tmp_dir)
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
os.remove(os.path.join(self._data_path, file))
@@ -192,6 +192,7 @@ def backup_data(path, backup_path, move=True):
"""
backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)
# backup files in data dir(skipping dirs and satellites.xml)
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
src, dst = os.path.join(path, file), backup_path + file

View File

@@ -41,7 +41,7 @@ Author: Dmitriy Yefremov
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">0.4.7 Pre-alpha</property>
<property name="copyright">2018-2019 Dmitriy Yefremov
<property name="copyright">2018-2020 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for MacOS.
(Experimental)</property>
@@ -51,7 +51,8 @@ Author: Dmitriy Yefremov
<property name="authors">Dmitriy Yefremov
</property>
<property name="translator_credits" translatable="yes">translator-credits</property>
<property name="logo_icon_name">accessories-text-editor</property>
<property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt; mfgeg&lt;/a&gt;</property>
<property name="logo_icon_name">demon-editor</property>
<property name="wrap_license">True</property>
<property name="license_type">mit-x11</property>
<child>

View File

@@ -104,7 +104,7 @@ def get_file_chooser_dialog(transient, text, settings, action_type, file_filter)
if file_filter is not None:
dialog.add_filter(file_filter)
path = settings.data_dir_path
path = settings.data_local_path
dialog.set_current_folder(path)
response = dialog.run()

View File

@@ -26,7 +26,7 @@ THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
@@ -47,6 +47,7 @@ Author: Dmitriy Yefremov
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="spacing">5</property>
<property name="show_close_button">True</property>
<child>
@@ -56,7 +57,6 @@ Author: Dmitriy Yefremov
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="receive_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
@@ -78,7 +78,6 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
@@ -100,120 +99,13 @@ Author: Dmitriy Yefremov
</child>
</object>
</child>
<child type="title">
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">2</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FTP-transfer</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="header_data_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="options_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Options</property>
<signal name="clicked" handler="on_preferences" swapped="no"/>
<signal name="clicked" handler="on_settings" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
@@ -238,13 +130,156 @@ Author: Dmitriy Yefremov
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="selection_data_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="halign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="active">0</property>
<property name="has_frame">False</property>
<property name="has_entry">True</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
<child internal-child="entry">
<object class="GtkEntry">
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="halign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="editable">False</property>
<property name="has_frame">False</property>
<property name="max_width_chars">9</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="main_settings_box_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
@@ -253,6 +288,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
@@ -319,7 +355,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
@@ -398,7 +434,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">2</property>
</packing>
</child>
<child>
@@ -590,7 +626,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">3</property>
</packing>
</child>
<child>
@@ -629,7 +665,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child>
@@ -685,9 +721,12 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>

View File

@@ -1,18 +1,20 @@
import os
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.connections import download_data, DownloadType, upload_data
from app.settings import Profile
from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import Gtk, UI_RESOURCES_PATH
class DownloadDialog:
def __init__(self, transient, settings, open_data_callback, update_settings_callback):
self._profile = settings.profile
self._s_type = settings.setting_type
self._settings = settings
self._open_data_callback = open_data_callback
self._update_settings_callback = update_settings_callback
@@ -20,11 +22,11 @@ class DownloadDialog:
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings_button": self.on_settings_button,
"on_preferences": self.on_preferences,
"on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed,
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
@@ -35,7 +37,6 @@ class DownloadDialog:
self._message_label = builder.get_object("info_bar_message_label")
self._text_view = builder.get_object("text_view")
self._expander = builder.get_object("expander")
self._host_entry = builder.get_object("host_entry")
self._data_path_entry = builder.get_object("data_path_entry")
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
@@ -52,20 +53,33 @@ class DownloadDialog:
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
self._profile_combo_box = builder.get_object("profile_combo_box")
self.init_settings()
def show(self):
self._dialog_window.show()
def init_settings(self):
self.update_profiles()
self.init_ui_settings()
@run_idle
def init_ui_settings(self):
self._host_entry.set_text(self._settings.host)
self._data_path_entry.set_text(self._settings.data_dir_path)
is_enigma = self._profile is Profile.ENIGMA_2
self._data_path_entry.set_text(self._settings.data_local_path)
is_enigma = self._s_type is SettingsType.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma)
def update_profiles(self):
self._profile_combo_box.remove_all()
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
self._profile_combo_box.set_active_id(self._settings.current_profile)
@run_idle
def on_receive(self, item):
self.download(True, self.get_download_type())
@@ -108,11 +122,11 @@ class DownloadDialog:
self._timeout_entry.set_text("")
self._current_property = label
def on_preferences(self, item):
def on_settings(self, item):
response = show_settings_dialog(self._dialog_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
self._profile = self._settings.profile
self.init_settings()
self._s_type = self._settings.setting_type
self.update_profiles()
gen = self._update_settings_callback()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@@ -121,6 +135,13 @@ class DownloadDialog:
self.on_settings_button(button)
break
def on_profile_changed(self, box):
active = box.get_active_text()
if active in self._settings.profiles:
self._settings.current_profile = active
self._profile_combo_box.set_active_id(active)
self.init_ui_settings()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@@ -134,9 +155,11 @@ class DownloadDialog:
try:
if download:
if backup and d_type is not DownloadType.SATELLITES:
data_path = self._settings.data_dir_path or self._data_path_entry.get_text()
backup_path = self._settings.backup_dir_path or data_path + "backup/"
data_path = self._settings.data_local_path or self._data_path_entry.get_text()
os.makedirs(os.path.dirname(data_path), exist_ok=True)
backup_path = self._settings.backup_local_path or data_path + "backup/"
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
else:
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)

View File

@@ -483,7 +483,7 @@ class EpgDialog:
# ***************** Options *********************#
def init_options(self):
epg_dat_path = self._settings.data_dir_path + "epg/"
epg_dat_path = self._settings.data_local_path + "epg/"
self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._settings.get("epg_options")

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg1541"
version="1.1"
viewBox="0 0 16.743339 16.72816"
height="63.224556"
width="63.281971"
sodipodi:docname="demon-editor.svg"
inkscape:version="0.92.4 (unknown)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="716"
id="namedview127"
showgrid="true"
fit-margin-left="0"
inkscape:zoom="6.1714295"
inkscape:cx="40.088627"
inkscape:cy="31.742631"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1541"
fit-margin-top="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid128"
originx="-10.604603"
originy="-1.1727724" />
</sodipodi:namedview>
<title
id="title1088">DeamonEditor Icons</title>
<defs
id="defs1535">
<linearGradient
id="linearGradient2198">
<stop
id="stop2194"
style="stop-color:#ffb320;stop-opacity:1"
offset="0" />
<stop
id="stop2196"
style="stop-color:#e7ff00;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient2192">
<stop
offset="0"
style="stop-color:#ffb320;stop-opacity:1"
id="stop2188" />
<stop
offset="1"
style="stop-color:#b3c54c;stop-opacity:1"
id="stop2190" />
</linearGradient>
<linearGradient
id="linearGradient3700-8">
<stop
offset="0"
style="stop-color:#2e4f84;stop-opacity:1"
id="stop2183" />
<stop
offset="1"
style="stop-color:#4c77c5;stop-opacity:1"
id="stop2185" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1844">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1826" />
<stop
id="stop1828"
offset="0.13293758"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.21261224"
id="stop1832" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.29780689"
id="stop1834" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.29780689"
id="stop1836" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1838" />
<stop
style="stop-opacity:1;stop-color:#696c76"
offset="0.71871042"
id="stop1840" />
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="1"
id="stop1842" />
</linearGradient>
<linearGradient
id="linearGradient1754"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1736"
offset="0"
style="stop-opacity:1;stop-color:#29282b" />
<stop
style="stop-color:#b5bdcf;stop-opacity:1"
offset="0.02582242"
id="stop1738" />
<stop
id="stop1740"
offset="0.14669065"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1742"
offset="0.21261224"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1744"
offset="0.29780689"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1746"
offset="0.29780689"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1748"
offset="0.45395693"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1750"
offset="0.71871042"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1752"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1606">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1590" />
<stop
id="stop1608"
offset="0.03065561"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#868c95"
offset="0.1125849"
id="stop1592" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.13955873"
id="stop1594" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.1915853"
id="stop1596" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.25400829"
id="stop1598" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1600" />
<stop
style="stop-opacity:1;stop-color:#ffffff"
offset="0.71871042"
id="stop1602" />
<stop
style="stop-opacity:1;stop-color:#1d191a"
offset="1"
id="stop1604" />
</linearGradient>
<linearGradient
id="linearGradient886-0">
<stop
style="stop-color:#535353;stop-opacity:1"
offset="0"
id="stop888" />
<stop
style="stop-color:#f0f0f0;stop-opacity:1"
offset="1"
id="stop890" />
</linearGradient>
<linearGradient
id="linearGradient1473">
<stop
id="stop1469"
offset="0"
style="stop-color:#ffffff;stop-opacity:0" />
<stop
id="stop1471"
offset="1"
style="stop-color:#ffffff;stop-opacity:0.96088022" />
</linearGradient>
<linearGradient
id="linearGradient2447-35">
<stop
id="stop2449"
offset="0"
style="stop-color:#00c62e;stop-opacity:1" />
<stop
id="stop2451"
offset="1"
style="stop-color:#136100;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3795-1">
<stop
id="stop3797-1"
offset="0"
style="stop-color:#803400;stop-opacity:1" />
<stop
id="stop3799-3"
offset="1"
style="stop-color:#c87137;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient2447-3">
<stop
style="stop-color:#c60300;stop-opacity:1"
offset="0"
id="stop2443" />
<stop
style="stop-color:#c40e00;stop-opacity:1"
offset="1"
id="stop2445" />
</linearGradient>
<linearGradient
id="linearGradient2458">
<stop
id="stop2454"
offset="0"
style="stop-color:#c60300;stop-opacity:1" />
<stop
id="stop2456"
offset="1"
style="stop-color:#ee6000;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3519">
<stop
id="stop3521"
offset="0"
style="stop-color:#1d2120;stop-opacity:1" />
<stop
id="stop3523"
offset="1"
style="stop-color:#545d5d;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient1446">
<stop
id="stop1442"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop1444"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient1547"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1529"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1531"
offset="0.0263736"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1533"
offset="0.263736"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1535"
offset="0.395604"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1537"
offset="0.39560401"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1539"
offset="0.42333773"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1541"
offset="0.56268591"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1543"
offset="0.62400264"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1545"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
id="linearGradient1527"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1509"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1511"
offset="0.0527472"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1513"
offset="0.142147"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1515"
offset="0.19700864"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1517"
offset="0.25031137"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1519"
offset="0.3710371"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1521"
offset="0.53961843"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1523"
offset="0.76283824"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1525"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-5.8254634,-78.732754)"
y2="1179.7145"
x2="66.791626"
y1="1188.7661"
x1="99.044022"
gradientUnits="userSpaceOnUse"
id="linearGradient1485"
xlink:href="#linearGradient1473" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-2.2733744,-70.526334)"
gradientUnits="userSpaceOnUse"
y2="1155.8046"
x2="67.311417"
y1="1161.6112"
x1="77.19442"
id="linearGradient1448"
xlink:href="#linearGradient1446" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-18.198747,-79.859424)"
gradientUnits="userSpaceOnUse"
y2="1186.8096"
x2="146.16808"
y1="1186.8096"
x1="131.86871"
id="linearGradient2180"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.20863982,0,0,0.20863982,-0.63935937,-13.031239)"
gradientUnits="userSpaceOnUse"
y2="1184.73"
x2="101.19952"
y1="1184.73"
x1="83.066002"
id="linearGradient2160"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-6.2370714,-102.12414)"
gradientUnits="userSpaceOnUse"
y2="1267.1335"
x2="117.99127"
y1="1267.1335"
x1="75.853806"
id="linearGradient2131"
xlink:href="#linearGradient886-0" />
<linearGradient
id="linearGradient3700-8-3">
<stop
id="stop3702-1"
style="stop-color:#2e4f84;stop-opacity:1"
offset="0" />
<stop
id="stop3704-8"
style="stop-color:#4c77c5;stop-opacity:1"
offset="1" />
</linearGradient>
<defs
id="defs4922">
<filter
style="color-interpolation-filters:sRGB"
id="Adobe_OpacityMaskFilter"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix4925" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="3.785"
y="4.675"
width="5.883"
height="73.013"
id="SVGID_2_">
<g
style="filter:url(#Adobe_OpacityMaskFilter)"
id="g4928">
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="3.7852001"
y1="41.181198"
x2="9.6680002"
y2="41.181198">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop4931" />
<stop
offset="0.0029"
style="stop-color:#FAFBFB"
id="stop4933" />
<stop
offset="0.0756"
style="stop-color:#BBBDBF"
id="stop4935" />
<stop
offset="0.1438"
style="stop-color:#898B8E"
id="stop4937" />
<stop
offset="0.2053"
style="stop-color:#646567"
id="stop4939" />
<stop
offset="0.259"
style="stop-color:#444446"
id="stop4941" />
<stop
offset="0.3028"
style="stop-color:#1D1C1D"
id="stop4943" />
<stop
offset="0.3313"
style="stop-color:#000000"
id="stop4945" />
</linearGradient>
<rect
style="fill:url(#SVGID_3_)"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013"
id="rect4947" />
</g>
</mask>
<filter
style="color-interpolation-filters:sRGB"
id="filter1268"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix1266" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="203.22046"
x2="23.551136"
y1="203.22046"
x1="13.487289"
id="linearGradient1160"
xlink:href="#linearGradient3519" />
<clipPath
id="clipPath1890"
clipPathUnits="userSpaceOnUse">
<circle
style="opacity:1;fill:#2d2d2d;fill-opacity:1;stroke:#434242;stroke-width:0.0575568;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle1892"
cx="78.548424"
cy="-31.019459"
r="6.4721422"
transform="scale(1,-1)" />
</clipPath>
<linearGradient
gradientTransform="translate(-0.24389927,14.877856)"
y2="27.314217"
x2="84.864914"
y1="27.314217"
x1="72.164909"
gradientUnits="userSpaceOnUse"
id="linearGradient2134"
xlink:href="#linearGradient3700-8-3" />
</defs>
<metadata
id="metadata1538">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>DeamonEditor Icons</dc:title>
<dc:publisher>
<cc:Agent>
<dc:title>mfgeg</dc:title>
</cc:Agent>
</dc:publisher>
<dc:date>7.1.2020</dc:date>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.1690805,0,0,1.1690805,-14.929261,-167.45253)"
id="layer1">
<g
transform="translate(0.86526724,-82.691658)"
id="g1351">
<path
d="m 13.715358,227.65981 h 2.97616 v 12.57332 h -2.97616 z m 5.79826,-1.73376 -0.327076,0.42227 c -0.179879,0.23226 -0.586303,0.67932 -0.903223,0.99412 -0.480677,0.47726 -0.640897,0.57235 -0.967777,0.57235 -0.223557,0 -0.380895,-0.0584 -0.624024,-0.25067 v 3.18307 c 0.172884,-0.0214 0.359647,-0.0333 0.575071,-0.0352 0.68403,-0.006 0.91815,0.0416 1.436333,0.29581 1.14586,0.56274 1.762492,1.5589 1.768251,2.8566 0.0094,2.10836 -1.870896,3.55161 -3.779655,3.16529 v 3.10345 c 4.345352,0.0758 4.093104,-2.37537 6.573781,-2.25837 1.486139,0.0748 1.333421,0.16555 1.796225,-1.064 l 0.259834,-0.6913 -0.803704,-0.66493 c -0.960855,-0.79478 -1.303983,-1.29079 -1.205013,-1.74132 0.06631,-0.30189 1.20818,-1.50093 1.779011,-1.86778 0.240181,-0.1543 0.244255,-0.17878 0.09575,-0.59661 -0.08522,-0.23979 -0.259522,-0.65746 -0.386789,-0.92744 l -0.23132,-0.49115 h -1.412666 c -1.868582,0 -1.802386,0.068 -1.805368,-1.82472 l -0.0021,-1.43582 -0.917745,-0.37171 z"
style="opacity:1;fill:#000000;fill-opacity:0.5372549;stroke:none;stroke-width:0.18460207;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
id="path2133"
inkscape:connector-curvature="0" />
<path
id="path2106"
style="opacity:1;fill:url(#linearGradient2131);fill-opacity:1;stroke:none;stroke-width:0.17733108;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 13.832581,227.93117 h 2.858936 v 12.07807 h -2.858936 z m 5.569881,-1.6655 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65257 -0.867647,0.95496 -0.46174,0.45847 -0.615654,0.5498 -0.929658,0.5498 -0.214752,0 -0.365893,-0.056 -0.599446,-0.24079 v 3.05768 c 0.166077,-0.0206 0.345483,-0.0319 0.55242,-0.0338 0.657089,-0.006 0.881987,0.0399 1.379764,0.28416 1.100724,0.54057 1.693073,1.49747 1.698606,2.74408 0.009,2.02535 -1.797211,3.41174 -3.63079,3.04061 v 2.98122 c 4.174205,0.0728 3.931888,-2.28179 6.314859,-2.1694 1.427604,0.0718 1.2809,0.15902 1.725477,-1.02213 l 0.249597,-0.66408 -0.772046,-0.63871 c -0.923009,-0.76345 -1.252622,-1.23996 -1.157552,-1.67277 0.0637,-0.29001 1.160592,-1.44177 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17174 0.09198,-0.57312 -0.08187,-0.23032 -0.249296,-0.63156 -0.371555,-0.89088 l -0.222207,-0.4718 h -1.357022 c -1.794983,0 -1.731396,0.0653 -1.734262,-1.75284 l -0.0021,-1.3793 -0.881603,-0.35705 z"
inkscape:connector-curvature="0" />
<path
id="path2151"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575465,0.0354 v 6.28854 c 1.910653,0.38671 3.792716,-1.05815 3.783336,-3.16865 -0.0058,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482341,1.17002 c 0.433747,-0.004 0.582202,0.0265 0.910784,0.18761 0.726595,0.35682 1.117604,0.98848 1.121256,1.81134 0.0059,1.33694 -1.186343,2.25211 -2.396695,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364655,-0.0225 z"
style="opacity:1;fill:url(#linearGradient2160);fill-opacity:1;stroke:none;stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient2180);stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 17.266981,230.95753 c -0.215636,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28853 c 1.910652,0.38672 3.792715,-1.05815 3.783336,-3.16865 -0.0057,-1.29897 -0.623065,-2.29597 -1.77006,-2.85927 -0.518697,-0.2545 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482343,1.17001 c 0.433747,-0.004 0.5822,0.0265 0.910783,0.18762 0.726594,0.35681 1.117605,0.98848 1.121257,1.81133 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00716 v -3.98359 c 0.109628,-0.0135 0.228055,-0.0214 0.364657,-0.0225 z"
id="path2170"
inkscape:connector-curvature="0" />
<path
id="path1422"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28854 c 1.910652,0.38671 3.792715,-1.05815 3.783335,-3.16865 -0.0057,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482342,1.17002 c 0.433748,-0.004 0.582201,0.0265 0.910784,0.18761 0.726594,0.35682 1.117605,0.98848 1.121257,1.81134 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364656,-0.0225 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1448);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="path1454"
d="m 19.402462,226.26149 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65259 -0.867644,0.95499 -0.461741,0.45847 -0.615657,0.54983 -0.929661,0.54983 -0.214752,0 -0.365893,-0.056 -0.599446,-0.2408 v -0.004 h -2.858741 v 12.0783 h 2.858741 c 4.174205,0.0728 3.931888,-2.28177 6.314861,-2.16937 1.427604,0.0718 1.280898,0.15899 1.725475,-1.02217 l 0.249597,-0.66402 -0.772046,-0.63873 c -0.923009,-0.76346 -1.252622,-1.23997 -1.157552,-1.67278 0.0637,-0.29001 1.160595,-1.44176 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17171 0.09198,-0.57309 -0.08187,-0.23032 -0.249296,-0.63158 -0.371555,-0.8909 l -0.222207,-0.47181 h -1.357025 c -1.794983,0 -1.731393,0.0653 -1.734259,-1.75286 l -0.0021,-1.37925 -0.8816,-0.35708 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1485);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<g
transform="translate(63.49728,-55.136805)"
id="g1174" />
<g
transform="translate(50.48327,11.624037)"
id="g1812" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -6,7 +6,7 @@ from app.eparser import get_bouquets, get_services
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.enigma.bouquets import get_bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import Profile
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
@@ -17,12 +17,12 @@ def import_bouquet(transient, model, path, settings, services, appender):
itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
pattern, f_pattern = None, None
profile = settings.profile
profile = settings.setting_type
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "userbouquet.*{}".format(pattern)
elif profile is Profile.NEUTRINO_MP:
elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
if bq_type is BqType.TV:
@@ -38,7 +38,7 @@ def import_bouquet(transient, model, path, settings, services, appender):
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
@@ -51,7 +51,7 @@ def import_bouquet(transient, model, path, settings, services, appender):
else:
p_itr = model.iter_parent(itr)
appender(bq, p_itr) if p_itr else appender(bq, itr)
elif profile is Profile.NEUTRINO_MP:
elif profile is SettingsType.NEUTRINO_MP:
if bq_type is BqType.WEBTV:
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
@@ -90,7 +90,7 @@ class ImportDialog:
self._services = {}
self._service_ids = service_ids
self._append = appender
self._profile = settings.profile
self._profile = settings.setting_type
self._settings = settings
self._bouquets = bouquets
@@ -125,7 +125,7 @@ class ImportDialog:
self._main_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
# Note! Getting default format ver. 4
services = get_services(path, self._profile, 4 if self._profile is Profile.ENIGMA_2 else 0)
services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0)
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:

View File

@@ -10,7 +10,7 @@ from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
from app.settings import Profile
from app.settings import SettingsType
from app.tools.yt import YouTube, PlayListParser
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
from .main_helper import get_base_model, get_iptv_url, on_popup_menu
@@ -42,7 +42,7 @@ def get_stream_type(box):
class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD):
def __init__(self, transient, view, services, bouquet, profile=SettingsType.ENIGMA_2, action=Action.ADD):
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
@@ -90,7 +90,7 @@ class IptvDialog:
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if profile is Profile.NEUTRINO_MP:
if profile is SettingsType.NEUTRINO_MP:
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
builder.get_object("iptv_type_label").set_visible(False)
builder.get_object("reference_entry").set_visible(False)
@@ -103,7 +103,7 @@ class IptvDialog:
if self._action is Action.ADD:
self._save_button.set_visible(False)
self._add_button.set_visible(True)
if self._profile is Profile.ENIGMA_2:
if self._profile is SettingsType.ENIGMA_2:
self._update_reference_entry()
self._stream_type_combobox.set_active(1)
elif self._action is Action.EDIT:
@@ -128,13 +128,13 @@ class IptvDialog:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.save_enigma2_data() if self._profile is Profile.ENIGMA_2 else self.save_neutrino_data()
self.save_enigma2_data() if self._profile is SettingsType.ENIGMA_2 else self.save_neutrino_data()
self._dialog.destroy()
def init_data(self, srv):
name, fav_id = srv[2], srv[7]
self._name_entry.set_text(name)
self.init_enigma2_data(fav_id) if self._profile is Profile.ENIGMA_2 else self.init_neutrino_data(fav_id)
self.init_enigma2_data(fav_id) if self._profile is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
def init_enigma2_data(self, fav_id):
data, sep, desc = fav_id.partition("#DESCRIPTION")
@@ -171,7 +171,7 @@ class IptvDialog:
self._description_entry.set_text(data[1])
def _update_reference_entry(self):
if self._profile is Profile.ENIGMA_2:
if self._profile is SettingsType.ENIGMA_2:
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
@@ -486,7 +486,7 @@ class IptvListConfigurationDialog:
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if self._profile is Profile.ENIGMA_2:
if self._profile is SettingsType.ENIGMA_2:
reset = self._reset_to_default_switch.get_active()
type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active()

View File

@@ -1,7 +1,7 @@
import os
import sys
from contextlib import suppress
from datetime import datetime
from functools import lru_cache
from itertools import chain
@@ -9,33 +9,33 @@ from gi.repository import GLib, Gio
from app.commons import run_idle, log, run_task, run_with_delay, init_logger
from app.connections import HttpAPI, HttpRequestType, download_data, DownloadType, upload_data, test_http, \
TestException
TestException, HttpApiException
from app.eparser import get_blacklist, write_blacklist, parse_m3u
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
from app.eparser.ecommons import CAS, Flag, BouquetService
from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.iptv import export_to_m3u
from app.eparser.neutrino.bouquets import BqType
from app.settings import Profile, Settings
from app.settings import SettingsType, Settings, SettingsException
from app.tools.media import Player
from app.ui.epg_dialog import EpgDialog
from app.ui.transmitter import LinksTransmitter
from .backup import BackupDialog, backup_data, clear_data_path
from .imports import ImportDialog, import_bouquet
from .download_dialog import DownloadDialog
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \
FavClickMode
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message
from .download_dialog import DownloadDialog
from .imports import ImportDialog, import_bouquet
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog
from .main_helper import insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services, \
scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picon, remove_picon, \
is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, get_selection, get_model_data, \
remove_all_unused_picons
from .picons_downloader import PiconsDialog
from .satellites_dialog import show_satellites_dialog
from .settings_dialog import show_settings_dialog
from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action
from .settings_dialog import show_settings_dialog
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \
FavClickMode
class Application(Gtk.Application):
@@ -152,8 +152,8 @@ class Application(Gtk.Application):
"on_create_bouquet_for_each_type": self.on_create_bouquet_for_each_type}
self._settings = Settings.get_instance()
self._profile = self._settings.profile
os.makedirs(os.path.dirname(self._settings.data_dir_path), exist_ok=True)
self._s_type = self._settings.setting_type
os.makedirs(os.path.dirname(self._settings.data_local_path), exist_ok=True)
# Used for copy/paste. When adding the previous data will not be deleted.
# Clearing only after the insertion!
self._rows_buffer = []
@@ -182,7 +182,6 @@ class Application(Gtk.Application):
self._EXTRA_COLOR = None # Color for services with a extra name for the bouquet
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "main_window.glade")
builder.connect_signals(self._handlers)
self._main_window = builder.get_object("main_window")
@@ -210,8 +209,7 @@ class Application(Gtk.Application):
self._app_info_box.bind_property("visible", builder.get_object("main_paned"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("toolbar_extra_item"), "visible", 4)
# Status bar
self._ip_label = builder.get_object("ip_label")
self._ip_label.set_text(self._settings.host)
self._profile_combo_box = builder.get_object("profile_combo_box")
self._receiver_info_box = builder.get_object("receiver_info_box")
self._receiver_info_label = builder.get_object("receiver_info_label")
self._signal_box = builder.get_object("signal_box")
@@ -226,6 +224,7 @@ class Application(Gtk.Application):
self._radio_count_label = builder.get_object("radio_count_label")
self._data_count_label = builder.get_object("data_count_label")
self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4)
self._receiver_info_box.bind_property("visible", builder.get_object("signal_box"), "visible")
# Force ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!!
self._services_view.connect("key-press-event", self.force_ctrl)
self._fav_view.connect("key-press-event", self.force_ctrl)
@@ -323,7 +322,15 @@ class Application(Gtk.Application):
self.update_profile_label()
self.init_drag_and_drop()
self.init_colors()
self.init_http_api()
if self._settings.load_last_config:
config = self._settings.get("last_config") or {}
self.init_profiles(config.get("last_profile", None))
last_bouquet = config.get("last_bouquet", None)
self.open_data(callback=lambda: self.open_bouquet(last_bouquet))
else:
self.init_profiles()
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def do_activate(self):
self._main_window.set_application(self)
@@ -332,9 +339,17 @@ class Application(Gtk.Application):
def do_shutdown(self):
""" Performs shutdown tasks """
self._settings.save() # storing current config
if self._settings.load_last_config:
self._settings.add("last_config", {"last_profile": self._settings.current_profile,
"last_bouquet": self._current_bq_name})
self._settings.save() # storing current settings
if self._player:
self._player.release()
if self._http_api:
self._http_api.close()
Gtk.Application.do_shutdown(self)
def do_command_line(self, command_line):
@@ -347,6 +362,12 @@ class Application(Gtk.Application):
self.activate()
return 0
def init_profiles(self, profile=None):
self.update_profiles()
self._profile_combo_box.set_active_id(profile if profile else self._settings.default_profile)
if profile:
self.set_profile(profile)
def init_drag_and_drop(self):
""" Enable drag-and-drop """
target = []
@@ -374,7 +395,7 @@ class Application(Gtk.Application):
If update=False - first call on program start, else - after options changes!
"""
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
if self._settings.use_colors:
new_rgb = Gdk.RGBA()
extra_rgb = Gdk.RGBA()
@@ -410,8 +431,6 @@ class Application(Gtk.Application):
@run_idle
def on_close_app(self, *args):
if self._http_api:
self._http_api.close()
self.quit()
def on_resize(self, window):
@@ -605,7 +624,7 @@ class Application(Gtk.Application):
# ***************** ####### *********************#
def get_bouquet_file_name(self, bouquet):
bouquet_file_name = "{}userbouquet.{}.{}".format(self._settings.get(self._profile).get("data_dir_path"),
bouquet_file_name = "{}userbouquet.{}.{}".format(self._settings.get(self._s_type).get("data_dir_path"),
*bouquet.split(":"))
return bouquet_file_name
@@ -839,7 +858,7 @@ class Application(Gtk.Application):
DownloadDialog(transient=self._main_window,
settings=self._settings,
open_data_callback=self.open_data,
update_settings_callback=self.update_options).show()
update_settings_callback=self.update_settings).show()
@run_task
def on_download_data(self):
@@ -855,15 +874,15 @@ class Application(Gtk.Application):
@run_task
def on_upload_data(self, download_type):
try:
profile = self._profile
profile = self._s_type
opts = self._settings
use_http = profile is Profile.ENIGMA_2
use_http = profile is SettingsType.ENIGMA_2
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
host, port, user, password = opts.host, opts.http_port, opts.http_user, opts.http_password
try:
test_http(host, port, user, password, skip_message=True)
except TestException:
test_http(host, port, user, password, use_ssl=opts.http_use_ssl, skip_message=True)
except (TestException, HttpApiException):
use_http = False
upload_data(settings=opts,
@@ -880,26 +899,27 @@ class Application(Gtk.Application):
return
self.open_data(response)
def open_data(self, data_path=None):
def open_data(self, data_path=None, callback=None):
""" Opening data and fill views. """
gen = self.update_data(data_path)
gen = self.update_data(data_path, callback)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_DEFAULT_IDLE)
def update_data(self, data_path):
def update_data(self, data_path, callback=None):
self._profile_combo_box.set_sensitive(False)
self._wait_dialog.show()
yield True
data_path = self._settings.data_dir_path if data_path is None else data_path
data_path = self._settings.data_local_path if data_path is None else data_path
yield from self.clear_current_data()
try:
prf = self._profile
prf = self._s_type
black_list = get_blacklist(data_path)
bouquets = get_bouquets(data_path, prf)
yield True
services = get_services(data_path, prf, self.get_format_version() if prf is Profile.ENIGMA_2 else 0)
services = get_services(data_path, prf, self.get_format_version() if prf is SettingsType.ENIGMA_2 else 0)
yield True
update_picons_data(self._settings.picons_dir_path, self._picons)
update_picons_data(self._settings.picons_local_path, self._picons)
yield True
except FileNotFoundError as e:
msg = get_message("Please, download files from receiver or setup your path for read data!")
@@ -917,6 +937,9 @@ class Application(Gtk.Application):
yield from self.append_data(bouquets, services)
finally:
self._wait_dialog.hide()
self._profile_combo_box.set_sensitive(True)
if callback:
callback()
yield True
def append_data(self, bouquets, services):
@@ -981,6 +1004,17 @@ class Application(Gtk.Application):
if extra_services:
self._extra_bouquets[bq_id] = extra_services
@run_idle
def open_bouquet(self, name):
""" Find and open bouquet by name """
for r in self._bouquets_model:
for i in r.iterchildren():
if i[Column.BQ_NAME] == name:
self._bouquets_view.expand_row(self._bouquets_model.get_path(r.iter), Column.BQ_NAME)
self._bouquets_view.set_cursor(i.path)
self._bouquets_view.row_activated(i.path, self._bouquets_view.get_column(Column.BQ_NAME))
break
def append_services(self, services):
for srv in services:
# adding channels to dict with fav_id as keys
@@ -1019,6 +1053,7 @@ class Application(Gtk.Application):
self._blacklist.clear()
self._services.clear()
self._rows_buffer.clear()
self._picons.clear()
self._bouquets.clear()
self._extra_bouquets.clear()
self._current_bq_name = None
@@ -1039,9 +1074,9 @@ class Application(Gtk.Application):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def save_data(self):
profile = self._profile
path = self._settings.data_dir_path
backup_path = self._settings.backup_dir_path
profile = self._s_type
path = self._settings.data_local_path
backup_path = self._settings.backup_local_path
# Backup data or clearing data path
backup_data(path, backup_path) if self._settings.backup_before_save else clear_data_path(path)
yield True
@@ -1061,7 +1096,7 @@ class Application(Gtk.Application):
favs = self._bouquets[bq_id]
ex_s = self._extra_bouquets.get(bq_id)
bq_s = list(filter(None, [self._services.get(f_id, None) for f_id in favs]))
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
bq_s = list(map(lambda s: s._replace(service=ex_s.get(s.fav_id, None) if ex_s else None), bq_s))
bq = Bouquet(bq_name, bq_type, bq_s, locked, hidden)
bqs.append(bq)
@@ -1075,10 +1110,10 @@ class Application(Gtk.Application):
# Getting services
services_model = get_base_model(self._services_view.get_model())
services = [Service(*row[: Column.SRV_TOOLTIP]) for row in services_model]
write_services(path, services, profile, self.get_format_version() if profile is Profile.ENIGMA_2 else 0)
write_services(path, services, profile, self.get_format_version() if profile is SettingsType.ENIGMA_2 else 0)
yield True
# removing bouquet files
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
# blacklist
write_blacklist(path, self._blacklist)
yield True
@@ -1088,7 +1123,7 @@ class Application(Gtk.Application):
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
return
gen = self.create_new_configuration(self._profile)
gen = self.create_new_configuration(self._s_type)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def create_new_configuration(self, profile):
@@ -1098,12 +1133,12 @@ class Application(Gtk.Application):
c_gen = self.clear_current_data()
yield from c_gen
if profile is Profile.ENIGMA_2:
if profile is SettingsType.ENIGMA_2:
parent = self._bouquets_model.append(None, ["Favourites (TV)", None, None, BqType.TV.value])
self.append_bouquet(Bouquet("Favourites (TV)", BqType.TV.value, [], None, None), parent)
parent = self._bouquets_model.append(None, ["Favourites (Radio)", None, None, BqType.RADIO.value])
self.append_bouquet(Bouquet("Favourites (Radio)", BqType.RADIO.value, [], None, None), parent)
elif profile is Profile.NEUTRINO_MP:
elif profile is SettingsType.NEUTRINO_MP:
self._bouquets_model.append(None, ["Providers", None, None, BqType.BOUQUET.value])
self._bouquets_model.append(None, ["FAV", None, None, BqType.TV.value])
self._bouquets_model.append(None, ["WEBTV", None, None, BqType.WEBTV.value])
@@ -1171,7 +1206,7 @@ class Application(Gtk.Application):
self.show_error_dialog("Error. No bouquet is selected!")
return
if self._profile is Profile.NEUTRINO_MP and self._bq_selected.endswith(BqType.WEBTV.value):
if self._s_type is SettingsType.NEUTRINO_MP and self._bq_selected.endswith(BqType.WEBTV.value):
self.show_error_dialog("Operation not allowed in this context!")
return
@@ -1196,25 +1231,49 @@ class Application(Gtk.Application):
for v in [view, *args]:
v.get_selection().unselect_all()
def on_preferences(self, action, value):
def on_settings(self, action, value):
response = show_settings_dialog(self._main_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
gen = self.update_options()
gen = self.update_settings()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_options(self):
profile = self._settings.profile
self._ip_label.set_text(self._settings.host)
if profile != self._profile:
def update_settings(self):
s_type = self._settings.setting_type
if s_type != self._s_type:
yield from self.show_app_info(True)
self._profile = profile
self._s_type = s_type
c_gen = self.clear_current_data()
yield from c_gen
self.update_profile_label()
self.init_colors(True)
self.init_profiles()
yield True
self.init_http_api()
yield True
gen = self.init_http_api()
yield from gen
def on_profile_changed(self, entry):
if self._app_info_box.get_visible():
self.update_profile_label()
return
active = self._profile_combo_box.get_active_text()
if active in self._settings.profiles:
self.set_profile(active)
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
self.open_data()
def set_profile(self, active):
self._settings.current_profile = active
self._s_type = self._settings.setting_type
self._profile_combo_box.set_tooltip_text(self._profile_combo_box.get_tooltip_text() + self._settings.host)
self.update_profile_label()
def update_profiles(self):
self._profile_combo_box.remove_all()
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
def on_tree_view_key_press(self, view, event):
""" Handling keystrokes on press """
@@ -1312,7 +1371,7 @@ class Application(Gtk.Application):
self._tool_elements[elem].set_sensitive(not_empty)
if elem == "bouquets_paste_popup_item":
self._tool_elements[elem].set_sensitive(not_empty and self._bouquets_buffer)
if self._profile is Profile.NEUTRINO_MP:
if self._s_type is SettingsType.NEUTRINO_MP:
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
else:
@@ -1328,17 +1387,17 @@ class Application(Gtk.Application):
for elem in self._BOUQUET_ELEMENTS:
self._tool_elements[elem].set_sensitive(False)
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty and self._profile is Profile.ENIGMA_2)
self._tool_elements[elem].set_sensitive(not_empty and self._s_type is SettingsType.ENIGMA_2)
for elem in self._FAV_IPTV_ELEMENTS:
is_iptv = self._bq_selected and not is_service
if self._profile is Profile.NEUTRINO_MP:
if self._s_type is SettingsType.NEUTRINO_MP:
is_iptv = is_iptv and BqType(self._bq_selected.split(":")[1]) is BqType.WEBTV
self._tool_elements[elem].set_sensitive(is_iptv)
for elem in self._COMMONS_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
if self._profile is not Profile.ENIGMA_2:
if self._s_type is not SettingsType.ENIGMA_2:
for elem in self._FAV_ENIGMA_ELEMENTS:
self._tool_elements[elem].set_sensitive(False)
@@ -1349,9 +1408,9 @@ class Application(Gtk.Application):
self.set_service_flags(Flag.LOCK)
def set_service_flags(self, flag):
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
set_flags(flag, self._services_view, self._fav_view, self._services, self._blacklist)
elif self._profile is Profile.NEUTRINO_MP and self._bq_selected:
elif self._s_type is SettingsType.NEUTRINO_MP and self._bq_selected:
model, paths = self._bouquets_view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
value = model.get_value(itr, 1 if flag is Flag.LOCK else 2)
@@ -1400,10 +1459,12 @@ class Application(Gtk.Application):
return
elif self._fav_click_mode is FavClickMode.STREAM:
self.on_play_stream()
elif self._fav_click_mode is FavClickMode.PLAY:
elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
self.on_zap(self.on_watch)
elif self._fav_click_mode is FavClickMode.ZAP:
self.on_zap()
elif self._fav_click_mode is FavClickMode.PLAY:
self.on_stream()
else:
return self.on_view_popup_menu(menu, event)
@@ -1414,14 +1475,14 @@ class Application(Gtk.Application):
self._fav_view,
self._services,
self._bouquets.get(self._bq_selected, None),
self._profile,
self._s_type,
Action.ADD).show()
if response != Gtk.ResponseType.CANCEL:
self.update_fav_num_column(self._fav_model)
@run_idle
def on_iptv_list_configuration(self, action, value):
if self._profile is Profile.NEUTRINO_MP:
if self._s_type is SettingsType.NEUTRINO_MP:
self.show_error_dialog("Neutrino at the moment not supported!")
return
@@ -1435,7 +1496,7 @@ class Application(Gtk.Application):
bq = self._bouquets.get(self._bq_selected, [])
IptvListConfigurationDialog(self._main_window, self._services, iptv_rows, bq,
self._fav_model, self._profile).show()
self._fav_model, self._s_type).show()
@run_idle
def on_remove_all_unavailable(self, action, value=None):
@@ -1451,7 +1512,7 @@ class Application(Gtk.Application):
return
fav_bqt = self._bouquets.get(self._bq_selected, None)
response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._profile).show()
response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._s_type).show()
if response:
next(self.remove_favs(response, self._fav_model), False)
@@ -1459,7 +1520,7 @@ class Application(Gtk.Application):
@run_idle
def on_epg_list_configuration(self, action, value=None):
if self._profile is not Profile.ENIGMA_2:
if self._s_type is not SettingsType.ENIGMA_2:
self.show_error_dialog("Only Enigma2 is supported!")
return
@@ -1477,7 +1538,7 @@ class Application(Gtk.Application):
if not self._bq_selected:
return
YtListImportDialog(self._main_window, self._profile, self.append_imported_services).show()
YtListImportDialog(self._main_window, self._s_type, self.append_imported_services).show()
def on_import_m3u(self, action, value=None):
""" Imports iptv from m3u files. """
@@ -1489,7 +1550,7 @@ class Application(Gtk.Application):
self.show_error_dialog("No m3u file is selected!")
return
channels = parse_m3u(response, self._profile)
channels = parse_m3u(response, self._s_type)
if channels and self._bq_selected:
self.append_imported_services(channels)
@@ -1520,7 +1581,7 @@ class Application(Gtk.Application):
try:
bq = Bouquet(self._current_bq_name, None, bq_services, None, None)
export_to_m3u(response, bq, self._profile)
export_to_m3u(response, bq, self._s_type)
except Exception as e:
self.show_error_dialog(str(e))
else:
@@ -1532,7 +1593,7 @@ class Application(Gtk.Application):
self.show_error_dialog("No selected item!")
return
appender = self.append_bouquet if self._profile is Profile.ENIGMA_2 else self.append_bouquets
appender = self.append_bouquet if self._s_type is SettingsType.ENIGMA_2 else self.append_bouquets
import_bouquet(self._main_window, model, paths[0], self._settings, self._services, appender)
def on_import_bouquets(self, action, value=None):
@@ -1573,7 +1634,7 @@ class Application(Gtk.Application):
self.show_error_dialog("Not allowed in this context!")
return
url = get_iptv_url(row, self._profile)
url = get_iptv_url(row, self._s_type)
self.update_player_buttons()
if not url:
return
@@ -1583,7 +1644,9 @@ class Application(Gtk.Application):
if not self._player:
try:
self._player = Player.get_instance(rewind_callback=self.on_player_duration_changed,
position_callback=self.on_player_time_changed)
position_callback=self.on_player_time_changed,
error_callback=self.on_player_error,
playing_callback=self.set_playback_elms_active)
except (ImportError, NameError, AttributeError):
self.show_error_dialog("No VLC is found. Check that it is installed!")
return
@@ -1594,6 +1657,7 @@ class Application(Gtk.Application):
self._player_box.set_size_request(w * 0.6, -1)
self._player_box.set_visible(True)
self._fav_view.set_sensitive(False)
GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW)
def on_player_stop(self, item=None):
@@ -1602,10 +1666,19 @@ class Application(Gtk.Application):
def on_player_previous(self, item):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
self.on_play_stream()
self.set_player_action()
def on_player_next(self, item):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.set_player_action()
@run_with_delay(1)
def set_player_action(self):
if self._fav_click_mode is FavClickMode.PLAY:
self.on_stream()
elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
self.on_zap(self.on_watch)
elif self._fav_click_mode is FavClickMode.STREAM:
self.on_play_stream()
def on_player_rewind(self, scale, scroll_type, value):
@@ -1621,21 +1694,32 @@ class Application(Gtk.Application):
def on_player_close(self, item=None):
if self._player:
self._player.stop()
self.set_playback_elms_active()
GLib.idle_add(self._player_box.set_visible, False, priority=GLib.PRIORITY_LOW)
@lru_cache(maxsize=1)
def on_player_duration_changed(self, duration):
self._player_scale.set_value(0)
self._player_scale.get_adjustment().set_upper(duration)
GLib.idle_add(self._player_rewind_box.set_visible, duration > 0)
GLib.idle_add(self._player_current_time_label.set_text, "0")
GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration))
GLib.idle_add(self._player_rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._player_current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration), priority=GLib.PRIORITY_LOW)
def on_player_time_changed(self, t):
if not self._full_screen and self._player_rewind_box.get_visible():
GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t),
priority=GLib.PRIORITY_LOW)
def on_player_error(self):
self.set_playback_elms_active()
self.show_error_dialog("Can't Playback!")
@run_idle
def set_playback_elms_active(self):
self._fav_view.set_sensitive(True)
self._fav_view.do_grab_focus(self._fav_view)
def get_time_str(self, duration):
""" returns a string representation of time from duration in milliseconds """
m, s = divmod(duration // 1000, 60)
@@ -1684,30 +1768,35 @@ class Application(Gtk.Application):
if not state & Gdk.WindowState.ICONIFIED and self._links_transmitter:
self._links_transmitter.hide()
# ************************ HTTP API ****************************#
# ************************ HTTP API **************************** #
@run_task
def init_http_api(self):
self._fav_click_mode = FavClickMode(self._settings.fav_click_mode)
http_api_enable = self._settings.http_api_support
status = all((http_api_enable, self._profile is Profile.ENIGMA_2, not self._receiver_info_box.get_visible()))
GLib.idle_add(self._http_status_image.set_visible, status)
st = all((http_api_enable, self._s_type is SettingsType.ENIGMA_2, not self._receiver_info_box.get_visible()))
GLib.idle_add(self._http_status_image.set_visible, st)
if self._profile is Profile.NEUTRINO_MP or not http_api_enable:
self.update_info_boxes_visible(False)
if self._s_type is SettingsType.NEUTRINO_MP or not http_api_enable:
GLib.idle_add(self._receiver_info_box.set_visible, False)
if self._http_api:
self._http_api.close()
yield True
self._http_api = None
self.init_send_to(False)
return
if not self._http_api:
self._http_api = HttpAPI(self._settings.host, self._settings.http_port,
self._settings.http_user, self._settings.http_password)
current_profile = self._profile_combo_box.get_active_text()
if current_profile in self._settings.profiles:
self._settings.current_profile = current_profile
if not self._http_api:
self._http_api = HttpAPI(self._settings)
GLib.timeout_add_seconds(3, self.update_info, priority=GLib.PRIORITY_LOW)
else:
self._http_api.init()
self.init_send_to(http_api_enable and self._settings.enable_send_to)
yield True
@run_idle
def init_send_to(self, enable):
@@ -1716,9 +1805,23 @@ class Application(Gtk.Application):
elif self._links_transmitter:
self._links_transmitter.show(enable)
def on_stream(self, item=None):
path, column = self._fav_view.get_cursor()
if not path or not self._http_api:
return
ref = self.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self._player.stop()
self._http_api.send(HttpRequestType.STREAM, ref, self.watch)
def on_watch(self, item=None):
""" Switch to the channel and watch in the player """
self._http_api.send(HttpRequestType.STREAM, None, self.watch)
self._http_api.send(HttpRequestType.STREAM_CURRENT, None, self.watch)
def watch(self, m3u):
if m3u:
@@ -1733,67 +1836,94 @@ class Application(Gtk.Application):
if not path or not self._http_api:
return
ref = self.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self._player.stop()
def zap(rq):
if rq and rq.get("e2state", False):
GLib.idle_add(scroll_to, path, self._fav_view)
if callback:
callback()
self._http_api.send(HttpRequestType.ZAP, ref, zap)
def get_service_ref(self, path):
row = self._fav_model[path][:]
srv = self._services.get(row[Column.FAV_ID], None)
srv_type, fav_id = row[Column.FAV_TYPE], row[Column.FAV_ID]
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
self.show_error_dialog("Not allowed in this context!")
return
srv = self._services.get(fav_id, None)
if srv and srv.transponder:
ref = srv.picon_id.rstrip(".png").replace("_", ":")
def zap(rq):
if rq and rq.get("result", False):
GLib.idle_add(scroll_to, path, self._fav_view)
if callback is not None:
callback()
self._http_api.send(HttpRequestType.ZAP, ref, zap)
return srv.picon_id.rstrip(".png").replace("_", ":")
def update_info(self):
""" Updating current info over HTTP API """
if not self._http_api:
if not self._http_api or self._s_type is SettingsType.NEUTRINO_MP:
GLib.idle_add(self._http_status_image.set_visible, False)
GLib.idle_add(self._receiver_info_box.set_visible, False)
return False
self._http_api.send(HttpRequestType.INFO, None, self.update_receiver_info)
self._http_api.send(HttpRequestType.INFO, None, self.update_service_info)
return True
def update_receiver_info(self, info):
res_info = info.get("info", None) if info else None
if res_info:
image = res_info.get("friendlyimagedistro", "")
image_ver = res_info.get("imagever", "")
brand = res_info.get("brand", "")
model = res_info.get("model", "")
info_text = "{} {} Image: {} {}".format(brand, model, image, image_ver)
GLib.idle_add(self._receiver_info_label.set_text, info_text)
GLib.idle_add(self._receiver_info_box.set_visible, bool(res_info))
error_code = info.get("error_code", 0) if info else 0
GLib.idle_add(self._receiver_info_box.set_visible, error_code == 0, priority=GLib.PRIORITY_LOW)
def update_service_info(self, info):
service_info = info.get("service", None) if info else None
if service_info:
GLib.idle_add(self._service_name_label.set_text, service_info.get("name", ""))
if service_info.get("onid", None) and self._http_api:
self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal)
self._http_api.send(HttpRequestType.STATUS, None, self.update_status)
GLib.idle_add(self._signal_box.set_visible, bool(service_info))
if error_code < 0:
return
elif error_code == 412:
self._http_api.init()
return
res_info = info.get("e2about", None) if info else None
if res_info:
image = info.get("e2distroversion", "")
image_ver = info.get("e2imageversion", "")
model = info.get("e2model", "")
info_text = "{} Image: {} {}".format(model, image, image_ver)
GLib.idle_add(self._receiver_info_label.set_text, info_text, priority=GLib.PRIORITY_LOW)
service_name = info.get("e2servicename", None) or ""
GLib.idle_add(self._service_name_label.set_text, service_name, priority=GLib.PRIORITY_LOW)
if service_name:
self.update_service_info()
def update_service_info(self):
if self._http_api:
self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal)
self._http_api.send(HttpRequestType.CURRENT, None, self.update_status)
def update_signal(self, sig):
self.set_signal(sig.get("snr", 0) if sig else 0)
self.set_signal(sig.get("e2snr", "0 %") if sig else "0 %")
@lru_cache(maxsize=2)
def set_signal(self, val):
self._signal_level_bar.set_value(val if isinstance(val, int) else 0)
self._signal_level_bar.set_visible(val)
val = val.strip().rstrip("%") or 0
with suppress(ValueError):
self._signal_level_bar.set_value(int(val))
GLib.idle_add(self._signal_level_bar.set_visible, val != "N/A")
def update_status(self, status):
if status:
dsc = "{} {} - {}".format(status.get("currservice_name", ""),
status.get("currservice_begin", ""),
status.get("currservice_end", ""))
@run_idle
def update_status(self, evn):
if evn:
s_duration = evn.get("e2eventstart", 0)
self._service_epg_label.set_visible(bool(s_duration))
if not s_duration:
return
s_duration = int(s_duration)
s_time = datetime.fromtimestamp(s_duration)
end_time = datetime.fromtimestamp(s_duration + int(evn.get("e2eventduration", "0") or "0"))
title = evn.get("e2eventtitle", "")
dsc = "{} {}:{} - {}:{}".format(title, s_time.hour, s_time.minute, end_time.hour, end_time.minute)
self._service_epg_label.set_text(dsc)
self._service_epg_label.set_tooltip_text(status.get("currservice_description", ""))
self._service_epg_label.set_tooltip_text(evn.get("e2eventdescription", ""))
# ***************** Filter and search *********************#
@@ -1817,7 +1947,7 @@ class Application(Gtk.Application):
self._sat_positions.clear()
sat_positions = set()
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
terrestrial = False
cable = False
@@ -1834,7 +1964,7 @@ class Application(Gtk.Application):
self._sat_positions.append("T")
if cable:
self._sat_positions.append("C")
elif self._profile is Profile.NEUTRINO_MP:
elif self._s_type is SettingsType.NEUTRINO_MP:
list(map(lambda s: sat_positions.add(float(s.pos)), filter(lambda s: s.pos, self._services.values())))
self._sat_positions.extend(map(str, sorted(sat_positions)))
@@ -1913,7 +2043,7 @@ class Application(Gtk.Application):
self._fav_view,
self._services,
self._bouquets.get(self._bq_selected, None),
self._profile,
self._s_type,
Action.EDIT).show()
self.on_locate_in_services(view)
@@ -2031,7 +2161,7 @@ class Application(Gtk.Application):
@run_idle
def on_picons_loader_show(self, action, value):
ids = {}
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
for r in self._services_model:
data = r[Column.SRV_PICON_ID].split("_")
ids["{}:{}:{}".format(data[3], data[5], data[6])] = r[Column.SRV_PICON_ID]
@@ -2041,7 +2171,7 @@ class Application(Gtk.Application):
@run_task
def update_picons(self):
update_picons_data(self._settings.picons_dir_path, self._picons)
update_picons_data(self._settings.picons_local_path, self._picons)
append_picons(self._picons, self._services_model)
def on_assign_picon(self, view):
@@ -2094,33 +2224,40 @@ class Application(Gtk.Application):
def create_bouquets(self, g_type):
gen_bouquets(self._services_view, self._bouquets_view, self._main_window, g_type, self._TV_TYPES,
self._profile, self.append_bouquet)
self._s_type, self.append_bouquet)
# ***************** Profile label *********************#
@run_idle
def update_profile_label(self):
if self._profile is Profile.ENIGMA_2:
ver = self.get_format_version()
self._main_window.set_title("DemonEditor [{} Enigma2 v.{}]".format(get_message("Profile:"), ver))
elif self._profile is Profile.NEUTRINO_MP:
self._main_window.set_title("DemonEditor [{} Neutrino-MP]".format(get_message("Profile:")))
label, sep, ip = self._profile_combo_box.get_tooltip_text().partition(":")
profile_name = self._profile_combo_box.get_active_text()
self._profile_combo_box.set_tooltip_text("{}: {}".format(label, self._settings.host))
msg = get_message("Profile:")
if self._s_type is SettingsType.ENIGMA_2:
self._main_window.set_title.set_subtitle("DemonEditor [{} {} Enigma2 v.{}]".format(msg, profile_name, self.get_format_version()))
elif self._s_type is SettingsType.NEUTRINO_MP:
self._main_window.set_title("DemonEditor [{} {} Neutrino-MP]".format(msg, profile_name))
def get_format_version(self):
return 5 if self._settings.v5_support else 4
@run_idle
def update_info_boxes_visible(self, visible):
self._signal_box.set_visible(visible)
self._receiver_info_box.set_visible(visible)
@run_idle
def show_error_dialog(self, message):
show_dialog(DialogType.ERROR, self._main_window, message)
def start_app():
app = Application()
app.run(sys.argv)
try:
Settings.get_instance()
except SettingsException as e:
msg = "{} \n{}".format(e, "All setting were reset. Restart the program!")
show_dialog(DialogType.INFO, transient=Gtk.Dialog(), text=msg)
Settings.reset_to_default()
else:
app = Application()
app.run(sys.argv)
if __name__ == "__main__":

View File

@@ -9,9 +9,9 @@ from app.commons import run_task
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.settings import Profile
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
# ***************** Markers *******************#
@@ -347,7 +347,9 @@ def update_picons_data(path, picons):
return
for file in os.listdir(path):
picons[file] = get_picon_pixbuf(path + file)
pf = get_picon_pixbuf(path + file)
if pf:
picons[file] = pf
def append_picons(picons, model):
@@ -382,7 +384,7 @@ def assign_picon(target, srv_view, fav_view, transient, picons, settings, servic
if picon_id:
if os.path.isfile(response):
picons_path = settings.picons_dir_path
picons_path = settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id
shutil.copy(response, picon_file)
@@ -464,8 +466,8 @@ def remove_all_unused_picons(settings, picons, services):
def remove_picons(settings, picon_ids, picons):
pions_path = settings.picons_dir_path
backup_path = settings.backup_dir_path + "picons/"
pions_path = settings.picons_local_path
backup_path = settings.backup_local_path + "picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
@@ -487,12 +489,15 @@ def is_only_one_item_selected(paths, transient):
def get_picon_pixbuf(path):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
except GLib.GError as e:
pass
# ***************** Bouquets *********************#
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback):
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
@@ -502,7 +507,7 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback
index = Column.SRV_POS
model, paths = view.get_selection().get_selected_rows()
bq_type = BqType.BOUQUET.value if profile is Profile.NEUTRINO_MP else BqType.TV.value
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE):
if not is_only_one_item_selected(paths, transient):
return
@@ -511,17 +516,17 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback
bq_type = BqType.RADIO.value
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], profile)
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
else:
wait_dialog = WaitDialog(transient)
wait_dialog.show()
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
{row[index] for row in model}, profile, wait_dialog)
{row[index] for row in model}, s_type, wait_dialog)
@run_task
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, profile, wait_dialog=None):
bq_index = 0 if profile is Profile.ENIGMA_2 else 1
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None):
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
bqs_model = bq_view.get_model()
bouquets_names = get_bouquets_names(bqs_model)
@@ -583,14 +588,14 @@ def append_text_to_tview(char, view):
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
def get_iptv_url(row, profile):
def get_iptv_url(row, s_type):
""" Returns url from iptv type row """
data = row[Column.FAV_ID].split(":" if profile is Profile.ENIGMA_2 else "::")
if profile is Profile.ENIGMA_2:
data = row[Column.FAV_ID].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
if s_type is SettingsType.ENIGMA_2:
data = list(filter(lambda x: "http" in x, data))
if data:
url = data[0]
return urllib.request.unquote(url) if profile is Profile.ENIGMA_2 else url
return urllib.request.unquote(url) if s_type is SettingsType.ENIGMA_2 else url
def on_popup_menu(menu, event):

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2019 Dmitriy Yefremov
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,7 @@ THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
@@ -674,9 +674,10 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkApplicationWindow" id="main_window">
<property name="width_request">640</property>
<property name="height_request">480</property>
<property name="can_focus">False</property>
<property name="window_position">center</property>
<property name="icon_name">accessories-text-editor</property>
<property name="icon_name">demon-editor</property>
<property name="gravity">center</property>
<property name="startup_id">DemonEditor</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
@@ -963,6 +964,9 @@ Author: Dmitriy Yefremov
<object class="GtkScale" id="player_scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="adjustment">player_scale_adjustment</property>
<property name="restrict_to_fill_level">False</property>
<property name="fill_level">0</property>
@@ -2207,7 +2211,6 @@ Author: Dmitriy Yefremov
<object class="GtkSeparator" id="bottom_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">2</property>
</object>
<packing>
<property name="expand">False</property>
@@ -2240,7 +2243,7 @@ Author: Dmitriy Yefremov
<object class="GtkImage" id="app_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-edit</property>
<property name="icon_name">demon-editor</property>
<property name="icon_size">6</property>
</object>
<packing>
@@ -2289,14 +2292,13 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="status_bar_box">
<property name="height_request">30</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="receiver_info_box">
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_left">10</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="info_image">
<property name="visible">True</property>
@@ -2333,62 +2335,56 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child type="center">
<object class="GtkBox" id="ip_status_box">
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="tooltip_text" translatable="yes">Current IP:</property>
<property name="halign">center</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage" id="ip_status_image">
<property name="visible">True</property>
<property name="valign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="active">0</property>
<property name="has_entry">True</property>
<child internal-child="entry">
<object class="GtkEntry" id="profile_entry">
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
<property name="has_tooltip">True</property>
<property name="halign">baseline</property>
<property name="valign">baseline</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="editable">False</property>
<property name="has_frame">False</property>
<property name="max_width_chars">9</property>
<property name="overwrite_mode">True</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_stock">gtk-connect</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ip_status_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Current IP:</property>
<attributes>
<attribute name="size" value="8000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ip_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">127.0.0.1</property>
<attributes>
<attribute name="size" value="8000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="http_status_image">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">No connection to the receiver</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="icon_name">network-offline</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="signal_box">
<property name="can_focus">False</property>
@@ -2477,21 +2473,6 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkImage" id="http_status_image">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">No connection to the receiver</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="icon_name">network-offline</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>

View File

@@ -9,7 +9,7 @@ from gi.repository import GLib, GdkPixbuf
from app.commons import run_idle, run_task
from app.connections import upload_data, DownloadType
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
from app.settings import Profile
from app.settings import SettingsType
from app.tools.satellites import SatellitesParser, SatelliteSource
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, TV_ICON
from .dialogs import show_dialog, DialogType, get_message
@@ -86,13 +86,13 @@ class PiconsDialog:
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._settings = settings
self._profile = settings.profile
self._s_type = settings.setting_type
self._ip_entry.set_text(self._settings.host)
self._picons_entry.set_text(self._settings.picons_path)
self._picons_path = self._settings.picons_dir_path
self._picons_path = self._settings.picons_local_path
self._picons_dir_entry.set_text(self._picons_path)
if not len(self._picon_ids) and self._profile is Profile.ENIGMA_2:
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
message = get_message("To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
@@ -342,7 +342,7 @@ class PiconsDialog:
self._expander.set_expanded(True)
convert_to(src_path=picons_path,
dest_path=save_path,
profile=Profile.ENIGMA_2,
s_type=SettingsType.ENIGMA_2,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
@@ -362,10 +362,10 @@ class PiconsDialog:
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = Profile.ENIGMA_2
picon_format = SettingsType.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = Profile.NEUTRINO_MP
picon_format = SettingsType.NEUTRINO_MP
return picon_format

View File

@@ -25,7 +25,7 @@ class SatellitesDialog:
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, settings):
self._data_path = settings.data_dir_path + "satellites.xml"
self._data_path = settings.data_local_path + "satellites.xml"
self._settings = settings
handlers = {"on_open": self.on_open,

View File

@@ -6,7 +6,7 @@ from app.eparser import Service
from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \
get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC
from app.settings import Profile
from app.settings import SettingsType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .main_helper import get_base_model
@@ -52,10 +52,10 @@ class ServiceDetailsDialog:
self._dialog = builder.get_object("service_details_dialog")
self._dialog.set_transient_for(transient)
self._profile = settings.profile
self._s_type = settings.setting_type
self._tr_type = None
self._satellites_xml_path = settings.data_dir_path + "satellites.xml"
self._picons_dir_path = settings.picons_dir_path
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
self._picons_dir_path = settings.picons_local_path
self._services_view = srv_view
self._fav_view = fav_view
self._action = action
@@ -69,7 +69,7 @@ class ServiceDetailsDialog:
# Patterns
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-z]{4})(,C:[0-9a-z]{4})*")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
@@ -197,7 +197,7 @@ class ServiceDetailsDialog:
self._package_entry.set_text(srv.package)
self._sid_entry.set_text(str(int(srv.ssid, 16)))
# Transponder
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
self._tr_type = TrType(srv.transponder_type)
self._freq_entry.set_text(srv.freq)
self._rate_entry.set_text(srv.rate)
@@ -211,10 +211,10 @@ class ServiceDetailsDialog:
else:
self.set_sat_positions(srv.pos)
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
self.init_enigma2_service_data(srv)
self.init_enigma2_transponder_data(srv)
elif self._profile is Profile.NEUTRINO_MP:
elif self._s_type is SettingsType.NEUTRINO_MP:
self.init_neutrino_data(srv)
self.init_neutrino_ui_elements()
@@ -484,9 +484,9 @@ class ServiceDetailsDialog:
transponder=transponder)
def get_flags(self):
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
return self.get_enigma2_flags()
elif self._profile is Profile.NEUTRINO_MP:
elif self._s_type is SettingsType.NEUTRINO_MP:
return self._old_service.flags_cas
def get_enigma2_flags(self):
@@ -532,12 +532,12 @@ class ServiceDetailsDialog:
net_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
service_type = self._srv_type_entry.get_text()
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
namespace = int(self._namespace_entry.get_text())
data_id = self._ENIGMA2_DATA_ID.format(ssid, namespace, tr_id, net_id, service_type, 0)
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
return fav_id, data_id
elif self._profile is Profile.NEUTRINO_MP:
elif self._s_type is SettingsType.NEUTRINO_MP:
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
return fav_id, self._old_service.data_id
@@ -548,7 +548,7 @@ class ServiceDetailsDialog:
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
if self._tr_type is TrType.Satellite or self._profile is Profile.NEUTRINO_MP:
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
@@ -571,7 +571,7 @@ class ServiceDetailsDialog:
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!!
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, srv_sys)
if sys == "DVB-S":
return dvb_s_tr
@@ -585,7 +585,7 @@ class ServiceDetailsDialog:
st_id = self._stream_id_entry.get_text()
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
elif self._profile is Profile.NEUTRINO_MP:
elif self._s_type is SettingsType.NEUTRINO_MP:
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
srv_sys = None
@@ -682,7 +682,7 @@ class ServiceDetailsDialog:
return True
def update_reference(self, entry, event=None):
if not self.is_data_correct() or (event is None and self._profile is Profile.NEUTRINO_MP):
if not self.is_data_correct() or (event is None and self._s_type is SettingsType.NEUTRINO_MP):
return
self.update_reference_entry()
@@ -691,7 +691,7 @@ class ServiceDetailsDialog:
ssid = int(self._sid_entry.get_text())
tid = int(self._transponder_id_entry.get_text())
nid = int(self._network_id_entry.get_text())
if self._profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
on_id = int(self._namespace_entry.get_text())
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
self._reference_entry.set_text(ref)

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
import os
from enum import Enum
from pathlib import Path
from app.commons import run_task, run_idle
from app.connections import test_telnet, test_ftp, TestException, test_http
from app.settings import Profile
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, FavClickMode
from .main_helper import update_entry_data
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
from app.settings import SettingsType, Settings
from app.ui.dialogs import show_dialog, DialogType
from .main_helper import update_entry_data, scroll_to
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
def show_settings_dialog(transient, options):
@@ -19,26 +22,42 @@ class Property(Enum):
class SettingsDialog:
def __init__(self, transient, settings):
def __init__(self, transient, settings: Settings):
handlers = {"on_field_icon_press": self.on_field_icon_press,
"on_profile_changed": self.on_profile_changed,
"on_settings_type_changed": self.on_settings_type_changed,
"on_reset": self.on_reset,
"on_response": self.on_response,
"apply_settings": self.apply_settings,
"on_apply_profile_settings": self.on_apply_profile_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch_state": self.on_set_color_switch_state,
"on_http_mode_switch_state": self.on_http_mode_switch_state,
"on_yt_dl_switch_state": self.on_yt_dl_switch_state,
"on_send_to_switch_state": self.on_send_to_switch_state}
"on_send_to_switch_state": self.on_send_to_switch_state,
"on_profile_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit,
"on_profile_remove": self.on_profile_remove,
"on_profile_deleted": self.on_profile_deleted,
"on_profile_inserted": self.on_profile_inserted,
"on_profile_edited": self.on_profile_edited,
"on_profile_selected": self.on_profile_selected,
"on_profile_set_default": self.on_profile_set_default,
"on_lang_changed": self.on_lang_changed,
"on_main_settings_visible": self.on_main_settings_visible,
"on_network_settings_visible": self.on_network_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
"on_click_mode_togged": self.on_click_mode_togged,
"on_view_popup_menu": self.on_view_popup_menu}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("settings_dialog")
self._dialog.set_transient_for(transient)
self._header_bar = builder.get_object("header_bar")
self._main_stack = builder.get_object("main_stack")
# Network
self._host_field = builder.get_object("host_field")
self._port_field = builder.get_object("port_field")
@@ -47,6 +66,7 @@ class SettingsDialog:
self._http_login_field = builder.get_object("http_login_field")
self._http_password_field = builder.get_object("http_password_field")
self._http_port_field = builder.get_object("http_port_field")
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
self._telnet_login_field = builder.get_object("telnet_login_field")
self._telnet_password_field = builder.get_object("telnet_password_field")
self._telnet_port_field = builder.get_object("telnet_port_field")
@@ -64,7 +84,7 @@ class SettingsDialog:
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Profile
# Settings type
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
@@ -77,6 +97,7 @@ class SettingsDialog:
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
# HTTP API
self._support_http_api_switch = builder.get_object("support_http_api_switch")
self._enable_y_dl_switch = builder.get_object("enable_y_dl_switch")
@@ -85,46 +106,77 @@ class SettingsDialog:
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
self._click_mode_play_button = builder.get_object("click_mode_play_button")
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive")
self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive")
self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive")
# Profiles
self._profile_view = builder.get_object("profile_tree_view")
self._profile_add_button = builder.get_object("profile_add_button")
self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", builder.get_object("header_separator"), "visible")
# Language
self._lang_combo_box = builder.get_object("lang_combo_box")
# Settings
self._settings = settings
self._active_profile = settings.profile
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._profiles = self._settings.profiles
self._s_type = self._settings.setting_type
self.set_settings()
self.init_ui_elements(self._active_profile)
self.init_ui_elements(self._s_type)
self.init_profiles()
def init_ui_elements(self, profile):
is_enigma_profile = profile is Profile.ENIGMA_2
self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP)
@run_idle
def init_ui_elements(self, s_type):
is_enigma_profile = s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
self.update_header_bar()
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
self._program_frame.set_sensitive(is_enigma_profile)
self._extra_support_grid.set_sensitive(is_enigma_profile)
http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._lang_combo_box.set_active_id(self._settings.language)
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
self.apply_settings()
self._dialog.destroy()
def init_profiles(self):
p_def = self._settings.default_profile
for p in self._profiles:
self._profile_view.get_model().append((p, DEFAULT_ICON if p == p_def else None))
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
return response
def update_header_bar(self):
label, sep, st = self._header_bar.get_subtitle().partition(":")
if self._s_type is SettingsType.ENIGMA_2:
self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label()))
elif self._s_type is SettingsType.NEUTRINO_MP:
self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label()))
def show(self):
self._dialog.run()
def on_response(self, dialog, resp):
if resp == Gtk.ResponseType.OK and not self.apply_settings():
return
self._dialog.destroy()
return resp
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
def on_profile_changed(self, item):
profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
self._active_profile = profile
self._settings.profile = profile
self.set_settings()
def on_settings_type_changed(self, item):
profile = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._s_type = profile
self._settings.setting_type = profile
self.on_reset()
self.init_ui_elements(profile)
def on_reset(self, item):
def on_reset(self, item=None):
self._settings.reset()
self.set_settings()
@@ -136,6 +188,7 @@ class SettingsDialog:
self._http_login_field.set_text(self._settings.http_user)
self._http_password_field.set_text(self._settings.http_password)
self._http_port_field.set_text(self._settings.http_port)
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
self._telnet_login_field.set_text(self._settings.telnet_user)
self._telnet_password_field.set_text(self._settings.telnet_password)
self._telnet_port_field.set_text(self._settings.telnet_port)
@@ -144,14 +197,15 @@ class SettingsDialog:
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
self._picons_field.set_text(self._settings.picons_path)
self._data_dir_field.set_text(self._settings.data_dir_path)
self._picons_dir_field.set_text(self._settings.picons_dir_path)
self._backup_dir_field.set_text(self._settings.backup_dir_path)
self._data_dir_field.set_text(self._settings.data_local_path)
self._picons_dir_field.set_text(self._settings.picons_local_path)
self._backup_dir_field.set_text(self._settings.backup_local_path)
self._before_save_switch.set_active(self._settings.backup_before_save)
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
self.set_fav_click_mode(self._settings.fav_click_mode)
self._load_on_startup_switch.set_active(self._settings.load_last_config)
if self._active_profile is Profile.ENIGMA_2:
if self._s_type is SettingsType.ENIGMA_2:
self._support_ver5_switch.set_active(self._settings.v5_support)
self._support_http_api_switch.set_active(self._settings.http_api_support)
self._enable_y_dl_switch.set_active(self._settings.enable_yt_dl)
@@ -164,9 +218,9 @@ class SettingsDialog:
self._new_color_button.set_rgba(new_rgb)
self._extra_color_button.set_rgba(extra_rgb)
def apply_settings(self, item=None):
self._active_profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
self._settings.profile = self._active_profile
def on_apply_profile_settings(self, item):
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._settings.setting_type = self._s_type
self._settings.host = self._host_field.get_text()
self._settings.port = self._port_field.get_text()
self._settings.user = self._login_field.get_text()
@@ -174,6 +228,7 @@ class SettingsDialog:
self._settings.http_user = self._http_login_field.get_text()
self._settings.http_password = self._http_password_field.get_text()
self._settings.http_port = self._http_port_field.get_text()
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
self._settings.telnet_user = self._telnet_login_field.get_text()
self._settings.telnet_password = self._telnet_password_field.get_text()
self._settings.telnet_port = self._telnet_port_field.get_text()
@@ -182,23 +237,33 @@ class SettingsDialog:
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
self._settings.picons_path = self._picons_field.get_text()
self._settings.data_dir_path = self._data_dir_field.get_text()
self._settings.picons_dir_path = self._picons_dir_field.get_text()
self._settings.backup_dir_path = self._backup_dir_field.get_text()
self._settings.backup_before_save = self._before_save_switch.get_active()
self._settings.backup_before_downloading = self._before_downloading_switch.get_active()
self._settings.fav_click_mode = self.get_fav_click_mode()
self._settings.data_local_path = self._data_dir_field.get_text()
self._settings.picons_local_path = self._picons_dir_field.get_text()
self._settings.backup_local_path = self._backup_dir_field.get_text()
if self._active_profile is Profile.ENIGMA_2:
self._settings.use_colors = self._set_color_switch.get_active()
self._settings.new_color = self._new_color_button.get_rgba().to_string()
self._settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._settings.v5_support = self._support_ver5_switch.get_active()
self._settings.http_api_support = self._support_http_api_switch.get_active()
self._settings.enable_yt_dl = self._enable_y_dl_switch.get_active()
self._settings.enable_send_to = self._enable_send_to_switch.get_active()
def apply_settings(self, item=None):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._settings.save()
self._ext_settings.profiles = self._settings.profiles
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
self._ext_settings.language = self._lang_combo_box.get_active_id()
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
if self._s_type is SettingsType.ENIGMA_2:
self._ext_settings.use_colors = self._set_color_switch.get_active()
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
self._ext_settings.enable_yt_dl = self._enable_y_dl_switch.get_active()
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
self._ext_settings.save()
return True
@run_task
def on_connection_test(self, item):
@@ -216,10 +281,13 @@ class SettingsDialog:
def test_http(self):
user, password = self._http_login_field.get_text(), self._http_password_field.get_text()
host, port = self._host_field.get_text(), self._http_port_field.get_text()
use_ssl = self._http_use_ssl_check_button.get_active()
try:
self.show_info_message(test_http(host, port, user, password), Gtk.MessageType.INFO)
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except HttpApiException as e:
self.show_info_message(str(e), Gtk.MessageType.WARNING)
finally:
self.show_spinner(False)
@@ -263,7 +331,9 @@ class SettingsDialog:
def on_http_mode_switch_state(self, switch, state):
self._click_mode_zap_button.set_sensitive(state)
if self._click_mode_play_button.get_active() or self._click_mode_zap_button.get_active():
if any((self._click_mode_play_button.get_active(),
self._click_mode_zap_button.get_active(),
self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True)
def on_yt_dl_switch_state(self, switch, state):
@@ -272,6 +342,123 @@ class SettingsDialog:
def on_send_to_switch_state(self, switch, state):
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
def on_profile_add(self, item):
model = self._profile_view.get_model()
count = 0
name = "profile"
while name in self._profiles:
count += 1
name = "profile{}".format(count)
self._profiles[name] = self._s_type.get_default_settings()
model.append((name, None))
scroll_to(len(model) - 1, self._profile_view)
self.on_profile_selected(self._profile_view)
p = name + "/"
self._settings.data_local_path += p
self._settings.picons_local_path += p
self._settings.backup_local_path += p
self.on_reset()
def on_profile_edit(self, item=None):
model, paths = self._profile_view.get_selection().get_selected_rows()
self._profile_view.set_cursor(paths, self._profile_view.get_column(0), True)
def on_profile_remove(self, item):
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
row = model[paths]
is_default = row[1]
self._profiles.pop(row[0], None)
del model[paths]
if is_default:
model.set_value(model.get_iter_first(), 1, DEFAULT_ICON)
def on_profile_deleted(self, model, paths):
self._profile_remove_button.set_sensitive(len(model) > 1)
def on_profile_edited(self, render, path, new_value):
p_name = render.get_property("text")
p_name = self._profiles.pop(p_name, None)
if p_name:
row = self._profile_view.get_model()[path]
row[0] = new_value
self._profiles[new_value] = p_name
if p_name != new_value:
self.update_local_paths(new_value)
self.on_profile_selected(self._profile_view)
def update_local_paths(self, p_name, force_rename=False):
data_path = self._settings.data_local_path
picons_path = self._settings.picons_local_path
backup_path = self._settings.backup_local_path
self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name)
self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name)
self._settings.backup_local_path = "{}/{}/".format(Path(backup_path).parent, p_name)
if force_rename:
try:
if os.path.isdir(picons_path):
os.rename(picons_path, self._settings.picons_local_path)
if os.path.isdir(data_path):
os.rename(data_path, self._settings.data_local_path)
if os.path.isdir(backup_path):
os.rename(backup_path, self._settings.backup_local_path)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_profile_selected(self, view):
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
profile = model.get_value(model.get_iter(paths), 0)
self._settings.current_profile = profile
if self._settings.setting_type is SettingsType.ENIGMA_2:
self._enigma_radio_button.activate()
else:
self._neutrino_radio_button.activate()
self.set_settings()
def on_profile_set_default(self, item):
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
itr = model.get_iter(paths)
model.foreach(lambda m, p, i: model.set_value(i, 1, None))
model.set_value(itr, 1, DEFAULT_ICON)
self._settings.default_profile = model.get_value(itr, 0)
def on_profile_inserted(self, model, path, itr):
self._profile_remove_button.set_sensitive(len(model) > 1)
def on_lang_changed(self, box):
if box.get_active_id() != self._settings.language:
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_main_settings_visible(self, stack, param):
self._apply_profile_button.set_visible(stack.get_visible_child_name() == "profiles")
def on_network_settings_visible(self, stack, param):
self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP)
def on_http_use_ssl_toggled(self, button):
active = button.get_active()
self._settings.http_use_ssl = active
port = "443" if active else "80"
self._http_port_field.set_text(port)
self._settings.http_port = port
def on_click_mode_togged(self, button):
if self._main_stack.get_visible_child_name() != "extra":
return
mode = self.get_fav_click_mode()
if mode is FavClickMode.PLAY:
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
else:
self.on_info_bar_close()
@run_idle
def set_fav_click_mode(self, mode):
mode = FavClickMode(mode)
@@ -279,17 +466,24 @@ class SettingsDialog:
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
self._click_mode_zap_and_play_button.set_active(mode is FavClickMode.ZAP_PLAY)
def get_fav_click_mode(self):
if self._click_mode_zap_button.get_active():
return FavClickMode.ZAP
if self._click_mode_play_button.get_active():
return FavClickMode.PLAY
if self._click_mode_zap_and_play_button.get_active():
return FavClickMode.ZAP_PLAY
if self._click_mode_stream_button.get_active():
return FavClickMode.STREAM
return FavClickMode.DISABLED
def on_view_popup_menu(self, menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
if __name__ == "__main__":
pass

View File

@@ -87,7 +87,7 @@ class LinksTransmitter:
def on_play(self, res):
""" Play callback """
GLib.idle_add(self._url_entry.set_sensitive, True)
res = res.get("result", None) if res else res
res = res.get("e2state", None) if res else res
self._url_entry.set_name("GtkEntry" if res else "digit-entry")
def on_exit(self, item=None):

View File

@@ -6,6 +6,10 @@ import sys
import gi
from enum import Enum, IntEnum
import gi
from app.settings import Settings, SettingsException
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
@@ -21,12 +25,22 @@ IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# translation
os.environ["LANG"] = "{}.{}".format(*locale.getlocale())
TEXT_DOMAIN = "demon-editor"
try:
settings = Settings.get_instance()
except SettingsException:
pass
else:
os.environ["LANGUAGE"] = settings.language
if UI_RESOURCES_PATH == "app/ui/":
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
LANG_PATH = GTK_PATH + "/share/locale" if GTK_PATH else UI_RESOURCES_PATH + "lang"
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
if sys.platform != "darwin":
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
theme = Gtk.IconTheme.get_default()
theme.append_search_path(UI_RESOURCES_PATH + "icons")
_IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None
CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon(
"emblem-readonly", 16, 0) else _IMAGE_MISSING
@@ -36,6 +50,7 @@ HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.lookup_icon("emblem-shared", 16, 0) else None
EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None
DEFAULT_ICON = theme.load_icon("emblem-default", 16, 0) if theme.lookup_icon("emblem-default", 16, 0) else None
class KeyboardKey(Enum):
@@ -94,6 +109,7 @@ class FavClickMode(IntEnum):
STREAM = 1
PLAY = 2
ZAP = 3
ZAP_PLAY = 4
class ViewTarget(Enum):

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python3
from app.ui.main_app_window import start_app
start_app()

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg1541"
version="1.1"
viewBox="0 0 16.743339 16.72816"
height="63.224556"
width="63.281971"
sodipodi:docname="demon-editor.svg"
inkscape:version="0.92.4 (unknown)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="716"
id="namedview127"
showgrid="true"
fit-margin-left="0"
inkscape:zoom="6.1714295"
inkscape:cx="40.088627"
inkscape:cy="31.742631"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1541"
fit-margin-top="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid128"
originx="-10.604603"
originy="-1.1727724" />
</sodipodi:namedview>
<title
id="title1088">DeamonEditor Icons</title>
<defs
id="defs1535">
<linearGradient
id="linearGradient2198">
<stop
id="stop2194"
style="stop-color:#ffb320;stop-opacity:1"
offset="0" />
<stop
id="stop2196"
style="stop-color:#e7ff00;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient2192">
<stop
offset="0"
style="stop-color:#ffb320;stop-opacity:1"
id="stop2188" />
<stop
offset="1"
style="stop-color:#b3c54c;stop-opacity:1"
id="stop2190" />
</linearGradient>
<linearGradient
id="linearGradient3700-8">
<stop
offset="0"
style="stop-color:#2e4f84;stop-opacity:1"
id="stop2183" />
<stop
offset="1"
style="stop-color:#4c77c5;stop-opacity:1"
id="stop2185" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1844">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1826" />
<stop
id="stop1828"
offset="0.13293758"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.21261224"
id="stop1832" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.29780689"
id="stop1834" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.29780689"
id="stop1836" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1838" />
<stop
style="stop-opacity:1;stop-color:#696c76"
offset="0.71871042"
id="stop1840" />
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="1"
id="stop1842" />
</linearGradient>
<linearGradient
id="linearGradient1754"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1736"
offset="0"
style="stop-opacity:1;stop-color:#29282b" />
<stop
style="stop-color:#b5bdcf;stop-opacity:1"
offset="0.02582242"
id="stop1738" />
<stop
id="stop1740"
offset="0.14669065"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1742"
offset="0.21261224"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1744"
offset="0.29780689"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1746"
offset="0.29780689"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1748"
offset="0.45395693"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1750"
offset="0.71871042"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1752"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1606">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1590" />
<stop
id="stop1608"
offset="0.03065561"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#868c95"
offset="0.1125849"
id="stop1592" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.13955873"
id="stop1594" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.1915853"
id="stop1596" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.25400829"
id="stop1598" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1600" />
<stop
style="stop-opacity:1;stop-color:#ffffff"
offset="0.71871042"
id="stop1602" />
<stop
style="stop-opacity:1;stop-color:#1d191a"
offset="1"
id="stop1604" />
</linearGradient>
<linearGradient
id="linearGradient886-0">
<stop
style="stop-color:#535353;stop-opacity:1"
offset="0"
id="stop888" />
<stop
style="stop-color:#f0f0f0;stop-opacity:1"
offset="1"
id="stop890" />
</linearGradient>
<linearGradient
id="linearGradient1473">
<stop
id="stop1469"
offset="0"
style="stop-color:#ffffff;stop-opacity:0" />
<stop
id="stop1471"
offset="1"
style="stop-color:#ffffff;stop-opacity:0.96088022" />
</linearGradient>
<linearGradient
id="linearGradient2447-35">
<stop
id="stop2449"
offset="0"
style="stop-color:#00c62e;stop-opacity:1" />
<stop
id="stop2451"
offset="1"
style="stop-color:#136100;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3795-1">
<stop
id="stop3797-1"
offset="0"
style="stop-color:#803400;stop-opacity:1" />
<stop
id="stop3799-3"
offset="1"
style="stop-color:#c87137;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient2447-3">
<stop
style="stop-color:#c60300;stop-opacity:1"
offset="0"
id="stop2443" />
<stop
style="stop-color:#c40e00;stop-opacity:1"
offset="1"
id="stop2445" />
</linearGradient>
<linearGradient
id="linearGradient2458">
<stop
id="stop2454"
offset="0"
style="stop-color:#c60300;stop-opacity:1" />
<stop
id="stop2456"
offset="1"
style="stop-color:#ee6000;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3519">
<stop
id="stop3521"
offset="0"
style="stop-color:#1d2120;stop-opacity:1" />
<stop
id="stop3523"
offset="1"
style="stop-color:#545d5d;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient1446">
<stop
id="stop1442"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop1444"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient1547"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1529"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1531"
offset="0.0263736"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1533"
offset="0.263736"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1535"
offset="0.395604"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1537"
offset="0.39560401"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1539"
offset="0.42333773"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1541"
offset="0.56268591"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1543"
offset="0.62400264"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1545"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
id="linearGradient1527"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1509"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1511"
offset="0.0527472"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1513"
offset="0.142147"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1515"
offset="0.19700864"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1517"
offset="0.25031137"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1519"
offset="0.3710371"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1521"
offset="0.53961843"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1523"
offset="0.76283824"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1525"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-5.8254634,-78.732754)"
y2="1179.7145"
x2="66.791626"
y1="1188.7661"
x1="99.044022"
gradientUnits="userSpaceOnUse"
id="linearGradient1485"
xlink:href="#linearGradient1473" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-2.2733744,-70.526334)"
gradientUnits="userSpaceOnUse"
y2="1155.8046"
x2="67.311417"
y1="1161.6112"
x1="77.19442"
id="linearGradient1448"
xlink:href="#linearGradient1446" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-18.198747,-79.859424)"
gradientUnits="userSpaceOnUse"
y2="1186.8096"
x2="146.16808"
y1="1186.8096"
x1="131.86871"
id="linearGradient2180"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.20863982,0,0,0.20863982,-0.63935937,-13.031239)"
gradientUnits="userSpaceOnUse"
y2="1184.73"
x2="101.19952"
y1="1184.73"
x1="83.066002"
id="linearGradient2160"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-6.2370714,-102.12414)"
gradientUnits="userSpaceOnUse"
y2="1267.1335"
x2="117.99127"
y1="1267.1335"
x1="75.853806"
id="linearGradient2131"
xlink:href="#linearGradient886-0" />
<linearGradient
id="linearGradient3700-8-3">
<stop
id="stop3702-1"
style="stop-color:#2e4f84;stop-opacity:1"
offset="0" />
<stop
id="stop3704-8"
style="stop-color:#4c77c5;stop-opacity:1"
offset="1" />
</linearGradient>
<defs
id="defs4922">
<filter
style="color-interpolation-filters:sRGB"
id="Adobe_OpacityMaskFilter"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix4925" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="3.785"
y="4.675"
width="5.883"
height="73.013"
id="SVGID_2_">
<g
style="filter:url(#Adobe_OpacityMaskFilter)"
id="g4928">
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="3.7852001"
y1="41.181198"
x2="9.6680002"
y2="41.181198">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop4931" />
<stop
offset="0.0029"
style="stop-color:#FAFBFB"
id="stop4933" />
<stop
offset="0.0756"
style="stop-color:#BBBDBF"
id="stop4935" />
<stop
offset="0.1438"
style="stop-color:#898B8E"
id="stop4937" />
<stop
offset="0.2053"
style="stop-color:#646567"
id="stop4939" />
<stop
offset="0.259"
style="stop-color:#444446"
id="stop4941" />
<stop
offset="0.3028"
style="stop-color:#1D1C1D"
id="stop4943" />
<stop
offset="0.3313"
style="stop-color:#000000"
id="stop4945" />
</linearGradient>
<rect
style="fill:url(#SVGID_3_)"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013"
id="rect4947" />
</g>
</mask>
<filter
style="color-interpolation-filters:sRGB"
id="filter1268"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix1266" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="203.22046"
x2="23.551136"
y1="203.22046"
x1="13.487289"
id="linearGradient1160"
xlink:href="#linearGradient3519" />
<clipPath
id="clipPath1890"
clipPathUnits="userSpaceOnUse">
<circle
style="opacity:1;fill:#2d2d2d;fill-opacity:1;stroke:#434242;stroke-width:0.0575568;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle1892"
cx="78.548424"
cy="-31.019459"
r="6.4721422"
transform="scale(1,-1)" />
</clipPath>
<linearGradient
gradientTransform="translate(-0.24389927,14.877856)"
y2="27.314217"
x2="84.864914"
y1="27.314217"
x1="72.164909"
gradientUnits="userSpaceOnUse"
id="linearGradient2134"
xlink:href="#linearGradient3700-8-3" />
</defs>
<metadata
id="metadata1538">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>DeamonEditor Icons</dc:title>
<dc:publisher>
<cc:Agent>
<dc:title>mfgeg</dc:title>
</cc:Agent>
</dc:publisher>
<dc:date>7.1.2020</dc:date>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.1690805,0,0,1.1690805,-14.929261,-167.45253)"
id="layer1">
<g
transform="translate(0.86526724,-82.691658)"
id="g1351">
<path
d="m 13.715358,227.65981 h 2.97616 v 12.57332 h -2.97616 z m 5.79826,-1.73376 -0.327076,0.42227 c -0.179879,0.23226 -0.586303,0.67932 -0.903223,0.99412 -0.480677,0.47726 -0.640897,0.57235 -0.967777,0.57235 -0.223557,0 -0.380895,-0.0584 -0.624024,-0.25067 v 3.18307 c 0.172884,-0.0214 0.359647,-0.0333 0.575071,-0.0352 0.68403,-0.006 0.91815,0.0416 1.436333,0.29581 1.14586,0.56274 1.762492,1.5589 1.768251,2.8566 0.0094,2.10836 -1.870896,3.55161 -3.779655,3.16529 v 3.10345 c 4.345352,0.0758 4.093104,-2.37537 6.573781,-2.25837 1.486139,0.0748 1.333421,0.16555 1.796225,-1.064 l 0.259834,-0.6913 -0.803704,-0.66493 c -0.960855,-0.79478 -1.303983,-1.29079 -1.205013,-1.74132 0.06631,-0.30189 1.20818,-1.50093 1.779011,-1.86778 0.240181,-0.1543 0.244255,-0.17878 0.09575,-0.59661 -0.08522,-0.23979 -0.259522,-0.65746 -0.386789,-0.92744 l -0.23132,-0.49115 h -1.412666 c -1.868582,0 -1.802386,0.068 -1.805368,-1.82472 l -0.0021,-1.43582 -0.917745,-0.37171 z"
style="opacity:1;fill:#000000;fill-opacity:0.5372549;stroke:none;stroke-width:0.18460207;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
id="path2133"
inkscape:connector-curvature="0" />
<path
id="path2106"
style="opacity:1;fill:url(#linearGradient2131);fill-opacity:1;stroke:none;stroke-width:0.17733108;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 13.832581,227.93117 h 2.858936 v 12.07807 h -2.858936 z m 5.569881,-1.6655 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65257 -0.867647,0.95496 -0.46174,0.45847 -0.615654,0.5498 -0.929658,0.5498 -0.214752,0 -0.365893,-0.056 -0.599446,-0.24079 v 3.05768 c 0.166077,-0.0206 0.345483,-0.0319 0.55242,-0.0338 0.657089,-0.006 0.881987,0.0399 1.379764,0.28416 1.100724,0.54057 1.693073,1.49747 1.698606,2.74408 0.009,2.02535 -1.797211,3.41174 -3.63079,3.04061 v 2.98122 c 4.174205,0.0728 3.931888,-2.28179 6.314859,-2.1694 1.427604,0.0718 1.2809,0.15902 1.725477,-1.02213 l 0.249597,-0.66408 -0.772046,-0.63871 c -0.923009,-0.76345 -1.252622,-1.23996 -1.157552,-1.67277 0.0637,-0.29001 1.160592,-1.44177 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17174 0.09198,-0.57312 -0.08187,-0.23032 -0.249296,-0.63156 -0.371555,-0.89088 l -0.222207,-0.4718 h -1.357022 c -1.794983,0 -1.731396,0.0653 -1.734262,-1.75284 l -0.0021,-1.3793 -0.881603,-0.35705 z"
inkscape:connector-curvature="0" />
<path
id="path2151"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575465,0.0354 v 6.28854 c 1.910653,0.38671 3.792716,-1.05815 3.783336,-3.16865 -0.0058,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482341,1.17002 c 0.433747,-0.004 0.582202,0.0265 0.910784,0.18761 0.726595,0.35682 1.117604,0.98848 1.121256,1.81134 0.0059,1.33694 -1.186343,2.25211 -2.396695,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364655,-0.0225 z"
style="opacity:1;fill:url(#linearGradient2160);fill-opacity:1;stroke:none;stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient2180);stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 17.266981,230.95753 c -0.215636,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28853 c 1.910652,0.38672 3.792715,-1.05815 3.783336,-3.16865 -0.0057,-1.29897 -0.623065,-2.29597 -1.77006,-2.85927 -0.518697,-0.2545 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482343,1.17001 c 0.433747,-0.004 0.5822,0.0265 0.910783,0.18762 0.726594,0.35681 1.117605,0.98848 1.121257,1.81133 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00716 v -3.98359 c 0.109628,-0.0135 0.228055,-0.0214 0.364657,-0.0225 z"
id="path2170"
inkscape:connector-curvature="0" />
<path
id="path1422"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28854 c 1.910652,0.38671 3.792715,-1.05815 3.783335,-3.16865 -0.0057,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482342,1.17002 c 0.433748,-0.004 0.582201,0.0265 0.910784,0.18761 0.726594,0.35682 1.117605,0.98848 1.121257,1.81134 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364656,-0.0225 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1448);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="path1454"
d="m 19.402462,226.26149 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65259 -0.867644,0.95499 -0.461741,0.45847 -0.615657,0.54983 -0.929661,0.54983 -0.214752,0 -0.365893,-0.056 -0.599446,-0.2408 v -0.004 h -2.858741 v 12.0783 h 2.858741 c 4.174205,0.0728 3.931888,-2.28177 6.314861,-2.16937 1.427604,0.0718 1.280898,0.15899 1.725475,-1.02217 l 0.249597,-0.66402 -0.772046,-0.63873 c -0.923009,-0.76346 -1.252622,-1.23997 -1.157552,-1.67278 0.0637,-0.29001 1.160595,-1.44176 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17171 0.09198,-0.57309 -0.08187,-0.23032 -0.249296,-0.63158 -0.371555,-0.8909 l -0.222207,-0.47181 h -1.357025 c -1.794983,0 -1.731393,0.0653 -1.734259,-1.75286 l -0.0021,-1.37925 -0.8816,-0.35708 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1485);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<g
transform="translate(63.49728,-55.136805)"
id="g1174" />
<g
transform="translate(50.48327,11.624037)"
id="g1812" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB