diff --git a/app/ui/control.glade b/app/ui/control.glade index 042cf7b3..2aaf64bf 100644 --- a/app/ui/control.glade +++ b/app/ui/control.glade @@ -27,13 +27,19 @@ Author: Dmitriy Yefremov --> - + - - + + + + False + + + + 23 1 @@ -85,8 +91,8 @@ Author: Dmitriy Yefremov True True Add timer - app.on_timer_add_from_event True + True @@ -204,10 +210,15 @@ Author: Dmitriy Yefremov True True epg_model + True + True both 2 + - + + multiple + @@ -282,12 +293,14 @@ Author: Dmitriy Yefremov False + left + none True False - 10 - 10 + 5 + 5 5 5 vertical @@ -389,9 +402,9 @@ Author: Dmitriy Yefremov True True True - app.on_timer_begins_set Set True + False @@ -409,12 +422,14 @@ Author: Dmitriy Yefremov False + bottom + none True False - 10 - 10 + 5 + 5 5 5 vertical @@ -516,9 +531,9 @@ Author: Dmitriy Yefremov True True True - app.on_timer_ends_set Set True + False @@ -529,600 +544,557 @@ Author: Dmitriy Yefremov - + + True False - Timer - dialog - - - - - + 5 + 5 + 5 + 5 + 0 + in + + + True False - vertical - 2 - - + center + 5 + 5 + 5 + 5 + 5 + 5 + + + True False - end + Service: + 0 + + + 0 + 3 + + + + + True + False + Description: + 0 + + + 0 + 2 + + + + + True + False + Name: + 0 + + + 0 + 1 + + + + + True + True + document-edit-symbolic + + + 1 + 3 + + + + + True + True + document-edit-symbolic + + + 1 + 2 + + + + + True + True + document-edit-symbolic + + + 1 + 1 + + + + + True + False + Enabled: + 0 + + + 0 + 0 + + + + + True + True + end + + + 1 + 0 + + + + + True + False + Begins: + 0 + + + 0 + 4 + + + + + True + True + False + True + none + timer_begins_popover - - Cancel + True True - True - app.on_timer_cancel + False + gtk-edit + + + + + + 1 + 4 + + + + + True + False + Ends: + 0 + + + 0 + 5 + + + + + True + True + False + True + none + timer_ends_popover + + + True + True + False + document-edit-symbolic + + + + + + 1 + 5 + + + + + True + False + Repeated: + 0 + + + 0 + 6 + + + + + True + False + True + + + True + True + False + center + True - True - True + 0 + 1 + + + + + True + True + False + center + True + + + 1 + 1 + + + + + True + True + False + center + True + + + 2 + 1 + + + + + True + True + False + center + True + + + 3 + 1 + + + + + True + True + False + center + True + + + 4 + 1 + + + + + + True + True + False + center + True + + + 5 + 1 + + + + + + True + True + False + center + True + + + 6 + 1 + + + + + True + False + Mo + + + 0 + 0 + + + + + True + False + Tu + + + 1 + 0 + + + + + True + False + We + + + 2 + 0 + + + + + True + False + Th + + + 3 + 0 + + + + + True + False + Fr + + + 4 + 0 + + + + + True + False + Sa + + + 5 + 0 + + + + + True + False + Su + + + 6 + 0 + + + + + 1 + 6 + + + + + True + False + Action: + 0 + + + 0 + 7 + + + + + True + False + 0 + + Record + Zap + + + + 1 + 7 + + + + + True + False + After event: + 0 + + + 0 + 8 + + + + + True + False + 3 + + Do Nothing + Standby + Shut down + Auto + + + + 1 + 8 + + + + + True + False + Service reference: + 0 + + + 0 + 9 + + + + + True + False + False + 25 + + + 1 + 9 + + + + + True + False + Event ID: + 0 + + + 0 + 10 + + + + + True + False + False + + + 1 + 10 + + + + + True + False + Location: + 0 + + + 0 + 12 + + + + + True + False + True + document-edit-symbolic + Default + + + 1 + 12 + + + + + True + False + end + 5 + + + True + False + document-edit-symbolic + + + False + False 0 - - Save + True True - True - app.on_timer_save - True - True + False + False + end 1 - False - False - 0 + 1 + 11 - - True - False - center - 5 - 5 - 5 - 5 - - - True - False - Service: - 0 - - - 0 - 3 - - - - - True - False - Description: - 0 - - - 0 - 2 - - - - - True - False - Name: - 0 - - - 0 - 1 - - - - - True - True - document-edit-symbolic - - - 1 - 3 - - - - - True - True - document-edit-symbolic - - - 1 - 2 - - - - - True - True - document-edit-symbolic - - - 1 - 1 - - - - - True - False - Enabled: - 0 - - - 0 - 0 - - - - - True - True - end - - - 1 - 0 - - - - - True - False - Action: - 0 - - - 0 - 9 - - - - - True - False - Repeated: - 0 - - - 0 - 8 - - - - - True - False - Ends: - 0 - - - 0 - 7 - - - - - True - False - Begins: - 0 - - - 0 - 6 - - - - - True - False - 0 - - Record - Zap - - - - 1 - 9 - - - - - True - False - True - - - True - True - False - center - True - - - 0 - 1 - - - - - True - True - False - center - True - - - 1 - 1 - - - - - True - True - False - center - True - - - 2 - 1 - - - - - True - True - False - center - True - - - 3 - 1 - - - - - True - True - False - center - True - - - 4 - 1 - - - - - - True - True - False - center - True - - - 5 - 1 - - - - - - True - True - False - center - True - - - 6 - 1 - - - - - True - False - Mo - - - 0 - 0 - - - - - True - False - Tu - - - 1 - 0 - - - - - True - False - We - - - 2 - 0 - - - - - True - False - Th - - - 3 - 0 - - - - - True - False - Fr - - - 4 - 0 - - - - - True - False - Sa - - - 5 - 0 - - - - - True - False - Su - - - 6 - 0 - - - - - 1 - 8 - - - - - True - True - False - True - none - timer_ends_popover - - - True - True - False - document-edit-symbolic - - - - - - 1 - 7 - - - - - True - False - Service reference: - 0 - - - 0 - 4 - - - - - True - True - False - - - 1 - 4 - - - - - True - False - Event ID: - 0 - - - 0 - 5 - - - - - True - True - False - - - 1 - 5 - - - - - True - True - False - True - none - timer_begins_popover - - - True - True - False - gtk-edit - - - - - - 1 - 6 - - - - - True - False - After event: - 0 - - - 0 - 10 - - - - - True - False - 3 - - Do Nothing - Standby - Shut down - Auto - - - - 1 - 10 - - - - - True - False - end - 5 - - - True - False - document-edit-symbolic - - - False - False - 0 - - - - - True - True - - - False - False - end - 1 - - - - - 1 - 11 - - - - - True - False - True - document-edit-symbolic - Default - - - 1 - 12 - - - - - True - False - Location: - 0 - - - 0 - 12 - - - - - - - - True - True - 1 - + + + + @@ -1496,6 +1468,8 @@ Author: Dmitriy Yefremov + + @@ -1534,9 +1508,9 @@ Author: Dmitriy Yefremov True - False - True + False True + True @@ -1551,31 +1525,14 @@ Author: Dmitriy Yefremov 0 - - - False - True - True - - - True - False - user-trash-symbolic - - - - - False - True - 1 - - True - True + False + False True Edit + True @@ -1584,12 +1541,55 @@ Author: Dmitriy Yefremov + + False + True + 1 + + + + + True + False + False + True + + + + True + False + user-trash-symbolic + + + False True 2 + + + True + False + False + Details + False + + + True + False + emblem-important-symbolic + + + + + False + True + end + 3 + + False @@ -1693,22 +1693,37 @@ Author: Dmitriy Yefremov True True timer_model + True both 3 + + + + - + + multiple + 150 Name 0.5 + + + 5 + + + 0 + + 5 - 0 + 1 @@ -1724,7 +1739,7 @@ Author: Dmitriy Yefremov 0.49000000953674316 - 1 + 2 @@ -1739,7 +1754,7 @@ Author: Dmitriy Yefremov 5 - 2 + 3 @@ -1754,7 +1769,7 @@ Author: Dmitriy Yefremov end - 3 + 4 @@ -1768,6 +1783,469 @@ Author: Dmitriy Yefremov 2 + + + False + 5 + 0 + in + + + True + False + center + 5 + 5 + 5 + 5 + 5 + 5 + + + True + False + Service reference: + 0 + + + 0 + 1 + + + + + True + False + Event ID: + 1 + + + 2 + 1 + + + + + True + False + Begins: + 1 + + + 0 + 2 + + + + + True + False + Ends: + 1 + + + 2 + 2 + + + + + True + False + Action: + 1 + + + 0 + 3 + + + + + True + False + 10 + After event: + 1 + + + 2 + 3 + + + + + True + False + N/A + + + 1 + 3 + + + + + True + False + N/A + end + + + 3 + 3 + + + + + True + False + N/A + end + + + 3 + 2 + + + + + True + False + N/A + end + 10 + + + 3 + 1 + + + + + True + False + N/A + + + 1 + 2 + + + + + True + False + N/A + end + 30 + + + 1 + 1 + + + + + True + False + Repeated: + 1 + + + 0 + 4 + + + + + True + False + True + + + True + True + False + center + True + + + 0 + 1 + + + + + True + True + False + center + True + + + 1 + 1 + + + + + True + True + False + center + True + + + 2 + 1 + + + + + True + True + False + center + True + + + 3 + 1 + + + + + True + True + False + center + True + + + 4 + 1 + + + + + + True + True + False + center + True + + + 5 + 1 + + + + + + True + True + False + center + True + + + 6 + 1 + + + + + True + False + Mo + + + 0 + 0 + + + + + True + False + Tu + + + 1 + 0 + + + + + True + False + We + + + 2 + 0 + + + + + True + False + Th + + + 3 + 0 + + + + + True + False + Fr + + + 4 + 0 + + + + + True + False + Sa + + + 5 + 0 + + + + + True + False + Su + + + 6 + 0 + + + + + 1 + 4 + + + + + True + False + center + Location: + 1 + + + 0 + 5 + + + + + True + False + center + False + + + 1 + 5 + + + + + True + False + center + 5 + + + True + False + start + alarm-symbolic + + + False + True + 0 + + + + + True + False + Enabled: + 0 + + + False + True + 1 + + + + + True + False + end + + + False + True + 2 + + + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + 3 + + @@ -1972,9 +2450,10 @@ Author: Dmitriy Yefremov True False - 5 - 5 + 20 + 20 10 + 5 vertical 2 diff --git a/app/ui/control.py b/app/ui/control.py index 5520ce7e..4e389c89 100644 --- a/app/ui/control.py +++ b/app/ui/control.py @@ -29,19 +29,21 @@ """ Receiver control module via HTTP API. """ import os from datetime import datetime +from enum import Enum from ftplib import all_errors from urllib.parse import quote from gi.repository import GLib -from .dialogs import get_builder, show_dialog, DialogType -from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column +from .dialogs import get_builder, show_dialog, DialogType, get_message +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column, KeyboardKey from ..commons import run_task, run_with_delay, log, run_idle from ..connections import HttpAPI, UtfFTP +from ..eparser.ecommons import BqServiceType from ..settings import IS_DARWIN, PlayStreamsMode -class EpgBox(Gtk.Box): +class EpgTool(Gtk.Box): def __init__(self, app, http_api, *args, **kwargs): super().__init__(*args, **kwargs) @@ -49,21 +51,58 @@ class EpgBox(Gtk.Box): self._app = app self._app.connect("fav-changed", self.on_service_changed) - handlers = {"on_epg_press": self.on_epg_press} + handlers = {"on_epg_press": self.on_epg_press, + "on_timer_add": self.on_timer_add} builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, objects=("epg_frame", "epg_model")) self._view = builder.get_object("epg_view") self.pack_start(builder.get_object("epg_frame"), True, True, 0) self.show() - def on_epg_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.set_timer_from_event_data(row.event_data) + def on_timer_add(self, action=None, value=None): + model, paths = self._view.get_selection().get_selected_rows() + p_count = len(paths) + + if p_count == 1: + dialog = TimerTool.TimerDialog(TimerTool.TimerAction.EVENT, model[paths][-1]) + response = dialog.run() + if response == Gtk.ResponseType.OK: + pass + dialog.destroy() + elif p_count > 1: + if show_dialog(DialogType.QUESTION, self._app.app_window, + "Add timers for selected events?") != Gtk.ResponseType.OK: + return True + + self.add_timers_list((model[p][-1] for p in paths)) + else: + self._app.show_error_message("No selected item!") + + def add_timers_list(self, paths): + ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0" + refs = [ref_str.format(ev.get("e2eventservicereference", ""), ev.get("e2eventid", "")) for ev in paths] + + gen = self.write_timers_list(refs) + GLib.idle_add(lambda: next(gen, False)) + + def write_timers_list(self, refs): + self._app.wait_dialog.show() + tasks = list(refs) + for ref in refs: + self._http_api.send(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop()) + yield True + + while tasks: + yield True + + self._app.emit("change-page", Page.TIMERS.value) + + def on_epg_press(self, view, event): + if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0: + self.on_timer_add() def on_service_changed(self, app, ref): - self._app._wait_dialog.show() + self._app.wait_dialog.show() self._http_api.send(HttpAPI.Request.EPG, quote(ref), self.update_epg_data) @run_idle @@ -71,7 +110,7 @@ class EpgBox(Gtk.Box): model = self._view.get_model() model.clear() list(map(model.append, (self.get_event_row(e) for e in epg.get("event_list", [])))) - self._app._wait_dialog.hide() + self._app.wait_dialog.hide() def get_event_row(self, event): title = event.get("e2eventtitle", "") @@ -85,25 +124,292 @@ class EpgBox(Gtk.Box): return title, time, desc, event -class TimersBox(Gtk.Box): +class TimerTool(Gtk.Box): + TIME_STR = "%Y-%m-%d %H:%M" + + ACTION = {"0": "Record", "1": "Zap"} + + AFTER_EVENT = {"0": "Do Nothing", + "1": "Standby", + "2": "Shut down", + "3": "Auto"} + + class TimerAction(Enum): + ADD = 0 + EVENT = 1 + CHANGE = 2 + + class TimerDialog(Gtk.Dialog): + def __init__(self, action=None, timer_data=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._action = action or TimerTool.TimerAction.ADD + self._timer_data = timer_data or {} + self._request = "" + + handlers = {"on_timer_begins_set": self.on_timer_begins_set, + "on_timer_ends_set": self.on_timer_ends_set} + + builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, + objects=("timer_dialog_frame", "timer_ends_popover", "end_hour_adjustment", + "min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment", + "min_begins_adjustment")) + + self.set_title(get_message("Timer")) + self.set_modal(True) + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + self.set_resizable(False) + + self._timer_name_entry = builder.get_object("timer_name_entry") + self._timer_desc_entry = builder.get_object("timer_desc_entry") + self._timer_service_entry = builder.get_object("timer_service_entry") + self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry") + self._timer_event_id_entry = builder.get_object("timer_event_id_entry") + self._timer_begins_entry = builder.get_object("timer_begins_entry") + self._timer_ends_entry = builder.get_object("timer_ends_entry") + self._timer_begins_calendar = builder.get_object("timer_begins_calendar") + self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button") + self._timer_begins_min_button = builder.get_object("timer_begins_min_button") + self._timer_ends_calendar = builder.get_object("timer_ends_calendar") + self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button") + self._timer_ends_min_button = builder.get_object("timer_ends_min_button") + self._timer_enabled_switch = builder.get_object("timer_enabled_switch") + self._timer_action_combo_box = builder.get_object("timer_action_combo_box") + self._timer_after_combo_box = builder.get_object("timer_after_combo_box") + self._days_buttons = (builder.get_object("timer_mo_check_button"), + builder.get_object("timer_tu_check_button"), + builder.get_object("timer_we_check_button"), + builder.get_object("timer_th_check_button"), + builder.get_object("timer_fr_check_button"), + builder.get_object("timer_sa_check_button"), + builder.get_object("timer_su_check_button")) + + self._timer_location_switch = builder.get_object("timer_location_switch") + self._timer_location_entry = builder.get_object("timer_location_entry") + self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive") + # Disable DnD for timer entries. + self._timer_name_entry.drag_dest_unset() + self._timer_desc_entry.drag_dest_unset() + self._timer_service_entry.drag_dest_unset() + + self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CLOSE, get_message("Save"), Gtk.ResponseType.OK) + self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5) + + if self._action is TimerTool.TimerAction.ADD: + self.set_timer_for_add() + elif self._action is TimerTool.TimerAction.CHANGE: + self.set_timer_for_edit() + elif self._action is TimerTool.TimerAction.EVENT: + self.set_timer_from_event_data() + else: + log("{} error: No action set for timer!".format(__class__.__name__)) + + @property + def request(self): + return self._request + + def run(self): + resp = super().run() + if resp == Gtk.ResponseType.OK: + self._request = self.get_request() + return resp + + def get_request(self): + """ Constructs str representation of add/update request. """ + args = [] + t_data = self.get_timer_data() + s_ref = quote(t_data.get("sRef", "")) + + if self._action is TimerTool.TimerAction.EVENT: + args.append("timeraddbyeventid?sRef={}".format(s_ref)) + args.append("eventid={}".format(t_data.get("eit", "0"))) + args.append("justplay={}".format(t_data.get("justplay", ""))) + args.append("tags={}".format("")) + else: + if self._action is TimerTool.TimerAction.ADD: + args.append("timeradd?sRef={}".format(s_ref)) + args.append("deleteOldOnSave={}".format(0)) + elif self._action is TimerTool.TimerAction.CHANGE: + args.append("timerchange?sRef={}".format(s_ref)) + args.append("channelOld={}".format(s_ref)) + args.append("beginOld={}".format(self._timer_data.get("e2timebegin", "0"))) + args.append("endOld={}".format(self._timer_data.get("e2timeend", "0"))) + args.append("deleteOldOnSave={}".format(1)) + + args.append("begin={}".format(t_data.get("begin", ""))) + args.append("end={}".format(t_data.get("end", ""))) + args.append("name={}".format(quote(t_data.get("name", "")))) + args.append("description={}".format(quote(t_data.get("description", "")))) + args.append("tags={}".format("")) + args.append("eit={}".format("0")) + args.append("disabled={}".format(t_data.get("disabled", "1"))) + args.append("justplay={}".format(t_data.get("justplay", "1"))) + args.append("afterevent={}".format(t_data.get("afterevent", "0"))) + args.append("repeated={}".format(TimerTool.get_repetition_flags(self._days_buttons))) + + if self._timer_location_switch.get_active(): + args.append("dirname={}".format(self._timer_location_entry.get_text())) + + return "&".join(args) + + def on_timer_begins_set(self, action, value=None): + self.set_begins_date(self.get_begins_date()) + + def on_timer_ends_set(self, action, value=None): + self.set_ends_date(self.get_ends_date()) + + def get_begins_date(self): + date = self._timer_begins_calendar.get_date() + return datetime(year=date.year, month=date.month + 1, day=date.day, + hour=int(self._timer_begins_hr_button.get_value()), + minute=int(self._timer_begins_min_button.get_value())) + + def set_begins_date(self, date): + hour = date.hour + minute = date.minute + self._timer_begins_hr_button.set_value(hour) + self._timer_begins_min_button.set_value(minute) + self._timer_begins_calendar.select_day(date.day) + self._timer_begins_calendar.select_month(date.month - 1, date.year) + self._timer_begins_entry.set_text( + "{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute)) + + def get_ends_date(self): + date = self._timer_ends_calendar.get_date() + return datetime(year=date.year, month=date.month + 1, day=date.day, + hour=int(self._timer_ends_hr_button.get_value()), + minute=int(self._timer_ends_min_button.get_value())) + + def set_ends_date(self, date): + hour = date.hour + minute = date.minute + self._timer_ends_hr_button.set_value(hour) + self._timer_ends_min_button.set_value(minute) + self._timer_ends_calendar.select_day(date.day) + self._timer_ends_calendar.select_month(date.month - 1, date.year) + self._timer_ends_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute)) + + def set_timer_for_add(self): + self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "")) + self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", "")) + date = datetime.now() + self.set_begins_date(date) + self.set_ends_date(date) + self._timer_event_id_entry.set_text("") + self._timer_location_switch.set_active(False) + TimerTool.set_repetition_flags(0, self._days_buttons) + + def set_timer_for_edit(self): + self._timer_name_entry.set_text(self._timer_data.get("e2name", "")) + self._timer_desc_entry.set_text(self._timer_data.get("e2description", "") or "") + self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "") or "") + self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", "")) + self._timer_event_id_entry.set_text(self._timer_data.get("e2eit", "")) + self._timer_enabled_switch.set_active((self._timer_data.get("e2disabled", "0") == "0")) + self._timer_action_combo_box.set_active_id(self._timer_data.get("e2justplay", "0")) + self._timer_after_combo_box.set_active_id(self._timer_data.get("e2afterevent", "0")) + self.set_time_data(int(self._timer_data.get("e2timebegin", "0")), + int(self._timer_data.get("e2timeend", "0"))) + location = self._timer_data.get("e2location", "") + self._timer_location_entry.set_text("" if location == "None" else location) + TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons) + + def set_timer_from_event_data(self): + self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", "")) + self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", "")) + self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", "")) + self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", "")) + self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", "")) + self._timer_action_combo_box.set_active_id("1") + self._timer_after_combo_box.set_active_id("3") + start_time = int(self._timer_data.get("e2eventstart", "0")) + self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0"))) + + def set_time_data(self, start_time, end_time): + """ Sets values for time widgets. """ + ev_time_start = datetime.fromtimestamp(start_time) or datetime.now() + ev_time_end = datetime.fromtimestamp(end_time) or datetime.now() + self._timer_begins_entry.set_text(ev_time_start.strftime(TimerTool.TIME_STR)) + self._timer_ends_entry.set_text(ev_time_end.strftime(TimerTool.TIME_STR)) + self._timer_begins_calendar.select_day(ev_time_start.day) + self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year) + self._timer_ends_calendar.select_day(ev_time_end.day) + self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year) + self._timer_begins_hr_button.set_value(ev_time_start.hour) + self._timer_begins_min_button.set_value(ev_time_start.minute) + self._timer_ends_hr_button.set_value(ev_time_end.hour) + self._timer_ends_min_button.set_value(ev_time_end.minute) + + def get_timer_data(self): + """ Returns timer data as a dict. """ + return {"sRef": self._timer_service_ref_entry.get_text(), + "begin": int( + datetime.strptime(self._timer_begins_entry.get_text(), TimerTool.TIME_STR).timestamp()), + "end": int(datetime.strptime(self._timer_ends_entry.get_text(), TimerTool.TIME_STR).timestamp()), + "name": self._timer_name_entry.get_text(), + "description": self._timer_desc_entry.get_text(), + "dirname": "", + "eit": self._timer_event_id_entry.get_text(), + "disabled": int(not self._timer_enabled_switch.get_active()), + "justplay": self._timer_action_combo_box.get_active_id(), + "afterevent": self._timer_after_combo_box.get_active_id(), + "repeated": TimerTool.get_repetition_flags(self._days_buttons)} + def __init__(self, app, http_api, *args, **kwargs): super().__init__(*args, **kwargs) self._http_api = http_api self._app = app self._app.connect("page-changed", self.update_timer_list) + # Icon. + theme = Gtk.IconTheme.get_default() + icon = "alarm-symbolic" + self._icon = theme.load_icon(icon, 16, 0) if theme.lookup_icon(icon, 16, 0) else None - handlers = {} + handlers = {"on_timer_add": self.on_timer_add, + "on_timer_edit": self.on_timer_edit, + "on_timer_remove": self.on_timer_remove, + "on_timers_press": self.on_timers_press, + "on_timers_key_press": self.on_timers_key_press, + "on_timer_cursor_changed": self.on_timer_cursor_changed, + "on_timers_drag_data_received": self.on_timers_drag_data_received} builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, objects=("timers_frame", "timer_model")) self._view = builder.get_object("timer_view") self._remove_button = builder.get_object("timer_remove_button") + self._remove_button.bind_property("sensitive", builder.get_object("timer_edit_button"), "sensitive") + self._info_button = builder.get_object("timer_info_check_button") + self._info_button.bind_property("active", builder.get_object("timer_info_frame"), "visible") + self._info_enabled_switch = builder.get_object("timer_info_enabled_switch") + self._ref_info_label = builder.get_object("timer_ref_value_label") + self._event_id_info_label = builder.get_object("timer_event_id_value_label") + self._begins_info_label = builder.get_object("timer_begins_value_label") + self._ends_info_label = builder.get_object("timer_ends_value_label") + self._action_info_label = builder.get_object("timer_action_value_label") + self._after_info_label = builder.get_object("timer_after_value_label") + self._timer_location_switch = builder.get_object("timer_location_switch") + self._info_location_entry = builder.get_object("timer_info_location_entry") + self._days_buttons = (builder.get_object("timer_info_mo_check_button"), + builder.get_object("timer_info_tu_check_button"), + builder.get_object("timer_info_we_check_button"), + builder.get_object("timer_info_th_check_button"), + builder.get_object("timer_info_fr_check_button"), + builder.get_object("timer_info_sa_check_button"), + builder.get_object("timer_info_su_check_button")) + # Disable button presses. + list(map(lambda b: b.connect("button-press-event", lambda bx, e: True), self._days_buttons)) + self._info_enabled_switch.connect("button-press-event", lambda b, e: True) + # DnD initialization for the timer list. + self._view.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY) + self._view.drag_dest_add_text_targets() + self.pack_start(builder.get_object("timers_frame"), True, True, 0) self.show() def update_timer_list(self, app, page): if page is Page.TIMERS: - self._app._wait_dialog.show() + self._app.wait_dialog.show() self._http_api.send(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data) @run_idle @@ -111,10 +417,11 @@ class TimersBox(Gtk.Box): model = self._view.get_model() model.clear() list(map(model.append, (self.get_timer_row(t) for t in timers.get("timer_list", [])))) - self._remove_button.set_visible(len(model)) - self._app._wait_dialog.hide() + self._remove_button.set_sensitive(len(model)) + self._app.wait_dialog.hide() def get_timer_row(self, timer): + disabled = self._icon if timer.get("e2disabled", "0") == "0" else None name = timer.get("e2name", "") or "" description = timer.get("e2description", "") or "" service = timer.get("e2servicename", "") or "" @@ -122,10 +429,198 @@ class TimersBox(Gtk.Box): end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0"))) time = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M")) - return name, service, time, description, timer + return disabled, name, service, time, description, timer + + def on_timer_add(self, timer=None, value=None): + model, paths = self._app.fav_view.get_selection().get_selected_rows() + p_count = len(paths) + + if p_count == 1: + service = self._app.current_services.get(model[paths][Column.FAV_ID], None) + if service: + self.add_timer({"e2servicename": service.service, + "e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")}) + elif p_count > 1: + self._app.show_error_message("Please, select only one item!") + else: + self._app.show_error_message("No selected item!") + + def add_timer(self, timer_data): + dialog = self.TimerDialog(self.TimerAction.ADD, timer_data) + response = dialog.run() + if response == Gtk.ResponseType.OK: + self._http_api.send(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback) + dialog.destroy() + + def on_timer_edit(self, action=None, value=None): + model, paths = self._view.get_selection().get_selected_rows() + if len(paths) > 1: + self._app.show_error_message("Please, select only one item!") + return + + dialog = self.TimerDialog(self.TimerAction.CHANGE, model[paths][-1]) + response = dialog.run() + if response == Gtk.ResponseType.OK: + self._http_api.send(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback) + dialog.destroy() + + @run_idle + def timer_add_edit_callback(self, resp): + if "error_code" in resp: + msg = "Error getting timer status.\n{}".format(resp.get("error_code")) + self._app.show_error_message(msg) + log(msg) + return + + state = resp.get("e2state", None) + if state == "False": + msg = resp.get("e2statetext", "") + self._app.show_error_message(msg) + log(msg) + if state == "True": + msg = resp.get("e2statetext", "") + log(msg) + self._app.show_info_message(msg, Gtk.MessageType.INFO) + self.update_timer_list(self._app, Page.TIMERS) + else: + log("Error getting timer status. No response!") + + def on_timer_remove(self, action=None, value=None): + model, paths = self._view.get_selection().get_selected_rows() + if not paths or show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK: + return + + refs = {} + for path in paths: + timer = model[path][-1] + ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")), + timer.get("e2timebegin", ""), + timer.get("e2timeend", "")) + refs[ref] = model.get_iter(path) + + self._app.wait_dialog.show("Deleting data...") + gen = self.remove_timers(refs) + GLib.idle_add(lambda: next(gen, False)) + + def remove_timers(self, refs): + tasks = list(refs) + removed = set() + for ref in refs: + yield from self.remove_timer(ref, removed, tasks) + + while tasks: + yield True + + model = self._view.get_model() + list(map(model.remove, (refs[ref] for ref in refs if ref in removed))) + self._app.wait_dialog.hide() + self._remove_button.set_sensitive(len(model)) + yield True + + def remove_timer(self, ref, removed, tasks=None): + def callback(resp): + if resp.get("e2state", "") == "True": + log(resp.get("e2statetext", "")) + removed.add(ref) + else: + log(resp.get("e2statetext", None) or "Timer deletion error.") + if tasks: + tasks.pop() + + self._http_api.send(HttpAPI.Request.TIMER, ref, callback) + yield True + + def on_timers_press(self, view, event): + if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0: + self.on_timer_edit() + + def on_timers_key_press(self, view, event): + key_code = event.hardware_keycode + if not KeyboardKey.value_exist(key_code): + return + + key = KeyboardKey(key_code) + if key is KeyboardKey.DELETE: + self.on_timer_remove() + + def on_timer_cursor_changed(self, view): + path, column = view.get_cursor() + if not path: + return + + timer = view.get_model()[path][-1] + self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0")) + self._ref_info_label.set_text(timer.get("e2servicereference", "")) + self._event_id_info_label.set_text(timer.get("e2eit", "")) + self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0"))) + self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0"))) + self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0"))))) + self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0"))))) + self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons) + location = timer.get("e2location", "") + self._info_location_entry.set_text("" if location == "None" else location) + + @staticmethod + def get_repetition_flags(boxes): + """ Returns flags for repetition. + + @param boxes: Buttons tuple for the days of the week. + """ + day_flags = 0 + for i, box in enumerate(boxes): + if box.get_active(): + day_flags = day_flags | (1 << i) + + return day_flags + + @staticmethod + def set_repetition_flags(flags, boxes): + """ Sets flags for repetition. + + @param flags: Flags value. + @param boxes: Buttons tuple for the days of the week. + """ + for i, box in enumerate(boxes): + box.set_active(flags & 1 == 1) + flags = flags >> 1 + + # ***************** Drag-and-drop ********************* # + + def on_timers_drag_data_received(self, box, context, x, y, data, info, time): + txt = data.get_text() + if txt: + itr_str, sep, source = txt.partition(self._app.DRAG_SEP) + if not source: + return + + itrs = itr_str.split(",") + if len(itrs) > 1: + self._app.show_error_message("Please, select only one item!") + return + + fav_id = None + if source == self._app.FAV_MODEL_NAME: + model = self._app.fav_view.get_model() + fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID) + elif source == self._app.SERVICE_MODEL_NAME: + model = self._app.services_view.get_model() + fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID) + + service = self._app.current_services.get(fav_id, None) + if service: + if service.service_type == BqServiceType.ALT.name: + msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!")) + show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg) + context.finish(False, False, time) + return + + self.add_timer({"e2servicename": service.service, + "e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")}) + + context.finish(True, False, time) -class RecordingsBox(Gtk.Box): +class RecordingsTool(Gtk.Box): ROOT = ".." DEFAULT_PATH = "/hdd" @@ -276,7 +771,7 @@ class RecordingsBox(Gtk.Box): self._rec_view.get_column(c).set_visible(state) -class ControlBox(Gtk.Box): +class ControlTool(Gtk.Box): def __init__(self, app, http_api, settings, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/app/ui/main.py b/app/ui/main.py index 6b8e5ffa..89188469 100644 --- a/app/ui/main.py +++ b/app/ui/main.py @@ -48,7 +48,7 @@ from app.eparser.neutrino.bouquets import BqType from app.settings import (SettingsType, Settings, SettingsException, SettingsReadException, IS_DARWIN, PlayStreamsMode) from app.tools.media import Recorder -from app.ui.control import ControlBox, EpgBox, TimersBox, RecordingsBox +from app.ui.control import ControlTool, EpgTool, TimerTool, RecordingsTool from app.ui.epg import EpgDialog from app.ui.ftp import FtpClientBox from app.ui.playback import PlayerBox @@ -253,6 +253,8 @@ class Application(Gtk.Application): GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) GObject.signal_new("page-changed", self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("change-page", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) GObject.signal_new("play-recording", self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) GObject.signal_new("play-current", self, GObject.SIGNAL_RUN_LAST, @@ -379,6 +381,7 @@ class Application(Gtk.Application): self._stack_recordings_box = builder.get_object("recordings_box") self._stack_ftp_box = builder.get_object("ftp_box") self._stack_control_box = builder.get_object("control_box") + self.connect("change-page", self.on_page_change) # Header bar. profile_box = builder.get_object("profile_combo_box") toolbar_box = builder.get_object("toolbar_main_box") @@ -707,13 +710,13 @@ class Application(Gtk.Application): 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: + if show_dialog(DialogType.QUESTION, self._main_window, msg) != Gtk.ResponseType.OK: 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?")) - if show_dialog(DialogType.QUESTION, self._main_window, msg) == Gtk.ResponseType.CANCEL: + if show_dialog(DialogType.QUESTION, self._main_window, msg) != Gtk.ResponseType.OK: return True self._recorder.release() @@ -757,15 +760,15 @@ class Application(Gtk.Application): box.pack_start(self._picon_manager, True, True, 0) def on_epg_realize(self, box): - self._epg_box = EpgBox(self, self._http_api) + self._epg_box = EpgTool(self, self._http_api) box.pack_start(self._epg_box, True, True, 0) def on_timers_realize(self, box): - self._epg_box = TimersBox(self, self._http_api) + self._epg_box = TimerTool(self, self._http_api) box.pack_start(self._epg_box, True, True, 0) def on_recordings_realize(self, box): - self._recordings_box = RecordingsBox(self, self._http_api, self._settings) + self._recordings_box = RecordingsTool(self, self._http_api, self._settings) box.pack_start(self._recordings_box, True, True, 0) self._player_box.connect("play", self._recordings_box.on_playback) self._player_box.connect("playback-close", self._recordings_box.on_playback_close) @@ -775,7 +778,7 @@ class Application(Gtk.Application): box.pack_start(self._ftp_client, True, True, 0) def on_control_realize(self, box: Gtk.HBox): - self._control_box = ControlBox(self, self._http_api, self._settings) + self._control_box = ControlTool(self, self._http_api, self._settings) box.pack_start(self._control_box, True, True, 0) def on_visible_page(self, stack, param): @@ -788,6 +791,9 @@ class Application(Gtk.Application): action.set_state(value) self._settings.add(action.get_name(), bool(value)) + def on_page_change(self, app, page_name): + self._stack.set_visible_child_name(page_name) + # ***************** Copy - Cut - Paste ********************* # def on_services_copy(self, view): @@ -3450,6 +3456,10 @@ class Application(Gtk.Application): def app_settings(self): return self._settings + @property + def wait_dialog(self): + return self._wait_dialog + @property def http_api(self): return self._http_api