mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2025-12-24 01:19:40 +01:00
399 lines
18 KiB
Python
399 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# The MIT License (MIT)
|
|
#
|
|
# Copyright (c) 2018-2025 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
|
|
#
|
|
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from ftplib import all_errors
|
|
from pathlib import Path
|
|
|
|
from gi.repository.GObject import BindingFlags
|
|
|
|
from app.commons import log, run_task
|
|
from app.connections import UtfFTP
|
|
from app.settings import IS_DARWIN
|
|
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.uicommons import HeaderBar
|
|
from .uicommons import Gtk, GLib
|
|
|
|
_OUTPUT_FILES = ("bootlogo",
|
|
"bootlogo_wait",
|
|
"backdrop",
|
|
"reboot",
|
|
"shutdown",
|
|
"radio")
|
|
_E2_STB_PATHS = ("/usr/share", "/usr/share/enigma2")
|
|
|
|
|
|
class BootLogoManager(Gtk.Window):
|
|
|
|
def __init__(self, app, **kwargs):
|
|
super().__init__(title=translate("Boot Logo"), icon_name="demon-editor", application=app,
|
|
transient_for=app.app_window, destroy_with_parent=True,
|
|
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
|
default_width=560, default_height=320, modal=False, **kwargs)
|
|
|
|
self._app = app
|
|
self._exe = f"{'./' if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') else ''}ffmpeg"
|
|
self._pix = None
|
|
self._img_path = None
|
|
|
|
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}
|
|
|
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
frame = Gtk.Frame(shadow_type=Gtk.ShadowType.IN, **base_margin)
|
|
frame.get_style_context().add_class("view")
|
|
data_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL, **base_margin)
|
|
data_box.set_margin_bottom(margin.get("margin_bottom", 5))
|
|
data_box.set_margin_start(10)
|
|
frame.add(data_box)
|
|
self._image_area = Gtk.DrawingArea()
|
|
self._image_area.connect("draw", self.on_image_draw)
|
|
data_box.pack_end(self._image_area, True, True, 5)
|
|
self.add(main_box)
|
|
# Buttons
|
|
add_path_button = Gtk.Button.new_from_icon_name("insert-image-symbolic", Gtk.IconSize.BUTTON)
|
|
add_path_button.set_tooltip_text(translate("Add image"))
|
|
add_path_button.set_always_show_image(True)
|
|
add_path_button.connect("clicked", self.on_add_image)
|
|
receive_button = Gtk.Button.new_from_icon_name("network-receive-symbolic", Gtk.IconSize.BUTTON)
|
|
receive_button.set_tooltip_text(translate("Download from the receiver"))
|
|
receive_button.set_always_show_image(True)
|
|
receive_button.connect("clicked", self.on_receive)
|
|
transmit_button = Gtk.Button.new_from_icon_name("network-transmit-symbolic", Gtk.IconSize.BUTTON)
|
|
transmit_button.set_tooltip_text(translate("Transfer to receiver"))
|
|
transmit_button.set_sensitive(False)
|
|
transmit_button.set_always_show_image(True)
|
|
transmit_button.connect("clicked", self.on_transmit)
|
|
self._convert_button = Gtk.Button.new_from_icon_name("object-rotate-right-symbolic", Gtk.IconSize.BUTTON)
|
|
self._convert_button.set_tooltip_text(translate("Convert"))
|
|
self._convert_button.set_always_show_image(True)
|
|
self._convert_button.set_sensitive(False)
|
|
self._convert_button.connect("clicked", self.on_convert)
|
|
self._convert_button.bind_property("sensitive", transmit_button, "sensitive", 4)
|
|
settings_close_button = Gtk.ModelButton(label=translate("Close"), centered=True, margin_top=5)
|
|
# Formats.
|
|
self._format_button = Gtk.ComboBoxText()
|
|
self._format_button.set_tooltip_text(translate("TV Format"))
|
|
self._format_button.append("hd720", "HD-Ready (720)")
|
|
self._format_button.append("hd1080", "Full HD (1080)")
|
|
self._format_button.set_active_id("hd720")
|
|
|
|
action_box = Gtk.ButtonBox()
|
|
action_box.set_layout(Gtk.ButtonBoxStyle.EXPAND)
|
|
action_box.add(add_path_button)
|
|
action_box.add(self._convert_button)
|
|
action_box.add(self._format_button)
|
|
data_box.pack_start(action_box, False, False, 0)
|
|
|
|
# Settings.
|
|
self._stb_path_property = "boot_logo_manager_stb_paths"
|
|
popover = Gtk.Popover()
|
|
settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, **base_margin)
|
|
file_name_box = Gtk.Box(spacing=5)
|
|
file_name_box.add(Gtk.Label(f"{translate('File')}:"))
|
|
self._file_combo_box = Gtk.ComboBoxText()
|
|
[self._file_combo_box.append(f"{f}.mvi", f) for f in _OUTPUT_FILES]
|
|
self._file_combo_box.set_active(0)
|
|
file_name_box.pack_start(self._file_combo_box, True, True, 0)
|
|
settings_box.add(file_name_box)
|
|
|
|
paths_box = Gtk.Box(spacing=5)
|
|
paths_box.add(Gtk.Label(translate("STB path:")))
|
|
self._path_combo_box = Gtk.ComboBoxText(has_entry=True)
|
|
self._path_entry = self._path_combo_box.get_child()
|
|
self._path_entry.set_can_focus(False)
|
|
self._path_entry.connect("focus-out-event", self.on_path_entry_focus_out)
|
|
# Init paths.
|
|
self._stb_paths = self._app.app_settings.get(self._stb_path_property, _E2_STB_PATHS)
|
|
[self._path_combo_box.append(p, p) for p in self._stb_paths]
|
|
self._path_combo_box.set_active_id(self._stb_paths[0])
|
|
paths_box.pack_start(self._path_combo_box, True, True, 0)
|
|
# Paths action box.
|
|
paths_action_box = Gtk.ButtonBox(homogeneous=True, layout_style=Gtk.ButtonBoxStyle.EXPAND)
|
|
self._remove_path_button = Gtk.Button.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON)
|
|
self._remove_path_button.set_tooltip_text(translate("Remove"))
|
|
self._remove_path_button.connect("clicked", self.on_remove_path)
|
|
add_e2_path_button = Gtk.Button.new_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON)
|
|
add_e2_path_button.set_tooltip_text(translate("Add"))
|
|
add_e2_path_button.connect("clicked", self.on_add_path)
|
|
cancel_path_button = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.BUTTON)
|
|
cancel_path_button.set_tooltip_text(translate("Cancel"))
|
|
apply_path_button = Gtk.Button.new_from_icon_name("insert-link-symbolic", Gtk.IconSize.BUTTON)
|
|
apply_path_button.set_tooltip_text(translate("Apply"))
|
|
apply_path_button.set_can_focus(False)
|
|
apply_path_button.connect("clicked", self.on_apply_path)
|
|
|
|
paths_action_box.add(self._remove_path_button)
|
|
paths_action_box.add(add_e2_path_button)
|
|
paths_action_box.add(cancel_path_button)
|
|
paths_action_box.add(apply_path_button)
|
|
paths_box.pack_end(paths_action_box, True, True, 0)
|
|
settings_box.add(paths_box)
|
|
settings_box.pack_end(settings_close_button, False, False, 0)
|
|
settings_box.show_all()
|
|
|
|
cancel_path_button.set_visible(False)
|
|
apply_path_button.set_visible(False)
|
|
self._path_entry.bind_property("has-focus", apply_path_button, "visible")
|
|
apply_path_button.bind_property("visible", cancel_path_button, "visible")
|
|
apply_path_button.bind_property("visible", add_e2_path_button, "visible", BindingFlags.INVERT_BOOLEAN)
|
|
apply_path_button.bind_property("visible", self._remove_path_button, "visible", BindingFlags.INVERT_BOOLEAN)
|
|
|
|
popover.add(settings_box)
|
|
popover.connect("closed", self.on_settings_closed)
|
|
settings_button = Gtk.MenuButton(popover=popover, valign=Gtk.Align.CENTER, tooltip_text=translate("Options"))
|
|
settings_button.add(Gtk.Image.new_from_icon_name("applications-system-symbolic", Gtk.IconSize.BUTTON))
|
|
|
|
# Header and toolbar.
|
|
if app.app_settings.use_header_bar:
|
|
header = HeaderBar(title=translate("Boot Logo"))
|
|
header.pack_start(receive_button)
|
|
header.pack_start(transmit_button)
|
|
header.pack_end(settings_button)
|
|
|
|
self.set_titlebar(header)
|
|
header.show_all()
|
|
else:
|
|
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
toolbar.get_style_context().add_class("primary-toolbar")
|
|
margin["margin_start"] = 15
|
|
margin["margin_top"] = 5
|
|
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
|
|
button_box.pack_start(receive_button, False, False, 0)
|
|
button_box.pack_start(transmit_button, False, False, 0)
|
|
toolbar.pack_start(button_box, True, True, 0)
|
|
toolbar.pack_end(settings_button, False, False, 0)
|
|
main_box.pack_start(toolbar, False, False, 0)
|
|
settings_button.set_margin_end(15)
|
|
|
|
main_box.pack_start(frame, True, True, 0)
|
|
main_box.show_all()
|
|
|
|
ws_property = "boot_logo_manager_window_size"
|
|
window_size = self._app.app_settings.get(ws_property, None)
|
|
if window_size:
|
|
self.resize(*window_size)
|
|
|
|
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
|
|
self.connect("realize", self.init)
|
|
|
|
def init(self, *args):
|
|
log(f"{self.__class__.__name__} [init] Checking FFmpeg...")
|
|
try:
|
|
out = subprocess.check_output([self._exe, "-version"], stderr=subprocess.STDOUT)
|
|
except FileNotFoundError as e:
|
|
msg = translate("Check if FFmpeg is installed!")
|
|
self._app.show_error_message(f"Error. {e} {msg}")
|
|
log(e)
|
|
else:
|
|
lines = out.decode(errors="ignore").splitlines()
|
|
log(lines[0] if lines else lines)
|
|
|
|
def on_add_path(self, button):
|
|
self._path_entry.set_can_focus(True)
|
|
self._path_entry.grab_focus()
|
|
|
|
def on_remove_path(self, button):
|
|
self._path_combo_box.remove(self._path_combo_box.get_active())
|
|
self._path_combo_box.set_active(0)
|
|
self._remove_path_button.set_sensitive(len(self._path_combo_box.get_model()) > 1)
|
|
|
|
def on_apply_path(self, button):
|
|
path = self._path_entry.get_text()
|
|
paths = {r[0] for r in self._path_combo_box.get_model()}
|
|
|
|
if path in paths:
|
|
self._app.show_error_message("This path already exists!")
|
|
return True
|
|
|
|
self._path_combo_box.append(path, path)
|
|
self._path_combo_box.set_active_id(path)
|
|
self._remove_path_button.grab_focus()
|
|
self._remove_path_button.set_sensitive(len(paths))
|
|
|
|
return False
|
|
|
|
def on_path_entry_focus_out(self, entry, event):
|
|
entry.set_can_focus(False)
|
|
active = self._path_combo_box.get_active_id()
|
|
txt = entry.get_text()
|
|
if active != txt:
|
|
entry.set_text(active or "")
|
|
|
|
def on_settings_closed(self, popover):
|
|
paths = tuple(r[0] for r in self._path_combo_box.get_model())
|
|
if paths != self._stb_paths:
|
|
self._stb_paths = paths
|
|
self._app.app_settings.add(self._stb_path_property, self._stb_paths)
|
|
|
|
def on_add_image(self, button):
|
|
file_filter = None
|
|
if IS_DARWIN:
|
|
file_filter = Gtk.FileFilter()
|
|
file_filter.set_name("*.jpg, *.jpeg, *.png")
|
|
file_filter.add_mime_type("image/jpeg")
|
|
file_filter.add_mime_type("image/png")
|
|
|
|
response = get_chooser_dialog(self._app.app_window, self._app.app_settings, "*.jpg, *.jpeg, *.png files",
|
|
("*.jpg", "*.jpeg", "*.png"), "Select image", file_filter)
|
|
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
|
return
|
|
|
|
self._img_path = response
|
|
self._pix = get_picon_pixbuf(response, -1)
|
|
self._convert_button.set_sensitive(True)
|
|
self._image_area.queue_draw()
|
|
|
|
def on_receive(self, button):
|
|
self.download_data(self._file_combo_box.get_active_id())
|
|
|
|
def on_transmit(self, button):
|
|
if show_dialog(DialogType.QUESTION, self) != Gtk.ResponseType.OK:
|
|
return True
|
|
|
|
mvi_file = Path(self._img_path).parent.joinpath(self._file_combo_box.get_active_id())
|
|
if not mvi_file.is_file():
|
|
log(self._app.show_error_message(translate("No *.mvi file found for the selected image!")))
|
|
return
|
|
|
|
self.transfer_data(mvi_file)
|
|
|
|
def on_convert(self, button):
|
|
self.convert_to_mvi()
|
|
|
|
def convert_to_mvi(self, frame_rate=25, bit_rate=2000):
|
|
path = Path(self._img_path)
|
|
if not path.is_file():
|
|
self._app.show_error_message(translate("No image selected!"))
|
|
return
|
|
|
|
output = path.parent.joinpath(self._file_combo_box.get_active_id())
|
|
ffmpeg_output = path.parent.joinpath(f"{self._file_combo_box.get_active_text()}.m2v")
|
|
|
|
cmd = [self._exe,
|
|
"-i", self._img_path,
|
|
"-r", str(frame_rate),
|
|
"-b", str(bit_rate),
|
|
"-s", self._format_button.get_active_id(),
|
|
ffmpeg_output]
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError as e:
|
|
self._app.show_error_message(f"{translate('Conversion error.')} {e}")
|
|
else:
|
|
with Image.open(self._img_path) as img:
|
|
width, height = img.size
|
|
if width != 1280 and height != 720:
|
|
log(f"{self.__class__.__name__} [convert] Resizing image...")
|
|
img.resize((1280, 720), Image.Resampling.LANCZOS)
|
|
tmp = path.parent.joinpath(f"{path.name}.tmp{path.suffix}").absolute()
|
|
cmd[2] = tmp
|
|
img.save(tmp)
|
|
|
|
# Processing image.
|
|
log(f"{self.__class__.__name__} [convert] Converting...")
|
|
subprocess.run(cmd)
|
|
if Path(ffmpeg_output).exists():
|
|
os.rename(ffmpeg_output, output)
|
|
log(f"{self.__class__.__name__} [convert] -> '{output}'. Done!")
|
|
|
|
if cmd[2] != self._img_path:
|
|
tmp_path = Path(cmd[2])
|
|
if tmp_path.exists():
|
|
tmp_path.unlink()
|
|
|
|
self._convert_button.set_sensitive(False)
|
|
|
|
def convert_to_image(self, video_path, img_path):
|
|
cmd = [self._exe, "-y", "-i", video_path, img_path]
|
|
subprocess.run(cmd)
|
|
|
|
@run_task
|
|
def download_data(self, f_name):
|
|
try:
|
|
settings = self._app.app_settings
|
|
with UtfFTP(host=settings.host, port=settings.port, user=settings.user, passwd=settings.password) as ftp:
|
|
ftp.encoding = "utf-8"
|
|
ftp.cwd(self._path_combo_box.get_active_id())
|
|
|
|
dest = Path(settings.profile_data_path).joinpath("bootlogo")
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
path = f"{dest}{os.sep}"
|
|
ftp.download_file(f_name, path)
|
|
vp = Path(f"{path}{f_name}")
|
|
img_path = f"{path}{f_name}.jpg"
|
|
|
|
if vp.exists():
|
|
rn_path = f"{path}{self._file_combo_box.get_active_text()}.m2v"
|
|
vp.rename(rn_path)
|
|
self.convert_to_image(rn_path, img_path)
|
|
self._pix = get_picon_pixbuf(img_path, -1)
|
|
GLib.idle_add(self._image_area.queue_draw)
|
|
|
|
except all_errors as e:
|
|
log(f"{self.__class__.__name__} [download error] {e}")
|
|
GLib.idle_add(self._app.show_error_message, f"{translate('Failed to download data:')} {e}")
|
|
|
|
@run_task
|
|
def transfer_data(self, f_path):
|
|
try:
|
|
settings = self._app.app_settings
|
|
with UtfFTP(host=settings.host, port=settings.port, user=settings.user, passwd=settings.password) as ftp:
|
|
ftp.encoding = "utf-8"
|
|
ftp.cwd(self._path_combo_box.get_active_id())
|
|
|
|
log(f"{self.__class__.__name__} [transfer data] Creating backup...")
|
|
backup_path = Path(settings.profile_backup_path).joinpath("bootlogo")
|
|
backup_path.mkdir(parents=True, exist_ok=True)
|
|
ftp.download_file(f_path.name, f"{backup_path}{os.sep}")
|
|
backup_file = backup_path.joinpath(f_path.name)
|
|
if backup_file.exists():
|
|
target = backup_path.joinpath(f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_{f_path.name}")
|
|
backup_file.rename(target)
|
|
|
|
ftp.send_file(f_path.name, f"{f_path.parent}{os.sep}")
|
|
|
|
except all_errors as e:
|
|
log(f"{self.__class__.__name__} [upload error] {e}")
|
|
GLib.idle_add(self._app.show_error_message, f"{translate('Data transfer error:')} {e}")
|
|
else:
|
|
self._app.show_info_message("Done!")
|
|
|
|
def on_image_draw(self, area, cr):
|
|
if self._pix:
|
|
redraw_image(area, cr, self._pix)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pass
|