Compare commits

...

53 Commits

Author SHA1 Message Date
DYefremov
b45dda8ada picon[cz] downloader improvement
* Added caching [as tmp] of the permalinks file.
2026-04-21 01:26:32 +03:00
DYefremov
e1577d8e0c add progressbar style 2026-04-19 22:15:13 +03:00
DYefremov
a184a7cc7f wait dialog style change 2026-04-19 22:10:44 +03:00
DYefremov
6a4ca77009 wait dialog refactoring 2026-04-17 01:14:31 +03:00
DYefremov
c1ed748a91 add custom progress bar class 2026-04-17 00:56:53 +03:00
DYefremov
d6791a9c89 remove warning on satellite page 2026-04-10 10:58:58 +03:00
DYefremov
1f0411fb3d run task -> migrate to Gio [test] 2026-04-09 15:12:17 +03:00
DYefremov
27a1838980 minor playback adjustment
* Changed window behavior for a separate mode.
2026-04-06 22:52:56 +03:00
DYefremov
c1bfb482e1 xmltv reader -> minor cleanup 2026-03-14 17:04:17 +03:00
DYefremov
411e012f5c bg task migration to Gio [test] 2026-03-13 15:51:18 +03:00
DYefremov
a7bc32d7ae xmltv reader -> download adjustment 2026-03-13 15:33:39 +03:00
DYefremov
ef3f69ece1 translations update -> [be, de, ru] 2026-03-03 13:35:26 +03:00
DYefremov
344f4905fc playback setting msg correction 2026-03-03 13:05:13 +03:00
DYefremov
51f36d14ec small refactoring -> [get dialogs string] 2026-03-03 12:21:02 +03:00
DYefremov
f2b31b2ac4 enable editing by double-clicking
* Enabled ability to open the editing dialog by double-clicking in the lists for Bouquets tab.
2026-02-26 13:49:33 +03:00
DYefremov
118734d7fb minor fix for EPG tab settings popover 2026-02-26 11:35:05 +03:00
DYefremov
d7b7f6571b copyright update 2026-02-23 12:10:32 +03:00
DYefremov
9728843b0a msg adjustment 2026-02-23 12:09:50 +03:00
DYefremov
033ac70c8a minor adjustment of favorites remove 2026-02-22 19:19:38 +03:00
DYefremov
85247e8307 version update -> 3.14.4 2026-02-22 16:49:06 +03:00
DYefremov
d38a896685 service edit dialog improvement
* Auto set the namespace value when the satellite position changes.
2026-02-22 15:53:25 +03:00
DYefremov
880908c163 service dialog refactoring 2026-02-22 15:14:32 +03:00
DYefremov
ec94d5ef46 fix creation transponder data for a new channel 2026-02-22 14:27:20 +03:00
DYefremov
49f9863922 version update -> 3.14.3 2026-02-18 16:19:04 +03:00
DYefremov
6d4249cf1e add *.patch for win build 2026-02-18 16:14:02 +03:00
DYefremov
0fc0ef1d3e win build start file update 2026-02-18 15:48:40 +03:00
DYefremov
c587f2bcdc win build logo update 2026-02-18 15:46:17 +03:00
DYefremov
5cd8c68589 add splash screen to win build 2026-02-15 22:37:16 +03:00
DYefremov
61690db0ee minor msg adjustment 2026-02-15 21:50:07 +03:00
DYefremov
fcc2b6b6a8 add app termination
* Added app termination while background tasks are still running.
2026-02-15 13:48:26 +03:00
DYefremov
a5412cd2b3 refactoring of keyboard key handling 2026-02-14 12:58:13 +03:00
DYefremov
a47a7417c2 remove favs refactoring 2026-02-14 10:08:09 +03:00
DYefremov
bdac77e88c picons downloader improvement
* Added subprocess creationflags.
2026-02-02 15:19:01 +03:00
DYefremov
8ab79a2937 *.spec files adjustment 2026-02-01 23:51:17 +03:00
DYefremov
8dc880577f bootlogo manager improvements
* Added subprocess flags to improve startup on Windows.
2026-01-31 12:38:13 +03:00
DYefremov
b1829651d3 it *.mo file update 2026-01-30 20:35:10 +03:00
mapi68
7339872de6 translations update -> it (#239) 2026-01-30 20:33:23 +03:00
DYefremov
8155643098 version update -> 3.14.2 2026-01-26 22:27:21 +03:00
audi06_19
87a1cde859 translations update -> tr (#237) 2026-01-25 18:08:04 +03:00
DYefremov
a591d31d01 translations update -> [be, de, ru] 2026-01-18 22:16:34 +03:00
DYefremov
e863c41117 minor fix 2026-01-18 21:55:26 +03:00
DYefremov
811539ae19 enable extension manager permanently 2026-01-18 16:09:21 +03:00
DYefremov
bc7327a6d5 extension manager adjustment 2026-01-11 00:27:43 +03:00
DYefremov
0c114964f2 fav channels numbering adjustment (#236) 2026-01-01 12:36:16 +03:00
DYefremov
b8a3e5e4c1 *.spec files adjustment 2025-11-15 12:21:14 +03:00
DYefremov
1d16e9e220 version update -> 3.14.1 2025-11-14 13:25:02 +03:00
DYefremov
c96b464cbc prevent double data loading
* Fixes double data loading when opening an external folder from the start page.
2025-11-14 13:22:51 +03:00
DYefremov
43821e6f50 add t2mi pid value support (#232) 2025-11-10 11:56:53 +03:00
DYefremov
e0e642db5a sk *.mo file update 2025-11-10 09:11:06 +03:00
EnoSat
2266fd4d3d Slovak translations update (#234) 2025-11-10 09:09:14 +03:00
EnoSat
6b8145c674 Add Slovak translations to desktop entry (#233) 2025-11-09 21:07:50 +03:00
DYefremov
fa89ab8608 add Slovak translation 2025-11-09 19:32:04 +03:00
EnoSat
da70b0fb18 Slovak language (#231)
Added Slovak localization.
2025-11-09 18:57:38 +03:00
55 changed files with 2214 additions and 300 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2025 Dmitriy Yefremov Copyright (c) 2018-2026 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,9 +1,38 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2026 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
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov <https://github.com/DYefremov>
#
import logging import logging
from collections import defaultdict from collections import defaultdict
from functools import wraps from functools import wraps
from threading import Thread, Timer from threading import Timer
from gi.repository import GLib from gi.repository import GLib
from gi.repository.Gio import Task
_LOG_FILE = "demon-editor.log" _LOG_FILE = "demon-editor.log"
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S" LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
@@ -41,12 +70,12 @@ def run_idle(func):
def run_task(func): def run_task(func):
""" Runs function in separate thread """ """ Runs a function in a separate thread. """
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
task = Thread(target=func, args=args, kwargs=kwargs, daemon=True) task = Task()
task.start() task.run_in_thread(lambda t, s, d, c: func(*args, **kwargs))
return wrapper return wrapper

View File

@@ -64,7 +64,7 @@ Terrestrial = namedtuple("Terrestrial", ["name", "flags", "countrycode", "transp
Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"]) Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"])
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system", Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"]) "modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id", "t2mi_pid"])
TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation", TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation",
"code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode", "code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode",
"hierarchy_information", "inversion", "plp_id"]) "hierarchy_information", "inversion", "plp_id"])

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2023 Dmitriy Yefremov # Copyright (c) 2018-2025 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -88,7 +88,8 @@ def get_sat_transponders(elem):
e.get("pls_mode", None), e.get("pls_mode", None),
e.get("pls_code", None), e.get("pls_code", None),
e.get("is_id", None), e.get("is_id", None),
e.get("t2mi_plp_id", None)) for e in elem.iter("transponder")] e.get("t2mi_plp_id", None),
e.get("t2mi_pid", None)) for e in elem.iter("transponder")]
def get_terrestrial(path): def get_terrestrial(path):

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -257,62 +257,64 @@ class XmlTvReader(Reader):
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}") log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
return return
with requests.get(url=self._url, stream=True) as resp: try:
if resp.reason == "OK": with requests.get(url=self._url, stream=True, timeout=(5, 5)) as resp:
suf = self._url[self._url.rfind("."):] if resp.reason == "OK":
if suf not in self.SUFFIXES: suf = self._url[self._url.rfind("."):]
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.") if suf not in self.SUFFIXES:
return log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
return
data_size = resp.headers.get("content-length") data_size = resp.headers.get("content-length")
if not data_size: if not data_size:
log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.") log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.")
if clb: return
clb()
return
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf: with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
downloaded = 0 downloaded = 0
data_size = int(data_size) data_size = int(data_size)
completed = set() completed = set()
for data in resp.iter_content(chunk_size=1024): for data in resp.iter_content(chunk_size=128):
downloaded += len(data) downloaded += len(data)
tf.write(data) tf.write(data)
done = int(100 * downloaded / data_size) done = int(100 * downloaded / data_size)
if done % 25 == 0 and done not in completed: if done % 25 == 0 and done not in completed:
completed.add(done) completed.add(done)
log(f"Downloading XMLTV file...{done}%" if done < 100 else "XMLTV file download complete.") log(f"Downloading XMLTV file...{done}%" if done < 100 else "XMLTV file download complete.")
tf.seek(0) tf.seek(0)
os.makedirs(os.path.dirname(self._path), exist_ok=True) os.makedirs(os.path.dirname(self._path), exist_ok=True)
if suf.endswith(".gz"): if suf.endswith(".gz"):
try: try:
shutil.copyfile(tf.name, self._path) shutil.copyfile(tf.name, self._path)
except OSError as e: except OSError as e:
log(f"{self.__class__.__name__} [download *.gz] error: {e}") log(f"{self.__class__.__name__} [download *.gz] error: {e}")
elif self._url.endswith((".xz", ".lzma")): elif self._url.endswith((".xz", ".lzma")):
import lzma import lzma
try: try:
with lzma.open(tf, "rb") as lzf: with lzma.open(tf, "rb") as lzf:
shutil.copyfileobj(lzf, self._path) shutil.copyfileobj(lzf, self._path)
except (lzma.LZMAError, OSError) as e: except (lzma.LZMAError, OSError) as e:
log(f"{self.__class__.__name__} [download *.xz] error: {e}") log(f"{self.__class__.__name__} [download *.xz] error: {e}")
else: else:
try: try:
import gzip import gzip
with gzip.open(self._path, "wb") as f_out: with gzip.open(self._path, "wb") as f_out:
shutil.copyfileobj(tf, f_out) shutil.copyfileobj(tf, f_out)
except OSError as e: except OSError as e:
log(f"{self.__class__.__name__} [download *.xml] error: {e}") log(f"{self.__class__.__name__} [download *.xml] error: {e}")
if IS_WIN and os.path.isfile(tf.name): if IS_WIN and os.path.isfile(tf.name):
tf.close() tf.close()
os.remove(tf.name) os.remove(tf.name)
else: else:
log(f"{self.__class__.__name__} [download] error: {resp.reason}") log(f"{self.__class__.__name__} [download] error: {resp.reason}")
except requests.exceptions.RequestException as e:
log(f"{self.__class__.__name__} [download] error: {e}")
return
if clb: if clb:
clb() clb()

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -31,9 +31,13 @@ import os
import re import re
import shutil import shutil
import subprocess import subprocess
import tempfile
from collections import namedtuple from collections import namedtuple
from datetime import datetime
from enum import IntEnum from enum import IntEnum
from html.parser import HTMLParser from html.parser import HTMLParser
from io import BytesIO
from pathlib import Path
import requests import requests
@@ -74,46 +78,76 @@ class PiconsCzDownloader:
self._provider_logos = {} self._provider_logos = {}
self._picon_ids = picon_ids self._picon_ids = picon_ids
self._appender = appender self._appender = appender
self._logo_map = self.get_logos_map()
self._name_map = self.get_name_map()
self._perm_cache_file = Path(tempfile.gettempdir()).joinpath("picon_cz_links")
# subprocess creation flags
self._sbp_flags = subprocess.CREATE_NO_WINDOW if IS_WIN else 0
@property
def providers(self):
return self._providers
def init(self): def init(self):
""" Initializes dict with values: download_id -> perm link and provider data. """ """ Initializes dict with values: download_id -> perm link and provider data. """
if self._perm_links: if self._perm_links:
return return
self._HEADER["Referer"] = self._PERM_URL if self._perm_cache_file.exists():
st = self._perm_cache_file.stat()
dif = datetime.now() - datetime.fromtimestamp(st.st_mtime)
# We will update daily.
if dif.days > 0:
self.download_permalinks()
else:
self.download_permalinks()
self.read_permalinks()
def download_permalinks(self):
self._HEADER["Referer"] = self._PERM_URL
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request: with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
if request.reason == "OK": if request.reason == "OK":
logo_map = self.get_logos_map() log(f"{self.__class__.__name__}: downloading permalinks file...")
name_map = self.get_name_map() buf = BytesIO()
[buf.write(chunk) for chunk in request.iter_content(chunk_size=128)]
buf.seek(0)
for line in request.iter_lines(): self._perm_cache_file.touch()
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1) self._perm_cache_file.write_bytes(buf.read())
if len(data) != 2:
continue
l_id, perm_link = data
self._perm_links[str(l_id)] = str(perm_link)
data = re.match(self._LINK_PATTERN, perm_link)
if data:
sat_pos = data.group(3)
# Logo url.
logo = logo_map.get(data.group(2), None)
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
self._providers[sat_pos].append(prv)
else:
self._providers[sat_pos] = [prv]
else: else:
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}") log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
raise PiconsError(request.reason) raise PiconsError(request.reason)
@property def read_permalinks(self):
def providers(self): with self._perm_cache_file.open(encoding="utf-8", errors="ignore") as f:
return self._providers for l in f.readlines():
data = l.split(maxsplit=1)
if len(data) != 2:
continue
data = l.split(maxsplit=1)
if len(data) != 2:
continue
l_id, perm_link = data
self._perm_links[str(l_id)] = str(perm_link)
self.update_provider_data(l_id, perm_link)
def update_provider_data(self, l_id, perm_link):
data = re.match(self._LINK_PATTERN, perm_link)
if data:
sat_pos = data.group(3)
# Logo url.
logo = self._logo_map.get(data.group(2), None)
l_name = self._name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
self._providers[sat_pos].append(prv)
else:
self._providers[sat_pos] = [prv]
def get_sat_providers(self, url): def get_sat_providers(self, url):
return self._providers.get(url, []) return self._providers.get(url, [])
@@ -149,7 +183,10 @@ class PiconsCzDownloader:
cmd = [exe, "l", src] cmd = [exe, "l", src]
try: try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() out, err = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=self._sbp_flags).communicate()
if err: if err:
log(f"{self.__class__.__name__} [extract] error: {err}") log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err) raise PiconsError(err)
@@ -174,7 +211,10 @@ class PiconsCzDownloader:
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"] cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
cmd.extend(to_extract) cmd.extend(to_extract)
try: try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() out, err = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=self._sbp_flags).communicate()
if err: if err:
log(f"{self.__class__.__name__} [extract] error: {err}") log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err) raise PiconsError(err)
@@ -207,7 +247,8 @@ class PiconsCzDownloader:
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
log(f"{self.__class__.__name__} error [get provider logo]: {e}") log(f"{self.__class__.__name__} error [get provider logo]: {e}")
def get_logos_map(self): @staticmethod
def get_logos_map():
return {"piconblack": "b50", return {"piconblack": "b50",
"picontransparent": "t50", "picontransparent": "t50",
"piconwhite": "w50", "piconwhite": "w50",
@@ -232,7 +273,8 @@ class PiconsCzDownloader:
"piconSNPblack": "b50", "piconSNPblack": "b50",
} }
def get_name_map(self): @staticmethod
def get_name_map():
return {"antiksat": "ANTIK", return {"antiksat": "ANTIK",
"digiczsk": "DIGI", "digiczsk": "DIGI",
"DTTitaly": "picon_trs-it", "DTTitaly": "picon_trs-it",

View File

@@ -338,7 +338,7 @@ class SatellitesParser(HTMLParser):
self.FEC.get(fec, None), self.FEC.get(fec, None),
self.SYSTEM.get(sys, None), self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None), self.MODULATION.get(mod, None),
pls_mode, pls_code, None, None) pls_mode, pls_code, None, None, None)
if is_transponder_valid(tr): if is_transponder_valid(tr):
trs.append(tr) trs.append(tr)
@@ -379,7 +379,7 @@ class SatellitesParser(HTMLParser):
self.FEC.get(fec, None), self.FEC.get(fec, None),
self.SYSTEM.get(sys, None), self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None), self.MODULATION.get(mod, None),
pls_mode, pls_code, is_id, None) pls_mode, pls_code, is_id, None, None)
if is_transponder_valid(tr): if is_transponder_valid(tr):
trs.append(tr) trs.append(tr)
@@ -421,7 +421,7 @@ class SatellitesParser(HTMLParser):
self.FEC.get(fec, None), self.FEC.get(fec, None),
self.SYSTEM.get(sys, None), self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None), self.MODULATION.get(mod, None),
pls_id, pls_code, is_id, None) pls_id, pls_code, is_id, None, None)
if is_transponder_valid(tr): if is_transponder_valid(tr):
trs.append(tr) trs.append(tr)

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -223,11 +223,11 @@ class BackupDialog:
self._settings.add("backup_tool_window_size", window.get_size()) self._settings.add("backup_tool_window_size", window.get_size())
def on_key_release(self, view, event): def on_key_release(self, view, event):
""" Handling keystrokes """ """ Handling keystrokes. """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@ from gi.repository.GObject import BindingFlags
from app.commons import log, run_task from app.commons import log, run_task
from app.connections import UtfFTP from app.connections import UtfFTP
from app.settings import IS_DARWIN from app.settings import IS_DARWIN, IS_WIN
from app.ui.dialogs import translate, get_chooser_dialog, show_dialog, DialogType from app.ui.dialogs import translate, get_chooser_dialog, show_dialog, DialogType
from app.ui.main_helper import get_picon_pixbuf, redraw_image from app.ui.main_helper import get_picon_pixbuf, redraw_image
from app.ui.uicommons import HeaderBar from app.ui.uicommons import HeaderBar
@@ -64,6 +64,8 @@ class BootLogoManager(Gtk.Window):
self._exe = f"{'./' if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') else ''}ffmpeg" self._exe = f"{'./' if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') else ''}ffmpeg"
self._pix = None self._pix = None
self._img_path = None self._img_path = None
# subprocess creation flags
self._sbp_flags = subprocess.CREATE_NO_WINDOW if IS_WIN else 0
margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5} margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5}
base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10} base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10}
@@ -209,7 +211,9 @@ class BootLogoManager(Gtk.Window):
def init(self, *args): def init(self, *args):
log(f"{self.__class__.__name__} [init] Checking FFmpeg...") log(f"{self.__class__.__name__} [init] Checking FFmpeg...")
try: try:
out = subprocess.check_output([self._exe, "-version"], stderr=subprocess.STDOUT) out = subprocess.check_output([self._exe, "-version"],
stderr=subprocess.STDOUT,
creationflags=self._sbp_flags)
except FileNotFoundError as e: except FileNotFoundError as e:
msg = translate("Check if FFmpeg is installed!") msg = translate("Check if FFmpeg is installed!")
self._app.show_error_message(f"Error. {e} {msg}") self._app.show_error_message(f"Error. {e} {msg}")
@@ -297,6 +301,11 @@ class BootLogoManager(Gtk.Window):
return return
output = path.parent.joinpath(self._file_combo_box.get_active_id()) output = path.parent.joinpath(self._file_combo_box.get_active_id())
if Path(output).exists():
msg = f"\n{translate('The file already exists!')}\n\n\t{translate('Are you sure?')}"
if show_dialog(DialogType.QUESTION, self, msg) != Gtk.ResponseType.OK:
return True
ffmpeg_output = path.parent.joinpath(f"{self._file_combo_box.get_active_text()}.m2v") ffmpeg_output = path.parent.joinpath(f"{self._file_combo_box.get_active_text()}.m2v")
cmd = [self._exe, cmd = [self._exe,
@@ -322,9 +331,9 @@ class BootLogoManager(Gtk.Window):
# Processing image. # Processing image.
log(f"{self.__class__.__name__} [convert] Converting...") log(f"{self.__class__.__name__} [convert] Converting...")
subprocess.run(cmd) subprocess.run(cmd, creationflags=self._sbp_flags)
if Path(ffmpeg_output).exists(): if Path(ffmpeg_output).exists():
os.rename(ffmpeg_output, output) os.replace(ffmpeg_output, output)
log(f"{self.__class__.__name__} [convert] -> '{output}'. Done!") log(f"{self.__class__.__name__} [convert] -> '{output}'. Done!")
if cmd[2] != self._img_path: if cmd[2] != self._img_path:
@@ -336,7 +345,7 @@ class BootLogoManager(Gtk.Window):
def convert_to_image(self, video_path, img_path): def convert_to_image(self, video_path, img_path):
cmd = [self._exe, "-y", "-i", video_path, img_path] cmd = [self._exe, "-y", "-i", video_path, img_path]
subprocess.run(cmd) subprocess.run(cmd, creationflags=self._sbp_flags)
@run_task @run_task
def download_data(self, f_name): def download_data(self, f_name):

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2024 Dmitriy Yefremov Copyright (c) 2018-2026 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. --> <!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2025 Dmitriy Yefremov --> <!-- interface-copyright 2018-2026 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog"> <object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property> <property name="icon_name">system-help</property>
<property name="type_hint">normal</property> <property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property> <property name="program_name">DemonEditor</property>
<property name="version">3.14.0 Beta</property> <property name="version">3.14.4 Beta</property>
<property name="copyright">2018-2025 Dmitriy Yefremov <property name="copyright">2018-2026 Dmitriy Yefremov
</property> </property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property> <property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property> <property name="website">https://dyefremov.github.io/DemonEditor/</property>
@@ -158,6 +158,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="resizable">False</property> <property name="resizable">False</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="width-request">170</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property> <property name="destroy_with_parent">True</property>
<property name="type_hint">splashscreen</property> <property name="type_hint">splashscreen</property>
@@ -166,19 +167,19 @@ Author: Dmitriy Yefremov
<property name="decorated">False</property> <property name="decorated">False</property>
<child> <child>
<object class="GtkBox" id="wait_dialog_box"> <object class="GtkBox" id="wait_dialog_box">
<property name="width_request">100</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_top">5</property> <property name="margin-top">10</property>
<property name="margin_bottom">5</property> <property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin_end">10</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkSpinner" id="spinner"> <object class="LoadingProgressBar" id="progress">
<property name="width_request">150</property> <property name="visible" bind-source="wait_dialog" bind-property="visible">True</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="active">True</property> <property name="show-text">True</property>
<property name="text" translatable="yes">Loading data...</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -186,24 +187,10 @@ Author: Dmitriy Yefremov
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="wait_dialog_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object> </object>
</child> <!-- NOP --> </child> <!-- NOP -->
<style> <style>
<class name="app-notification"/> <class name="primary-toolbar"/>
</style> </style>
</object> </object>
</interface> </interface>

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -34,7 +34,7 @@ from functools import lru_cache
from pathlib import Path from pathlib import Path
from app.commons import run_idle from app.commons import run_idle
from app.settings import SEP, IS_WIN, USE_HEADER_BAR from app.settings import SEP, USE_HEADER_BAR, IS_LINUX
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
@@ -101,8 +101,8 @@ class WaitDialog:
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient) builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
self._dialog = dialog self._dialog = dialog
self._dialog.set_transient_for(transient) self._dialog.set_transient_for(transient)
self._label = builder.get_object("wait_dialog_label") self._progress = builder.get_object("progress")
self._default_text = text or self._label.get_text() self._default_text = text or self._progress.get_text()
def show(self, text=None): def show(self, text=None):
self.set_text(text) self.set_text(text)
@@ -110,7 +110,7 @@ class WaitDialog:
@run_idle @run_idle
def set_text(self, text): def set_text(self, text):
self._label.set_text(translate(text or self._default_text)) self._progress.set_text(translate(text or self._default_text))
@run_idle @run_idle
def hide(self): def hide(self):
@@ -228,7 +228,7 @@ def translate(message):
@lru_cache(maxsize=5) @lru_cache(maxsize=5)
def get_dialogs_string(path, tag="property"): def get_dialogs_string(path, tag="property"):
if IS_WIN: if not IS_LINUX:
return translate_xml(path, tag) return translate_xml(path, tag)
else: else:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
@@ -257,7 +257,7 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
def translate_xml(path, tag="property"): def translate_xml(path, tag="property"):
""" Used to translate GUI from * .glade files in MS Windows. """ Used to translate GUI from *.glade files to macOS and MS Windows.
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569 More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
""" """

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -568,6 +568,7 @@ class TabEpgSettingsPopover(EpgSettingsPopover):
self._url_combo_box.get_model().clear() self._url_combo_box.get_model().clear()
[self._url_combo_box.append(i, i) for i in settings.epg_xml_sources if i] [self._url_combo_box.append(i, i) for i in settings.epg_xml_sources if i]
self._url_combo_box.set_active_id(settings.epg_xml_source) self._url_combo_box.set_active_id(settings.epg_xml_source)
self._remove_url_button.set_sensitive(len(self._url_combo_box.get_model()) > 1)
def on_apply(self, button): def on_apply(self, button):
settings = self._app.app_settings settings = self._app.app_settings
@@ -1217,10 +1218,10 @@ class EpgDialog:
def on_key_press(self, view, event): def on_key_press(self, view, event):
""" Handling keystrokes """ """ Handling keystrokes """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if ctrl and key is KeyboardKey.C: if ctrl and key is KeyboardKey.C:

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2023-2024 Dmitriy Yefremov # Copyright (c) 2023-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -35,7 +35,7 @@ import requests
from gi.repository import Gtk, Gdk, GLib, Pango, GObject from gi.repository import Gtk, Gdk, GLib, Pango, GObject
from app.commons import log, run_task, run_idle from app.commons import log, run_task, run_idle
from app.ui.dialogs import translate from app.ui.dialogs import translate, show_dialog, DialogType
from app.ui.uicommons import HeaderBar from app.ui.uicommons import HeaderBar
EXT_URL = "https://api.github.com/repos/DYefremov/demoneditor-extensions/contents/extensions/" EXT_URL = "https://api.github.com/repos/DYefremov/demoneditor-extensions/contents/extensions/"
@@ -48,7 +48,7 @@ HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:112.0) Gecko/20100101
class ExtensionManager(Gtk.Window): class ExtensionManager(Gtk.Window):
ICON_INFO = "emblem-important-symbolic" ICON_INFO = "emblem-synchronizing-symbolic"
ICON_UPDATE = "network-receive-symbolic" ICON_UPDATE = "network-receive-symbolic"
class Column(IntEnum): class Column(IntEnum):
@@ -193,6 +193,22 @@ class ExtensionManager(Gtk.Window):
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size())) self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
self.connect("realize", self.init) self.connect("realize", self.init)
self.connect("show", self.on_show)
def on_show(self, window):
enabled = self._app.app_settings.extensions_support
self.set_sensitive(enabled)
if not enabled:
msg = f"\n{translate('Extension support is disabled!')}\n\n\t{translate('Do you want to enable it?')}"
if show_dialog(DialogType.QUESTION, self, msg) != Gtk.ResponseType.OK:
self.close()
return True
self._app.app_settings.extensions_support = True
self._app.show_info_message(translate('Restart the program to apply all changes.'), Gtk.MessageType.WARNING)
self.close()
return False
def init(self, widget): def init(self, widget):
self._load_spinner.start() self._load_spinner.start()
@@ -281,6 +297,7 @@ class ExtensionManager(Gtk.Window):
ext_ver = ext[0].VERSION ext_ver = ext[0].VERSION
path = ext[1] path = ext[1]
if ext_ver < ver: if ext_ver < ver:
desc = f"[ Update -> ver. {ver} ] {desc}"
ver = ext_ver ver = ext_ver
info = self.ICON_INFO info = self.ICON_INFO

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -856,11 +856,10 @@ class FtpClientBox(Gtk.HBox):
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model] self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
def on_view_key_press(self, view, event): def on_view_key_press(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.F7: if key is KeyboardKey.F7:

View File

@@ -426,10 +426,9 @@ class ImportDialog:
def on_key_press(self, view, event): def on_key_press(self, view, event):
""" Handling keystrokes """ """ Handling keystrokes """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE: if key is KeyboardKey.SPACE:
model = view.get_model() model = view.get_model()

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -1331,10 +1331,9 @@ class YtListImportDialog:
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select)) view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_key_press(self, view, event): def on_key_press(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE: if key is KeyboardKey.SPACE:
path, column = view.get_cursor() path, column = view.get_cursor()

Binary file not shown.

View File

@@ -1852,7 +1852,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="app_ver_label"> <object class="GtkLabel" id="app_ver_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="label">3.14.0 Beta</property> <property name="label">3.14.4 Beta</property>
<attributes> <attributes>
<attribute name="weight" value="bold"/> <attribute name="weight" value="bold"/>
</attributes> </attributes>

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -69,7 +69,7 @@ from .iptv import (IptvDialog, SearchUnavailableDialog, IptvListConfigurationDia
from .main_helper import * from .main_helper import *
from .picons import PiconManager from .picons import PiconManager
from .search import SearchProvider from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action from .service_dialog import ServiceDetailsDialog, Action
from .settings_dialog import SettingsDialog from .settings_dialog import SettingsDialog
from .uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, from .uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column,
MOD_MASK, APP_FONT, Page, HeaderBar, LINK_ICON) MOD_MASK, APP_FONT, Page, HeaderBar, LINK_ICON)
@@ -79,7 +79,7 @@ from .xml.edit import SatellitesTool
class Application(Gtk.Application): class Application(Gtk.Application):
""" Main application class. """ """ Main application class. """
VERSION = "3.14.0" VERSION = "3.14.4"
SERVICE_MODEL = "services_list_store" SERVICE_MODEL = "services_list_store"
FAV_MODEL = "fav_list_store" FAV_MODEL = "fav_list_store"
@@ -88,12 +88,13 @@ class Application(Gtk.Application):
IPTV_MODEL = "iptv_list_store" IPTV_MODEL = "iptv_list_store"
DRAG_SEP = "::::" DRAG_SEP = "::::"
MARKER_TYPES = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name} MARKER_TYPES = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
NON_REF_TYPES = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
DEL_FACTOR = 100 # Batch size to delete in one pass. DEL_FACTOR = 100 # Batch size to delete in one pass.
FAV_FACTOR = DEL_FACTOR * 5 FAV_FACTOR = DEL_FACTOR * 5
_TV_TYPES = ("TV", "TV (HD)", "TV (UHD)", "TV (H264)") _TV_TYPES = {"TV", "TV (HD)", "TV (UHD)", "TV (H264)"}
BG_TASK_LIMIT = 5 BG_TASK_LIMIT = 5
@@ -721,18 +722,21 @@ class Application(Gtk.Application):
self.on_iptv_list_configuration, self.on_remove_all_unavailable): self.on_iptv_list_configuration, self.on_remove_all_unavailable):
iptv_elem.bind_property("sensitive", self.set_action(h.__name__, h, False), "enabled") iptv_elem.bind_property("sensitive", self.set_action(h.__name__, h, False), "enabled")
if self._settings.extensions_support: self.init_extensions(builder)
self.init_extensions(builder)
def init_extensions(self, builder): def init_extensions(self, builder):
import pkgutil
from importlib.util import module_from_spec
from app.ui.extensions.management import ExtensionManager from app.ui.extensions.management import ExtensionManager
# Extensions (Plugins) section. # Extensions (Plugins) section.
ext_section = builder.get_object(f"{'mac_' if IS_DARWIN else ''}extension_section") ext_section = builder.get_object(f"{'mac_' if IS_DARWIN else ''}extension_section")
self.set_action("on_extension_manager", lambda a, v: ExtensionManager(self).show()) self.set_action("on_extension_manager", lambda a, v: ExtensionManager(self).show())
ext_section.append_item(Gio.MenuItem.new(translate("Extension Manager"), "app.on_extension_manager")) ext_section.append_item(Gio.MenuItem.new(translate("Extension Manager"), "app.on_extension_manager"))
if not self._settings.extensions_support:
return
import pkgutil
from importlib.util import module_from_spec
ext_path = f"{self._settings.default_data_path}tools{os.sep}extensions" ext_path = f"{self._settings.default_data_path}tools{os.sep}extensions"
ext_paths = [f"{os.path.dirname(__file__)}{os.sep}extensions", ext_path, "extensions"] ext_paths = [f"{os.path.dirname(__file__)}{os.sep}extensions", ext_path, "extensions"]
extensions = {} extensions = {}
@@ -1104,7 +1108,17 @@ class Application(Gtk.Application):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
return True return True
else: else:
GLib.idle_add(self.quit) if len(self._task_box):
msg = f"{translate('There are running background tasks!')}\n\n\t\t{translate('Are you sure?')}"
if show_dialog(DialogType.QUESTION, self._main_window, msg) != Gtk.ResponseType.OK:
return True
log("Terminating the application...")
import signal
os.kill(os.getpid(), signal.SIGTERM)
GLib.idle_add(self.quit, priority=GLib.PRIORITY_HIGH)
return False
def on_main_window_state(self, window, event): def on_main_window_state(self, window, event):
if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED: if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED:
@@ -1214,7 +1228,7 @@ class Application(Gtk.Application):
msg = translate("Restart the program to apply all changes.") msg = translate("Restart the program to apply all changes.")
if value: if value:
warn = "It can cause some problems." warn = translate("It can cause some problems.")
msg = f"{translate('EXPERIMENTAL!')} {warn} {msg}" msg = f"{translate('EXPERIMENTAL!')} {warn} {msg}"
self.show_info_message(msg, Gtk.MessageType.WARNING) self.show_info_message(msg, Gtk.MessageType.WARNING)
@@ -1288,7 +1302,7 @@ class Application(Gtk.Application):
def fav_service_data_func(self, column, renderer, model, itr, data): def fav_service_data_func(self, column, renderer, model, itr, data):
if self._display_epg and self._s_type is SettingsType.ENIGMA_2: if self._display_epg and self._s_type is SettingsType.ENIGMA_2:
srv_name = model.get_value(itr, Column.FAV_SERVICE) srv_name = model.get_value(itr, Column.FAV_SERVICE)
if model.get_value(itr, Column.FAV_TYPE) in self.MARKER_TYPES: if model.get_value(itr, Column.FAV_TYPE) in self.NON_REF_TYPES:
return True return True
event = self._epg_cache.get_current_event(srv_name) event = self._epg_cache.get_current_event(srv_name)
@@ -1473,7 +1487,7 @@ class Application(Gtk.Application):
priority = GLib.PRIORITY_LOW priority = GLib.PRIORITY_LOW
if model_name == self.FAV_MODEL: if model_name == self.FAV_MODEL:
gen = self.remove_favs(itrs, model) gen = self.remove_favorites(itrs, model)
elif model_name == self.BQ_MODEL: elif model_name == self.BQ_MODEL:
gen = self.delete_bouquets(itrs, model) gen = self.delete_bouquets(itrs, model)
priority = GLib.PRIORITY_DEFAULT priority = GLib.PRIORITY_DEFAULT
@@ -1489,18 +1503,24 @@ class Application(Gtk.Application):
return rows return rows
def remove_favs(self, itrs, model): def remove_favorites(self, itrs, model):
""" Deleting bouquet services. """ """ Deleting bouquet services. """
if self._bq_selected: if self._bq_selected:
fav_bouquet = self._bouquets.get(self._bq_selected, None) fav_bouquet = self._bouquets.get(self._bq_selected, None)
if fav_bouquet: if fav_bouquet:
removed = []
for index, itr in enumerate(itrs): for index, itr in enumerate(itrs):
del fav_bouquet[int(model.get_path(itr)[0])] path = model.get_path(itr)
p_index = int(path[0])
removed.append((p_index, tuple(model[path])))
del fav_bouquet[p_index]
self._fav_model.remove(itr) self._fav_model.remove(itr)
if index % self.DEL_FACTOR == 0: if index % self.DEL_FACTOR == 0:
yield True yield True
self.update_fav_num_column(model) self.update_fav_num_column(model)
self.emit("fav-removed", self._bq_selected) self.emit("fav-removed", removed)
self.on_model_changed(self._fav_model) self.on_model_changed(self._fav_model)
self._wait_dialog.hide() self._wait_dialog.hide()
@@ -2130,11 +2150,15 @@ class Application(Gtk.Application):
name, model = get_model_data(view) name, model = get_model_data(view)
self.delete_views_selection(name) self.delete_views_selection(name)
elif event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY: elif event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
self._select_enabled = True
if self._settings.main_list_playback and self._fav_click_mode is not PlaybackMode.DISABLED: if self._settings.main_list_playback and self._fav_click_mode is not PlaybackMode.DISABLED:
if view is self._services_view: if view is self._services_view:
self.emit("srv-clicked", self._fav_click_mode) self.emit("srv-clicked", self._fav_click_mode)
elif view is self._iptv_services_view: elif view is self._iptv_services_view:
self.emit("iptv-clicked", self._fav_click_mode) self.emit("iptv-clicked", self._fav_click_mode)
else:
if view is not self._fav_view:
self.on_edit()
def on_view_release(self, view, event): def on_view_release(self, view, event):
""" Handles a mouse click (release) to view. """ """ Handles a mouse click (release) to view. """
@@ -2272,6 +2296,7 @@ class Application(Gtk.Application):
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings, title="Open folder") response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings, title="Open folder")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return return
self.open_data(response) self.open_data(response)
def on_data_extract(self, app, page): def on_data_extract(self, app, page):
@@ -2347,6 +2372,7 @@ class Application(Gtk.Application):
self._alt_revealer.set_visible(False) self._alt_revealer.set_visible(False)
self._filter_services_button.set_active(False) self._filter_services_button.set_active(False)
self._wait_dialog.show() self._wait_dialog.show()
self._services_progress_bar.show()
yield from self.clear_current_data() yield from self.clear_current_data()
# Reset of sorting # Reset of sorting
@@ -2405,6 +2431,7 @@ class Application(Gtk.Application):
finally: finally:
self._profile_combo_box.set_sensitive(True) self._profile_combo_box.set_sensitive(True)
self._wait_dialog.hide() self._wait_dialog.hide()
self._services_progress_bar.hide()
self.emit("data-load-done", self._settings.current_profile) self.emit("data-load-done", self._settings.current_profile)
def append_data(self, bouquets, services): def append_data(self, bouquets, services):
@@ -2536,7 +2563,6 @@ class Application(Gtk.Application):
break break
def append_services(self, services): def append_services(self, services):
self._services_progress_bar.show()
to_add = [] to_add = []
for srv in services: for srv in services:
if srv.fav_id not in self._services: if srv.fav_id not in self._services:
@@ -2557,7 +2583,6 @@ class Application(Gtk.Application):
self._services_progress_bar.set_fraction(index / size) self._services_progress_bar.set_fraction(index / size)
yield True yield True
self._services_progress_bar.hide()
yield True yield True
def append_iptv_data(self, services=None): def append_iptv_data(self, services=None):
@@ -2779,7 +2804,7 @@ class Application(Gtk.Application):
self._alt_revealer.set_visible(False) self._alt_revealer.set_visible(False)
self.on_info_bar_close() self.on_info_bar_close()
if self._page is Page.EPG and srv.service_type not in self.MARKER_TYPES: if self._page is Page.EPG and srv.service_type not in self.NON_REF_TYPES:
self.emit("fav-changed", srv) self.emit("fav-changed", srv)
def on_services_selection(self, model, path, column): def on_services_selection(self, model, path, column):
@@ -2963,11 +2988,10 @@ class Application(Gtk.Application):
def on_tree_view_key_press(self, view, event): def on_tree_view_key_press(self, view, event):
""" Handling keystrokes on press """ """ Handling keystrokes on press """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return False
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.F: if key is KeyboardKey.F:
if ctrl: if ctrl:
@@ -3016,11 +3040,10 @@ class Application(Gtk.Application):
def on_tree_view_key_release(self, view, event): def on_tree_view_key_release(self, view, event):
""" Handling keystrokes on release """ """ Handling keystrokes on release """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
shift = event.state & Gdk.ModifierType.SHIFT_MASK shift = event.state & Gdk.ModifierType.SHIFT_MASK
model_name, model = get_model_data(view) model_name, model = get_model_data(view)
@@ -3158,9 +3181,9 @@ class Application(Gtk.Application):
def on_fav_press(self, menu, event): def on_fav_press(self, menu, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS: if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
if self._fav_click_mode is PlaybackMode.DISABLED: if self._fav_click_mode is PlaybackMode.DISABLED:
return self.on_service_edit(self._fav_view)
else:
self.emit("fav-clicked", self._fav_click_mode) self.emit("fav-clicked", self._fav_click_mode)
else: else:
return self.on_view_popup_menu(menu, event) return self.on_view_popup_menu(menu, event)
@@ -3245,7 +3268,7 @@ class Application(Gtk.Application):
fav_bqt = self._bouquets.get(self._bq_selected, None) fav_bqt = self._bouquets.get(self._bq_selected, None)
response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._s_type).show() response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._s_type).show()
if response: if response:
gen = self.remove_favs(response, self._fav_model) gen = self.remove_favorites(response, self._fav_model)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_reference_assign(self, view): def on_reference_assign(self, view):
@@ -3676,7 +3699,7 @@ class Application(Gtk.Application):
row = self._fav_model[path][:] row = self._fav_model[path][:]
srv_type, fav_id = row[Column.FAV_TYPE], row[Column.FAV_ID] srv_type, fav_id = row[Column.FAV_TYPE], row[Column.FAV_ID]
if srv_type in self.MARKER_TYPES and show_error: if srv_type in self.NON_REF_TYPES and show_error:
self.show_error_message("Not allowed in this context!") self.show_error_message("Not allowed in this context!")
return return
@@ -4036,7 +4059,7 @@ class Application(Gtk.Application):
if srv_type == BqServiceType.ALT.name: if srv_type == BqServiceType.ALT.name:
return self.show_error_message("Operation not allowed in this context!") return self.show_error_message("Operation not allowed in this context!")
if srv_type in self.MARKER_TYPES: if srv_type in self.NON_REF_TYPES:
return self.on_rename(view) return self.on_rename(view)
elif srv_type == BqServiceType.IPTV.name: elif srv_type == BqServiceType.IPTV.name:
return self.on_iptv_service_edit(model[paths][Column.FAV_ID], view) return self.on_iptv_service_edit(model[paths][Column.FAV_ID], view)
@@ -4166,7 +4189,7 @@ class Application(Gtk.Application):
""" Marks services with duplicate [names] in the fav list. """ """ Marks services with duplicate [names] in the fav list. """
from collections import Counter from collections import Counter
dup = Counter(r[Column.FAV_SERVICE] for r in self._fav_model if r[Column.FAV_TYPE] not in self.MARKER_TYPES) dup = Counter(r[Column.FAV_SERVICE] for r in self._fav_model if r[Column.FAV_TYPE] not in self.NON_REF_TYPES)
dup = {k for k, v in dup.items() if v > 1} dup = {k for k, v in dup.items() if v > 1}
for r in self._fav_model: for r in self._fav_model:
@@ -4187,7 +4210,7 @@ class Application(Gtk.Application):
if count: if count:
if show_dialog(DialogType.QUESTION, self._main_window) != Gtk.ResponseType.OK: if show_dialog(DialogType.QUESTION, self._main_window) != Gtk.ResponseType.OK:
return return
gen = self.remove_favs(to_remove, self._fav_model) gen = self.remove_favorites(to_remove, self._fav_model)
GLib.idle_add(lambda: next(gen, False)) GLib.idle_add(lambda: next(gen, False))
self.show_info_message(f"{translate('Done!')} {translate('Removed')}: {count}") self.show_info_message(f"{translate('Done!')} {translate('Removed')}: {count}")
else: else:
@@ -4527,11 +4550,10 @@ class Application(Gtk.Application):
self.emit("fav-changed", srv) self.emit("fav-changed", srv)
def on_alt_view_key_press(self, view, event): def on_alt_view_key_press(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if ctrl and key == KeyboardKey.V: if ctrl and key == KeyboardKey.V:

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -976,11 +976,10 @@ class PiconManager(Gtk.Box):
return True return True
def on_tree_view_key_press(self, view, event): def on_tree_view_key_press(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:
self.on_local_remove(view) self.on_local_remove(view)

View File

@@ -390,7 +390,7 @@ class PlayerBox(Gtk.Overlay):
width, height = size width, height = size
if self._playback_window: if self._playback_window:
self._playback_window.show() self._playback_window.present()
self._playback_window.set_title(title or self.get_playback_title()) self._playback_window.set_title(title or self.get_playback_title())
else: else:
self._playback_window = Gtk.Window(title=title or self.get_playback_title(), self._playback_window = Gtk.Window(title=title or self.get_playback_title(),

View File

@@ -304,11 +304,10 @@ class RecordingsTool(Gtk.Box):
self._filter_entry.grab_focus() if button.get_active() else self._filter_entry.set_text("") self._filter_entry.grab_focus() if button.get_active() else self._filter_entry.set_text("")
def on_recordings_key_press(self, view, event): def on_recordings_key_press(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:
self.on_recording_remove() self.on_recording_remove()

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2024 Dmitriy Yefremov Copyright (c) 2018-2026 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov --> <!-- interface-copyright 2018-2026 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="fec_list_store"> <object class="GtkListStore" id="fec_list_store">
<columns> <columns>

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -41,7 +41,7 @@ from .dialogs import show_dialog, DialogType, Action, get_builder
from .main_helper import get_base_model, scroll_to from .main_helper import get_base_model, scroll_to
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade" _UI_PATH = f"{UI_RESOURCES_PATH}service_dialog.glade"
class ServiceDetailsDialog: class ServiceDetailsDialog:
@@ -92,15 +92,15 @@ class ServiceDetailsDialog:
self._transponder_services_iters = None self._transponder_services_iters = None
self._current_model = None self._current_model = None
self._current_itr = None self._current_itr = None
# Patterns # Patterns.
self._DIGIT_PATTERN = re.compile("\\D") self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)") self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*") self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{1,4})(,c:[0-9]{2}[0-9a-fA-F]{1,4})*?") self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{1,4})(,c:[0-9]{2}[0-9a-fA-F]{1,4})*?")
# Buttons # Buttons.
self._apply_button = builder.get_object("apply_button") self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button") self._create_button = builder.get_object("create_button")
# style # Style.
self._style_provider = Gtk.CssProvider() self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css") self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
# initialization only digit elements # initialization only digit elements
@@ -352,7 +352,7 @@ class ServiceDetailsDialog:
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8])) self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9])) self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name) self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
self._tr_flag_entry.set_text(tr_data[7]) self._tr_flag_entry.set_text(tr_data[6])
if data_len > 12: if data_len > 12:
self._stream_id_entry.set_text(tr_data[11]) self._stream_id_entry.set_text(tr_data[11])
self._pls_code_entry.set_text(tr_data[12]) self._pls_code_entry.set_text(tr_data[12])
@@ -411,6 +411,12 @@ class ServiceDetailsDialog:
""" Sat positions initialisation """ """ Sat positions initialisation """
self._sat_pos_button.set_value(float(sat_pos[:-1])) self._sat_pos_button.set_value(float(sat_pos[:-1]))
self._pos_side_box.set_active_id(sat_pos[-1:]) self._pos_side_box.set_active_id(sat_pos[-1:])
self._sat_pos_button.connect("value-changed", self.on_sat_value_changed)
def on_sat_value_changed(self, button):
pos = int(self.get_sat_position())
namespace = int(f"{3600 - abs(pos) if pos < 0 else pos:04x}0000", 16)
self._namespace_entry.set_text(str(namespace))
def on_system_changed(self, box): def on_system_changed(self, box):
if not self._tr_edit_switch.get_active(): if not self._tr_edit_switch.get_active():
@@ -463,7 +469,7 @@ class ServiceDetailsDialog:
service, data = srv_data service, data = srv_data
itr = self._current_model.append(service + (None, data.get(Column.SRV_BACKGROUND, None))) itr = self._current_model.append(service + (None, data.get(Column.SRV_BACKGROUND, None)))
scroll_to(self._current_model.get_path(itr), self._services_view) scroll_to(self._current_model.get_path(itr), self._services_view)
return True return True
def on_edit(self): def on_edit(self):
@@ -678,14 +684,13 @@ class ServiceDetailsDialog:
sat_pos = self.get_sat_position() sat_pos = self.get_sat_position()
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id()) inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!! flag = self._tr_flag_entry.get_text() or "0"
if self._s_type is SettingsType.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) dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, flag)
if sys == "DVB-S": if sys == "DVB-S":
return dvb_s_tr return dvb_s_tr
if sys == "DVB-S2": if sys == "DVB-S2":
flag = self._tr_flag_entry.get_text()
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION)
roll_off = self.get_value_from_combobox_id(self._rolloff_combo_box, ROLL_OFF) roll_off = self.get_value_from_combobox_id(self._rolloff_combo_box, ROLL_OFF)
pilot = get_value_by_name(Pilot, self._pilot_combo_box.get_active_id()) pilot = get_value_by_name(Pilot, self._pilot_combo_box.get_active_id())
@@ -694,7 +699,7 @@ class ServiceDetailsDialog:
st_id = self._stream_id_entry.get_text() st_id = self._stream_id_entry.get_text()
pls = f":{st_id}:{pls_code}:{pls_mode}" if pls_mode and pls_code and st_id else "" pls = f":{st_id}:{pls_code}:{pls_mode}" if pls_mode and pls_code and st_id else ""
return f"{dvb_s_tr}:{flag}:{mod}:{roll_off}:{pilot}{pls}" return f"{dvb_s_tr}:1:{mod}:{roll_off}:{pilot}{pls}"
elif self._s_type is SettingsType.NEUTRINO_MP: elif self._s_type is SettingsType.NEUTRINO_MP:
tr_data = get_attributes(self._old_service.transponder) tr_data = get_attributes(self._old_service.transponder)

