Add docs and Help link + Toasts

This commit is contained in:
Jeena 2025-06-08 21:48:52 +09:00
parent 6c4f43a947
commit aa21483bd1
10 changed files with 225 additions and 118 deletions

74
docs/HELP.md Normal file
View file

@ -0,0 +1,74 @@
<p align="center">
<img src="../src/resources/net.jeena.Recoder.svg" width="120" height="120" alt="Recoder logo">
</p>
# Recoder — Help Guide
Recoder is a minimal, user-friendly tool for batch video transcoding. This quick guide walks you through using the app.
---
## 🚀 Getting Started
When you open Recoder, youll see a prompt inviting you to drop a video file or folder:
![Initial View](screenshot-1.png)
### 📂 Dropping Files or Folders
- You can drop **one video file** or **one folder** containing video files onto the app.
- The folder can have subdirectories, but Recoder will **not** process files recursively. Only files in the dropped folder itself will be processed.
- Non-video files will be ignored.
### 🔧 Preparing to Transcode
After you drop a folder into Recoder, it will list all the video files it found:
![Folder Loaded](./screenshot-2.png)
- A blue **Transcode** button appears once the files are ready to process.
- The **Clear icon** is always available — click it to cancel everything and reset the app if you're done or need to start over.
- The **menu button** gives access to Preferences and Help.
---
## 🎬 Transcoding
Click the Transcode button to start processing. While transcoding:
![Transcoding in Progress](./screenshot-3.png)
- The blue **Transcode** button is replaced by a **Pause** button, allowing you to temporarily stop the process.
- If paused, the button changes to **Resume**, so you can continue when you're ready.
- The **Clear button** can also be used during transcoding to cancel the process entirely and clear the current session.
By default:
- Transcoded files are saved into the same directory as the source, inside a subfolder named `{{source_folder_name}}-transcoded`.
- File names remain the same as the originals but with a `.mov` extension.
---
### ⚙️ Preferences
In Preferences, you can customize the single **output folder path** where transcoded files will be saved. This path controls both the folders location and name. You can use:
- `{{source_folder_name}}` to reuse the original folder name
- Relative paths like `../done/`
- Absolute paths like `/mnt/Export/`
- `~` to refer to your home directory
- Simple names like `output` to create a folder inside the source folder
- Any combination of the above, e.g. `../{{source_folder_name}}-dnxhd`
![Preferences](screenshot-4.png)
---
## 💡 Notes
- Make sure you have enough free space on your drive because both the original and transcoded files are kept, and transcoded files may be larger.
---
If you need more help, check the [GitHub repository](https://github.com/jeena/recoder) or open an issue.

BIN
docs/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -55,6 +55,11 @@ def main():
self.add_action(preferences_action) self.add_action(preferences_action)
self.set_accels_for_action("app.preferences", ["<Primary>comma"]) self.set_accels_for_action("app.preferences", ["<Primary>comma"])
help_action = Gio.SimpleAction.new("help", None)
help_action.connect("activate", self.on_help_activated)
self.add_action(help_action)
self.set_accels_for_action("app.help", ["F1"])
about_action = Gio.SimpleAction.new("about", None) about_action = Gio.SimpleAction.new("about", None)
about_action.connect("activate", self.on_about_activate) about_action.connect("activate", self.on_about_activate)
self.add_action(about_action) self.add_action(about_action)
@ -89,6 +94,9 @@ def main():
def on_preferences_close(self, window): def on_preferences_close(self, window):
window.set_visible(False) window.set_visible(False)
if window.prefs_changed:
window.prefs_changed = False
self.window.toast_overlay.add_toast(Adw.Toast.new("Preferences saved"))
# Don't destroy, just hide # Don't destroy, just hide
return True # stops further handlers, prevents default destruction return True # stops further handlers, prevents default destruction
@ -96,6 +104,16 @@ def main():
self.quit() self.quit()
return False # allow default handler to proceed return False # allow default handler to proceed
def on_help_activated(self, action, param):
uri = "https://github.com/jeena/recoder/blob/master/docs/HELP.md"
try:
Gio.AppInfo.launch_default_for_uri(uri, None)
self.window.toast_overlay.add_toast(Adw.Toast.new("Opening help in browser…"))
except GLib.Error as e:
self.window.toast_overlay.add_toast(Adw.Toast.new(f"Failed to open help: {e.message}"))
app = RecoderApp() app = RecoderApp()
return app.run(sys.argv) return app.run(sys.argv)

View file

@ -13,6 +13,7 @@ class RecoderPreferences(Adw.PreferencesWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.prefs_changed = False
self.settings = Gio.Settings.new("net.jeena.recoder.preferences") self.settings = Gio.Settings.new("net.jeena.recoder.preferences")
current_value = self.settings.get_string("output-folder-template") current_value = self.settings.get_string("output-folder-template")
@ -44,13 +45,11 @@ class RecoderPreferences(Adw.PreferencesWindow):
if self.validate_template(text): if self.validate_template(text):
self.settings.set_string("output-folder-template", text) self.settings.set_string("output-folder-template", text)
self.prefs_changed = True
entry.remove_css_class("error") entry.remove_css_class("error")
else: else:
entry.add_css_class("error") entry.add_css_class("error")
def on_setting_changed(self, settings, key): def on_setting_changed(self, settings, key):
if key == "output-folder-template": if key == "output-folder-template":
new_val = settings.get_string(key) new_val = settings.get_string(key)

View file

@ -22,6 +22,7 @@ from recoder.app import APP_NAME
class RecoderWindow(Adw.ApplicationWindow): class RecoderWindow(Adw.ApplicationWindow):
__gtype_name__ = "RecoderWindow" __gtype_name__ = "RecoderWindow"
toast_overlay = Gtk.Template.Child()
overlay = Gtk.Template.Child() overlay = Gtk.Template.Child()
drop_hint = Gtk.Template.Child() drop_hint = Gtk.Template.Child()
listbox = Gtk.Template.Child() listbox = Gtk.Template.Child()
@ -96,6 +97,10 @@ class RecoderWindow(Adw.ApplicationWindow):
self.file_items_to_process = file_items self.file_items_to_process = file_items
self.app_state_manager.state = AppState.FILES_LOADED self.app_state_manager.state = AppState.FILES_LOADED
count = len(self.file_items_to_process)
toast = Adw.Toast.new(f"{count} video file{'s' if count != 1 else ''} added")
self.toast_overlay.add_toast(toast)
return False return False
def clear_listbox(self): def clear_listbox(self):
@ -122,26 +127,30 @@ class RecoderWindow(Adw.ApplicationWindow):
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)
self.transcoder.start() self.transcoder.start()
self.app_state_manager.state = AppState.TRANSCODING self.app_state_manager.state = AppState.TRANSCODING
self.toast_overlay.add_toast(Adw.Toast.new("Starting transcoding"))
def pause_transcoding(self): def pause_transcoding(self):
if self.transcoder: if self.transcoder:
self.transcoder.pause() self.transcoder.pause()
self.is_paused = True self.is_paused = True
self.app_state_manager.state = AppState.PAUSED self.app_state_manager.state = AppState.PAUSED
self.toast_overlay.add_toast(Adw.Toast.new("Transcoding paused"))
def resume_transcoding(self): def resume_transcoding(self):
if self.transcoder: if self.transcoder:
self.transcoder.resume() self.transcoder.resume()
self.is_paused = False self.is_paused = False
self.app_state_manager.state = AppState.TRANSCODING self.app_state_manager.state = AppState.TRANSCODING
self.toast_overlay.add_toast(Adw.Toast.new("Resuming transcoding"))
def on_clear_clicked(self, button): def on_clear_clicked(self, button):
if self.transcoder and self.transcoder.is_processing: if self.transcoder and self.transcoder.is_processing:
self.transcoder.stop() self.transcoder.stop()
self.transcoder = None self.transcoder = None
self.clear_listbox()
self.app_state_manager.state = AppState.STOPPED self.app_state_manager.state = AppState.STOPPED
self.toast_overlay.add_toast(Adw.Toast.new("File list cleared"))
def on_transcoder_progress(self, transcoder, param): def on_transcoder_progress(self, transcoder, param):
self.progress_bar.set_fraction(transcoder.batch_progress / 100.0) self.progress_bar.set_fraction(transcoder.batch_progress / 100.0)
@ -150,6 +159,7 @@ class RecoderWindow(Adw.ApplicationWindow):
if transcoder.batch_status == BatchStatus.DONE: if transcoder.batch_status == BatchStatus.DONE:
play_complete_sound() play_complete_sound()
notify_done(APP_NAME, "Transcoding finished!") notify_done(APP_NAME, "Transcoding finished!")
self.toast_overlay.add_toast(Adw.Toast.new("Transcoding finished!"))
self.app_state_manager.state = AppState.DONE self.app_state_manager.state = AppState.DONE
elif transcoder.batch_status == BatchStatus.STOPPED: elif transcoder.batch_status == BatchStatus.STOPPED:
@ -157,4 +167,5 @@ class RecoderWindow(Adw.ApplicationWindow):
elif transcoder.batch_status == BatchStatus.ERROR: elif transcoder.batch_status == BatchStatus.ERROR:
notify_done(APP_NAME, "An error occurred during transcoding.") notify_done(APP_NAME, "An error occurred during transcoding.")
self.toast_overlay.add_toast(Adw.Toast.new("An error occurred during transcoding"))
self.app_state_manager.state = AppState.ERROR self.app_state_manager.state = AppState.ERROR

View file

@ -4,6 +4,5 @@
<file>preferences.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>
<file alias="net.jeena.Recoder.svg">../resources/net.jeena.Recoder.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View file

@ -8,6 +8,10 @@
<attribute name="label">Preferences</attribute> <attribute name="label">Preferences</attribute>
<attribute name="action">app.preferences</attribute> <attribute name="action">app.preferences</attribute>
</item> </item>
<item>
<attribute name="label">Help</attribute>
<attribute name="action">app.help</attribute>
</item>
<item> <item>
<attribute name="label">About Recoder</attribute> <attribute name="label">About Recoder</attribute>
<attribute name="action">app.about</attribute> <attribute name="action">app.about</attribute>
@ -24,143 +28,145 @@
<property name="default-height">400</property> <property name="default-height">400</property>
<child> <child>
<object class="AdwToolbarView" id="toolbar_view"> <object class="AdwToastOverlay" id="toast_overlay">
<child type="top"> <child>
<object class="AdwHeaderBar" id="header_bar"> <object class="AdwToolbarView" id="toolbar_view">
<child> <child type="top">
<object class="GtkButton" id="btn_transcode"> <object class="AdwHeaderBar" id="header_bar">
<property name="label">Transcode</property> <child>
<property name="sensitive">False</property> <object class="GtkButton" id="btn_transcode">
<property name="label">Transcode</property>
<property name="sensitive">False</property>
</object>
</child>
<child type="title">
<object class="GtkLabel" id="folder_label">
<property name="ellipsize">end</property>
<property name="hexpand">True</property>
<property name="halign">center</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main_menu</property>
<property name="tooltip-text">Menu</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="btn_clear">
<property name="icon-name">edit-clear-symbolic</property>
<property name="tooltip-text">Clear</property>
<property name="visible">False</property>
<property name="can-focus">False</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object> </object>
</child> </child>
<child type="title">
<object class="GtkLabel" id="folder_label">
<property name="ellipsize">end</property>
<property name="hexpand">True</property>
<property name="halign">center</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main_menu</property>
<property name="tooltip-text">Menu</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="btn_clear">
<property name="icon-name">edit-clear-symbolic</property>
<property name="tooltip-text">Clear</property>
<property name="visible">False</property>
<property name="can-focus">False</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object>
</child>
<property name="content"> <property name="content">
<object class="GtkBox" id="main_box"> <object class="GtkBox" id="main_box">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="vexpand">true</property>
<child>
<object class="GtkOverlay" id="overlay">
<property name="hexpand">true</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolled_window"> <object class="GtkOverlay" id="overlay">
<property name="hexpand">true</property> <property name="hexpand">true</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<child> <child>
<object class="GtkListBox" id="listbox"> <object class="GtkScrolledWindow" id="scrolled_window">
<property name="selection-mode">none</property> <property name="hexpand">true</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<property name="show-separators">True</property>
<style>
<class name="rich-list"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkBox" id="drop_hint">
<property name="orientation">vertical</property>
<property name="halign">fill</property>
<property name="valign">fill</property>
<property name="spacing">48</property>
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<!-- Inner box to center content -->
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spacing">48</property>
<child> <child>
<object class="GtkBox"> <object class="GtkListBox" id="listbox">
<property name="orientation">horizontal</property> <property name="selection-mode">none</property>
<property name="halign">center</property> <property name="vexpand">true</property>
<property name="spacing">48</property> <property name="show-separators">True</property>
<child>
<object class="GtkImage">
<property name="icon-name">video-x-generic-symbolic</property>
<property name="pixel-size">48</property>
<style>
<class name="dim-icon"/>
</style>
</object>
</child>
<child>
<object class="GtkImage">
<property name="icon-name">folder-symbolic</property>
<property name="pixel-size">48</property>
<style>
<class name="dim-icon"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Drop video files or folders here to get started</property>
<style> <style>
<class name="dim-label"/> <class name="rich-list"/>
</style> </style>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child type="overlay">
<object class="GtkBox" id="drop_hint">
<property name="orientation">vertical</property>
<property name="halign">fill</property>
<property name="valign">fill</property>
<property name="spacing">48</property>
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<!-- Inner box to center content -->
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spacing">48</property>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="halign">center</property>
<property name="spacing">48</property>
<child>
<object class="GtkImage">
<property name="icon-name">video-x-generic-symbolic</property>
<property name="pixel-size">48</property>
<style>
<class name="dim-icon"/>
</style>
</object>
</child>
<child>
<object class="GtkImage">
<property name="icon-name">folder-symbolic</property>
<property name="pixel-size">48</property>
<style>
<class name="dim-icon"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Drop video files or folders here to get started</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object> </object>
</child> </child>
<child>
<object class="GtkProgressBar" id="progress_bar">
<property name="hexpand">true</property>
<property name="visible">False</property>
</object>
</child>
</object> </object>
</child> </property>
<child>
<object class="GtkProgressBar" id="progress_bar">
<property name="hexpand">true</property>
<property name="visible">False</property>
</object>
</child>
</object> </object>
</property> </child>
</object> </object>
</child> </child>
</template> </template>