Compare commits

..

16 Commits

Author SHA1 Message Date
DYefremov
2926bb4bcd Russian, Belarusian and German translations update 2021-08-12 13:46:05 +03:00
DYefremov
299285c72d fixed switching to full screen mode 2021-08-12 13:37:51 +03:00
DYefremov
c9e256aeb0 minor style changes of the control panel 2021-08-11 10:43:55 +03:00
DYefremov
0c704ae2c8 added recordings tab to the control panel 2021-08-11 10:43:46 +03:00
DYefremov
14eccef755 minor filtering optimization 2021-08-11 10:42:11 +03:00
DYefremov
6a7d660ce0 Chinese translation correction 2021-08-11 10:42:08 +03:00
DYefremov
a4f84802f4 added Chinese translation 2021-08-11 10:42:01 +03:00
DYefremov
56d60fd092 added "Save as" feature 2021-08-11 10:41:52 +03:00
DYefremov
8025bfbd60 minor optimization of fav list loading 2021-08-11 10:40:55 +03:00
DYefremov
01f1211455 data deletion optimization 2021-08-11 10:40:34 +03:00
DYefremov
5551921b69 bump version 2021-08-11 10:40:24 +03:00
DYefremov
0147bfc1f4 loading services list in the background 2021-07-11 23:44:49 +03:00
DYefremov
e8bb215b4d copy tr *.mo file 2021-06-13 17:33:12 +03:00
audi06_19
3f76be2ca3 Turkish translations update (#48)
* Turkish translations update

* Update: Turkish translations update

Co-authored-by: audi06_19 <info@dreamosat-forum.com>
2021-06-13 17:33:05 +03:00
DYefremov
bd97fb4968 added bouquet service data validation 2021-06-13 17:32:41 +03:00
DYefremov
a7baaa2254 bump version 2021-06-13 17:32:19 +03:00
23 changed files with 1729 additions and 54 deletions

View File

@@ -509,6 +509,7 @@ class HttpAPI:
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_TS = "ts.m3u?file="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
@@ -530,6 +531,10 @@ class HttpAPI:
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
# Recordings
RECORDINGS = "movielist?dirname="
REC_DIRS = "getlocations"
REC_CURRENT = "getcurrlocation"
# Screenshot
GRUB = "grab?format=jpg&"
@@ -556,6 +561,17 @@ class HttpAPI:
WAKEUP = "4"
STANDBY = "5"
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,
Request.EPG,
Request.TIMER,
Request.RECORDINGS}
STREAM_REQUESTS = {Request.STREAM,
Request.STREAM_CURRENT,
Request.STREAM_TS}
def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -576,18 +592,14 @@ class HttpAPI:
url = self._base_url + req_type.value
data = self._data
if req_type is self.Request.ZAP or req_type is self.Request.STREAM:
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
url += urllib.parse.quote(ref)
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in (self.Request.REMOTE,
self.Request.POWER,
self.Request.VOL,
self.Request.EPG,
self.Request.TIMER):
elif req_type in self.PARAM_REQUESTS:
url += ref
def done_callback(f):
@@ -631,7 +643,7 @@ class HttpAPI:
def get_response(req_type, url, data=None):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT:
if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
@@ -647,6 +659,11 @@ def get_response(req_type, url, data=None):
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 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
#
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
@@ -166,6 +194,11 @@ class BouquetsReader:
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
data_len = len(srv_data)
if data_len < 10:
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
continue
s_type = srv_data[1]
if s_type == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
@@ -186,7 +219,7 @@ class BouquetsReader:
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if len(srv_data) == 12:
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))

View File