View File

@@ -2418,6 +2418,7 @@ Author: Dmitriy Yefremov
<item id="nl_NL">Nederlands</item> <item id="nl_NL">Nederlands</item>
<item id="pl_PL">Polski</item> <item id="pl_PL">Polski</item>
<item id="pt_PT">Português</item> <item id="pt_PT">Português</item>
<item id="sk_SK">Slovák</item>
<item id="tr_TR">Türkçe</item> <item id="tr_TR">Türkçe</item>
<item id="be_BY">Беларуская</item> <item id="be_BY">Беларуская</item>
<item id="ru_RU">Русский</item> <item id="ru_RU">Русский</item>

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -692,7 +692,7 @@ class SettingsDialog:
if mode is PlaybackMode.PLAY: if mode is PlaybackMode.PLAY:
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING) self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
elif mode is PlaybackMode.STREAM: elif mode is PlaybackMode.STREAM:
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING) self.show_info_message("Playback of IPTV streams only!", Gtk.MessageType.WARNING)
elif mode is PlaybackMode.DISABLED: elif mode is PlaybackMode.DISABLED:
self._allow_main_list_playback_switch.set_active(False) self._allow_main_list_playback_switch.set_active(False)
else: else:

View File

@@ -49,6 +49,10 @@ paned.vertical > separator {
background-size: 24px 2px; background-size: 24px 2px;
} }
progressbar > trough {
min-width: 75px;
}
.red-button { .red-button {
background-image: none; background-image: none;
background-color: red; background-color: red;

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2022 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +24,8 @@
# #
# Author: Dmitriy Yefremov # Author: Dmitriy Yefremov
# #
from app.ui.dialogs import translate from app.ui.dialogs import translate
from .uicommons import Gtk, GLib from .uicommons import Gtk, GLib
@@ -52,14 +54,13 @@ class BGTaskWidget(Gtk.Box):
self.pack_start(close_button, False, False, 0) self.pack_start(close_button, False, False, 0)
self.show_all() self.show_all()
# Just prototype. -> It may not work properly! # Just prototype. -> It may not work properly!
# TODO: Different options need to be tested. Possibly with normal threads. from gi.repository.Gio import Task, Cancellable
from concurrent.futures import ThreadPoolExecutor
self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT) self._task = Task.new(self, Cancellable.new(), lambda s, t: GLib.idle_add(self._app.emit, "task-done", self))
future = self._executor.submit(target, *args) self._task.set_priority(GLib.PRIORITY_LOW)
future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self)) self._task.set_return_on_cancel(True)
self._task.run_in_thread(lambda t, s, d, c: target(*args))
@property @property
def text(self): def text(self):
@@ -78,7 +79,10 @@ class BGTaskWidget(Gtk.Box):
self.set_tooltip_text(value) self.set_tooltip_text(value)
def cancel(self): def cancel(self):
self._executor.shutdown(wait=False) cancelable = self._task.get_cancellable()
if cancelable:
cancelable.cancel()
self._app.emit("task-canceled", None) self._app.emit("task-canceled", None)

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -129,11 +129,10 @@ class TelnetClient(Gtk.Box):
self.do_command() self.do_command()
return True return True
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return None return False
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if ctrl and key is KeyboardKey.C: if ctrl and key is KeyboardKey.C:
if self._tn and self._tn.sock: if self._tn and self._tn.sock:

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -460,11 +460,10 @@ class TimerTool(Gtk.Box):
on_popup_menu(menu, event) on_popup_menu(menu, event)
def on_timers_key_release(self, view, event): def on_timers_key_release(self, view, event):
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -290,14 +290,8 @@ class Column(IntEnum):
# *************** Keyboard keys *************** # # *************** Keyboard keys *************** #
class BaseKeyboardKey(Enum):
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
if IS_LINUX: if IS_LINUX:
class KeyboardKey(BaseKeyboardKey): class KeyboardKey(IntEnum):
""" The raw(hardware) codes [Linux] of the keyboard keys. """ """ The raw(hardware) codes [Linux] of the keyboard keys. """
E = 26 E = 26
R = 27 R = 27
@@ -335,8 +329,14 @@ if IS_LINUX:
PAGE_UP_KP = 81 PAGE_UP_KP = 81
PAGE_DOWN_KP = 89 PAGE_DOWN_KP = 89
UNDEFINED = -1
@classmethod
def _missing_(cls, value):
return cls.UNDEFINED
elif IS_DARWIN: elif IS_DARWIN:
class KeyboardKey(BaseKeyboardKey): class KeyboardKey(IntEnum):
""" The raw(hardware) codes [macOS] of the keyboard keys. """ """ The raw(hardware) codes [macOS] of the keyboard keys. """
F = 3 F = 3
E = 14 E = 14
@@ -376,8 +376,14 @@ elif IS_DARWIN:
PAGE_UP_KP = -1 PAGE_UP_KP = -1
PAGE_DOWN_KP = -1 PAGE_DOWN_KP = -1
UNDEFINED = -1
@classmethod
def _missing_(cls, value):
return cls.UNDEFINED
else: else:
class KeyboardKey(BaseKeyboardKey): class KeyboardKey(IntEnum):
""" The raw(hardware) codes [Windows] of the keyboard keys. """ """ The raw(hardware) codes [Windows] of the keyboard keys. """
E = 69 E = 69
R = 82 R = 82
@@ -415,6 +421,12 @@ else:
PAGE_UP_KP = -1 PAGE_UP_KP = -1
PAGE_DOWN_KP = -1 PAGE_DOWN_KP = -1
UNDEFINED = -1
@classmethod
def _missing_(cls, value):
return cls.UNDEFINED
# Keys for move in lists. KEY_KP_(NAME) for laptop! # Keys for move in lists. KEY_KP_(NAME) for laptop!
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP, MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
@@ -422,5 +434,27 @@ MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.HOME_KP, KeyboardKey.END_KP,
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP} KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
class LoadingProgressBar(Gtk.ProgressBar):
""" A custom class for a progress bar.
Used as an alternative to Gtk.Spinner to reduce CPU load.
"""
__gtype_name__ = "LoadingProgressBar"
def __init__(self, **properties):
super().__init__(**properties)
self.connect("notify::visible", self.on_visible)
def on_visible(self, bar, param):
if self.get_visible():
GLib.timeout_add(500, self.update, priority=GLib.PRIORITY_LOW)
def update(self):
self.pulse()
return self.get_visible()
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 <!-- Generated with glade 3.40.0
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2024 Dmitriy Yefremov Copyright (c) 2018-2025 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-css-provider-path style.css --> <!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov --> <!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<!-- n-columns=2 n-rows=4 --> <!-- n-columns=2 n-rows=4 -->
<object class="GtkGrid" id="cable_tr_box"> <object class="GtkGrid" id="cable_tr_box">
@@ -879,7 +879,7 @@ Author: Dmitriy Yefremov
</packing> </packing>
</child> </child>
<child> <child>
<!-- n-columns=2 n-rows=4 --> <!-- n-columns=2 n-rows=5 -->
<object class="GtkGrid" id="tr_dialog_grid2"> <object class="GtkGrid" id="tr_dialog_grid2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
@@ -911,7 +911,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property> <property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property> <property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property> <property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 262142</property> <property name="placeholder-text">0 - 262142</property>
<property name="input-purpose">digits</property> <property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/> <signal name="changed" handler="on_entry_changed" swapped="no"/>
</object> </object>
@@ -928,7 +928,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property> <property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property> <property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property> <property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 255</property> <property name="placeholder-text">0 - 255</property>
<property name="input-purpose">digits</property> <property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/> <signal name="changed" handler="on_entry_changed" swapped="no"/>
</object> </object>
@@ -945,7 +945,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property> <property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property> <property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property> <property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 255</property> <property name="placeholder-text">0 - 255</property>
<property name="input-purpose">digits</property> <property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/> <signal name="changed" handler="on_entry_changed" swapped="no"/>
</object> </object>
@@ -1095,6 +1095,58 @@ Author: Dmitriy Yefremov
<property name="top-attach">3</property> <property name="top-attach">3</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="tr_t2mi_pid_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">T2-MI PID</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="t2mi_pid_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="width-chars">5</property>
<property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="placeholder-text">0 - 8191</property>
<property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2024 Dmitriy Yefremov # Copyright (c) 2018-2025 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -207,6 +207,7 @@ class SatTransponderDialog(TransponderDialog):
self._pls_code_entry = builder.get_object("pls_code_entry") self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry") self._is_id_entry = builder.get_object("is_id_entry")
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry") self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
self._t2mi_pid_entry = builder.get_object("t2mi_pid_entry")
self.set_style_provider(self._freq_entry) self.set_style_provider(self._freq_entry)
self.set_style_provider(self._rate_entry) self.set_style_provider(self._rate_entry)
@@ -230,6 +231,7 @@ class SatTransponderDialog(TransponderDialog):
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "") self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "") self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "") self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
self._t2mi_pid_entry.set_text(transponder.t2mi_pid if transponder.t2mi_pid else "")
def to_transponder(self): def to_transponder(self):
return Transponder(frequency=self._freq_entry.get_text(), return Transponder(frequency=self._freq_entry.get_text(),
@@ -241,7 +243,8 @@ class SatTransponderDialog(TransponderDialog):
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()), pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(), pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text(), is_id=self._is_id_entry.get_text(),
t2mi_plp_id=self._t2mi_plp_id_entry.get_text()) t2mi_plp_id=self._t2mi_plp_id_entry.get_text(),
t2mi_pid=self._t2mi_pid_entry.get_text())
def is_accept(self): def is_accept(self):
tr = self.to_transponder() tr = self.to_transponder()
@@ -255,6 +258,8 @@ class SatTransponderDialog(TransponderDialog):
return False return False
elif self.digit_pattern.search(tr.t2mi_plp_id): elif self.digit_pattern.search(tr.t2mi_plp_id):
return False return False
elif self.digit_pattern.search(tr.t2mi_pid):
return False
return True return True

View File

@@ -2,7 +2,7 @@
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) 2018-2025 Dmitriy Yefremov # Copyright (c) 2018-2026 Dmitriy Yefremov
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -239,12 +239,6 @@ class SatellitesTool(Gtk.Box):
self._transponders_stack.set_visible_child_name(self._dvb_type) self._transponders_stack.set_visible_child_name(self._dvb_type)
self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT) self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT)
if self._dvb_type is self.DVB.SAT:
self._app.on_info_bar_close()
else:
self._app.show_info_message("EXPERIMENTAL!", Gtk.MessageType.WARNING)
def on_satellite_selection(self, view): def on_satellite_selection(self, view):
model = self._sat_tr_view.get_model() model = self._sat_tr_view.get_model()
model.clear() model.clear()
@@ -310,11 +304,10 @@ class SatellitesTool(Gtk.Box):
def on_key_press(self, view, event): def on_key_press(self, view, event):
""" Handling keystrokes. """ """ Handling keystrokes. """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:
@@ -329,12 +322,11 @@ class SatellitesTool(Gtk.Box):
view.do_unselect_all(view) view.do_unselect_all(view)
def on_tr_key_press(self, view, event): def on_tr_key_press(self, view, event):
""" Handling transponder view keystrokes. """ """ Handling transponder view keystrokes. """
key_code = event.hardware_keycode key = KeyboardKey(event.hardware_keycode)
if not KeyboardKey.value_exist(key_code): if key is KeyboardKey.UNDEFINED:
return return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:

View File

@@ -128,6 +128,8 @@ Author: Dmitriy Yefremov
<column type="gchararray"/> <column type="gchararray"/>
<!-- column-name t2mi_plp_id --> <!-- column-name t2mi_plp_id -->
<column type="gchararray"/> <column type="gchararray"/>
<!-- column-name t2mi_pid -->
<column type="gchararray"/>
</columns> </columns>
<signal name="row-deleted" handler="on_sat_tr_model_changed" swapped="no"/> <signal name="row-deleted" handler="on_sat_tr_model_changed" swapped="no"/>
<signal name="row-inserted" handler="on_sat_tr_model_changed" swapped="no"/> <signal name="row-inserted" handler="on_sat_tr_model_changed" swapped="no"/>

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
VER="3.14.0_Beta" VER="3.14.4_Beta"
B_PATH="dist/DemonEditor" B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor" DEB_PATH="$B_PATH/usr/share/demoneditor"

View File

@@ -1,5 +1,5 @@
Package: demon-editor Package: demon-editor
Version: 3.14.0-Beta Version: 3.14.4-Beta
Section: utils Section: utils
Priority: optional Priority: optional
Architecture: all Architecture: all

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: * Files: *
MIT License MIT License
Copyright (c) 2018-2025 Dmitriy Yefremov Copyright (c) 2018-2026 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -32,13 +32,13 @@ a = Analysis([EXE_NAME],
pathex=PATH_EXE, pathex=PATH_EXE,
binaries=None, binaries=None,
datas=ui_files, datas=ui_files,
hiddenimports=['fileinput', 'uuid', 'asyncio'], hiddenimports=['fileinput', 'uuid', 'asyncio', 'getpass'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
hooksconfig={ hooksconfig={
"gi": { "gi": {
"languages": ["en", "be", "es", "it", "nl", "languages": ["en", "be", "es", "it", "nl", "pl",
"pl", "pt", "ru", "tr", "zh_CN"], "pt", "ru", "sk", "tr", "zh_CN"],
"module-versions": { "module-versions": {
"Gtk": "3.0" "Gtk": "3.0"
}, },
@@ -81,8 +81,8 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor", 'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities', 'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13', 'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"3.14.0.{BUILD_DATE} Beta", 'CFBundleShortVersionString': f"3.14.4.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2018-2025, Dmitriy Yefremov", 'NSHumanReadableCopyright': u"Copyright © 2018-2026, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false', 'NSRequiresAquaSystemAppearance': 'false',
'NSHighResolutionCapable': 'true' 'NSHighResolutionCapable': 'true'
}) })

View File

@@ -7,8 +7,7 @@ PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
block_cipher = None block_cipher = None
excludes = ['app.tools.mpv', excludes = ['gi.repository.Gst',
'gi.repository.Gst',
'gi.repository.GstBase', 'gi.repository.GstBase',
'gi.repository.GstVideo', 'gi.repository.GstVideo',
'youtube_dl', 'youtube_dl',
@@ -30,13 +29,13 @@ a = Analysis([EXE_NAME],
pathex=PATH_EXE, pathex=PATH_EXE,
binaries=[], binaries=[],
datas=ui_files, datas=ui_files,
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio'], hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio', 'getpass'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
hooksconfig={ hooksconfig={
"gi": { "gi": {
"languages": ["en", "be", "es", "it", "nl", "languages": ["en", "be", "es", "it", "nl", "pl",
"pl", "pt", "ru", "tr", "zh_CN"], "pt", "ru", "sk", "tr", "zh_CN"],
"module-versions": { "module-versions": {
"Gtk": "3.0", "Gtk": "3.0",
"GtkSource": "3", "GtkSource": "3",
@@ -48,9 +47,19 @@ a = Analysis([EXE_NAME],
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher, cipher=block_cipher,
noarchive=False) noarchive=False)
pyz = PYZ(a.pure, a.zipped_data, pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher) cipher=block_cipher)
splash = Splash('logo.png',
binaries=a.binaries,
datas=a.datas)
exe = EXE(pyz, exe = EXE(pyz,
splash,
a.scripts, a.scripts,
[], [],
exclude_binaries=True, exclude_binaries=True,
@@ -59,13 +68,16 @@ exe = EXE(pyz,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
contents_directory='.', contents_directory='.',
strip=False, strip=False,
upx=True, upx=False,
console=False, console=False,
icon='icon.ico') icon='icon.ico')
coll = COLLECT(exe, coll = COLLECT(exe,
a.binaries, a.binaries,
a.zipfiles, a.zipfiles,
a.datas, a.datas,
splash.binaries,
strip=False, strip=False,
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],

View File

@@ -0,0 +1,26 @@
Subject: [PATCH] hide_splash
---
Index: app/ui/main.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/app/ui/main.py b/app/ui/main.py
--- a/app/ui/main.py (revision 0fc0ef1d3e80fc84f4da81e1117db63a1f1d3467)
+++ b/app/ui/main.py (date 1771419933854)
@@ -651,6 +651,15 @@
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
+ if hasattr(sys, "_MEIPASS"):
+ import pyi_splash
+
+ if pyi_splash.is_alive():
+ pyi_splash.close()
+
+ if self._main_window.is_suspended():
+ self._main_window.present()
+
def do_shutdown(self):
""" Performs shutdown tasks """
if self._settings.load_last_config:

BIN
build/win/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,14 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import sys
import ssl
if __name__ == "__main__": if __name__ == "__main__":
from multiprocessing import freeze_support if hasattr(sys, "_MEIPASS"):
import os
import ssl
from multiprocessing import freeze_support
os.environ["PYTHONUTF8"] = "1"
freeze_support()
# TODO There needs to be a more "correct" way.
ssl._create_default_https_context = ssl._create_unverified_context
from app.ui.main import start_app from app.ui.main import start_app
os.environ["PYTHONUTF8"] = "1"
# TODO There needs to be a more "correct" way.
ssl._create_default_https_context = ssl._create_unverified_context
freeze_support()
start_app() start_app()

View File

@@ -10,6 +10,7 @@ GenericName[nl]=Enigma2 boeket editor
GenericName[pl]=Edytor bukietów Enigma2 GenericName[pl]=Edytor bukietów Enigma2
GenericName[pt]=Editor de buquês Enigma2 GenericName[pt]=Editor de buquês Enigma2
GenericName[ru]=Редактор букетов Enigma2 GenericName[ru]=Редактор букетов Enigma2
GenericName[sk]=Enigma2 editor balíčkov
GenericName[tr]=Enigma2 buket düzenleyici GenericName[tr]=Enigma2 buket düzenleyici
GenericName[zh_CN]=Enigma2频道编辑器 GenericName[zh_CN]=Enigma2频道编辑器
Comment=Channel and satellite list editor for Enigma2 Comment=Channel and satellite list editor for Enigma2
@@ -21,6 +22,7 @@ Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2 Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
Comment[pt]=Editor de lista de canais e satélites para Enigma2 Comment[pt]=Editor de lista de canais e satélites para Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2 Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[sk]=Editor zoznamu kanálov a satelitov pre Enigma2
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
Comment[zh_CN]=Enigma2频道和卫星列表编辑器 Comment[zh_CN]=Enigma2频道和卫星列表编辑器
Icon=demon-editor Icon=demon-editor

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 Dmitriy Yefremov # Copyright (C) 2018-2026 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# #
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Dmitriy Yefremov" msgstr "Дзмітрый Яфрэмаў"
# Main # Main
msgid "Service" msgid "Service"
@@ -1596,3 +1596,21 @@ msgstr "Захаваць бягучыя змены"
msgid "Create a new service" msgid "Create a new service"
msgstr "Стварыць новы сэрвіс" msgstr "Стварыць новы сэрвіс"
msgid "Extension support is disabled!"
msgstr "Падтрымка пашырэнняў адключана!"
msgid "Do you want to enable it?"
msgstr "Жадаеце ўключыць?"
msgid "Playback of IPTV streams only!"
msgstr "Прайграванне толькі IPTV-патокаў!"
msgid "There are running background tasks!"
msgstr "Ідзе выкананне фонавых задач!"
msgid "Check if FFmpeg is installed!"
msgstr "Праверце, ці ўсталяваны FFmpeg!"
msgid "It can cause some problems."
msgstr "Гэта можа выклікаць некаторыя праблемы."

View File

@@ -1,8 +1,8 @@
# Copyright (C) 2018-2025 Dmitriy Yefremov # Copyright (C) 2018-2026 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# Charly, 2019. # Charly, 2019.
# Dmitriy Yefremov, 2020-2025. # Dmitriy Yefremov, 2020-2026.
# Thomas Schmidt, 2021. # Thomas Schmidt, 2021.
msgid "" msgid ""
msgstr "" msgstr ""
@@ -1610,3 +1610,21 @@ msgstr "Aktuelle Änderungen speichern"
msgid "Create a new service" msgid "Create a new service"
msgstr "Erstellen eines neuen Service" msgstr "Erstellen eines neuen Service"
msgid "Extension support is disabled!"
msgstr "Die Unterstützung für Erweiterungen ist deaktiviert!"
msgid "Do you want to enable it?"
msgstr "Möchtest du aktivieren?"
msgid "Playback of IPTV streams only!"
msgstr "Wiedergabe nur IPTV-Streams!"
msgid "There are running background tasks!"
msgstr "Es laufen Hintergrundprozesse!"
msgid "Check if FFmpeg is installed!"
msgstr "Prüfe ob FFmpeg installiert ist!"
msgid "It can cause some problems."
msgstr "Das kann zu einigen Problemen führen."

View File

@@ -1,11 +1,11 @@
# Copyright (C) 2018-2025 Dmitriy Yefremov # Copyright (C) 2018-2026 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025 Massimo Pissarello <mapi68@gmail.com> # SPDX-FileCopyrightText: 2022, 2023, 2024, 2025, 2026 Massimo Pissarello <mapi68@gmail.com>
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"PO-Revision-Date: 2025-08-30 09:41+0200\n" "PO-Revision-Date: 2026-01-30 17:27+0100\n"
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n" "Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
"Language-Team: Italian <>\n" "Language-Team: Italian <>\n"
"Language: it\n" "Language: it\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 25.08.0\n" "X-Generator: Lokalize 25.12.1\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Massimo Pissarello\nNicola Fanghella" msgstr "Massimo Pissarello\nNicola Fanghella"
@@ -1660,3 +1660,9 @@ msgstr "Salva le modifiche attuali"
msgid "Create a new service" msgid "Create a new service"
msgstr "Crea un nuovo servizio" msgstr "Crea un nuovo servizio"
msgid "Extension support is disabled!"
msgstr "Il supporto dell'estensione è disabilitato!"
msgid "Do you want to enable it?"
msgstr "Vuoi abilitarlo?"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 Dmitriy Yefremov # Copyright (C) 2018-2026 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# #
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Dmitriy Yefremov" msgstr "Дмитрий Ефремов"
# Main # Main
msgid "Service" msgid "Service"
@@ -1593,3 +1593,21 @@ msgstr "Сохранить текущие изменения"
msgid "Create a new service" msgid "Create a new service"
msgstr "Создать новый сервис" msgstr "Создать новый сервис"
msgid "Extension support is disabled!"
msgstr "Поддержка расширений отключена!"
msgid "Do you want to enable it?"
msgstr "Желаете включить?"
msgid "Playback of IPTV streams only!"
msgstr "Воспроизведение только IPTV-потоков!"
msgid "There are running background tasks!"
msgstr "Идет выполнение фоновых задач!"
msgid "Check if FFmpeg is installed!"
msgstr "Проверьте, установлен ли FFmpeg!"
msgid "It can cause some problems."
msgstr "Это может вызывать некоторые проблемы."

1603
po/sk/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: DemonEditor\n" "Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n" "POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2025-08-24 12:50+0300\n" "PO-Revision-Date: 2026-01-25 17:07+0300\n"
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n" "Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
"Language-Team: audi06_19 <info@dreamosat-forum.com>\n" "Language-Team: audi06_19 <info@dreamosat-forum.com>\n"
"Language: tr\n" "Language: tr\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.7\n" "X-Generator: Poedit 3.8\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>" msgstr "audi06_19 <info@dreamosat-forum.com>"
@@ -1627,3 +1627,9 @@ msgstr "Mevcut değişiklikleri kaydet"
msgid "Create a new service" msgid "Create a new service"
msgstr "Yeni bir hizmet oluşturun" msgstr "Yeni bir hizmet oluşturun"
msgid "Extension support is disabled!"
msgstr "Uzantı desteği devre dışı bırakıldı!"
msgid "Do you want to enable it?"
msgstr "Etkinleştirmek ister misiniz?"