Add preferences
This commit is contained in:
parent
3a341c2fe5
commit
4a6d85f7ea
9 changed files with 219 additions and 28 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,3 +11,4 @@ dist/
|
||||||
src/recoder.egg-info/
|
src/recoder.egg-info/
|
||||||
|
|
||||||
*.gresource
|
*.gresource
|
||||||
|
*.compiled
|
||||||
|
|
14
dev-run.sh
Executable file
14
dev-run.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Compile resources
|
||||||
|
glib-compile-resources src/resources/resources.xml \
|
||||||
|
--target=src/recoder/resources.gresource \
|
||||||
|
--sourcedir=src/resources
|
||||||
|
|
||||||
|
# Compile GSettings schemas (using correct path)
|
||||||
|
glib-compile-schemas src/resources
|
||||||
|
|
||||||
|
# Run the app with environment variables
|
||||||
|
GSETTINGS_SCHEMA_DIR=$(pwd)/src/resources \
|
||||||
|
PYTHONPATH=$(pwd)/src \
|
||||||
|
python -m recoder.app
|
|
@ -18,7 +18,7 @@ def load_resources():
|
||||||
def main():
|
def main():
|
||||||
load_resources()
|
load_resources()
|
||||||
|
|
||||||
from recoder.recoder_window import RecoderWindow # delayed import
|
from recoder.window import RecoderWindow # delayed import
|
||||||
|
|
||||||
class RecoderApp(Adw.Application):
|
class RecoderApp(Adw.Application):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
58
src/recoder/preferences.py
Normal file
58
src/recoder/preferences.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gtk", "4.0")
|
||||||
|
gi.require_version("Adw", "1")
|
||||||
|
from gi.repository import Gtk, Gio, Adw
|
||||||
|
import re
|
||||||
|
|
||||||
|
@Gtk.Template(resource_path="/net/jeena/recoder/preferences.ui")
|
||||||
|
class RecoderPreferences(Adw.PreferencesWindow):
|
||||||
|
__gtype_name__ = "RecoderPreferences"
|
||||||
|
|
||||||
|
output_folder_entry = Gtk.Template.Child()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.settings = Gio.Settings.new("net.jeena.recoder.preferences")
|
||||||
|
|
||||||
|
current_value = self.settings.get_string("output-folder-template")
|
||||||
|
self.output_folder_entry.set_text(current_value)
|
||||||
|
|
||||||
|
self.output_folder_entry.connect("changed", self.on_output_folder_changed)
|
||||||
|
self.settings.connect("changed::output-folder-template", self.on_setting_changed)
|
||||||
|
|
||||||
|
def validate_template(self, text):
|
||||||
|
allowed_pattern = r'^[\w\s\-./~${}]+$'
|
||||||
|
if not re.match(allowed_pattern, text):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if text.count("{{") != text.count("}}"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
var_pattern = r'\{\{([a-zA-Z0-9_]+)\}\}'
|
||||||
|
for var in re.findall(r'\{\{.*?\}\}', text):
|
||||||
|
if not re.match(var_pattern, var):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if '//' in text.replace('file://', ''):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def on_output_folder_changed(self, entry):
|
||||||
|
text = entry.get_text()
|
||||||
|
|
||||||
|
if self.validate_template(text):
|
||||||
|
self.settings.set_string("output-folder-template", text)
|
||||||
|
entry.remove_css_class("error")
|
||||||
|
else:
|
||||||
|
entry.add_css_class("error")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def on_setting_changed(self, settings, key):
|
||||||
|
if key == "output-folder-template":
|
||||||
|
new_val = settings.get_string(key)
|
||||||
|
if self.output_folder_entry.get_text() != new_val:
|
||||||
|
self.output_folder_entry.set_text(new_val)
|
|
@ -14,9 +14,10 @@ from recoder.utils import extract_video_files, notify_done, play_complete_sound
|
||||||
from recoder.file_entry_row import FileEntryRow
|
from recoder.file_entry_row import FileEntryRow
|
||||||
from recoder.drop_handler import DropHandler
|
from recoder.drop_handler import DropHandler
|
||||||
from recoder.app_state import AppState, AppStateManager, UIStateManager
|
from recoder.app_state import AppState, AppStateManager, UIStateManager
|
||||||
|
from recoder.preferences import RecoderPreferences
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui")
|
@Gtk.Template(resource_path="/net/jeena/recoder/window.ui")
|
||||||
class RecoderWindow(Adw.ApplicationWindow):
|
class RecoderWindow(Adw.ApplicationWindow):
|
||||||
__gtype_name__ = "RecoderWindow"
|
__gtype_name__ = "RecoderWindow"
|
||||||
|
|
||||||
|
@ -28,9 +29,18 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
btn_cancel = Gtk.Template.Child()
|
btn_cancel = Gtk.Template.Child()
|
||||||
progress_bar = Gtk.Template.Child()
|
progress_bar = Gtk.Template.Child()
|
||||||
folder_label = Gtk.Template.Child()
|
folder_label = Gtk.Template.Child()
|
||||||
|
btn_preferences = Gtk.Template.Child()
|
||||||
|
|
||||||
def __init__(self, application):
|
def __init__(self, application):
|
||||||
super().__init__(application=application)
|
super().__init__(application=application)
|
||||||
|
|
||||||
|
self.state_settings = Gio.Settings.new("net.jeena.recoder.state")
|
||||||
|
|
||||||
|
# Bind window size and state to your window properties
|
||||||
|
self.state_settings.bind("width", self, "default-width", Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
self.state_settings.bind("height", self, "default-height", Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
self.state_settings.bind("is-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
self.state_settings.bind("is-fullscreen", self, "fullscreened", Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
|
||||||
self.file_items_to_process = []
|
self.file_items_to_process = []
|
||||||
self.current_folder_name = None
|
self.current_folder_name = None
|
||||||
|
@ -44,6 +54,7 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
|
|
||||||
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
||||||
self.btn_cancel.connect("clicked", self.on_cancel_clicked)
|
self.btn_cancel.connect("clicked", self.on_cancel_clicked)
|
||||||
|
self.btn_preferences.connect("clicked", self.on_preferences_clicked)
|
||||||
|
|
||||||
self.app_state_manager.state = AppState.IDLE
|
self.app_state_manager.state = AppState.IDLE
|
||||||
|
|
||||||
|
@ -57,10 +68,24 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
|
|
||||||
Notify.init("Recoder")
|
Notify.init("Recoder")
|
||||||
|
|
||||||
def process_drop_value(self, value):
|
self.preferences_window = None
|
||||||
|
|
||||||
# value could be a list of Gio.File or a single Gio.File
|
def on_preferences_clicked(self, button):
|
||||||
# Assuming it's a list:
|
if self.preferences_window is None:
|
||||||
|
self.preferences_window = RecoderPreferences()
|
||||||
|
self.preferences_window.set_transient_for(self)
|
||||||
|
self.preferences_window.set_modal(True)
|
||||||
|
self.preferences_window.connect("close-request", self.on_preferences_window_close)
|
||||||
|
|
||||||
|
self.preferences_window.present()
|
||||||
|
|
||||||
|
def on_preferences_window_close(self, window):
|
||||||
|
window.hide()
|
||||||
|
# Don't destroy, just hide
|
||||||
|
return True # stops further handlers, prevents default destruction
|
||||||
|
|
||||||
|
|
||||||
|
def process_drop_value(self, value):
|
||||||
folder_file = None
|
folder_file = None
|
||||||
if isinstance(value, list) and len(value) > 0:
|
if isinstance(value, list) and len(value) > 0:
|
||||||
folder_file = value[0]
|
folder_file = value[0]
|
||||||
|
@ -68,7 +93,6 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
folder_file = value
|
folder_file = value
|
||||||
|
|
||||||
if folder_file:
|
if folder_file:
|
||||||
# Set the current folder name for UI
|
|
||||||
self.current_folder_name = folder_file.get_basename()
|
self.current_folder_name = folder_file.get_basename()
|
||||||
|
|
||||||
file_items = extract_video_files(value)
|
file_items = extract_video_files(value)
|
||||||
|
@ -107,8 +131,6 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
if not self.file_items_to_process:
|
if not self.file_items_to_process:
|
||||||
return
|
return
|
||||||
|
|
||||||
# no need to remove drop_controller here
|
|
||||||
|
|
||||||
self.transcoder = Transcoder(self.file_items_to_process)
|
self.transcoder = Transcoder(self.file_items_to_process)
|
||||||
self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress)
|
self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress)
|
||||||
self.transcoder.connect("notify::batch-status", self.on_transcoder_status)
|
self.transcoder.connect("notify::batch-status", self.on_transcoder_status)
|
36
src/resources/net.jeena.recoder.gschema.xml
Normal file
36
src/resources/net.jeena.recoder.gschema.xml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<schemalist>
|
||||||
|
<schema id="net.jeena.recoder.preferences" path="/net/jeena/recoder/preferences/" gettext-domain="recoder">
|
||||||
|
<key name="output-folder-template" type="s">
|
||||||
|
<default>'{{source_folder_name}}-transcoded'</default>
|
||||||
|
<summary>Template for output folder</summary>
|
||||||
|
<description>
|
||||||
|
Relative or absolute path template for transcoded files.
|
||||||
|
Supports {{source_folder_name}} as a variable.
|
||||||
|
</description>
|
||||||
|
</key>
|
||||||
|
</schema>
|
||||||
|
|
||||||
|
<schema id="net.jeena.recoder.state" path="/net/jeena/recoder/state/" gettext-domain="recoder">
|
||||||
|
<key name="width" type="i">
|
||||||
|
<default>600</default>
|
||||||
|
<summary>Window width</summary>
|
||||||
|
<description>Last saved width of the main window.</description>
|
||||||
|
</key>
|
||||||
|
<key name="height" type="i">
|
||||||
|
<default>350</default>
|
||||||
|
<summary>Window height</summary>
|
||||||
|
<description>Last saved height of the main window.</description>
|
||||||
|
</key>
|
||||||
|
<key name="is-maximized" type="b">
|
||||||
|
<default>false</default>
|
||||||
|
<summary>Window maximized state</summary>
|
||||||
|
<description>Whether the main window was maximized last time.</description>
|
||||||
|
</key>
|
||||||
|
<key name="is-fullscreen" type="b">
|
||||||
|
<default>false</default>
|
||||||
|
<summary>Window fullscreen state</summary>
|
||||||
|
<description>Whether the main window was fullscreen last time.</description>
|
||||||
|
</key>
|
||||||
|
</schema>
|
||||||
|
</schemalist>
|
||||||
|
|
52
src/resources/preferences.ui
Normal file
52
src/resources/preferences.ui
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="RecoderPreferences" parent="AdwPreferencesWindow">
|
||||||
|
<property name="title">Preferences</property>
|
||||||
|
<property name="default-width">600</property>
|
||||||
|
<property name="default-height">400</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesPage">
|
||||||
|
<property name="title">General</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="description_label">
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="justify">left</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
<property name="margin-bottom">12</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="use-markup">True</property>
|
||||||
|
<property name="selectable">True</property>
|
||||||
|
<property name="label">
|
||||||
|
<![CDATA[
|
||||||
|
You can use:
|
||||||
|
• <tt>{{source_folder_name}}</tt> to reuse the original folder name
|
||||||
|
• Relative paths like <tt>../done/</tt>
|
||||||
|
• Absolute paths like <tt>/mnt/Export/</tt>
|
||||||
|
• <tt>~</tt> to refer to your home directory
|
||||||
|
• Simple names like <tt>output</tt> to create the folder inside the source folder
|
||||||
|
• Any combination of the above, e.g. <tt>../{{source_folder_name}}-dnxhd</tt>
|
||||||
|
]]>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="AdwEntryRow" id="output_folder_entry">
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="title">Output Folder Template</property>
|
||||||
|
<property name="tooltip-text">Set output folder template</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
|
@ -1,6 +1,7 @@
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/net/jeena/recoder">
|
<gresource prefix="/net/jeena/recoder">
|
||||||
<file>recoder_window.ui</file>
|
<file>window.ui</file>
|
||||||
|
<file>preferences.ui</file>
|
||||||
<file>file_entry_row.ui</file>
|
<file>file_entry_row.ui</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
|
|
|
@ -12,25 +12,31 @@
|
||||||
<object class="AdwToolbarView" id="toolbar_view">
|
<object class="AdwToolbarView" id="toolbar_view">
|
||||||
<child type="top">
|
<child type="top">
|
||||||
<object class="AdwHeaderBar" id="header_bar">
|
<object class="AdwHeaderBar" id="header_bar">
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="btn_transcode">
|
<object class="GtkButton" id="btn_transcode">
|
||||||
<property name="label">Transcode</property>
|
<property name="label">Transcode</property>
|
||||||
<property name="sensitive">False</property>
|
<property name="sensitive">False</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child type="title">
|
<child type="title">
|
||||||
<object class="GtkLabel" id="folder_label">
|
<object class="GtkLabel" id="folder_label">
|
||||||
<property name="ellipsize">end</property>
|
<property name="ellipsize">end</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child type="end">
|
<child type="end">
|
||||||
<object class="GtkButton" id="btn_cancel">
|
<object class="GtkButton" id="btn_preferences">
|
||||||
<property name="label">Cancel</property>
|
<property name="icon-name">emblem-system-symbolic</property>
|
||||||
<property name="visible">False</property>
|
<property name="tooltip-text">Preferences</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child type="end">
|
||||||
|
<object class="GtkButton" id="btn_cancel">
|
||||||
|
<property name="label">Cancel</property>
|
||||||
|
<property name="visible">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
|
@ -75,6 +81,7 @@
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkProgressBar" id="progress_bar">
|
<object class="GtkProgressBar" id="progress_bar">
|
||||||
<property name="hexpand">true</property>
|
<property name="hexpand">true</property>
|
Loading…
Add table
Add a link
Reference in a new issue