@@ -57,7 +57,7 @@ class PiconsCzDownloader:
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/1.0.8", "Referer": ""}
_HEADER = {"User-Agent": "DemonEditor/1.0.10", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile("\\s+(1_.*\\.png).*")

View File

@@ -41,6 +41,10 @@
<attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save as</attribute>
<attribute name="action">app.on_data_save_as</attribute>
</item>
</section>
<section>
<item>

View File

@@ -325,6 +325,11 @@ Author: Dmitriy Yefremov
<property name="icon_name">view-refresh</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
<object class="GtkImage" id="restart_gui_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -386,8 +391,8 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">10</property>
<property name="stack">stack</property>
@@ -1722,6 +1727,103 @@ audio-volume-medium-symbolic</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport" id="recordings_view_port">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="recordings_list_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">multiple</property>
<property name="activate_on_single_click">False</property>
<signal name="button-press-event" handler="on_recordings_press" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="recordings_dir_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="on_recordings_dir_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_action_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkSearchEntry" id="recordings_filter_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">Filter</property>
<signal name="search-changed" handler="on_recording_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="recordings_remove_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="action_name">app.on_recording_remove</property>
<property name="image">remove_recording_image</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">recordings</property>
<property name="title" translatable="yes">Recordings</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>

View File

@@ -11,7 +11,7 @@ from .dialogs import get_builder
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
from ..connections import HttpAPI, UtfFTP
from ..eparser.ecommons import BqServiceType
@@ -24,6 +24,7 @@ class ControlBox(Gtk.HBox):
EPG = "epg"
TIMERS = "timers"
TIMER = "timer"
RECORDINGS = "recordings"
class EpgRow(Gtk.ListBoxRow):
def __init__(self, event: dict, **properties):
@@ -113,6 +114,75 @@ class ControlBox(Gtk.HBox):
EVENT = 1
CHANGE = 2
class RecordingsRow(Gtk.ListBoxRow):
def __init__(self, movie: dict, **properties):
super().__init__(**properties)
self._movie = movie
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._service = movie.get("e2servicename")
service_label = Gtk.Label()
service_label.set_markup("<b>{}</b>".format(self._service))
self._title = movie.get("e2title", "")
title_label = Gtk.Label(self._title)
self._desc = movie.get("e2description", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start_time = datetime.fromtimestamp(int(movie.get("e2time", "0")))
start_time_label = Gtk.Label()
start_time_label.set_margin_top(5)
start_time_label.set_markup("<b>{}</b>".format(start_time.strftime("%A, %H:%M")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
time_label.set_markup("<b>{}</b>".format(movie.get("e2length", "0")))
info_box = Gtk.HBox()
info_box.set_orientation(Gtk.Orientation.HORIZONTAL)
info_box.set_spacing(10)
info_box.pack_start(start_time_label, False, True, 5)
info_box.pack_end(time_label, False, True, 5)
h_box.add(service_label)
h_box.add(title_label)
h_box.add(description)
h_box.add(info_box)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.set_tooltip_text(movie.get("e2filename", ""))
self.add(h_box)
self.show_all()
@property
def movie(self):
return self._movie
@property
def service(self):
return self._service or ""
@property
def title(self):
return self._title or ""
@property
def desc(self):
return self._desc or ""
@property
def file(self):
return self._movie.get("e2filename", "")
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -129,7 +199,10 @@ class ControlBox(Gtk.HBox):
"on_epg_press": self.on_epg_press,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_timers_press": self.on_timers_press,
"on_timers_drag_data_received": self.on_timers_drag_data_received}
"on_timers_drag_data_received": self.on_timers_drag_data_received,
"on_recordings_press": self.on_recordings_press,
"on_recording_filter_changed": self.on_recording_filter_changed,
"on_recordings_dir_changed": self.on_recordings_dir_changed}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
@@ -185,6 +258,11 @@ class ControlBox(Gtk.HBox):
# DnD initialization for the timer list.
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._timers_list_box.drag_dest_add_text_targets()
# Recordings.
self._recordings_list_box = builder.get_object("recordings_list_box")
self._recordings_list_box.set_filter_func(self.recording_filter_function)
self._recordings_filter_entry = builder.get_object("recordings_filter_entry")
self._recordings_dir_box = builder.get_object("recordings_dir_box")
self.init_actions(app)
self.connect("hide", self.on_hide)
@@ -222,6 +300,8 @@ class ControlBox(Gtk.HBox):
app.set_action("on_timer_cancel", self.on_timer_cancel)
app.set_action("on_timer_begins_set", self.on_timer_begins_set)
app.set_action("on_timer_ends_set", self.on_timer_ends_set)
# Recordings
app.set_action("on_recording_remove", self.on_recording_remove)
@property
def update_epg(self):
@@ -234,6 +314,9 @@ class ControlBox(Gtk.HBox):
if tool is self.Tool.TIMERS:
self.update_timer_list()
if tool is self.Tool.RECORDINGS:
self.update_recordings_list()
if tool is not self.Tool.TIMER:
self._last_tool = tool
@@ -679,3 +762,66 @@ class ControlBox(Gtk.HBox):
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
self.on_timer_add()
context.finish(True, False, time)
# *********************** Recordings *************************** #
def on_recordings_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self._http_api.send(HttpAPI.Request.STREAM_TS,
row.movie.get("e2filename", ""),
self.on_play_recording)
def on_recording_filter_changed(self, entry):
self._recordings_list_box.invalidate_filter()
def recording_filter_function(self, row):
txt = self._recordings_filter_entry.get_text().upper()
return any((not txt, txt in row.service.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def on_recording_remove(self, action, value=None):
""" Removes recordings via FTP. """
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
rows = self._recordings_list_box.get_selected_rows()
if rows:
settings = self._app._settings
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
for r in rows:
resp = ftp.delete_file(r.file)
if resp.startswith("2"):
GLib.idle_add(self._recordings_list_box.remove, r)
else:
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=resp)
break
def on_recordings_dir_changed(self, box: Gtk.ComboBoxText):
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(box.get_active_id()), self.update_recordings_data)
def update_recordings_list(self):
if not len(self._recordings_dir_box.get_model()):
self._http_api.send(HttpAPI.Request.REC_CURRENT, "", self.update_current_rec_dir)
def update_current_rec_dir(self, current):
cur = current.get("e2location", None)
if cur:
self._recordings_dir_box.append(cur, cur)
self._http_api.send(HttpAPI.Request.REC_DIRS, "", self.update_rec_dirs)
def update_rec_dirs(self, dirs):
for d in dirs.get("rec_dirs", []):
self._recordings_dir_box.append(d, d)
@run_idle
def update_recordings_data(self, recordings):
list(map(self._recordings_list_box.remove, (r for r in self._recordings_list_box)))
list(map(lambda r: self._recordings_list_box.add(self.RecordingsRow(r)), recordings.get("recordings", [])))
def on_play_recording(self, m3u):
url = self._app.get_url_from_m3u(m3u)
if url:
self._app.play(url)

View File

@@ -40,7 +40,7 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">1.0.8 Alpha</property>
<property name="version">1.0.10 Alpha</property>
<property name="copyright">2018-2021 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for MS Windows.

Binary file not shown.

View File

@@ -74,8 +74,8 @@ class Application(Gtk.Application):
ALT_MODEL_NAME = "alt_list_store"
DRAG_SEP = "::::"
DEL_FACTOR = 50 # Batch size to delete in one pass.
FAV_FACTOR = DEL_FACTOR * 2
DEL_FACTOR = 100 # Batch size to delete in one pass.
FAV_FACTOR = DEL_FACTOR * 5
_TV_TYPES = ("TV", "TV (HD)", "TV (UHD)", "TV (H264)")
@@ -216,6 +216,7 @@ class Application(Gtk.Application):
self._alt_file = set()
self._alt_counter = 1
self._data_hash = 0
self._filter_cache = {}
# For bouquets with different names of services in bouquet and main list
self._extra_bouquets = {}
self._picons = {}
@@ -300,6 +301,7 @@ class Application(Gtk.Application):
self._tv_count_label = builder.get_object("tv_count_label")
self._radio_count_label = builder.get_object("radio_count_label")
self._data_count_label = builder.get_object("data_count_label")
self._services_load_spinner = builder.get_object("services_load_spinner")
self._signal_level_bar.bind_property("visible", builder.get_object("play_current_service_button"), "visible")
self._signal_level_bar.bind_property("visible", builder.get_object("record_button"), "visible")
self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4)
@@ -330,11 +332,14 @@ class Application(Gtk.Application):
# Filter
self._services_model_filter = builder.get_object("services_model_filter")
self._services_model_filter.set_visible_func(self.services_filter_function)
self._filter_tool_button = builder.get_object("filter_tool_button")
self._filter_entry = builder.get_object("filter_entry")
self._filter_box = builder.get_object("filter_box")
self._filter_types_model = builder.get_object("filter_types_list_store")
self._filter_sat_pos_model = builder.get_object("filter_sat_pos_list_store")
self._filter_only_free_button = builder.get_object("filter_only_free_button")
self._services_load_spinner.bind_property("active", self._filter_tool_button, "sensitive", 4)
self._services_load_spinner.bind_property("active", self._filter_box, "sensitive", 4)
# Player
self._player_box = builder.get_object("player_box")
self._player_event_box = builder.get_object("player_event_box")
@@ -454,6 +459,9 @@ class Application(Gtk.Application):
# Save
self._app_info_box.bind_property("visible", self.set_action("on_data_save", self.on_data_save, False),
"enabled", 4)
self._app_info_box.bind_property("visible", self.set_action("on_data_save_as", self.on_data_save_as, False),
"enabled", 4)
# Control
remote_action = Gio.SimpleAction.new_stateful("on_remote", None, GLib.Variant.new_boolean(False))
remote_action.connect("change-state", self.on_control)
@@ -670,6 +678,11 @@ class Application(Gtk.Application):
if not self._main_window.is_maximized():
self._settings.add("window_size", self._main_window.get_size())
if self._services_load_spinner.get_property("active"):
msg = "{}\n\n\t{}".format(get_message("Data loading in progress!"), get_message("Are you sure?"))
if show_dialog(DialogType.QUESTION, self._main_window, msg) == Gtk.ResponseType.CANCEL:
return True
if self._recorder:
if self._recorder.is_record():
msg = "{}\n\n\t{}".format(get_message("Recording in progress!"), get_message("Are you sure?"))
@@ -807,6 +820,10 @@ class Application(Gtk.Application):
returns deleted rows list!
"""
if self._services_load_spinner.get_property("active"):
show_dialog(DialogType.ERROR, self._main_window, get_message("Data loading in progress!"))
return
selection = view.get_selection()
model, paths = selection.get_selected_rows()
model_name = get_base_model(model).get_name()
@@ -845,6 +862,7 @@ class Application(Gtk.Application):
yield True
self.update_fav_num_column(model)
self.on_model_changed(self._fav_model)
self._wait_dialog.hide()
yield True
@@ -871,6 +889,7 @@ class Application(Gtk.Application):
for f_itr in filter(lambda r: r[Column.FAV_ID] in srv_ids_to_delete, self._fav_model):
self._fav_model.remove(f_itr.iter)
self.on_model_changed(self._services_model)
self.update_fav_num_column(self._fav_model)
self.update_sat_positions()
self._wait_dialog.hide()
@@ -894,6 +913,7 @@ class Application(Gtk.Application):
self._bq_selected = ""
self._bq_name_label.set_text(self._bq_selected)
self.on_model_changed(model)
self._wait_dialog.hide()
yield True
@@ -991,6 +1011,8 @@ class Application(Gtk.Application):
if not is_marker:
num += 1
row[Column.FAV_NUM] = 0 if is_marker else num
self.on_model_changed(model)
yield True
def update_bouquet_list(self):
@@ -1581,6 +1603,7 @@ class Application(Gtk.Application):
def update_data(self, data_path, callback=None):
self._profile_combo_box.set_sensitive(False)
self._alt_revealer.set_visible(False)
self._filter_tool_button.set_active(False)
self._wait_dialog.show()
yield from self.clear_current_data()
@@ -1633,8 +1656,6 @@ class Application(Gtk.Application):
else:
self.append_blacklist(black_list)
yield from self.append_data(bouquets, services)
finally:
self._wait_dialog.hide()
self._profile_combo_box.set_sensitive(True)
if callback:
callback()
@@ -1646,6 +1667,8 @@ class Application(Gtk.Application):
if self._filter_box.get_visible():
self.on_filter_changed()
yield True
finally:
self._wait_dialog.hide()
def append_data(self, bouquets, services):
if self._app_info_box.get_visible():
@@ -1737,7 +1760,9 @@ class Application(Gtk.Application):
# Adding channels to dict with fav_id as keys.
self._services[srv.fav_id] = srv
self.update_services_counts(len(self._services.values()))
factor = self.DEL_FACTOR * 2
self._wait_dialog.hide()
self._services_load_spinner.start()
factor = self.DEL_FACTOR
for index, srv in enumerate(services):
tooltip, background = None, None
@@ -1752,11 +1777,13 @@ class Application(Gtk.Application):
self._services_model.append(s)
if index % factor == 0:
yield True
self._services_load_spinner.stop()
yield True
def clear_current_data(self):
""" Clearing current data from lists """
if len(self._services_model) > self.DEL_FACTOR * 100:
if len(self._services_model) > self.DEL_FACTOR * 50:
self._wait_dialog.set_text("Deleting data...")
self._bouquets_model.clear()
@@ -1802,12 +1829,32 @@ class Application(Gtk.Application):
gen = self.save_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def save_data(self, callback=None):
def on_data_save_as(self, action=None, value=None):
if len(self._bouquets_model) == 0:
self.show_error_dialog("No data to save!")
return
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
create_dir=True)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
if os.listdir(response):
msg = "{}\n\n\t\t{}".format(get_message("The selected folder already contains files!"),
get_message("Are you sure?"))
if show_dialog(DialogType.QUESTION, self._main_window, msg) != Gtk.ResponseType.OK:
return
gen = self.save_data(lambda: show_dialog(DialogType.INFO, self._main_window, "Done!"), response)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def save_data(self, callback=None, ext_path=None):
profile = self._s_type
path = self._settings.data_local_path
path = ext_path or self._settings.data_local_path
backup_path = self._settings.backup_local_path
# Backup data or clearing data path
backup_data(path, backup_path) if self._settings.backup_before_save else clear_data_path(path)
backup_data(path, backup_path) if not ext_path and self._settings.backup_before_save else clear_data_path(path)
yield True
bouquets = []
@@ -1998,6 +2045,7 @@ class Application(Gtk.Application):
yield True
self._fav_view.set_model(self._fav_model)
self.on_model_changed(self._fav_model)
self._bouquets_view.set_sensitive(True)
self._bouquets_view.grab_focus()
yield True
@@ -2156,7 +2204,7 @@ class Application(Gtk.Application):
def on_view_focus(self, view, focus_event=None):
model_name, model = get_model_data(view)
not_empty = len(model) > 0 # if > 0 model has items
not_empty = len(model) > 0 if model else False
is_service = model_name == self.SERVICE_MODEL_NAME
if model_name == self.BQ_MODEL_NAME:
@@ -2212,8 +2260,7 @@ class Application(Gtk.Application):
value = None if value else LOCKED_ICON if flag is Flag.LOCK else HIDE_ICON
model.set_value(itr, 1 if flag is Flag.LOCK else 2, value)
@run_idle
def on_model_changed(self, model, path, itr=None):
def on_model_changed(self, model, path=None, itr=None):
model_name = model.get_name()
if model_name == self.FAV_MODEL_NAME:
@@ -2698,6 +2745,8 @@ class Application(Gtk.Application):
def update_state_on_full_screen(self, visible):
self._main_data_box.set_visible(visible)
self._player_tool_bar.set_visible(visible)
if self._control_box:
self._control_box.set_visible(visible)
self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible())
@run_idle
@@ -2827,7 +2876,7 @@ class Application(Gtk.Application):
GLib.idle_add(self._player_box.set_visible, True)
GLib.idle_add(self._app_info_box.set_visible, False)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.watch)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
def watch(self, data):
url = self.get_url_from_m3u(data)
@@ -3015,7 +3064,7 @@ class Application(Gtk.Application):
# ***************** Filter and search ********************* #
def on_filter_toggled(self, action, value):
if self._app_info_box.get_visible():
if self._app_info_box.get_visible() or self._services_load_spinner.get_property("active"):
return True
action.set_state(value)
@@ -3077,25 +3126,36 @@ class Application(Gtk.Application):
@run_with_delay(2)
def on_filter_changed(self, item=None):
self._services_load_spinner.start()
model = self._services_view.get_model()
self._services_view.set_model(None)
self.update_filter_cache()
self.update_filter_state(model)
@run_idle
def update_filter_state(self, model):
self._services_model_filter.refilter()
self._services_view.set_model(model)
GLib.idle_add(self._services_load_spinner.stop)
def update_filter_cache(self):
self._filter_cache.clear()
if not self._filter_box.is_visible():
return
txt = self._filter_entry.get_text().upper()
for r in self._services_model:
free = not r[Column.SRV_CODED] if self._filter_only_free_button.get_active() else True
self._filter_cache[r[Column.SRV_FAV_ID]] = all((r[Column.SRV_TYPE] in self._service_types,
r[Column.SRV_POS] in self._sat_positions, free,
txt in "".join((r[Column.SRV_SERVICE],
r[Column.SRV_PACKAGE],
r[Column.SRV_TYPE],
r[Column.SRV_SSID],
r[Column.SRV_POS])).upper()))
def services_filter_function(self, model, itr, data):
if not self._filter_box.is_visible():
return True
else:
r_txt = str(model.get(itr, Column.SRV_SERVICE, Column.SRV_PACKAGE, Column.SRV_TYPE, Column.SRV_SSID,
Column.SRV_FREQ, Column.SRV_RATE, Column.SRV_POL, Column.SRV_FEC, Column.SRV_SYSTEM,
Column.SRV_POS)).upper()
txt = self._filter_entry.get_text().upper() in r_txt
free = not model.get(itr, Column.SRV_CODED)[0] if self._filter_only_free_button.get_active() else True
srv_type, pos = model.get(itr, Column.SRV_TYPE, Column.SRV_POS)
return all((srv_type in self._service_types,
pos in self._sat_positions,
txt, free))
return self._filter_cache.get(model.get_value(itr, Column.SRV_FAV_ID), True)
def on_filter_type_toggled(self, toggle, path):
self.update_filter_toogle_model(self._filter_types_model, toggle, path, self._service_types)

View File

@@ -625,7 +625,7 @@ def get_base_paths(paths, model):
def get_model_data(view):
""" Returns model name and base model from the given view """
model = get_base_model(view.get_model())
model_name = model.get_name()
model_name = model.get_name() if model else ""
return model_name, model

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
<!-- Generated with glade 3.22.1
The MIT License (MIT)
@@ -189,7 +189,6 @@ Author: Dmitriy Yefremov
<!-- column-name type -->
<column type="gchararray"/>
</columns>
<signal name="row-deleted" handler="on_model_changed" swapped="no"/>
<signal name="row-inserted" handler="on_model_changed" swapped="no"/>
</object>
<object class="GtkImage" id="control_image">
@@ -222,8 +221,6 @@ Author: Dmitriy Yefremov
<!-- column-name background -->
<column type="GdkRGBA"/>
</columns>
<signal name="row-deleted" handler="on_model_changed" swapped="no"/>
<signal name="row-inserted" handler="on_model_changed" swapped="no"/>
</object>
<object class="GtkMenu" id="fav_popup_menu">
<property name="visible">True</property>
@@ -818,7 +815,6 @@ Author: Dmitriy Yefremov
<!-- column-name background -->
<column type="GdkRGBA"/>
</columns>
<signal name="row-deleted" handler="on_model_changed" swapped="no"/>
</object>
<object class="GtkTreeModelFilter" id="services_model_filter">
<property name="child_model">services_list_store</property>
@@ -2490,6 +2486,22 @@ Author: Dmitriy Yefremov
<property name="position">9</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="services_load_spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Loading data...</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_right">10</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">10</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -3279,7 +3291,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="app_ver_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">1.0.8 Alpha</property>
<property name="label">1.0.10 Alpha</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>

View File

@@ -65,6 +65,7 @@ class PiconsDialog:
self._filter_binding = None
self._services = None
self._current_picon_info = None
self._filter_cache = {}
# Downloader
self._sats = None
self._sat_names = None
@@ -865,8 +866,13 @@ class PiconsDialog:
self._filter_binding.unbind()
self._app.filter_entry.set_text("")
@run_with_delay(1)
@run_with_delay(0.5)
def on_picons_filter_changed(self, entry):
txt = entry.get_text().upper()
self._filter_cache.clear()
for s in self._app.current_services.values():
self._filter_cache[s.picon_id] = txt in s.service.upper()
GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)
@@ -886,8 +892,7 @@ class PiconsDialog:
return True
txt = self._picons_filter_entry.get_text().upper()
return txt in t.upper() or t in (
map(lambda s: s.picon_id, filter(lambda s: txt in s.service.upper(), self._app.current_services.values())))
return txt in t.upper() or self._filter_cache.get(t, False)
def on_picon_activated(self, view):
if self._info_toggle_button.get_active():

View File

@@ -2104,6 +2104,7 @@ Author: Dmitriy Yefremov
<item id="tr_TR" translatable="yes">Türkçe</item>
<item id="be_BY" translatable="yes">Беларуская</item>
<item id="ru_RU" translatable="yes">Русский</item>
<item id="zh_CN" translatable="yes">漢語</item>
</items>
<signal name="changed" handler="on_lang_changed" swapped="no"/>
</object>

View File

@@ -55,4 +55,11 @@ paned > separator {
border-radius: 0;
border-left-width: 0;
border-right-width: 1px;
}
}
.stack-switcher > button {
padding-left: 0.5em;
padding-right: 0.5em;
min-width: 5em;
min-height: 1.5em;
}

View File

@@ -1241,5 +1241,11 @@ msgstr "Загрузіць толькі для абранага букета"
msgid "The task is canceled!"
msgstr "Заданне скасавана!"
msgid "Data loading in progress!"
msgstr "Выконваецца загрузка дадзеных!"
msgid "Recordings"
msgstr "Запісы"
msgid "Help"
msgstr "Даведка"

View File

@@ -1255,5 +1255,11 @@ msgstr "Nur für ausgewähltes Bouquet laden"
msgid "The task is canceled!"
msgstr "Der Task wird abgebrochen!"
msgid "Data loading in progress!"
msgstr "Daten werden geladen!"
msgid "Recordings"
msgstr "Aufnahmen"
msgid "Help"
msgstr "Hilfe"

View File

@@ -1238,5 +1238,11 @@ msgstr "Загрузить только для выбранного букета
msgid "The task is canceled!"
msgstr "Задание отменено!"
msgid "Data loading in progress!"
msgstr "Выполняется загрузка данных!"
msgid "Recordings"
msgstr "Записи"
msgid "Help"
msgstr "Справка"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2021-02-22 23:53+0300\n"
"PO-Revision-Date: 2021-06-13 14:54+0300\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -1023,7 +1023,7 @@ msgid ""
msgstr ""
"Kaydedilmemiş değişiklikler var.\n"
"\n"
"\tŞimdi kaydedilsin mi?"
"\t Şimdi kaydedilsin mi?"
msgid ""
"Are you sure you want to change the order\n"
@@ -1245,3 +1245,27 @@ msgstr "Picon'lar indirin"
msgid "Errors:"
msgstr "Hatalar:"
msgid "Use to play streams:"
msgstr "Akışları oynatmak için kullanın:"
msgid "Font in the lists:"
msgstr "Listelerdeki yazı tipi:"
msgid "Picons size in the lists:"
msgstr "Listelerdeki Piconların boyutu:"
msgid "Logo size in tooltips:"
msgstr "Araç ipuçlarındaki logo boyutu:"
msgid "Save as"
msgstr "Farklı kaydet"
msgid "Mark duplicates"
msgstr "Yinelenenleri işaretle"
msgid "Load only for selected bouquet"
msgstr "Yalnızca seçilen buket için yükle"
msgid "The task is canceled!"
msgstr "Görev iptal edildi!"

1246
po/zh_CN/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff