Compare commits
1 Commits
41f39ba8cc
...
convert-to
Author | SHA1 | Date | |
---|---|---|---|
09c5af3821 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
.idea/
|
||||
*.zip
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# SolarFM
|
||||
|
||||
# SolarFM
|
||||
SolarFM is a Gtk+ Python file manager.
|
||||
|
||||
@@ -11,8 +13,8 @@ sudo apt-get install python3.8 wget python3-setproctitle python3-gi ffmpegthumbn
|
||||
|
||||
# TODO
|
||||
<ul>
|
||||
<li>Add simpleish preview plugin for various file types.</li>
|
||||
<li>Add simpleish bulk-renamer.</li>
|
||||
<li>Add simpleish plugin system to run bash/python scripts.</li>
|
||||
<li>Add DnD context awareness for over folder drop.</li>
|
||||
</ul>
|
||||
|
||||
# Images
|
||||
|
Binary file not shown.
@@ -1,64 +0,0 @@
|
||||
### Note
|
||||
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
|
||||
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
|
||||
|
||||
|
||||
### Manifest Example (All are required even if empty.)
|
||||
```
|
||||
class Manifest:
|
||||
name: str = "Example Plugin"
|
||||
author: str = "John Doe"
|
||||
version: str = "0.0.1"
|
||||
support: str = ""
|
||||
requests: {} = {
|
||||
'ui_target': "plugin_control_list",
|
||||
'pass_fm_events': "true"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Requests
|
||||
```
|
||||
requests: {} = {
|
||||
'ui_target': "plugin_control_list",
|
||||
'ui_target_id': "<some other Gtk Glade ID>", # Only needed if using "other" in "ui_target". See below for predefined "ui_target" options...
|
||||
'pass_fm_events': "true", # If empty or not present will be ignored.
|
||||
"pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin.
|
||||
'bind_keys': [f"{name}||send_message:<Control>f"],
|
||||
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
UI Targets:
|
||||
<ul>
|
||||
<li>main_Window</li>
|
||||
<li>main_menu_bar</li>
|
||||
<li>path_menu_bar</li>
|
||||
<li>plugin_control_list</li>
|
||||
<li>window_(1-4)</li>
|
||||
<li>context_menu</li>
|
||||
<li>other</li>
|
||||
</ul>
|
||||
|
||||
### Methods
|
||||
```
|
||||
# Must define and return a widget if "ui_target" is defined.
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._do_download)
|
||||
return button
|
||||
|
||||
# Must define in plugin if "pass_fm_events" is set to "true" string.
|
||||
def set_fm_event_system(self, fm_event_system):
|
||||
self._fm_event_system = fm_event_system
|
||||
|
||||
# Must define regardless if needed. Can just pass if plugin does stuff in its __init__
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
# Must define in plugin if "pass_ui_objects" is set and an array of valid glade UI IDs.
|
||||
def set_ui_object_collection(self, ui_objects):
|
||||
self._ui_objects = ui_objects
|
||||
|
||||
```
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,156 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<object class="GtkListStore" id="favorites_store">
|
||||
<columns>
|
||||
<!-- column-name Favorites -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkDialog" id="favorites_dialog">
|
||||
<property name="width-request">320</property>
|
||||
<property name="height-request">450</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
<property name="decorated">False</property>
|
||||
<property name="deletable">False</property>
|
||||
<property name="gravity">center</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label">gtk-delete</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="released" handler="_remove_from_favorite" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label">gtk-add</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="released" handler="_add_to_favorite" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label">gtk-close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="released" handler="_hide_favorites_menu" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_dir_lbl">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label" translatable="yes">Current Directory:</property>
|
||||
<property name="justify">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">favorites_store</property>
|
||||
<property name="headers-clickable">False</property>
|
||||
<signal name="button-release-event" handler="_set_selected_path" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<signal name="changed" handler="_set_selected" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn">
|
||||
<property name="title" translatable="yes">Favorites</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Favorites",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "main_menu_bttn_box_bar",
|
||||
"pass_fm_events": "true",
|
||||
"pass_ui_objects": ["path_entry"],
|
||||
"bind_keys": []
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,133 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, time, inspect, json
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Favorites" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self._GLADE_FILE = f"{self.path}/favorites.glade"
|
||||
self._FAVORITES_FILE = f"{self.path}/favorites.json"
|
||||
|
||||
self._favorites_dialog = None
|
||||
self._favorites_store = None
|
||||
self._favorites = None
|
||||
self._state = None
|
||||
self._selected = None
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._show_favorites_menu)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
|
||||
classes = [self]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
self._builder.connect_signals(handlers)
|
||||
|
||||
self._favorites_dialog = self._builder.get_object("favorites_dialog")
|
||||
self._favorites_store = self._builder.get_object("favorites_store")
|
||||
self._current_dir_lbl = self._builder.get_object("current_dir_lbl")
|
||||
|
||||
if os.path.exists(self._FAVORITES_FILE):
|
||||
with open(self._FAVORITES_FILE) as f:
|
||||
self._favorites = json.load(f)
|
||||
for favorite in self._favorites:
|
||||
self._favorites_store.append([favorite])
|
||||
else:
|
||||
with open(self._FAVORITES_FILE, 'a') as f:
|
||||
f.write('[]')
|
||||
|
||||
|
||||
@threaded
|
||||
def _get_state(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
self._state = self._event_message
|
||||
self._event_message = None
|
||||
|
||||
@threaded
|
||||
def _set_current_dir_lbl(self, widget=None, eve=None):
|
||||
self.wait_for_state()
|
||||
self._current_dir_lbl.set_label(f"Current Directory:\n{self._state.tab.get_current_directory()}")
|
||||
|
||||
def _add_to_favorite(self, state):
|
||||
current_directory = self._state.tab.get_current_directory()
|
||||
self._favorites_store.append([current_directory])
|
||||
self._favorites.append(current_directory)
|
||||
self._save_favorites()
|
||||
|
||||
def _remove_from_favorite(self, state):
|
||||
path = self._favorites_store.get_value(self._selected, 0)
|
||||
self._favorites_store.remove(self._selected)
|
||||
self._favorites.remove(path)
|
||||
self._save_favorites()
|
||||
|
||||
def _save_favorites(self):
|
||||
with open(self._FAVORITES_FILE, 'w') as outfile:
|
||||
json.dump(self._favorites, outfile, separators=(',', ':'), indent=4)
|
||||
|
||||
def _set_selected_path(self, widget=None, eve=None):
|
||||
path = self._favorites_store.get_value(self._selected, 0)
|
||||
self._ui_objects[0].set_text(path)
|
||||
|
||||
|
||||
|
||||
def _show_favorites_menu(self, widget=None, eve=None):
|
||||
self._state = None
|
||||
self._get_state()
|
||||
self._set_current_dir_lbl()
|
||||
self._favorites_dialog.run()
|
||||
|
||||
def _hide_favorites_menu(self, widget=None, eve=None):
|
||||
self._favorites_dialog.hide()
|
||||
|
||||
def _set_selected(self, user_data):
|
||||
selected = user_data.get_selected()[1]
|
||||
if selected:
|
||||
self._selected = selected
|
||||
|
||||
def wait_for_state(self):
|
||||
while not self._state:
|
||||
pass
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,685 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkDialog" id="file_properties_dialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">6</property>
|
||||
<property name="title" translatable="yes">File Properties</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="default-width">420</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<signal name="response" handler="on_filePropertiesDlg_response" swapped="no"/>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog_action_area">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="ok_button">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkNotebook" id="notebook">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="border-width">6</property>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="top-padding">6</property>
|
||||
<property name="bottom-padding">6</property>
|
||||
<property name="left-padding">12</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="general_table">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="n-rows">7</property>
|
||||
<property name="n-columns">2</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_filename">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>File _Name:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_name</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_name">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label20">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Location:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_location</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_location">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_target">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Link _Target:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_target</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_target">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label4">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Type:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="yalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">3</property>
|
||||
<property name="bottom-attach">4</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options">GTK_FILL</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="mime_type">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="yalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">3</property>
|
||||
<property name="bottom-attach">4</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label5">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Size:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">4</property>
|
||||
<property name="bottom-attach">5</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="file_size">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">4</property>
|
||||
<property name="bottom-attach">5</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label7">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Modified:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">5</property>
|
||||
<property name="bottom-attach">6</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label13">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Accessed:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">6</property>
|
||||
<property name="bottom-attach">7</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="mtime">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">5</property>
|
||||
<property name="bottom-attach">6</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="atime">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">6</property>
|
||||
<property name="bottom-attach">7</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="label1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">_Info</property>
|
||||
<property name="use-underline">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab-fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="top-padding">6</property>
|
||||
<property name="bottom-padding">6</property>
|
||||
<property name="left-padding">12</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table3">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="n-rows">2</property>
|
||||
<property name="n-columns">2</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="owner_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Owner:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_owner</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="group_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Group:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_group</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_owner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_group">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHSeparator" id="hseparator1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTable" id="table2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="n-rows">3</property>
|
||||
<property name="n-columns">5</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label17">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Owner:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label18">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Group:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label19">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Other:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="owner_r">
|
||||
<property name="label" translatable="yes">Read</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="group_r">
|
||||
<property name="label" translatable="yes">Read</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="others_r">
|
||||
<property name="label" translatable="yes">Read</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="owner_w">
|
||||
<property name="label" translatable="yes">Write</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="right-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="group_w">
|
||||
<property name="label" translatable="yes">Write</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="right-attach">3</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="others_w">
|
||||
<property name="label" translatable="yes">Write</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="right-attach">3</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="owner_x">
|
||||
<property name="label" translatable="yes">Execute</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="right-attach">4</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="group_x">
|
||||
<property name="label" translatable="yes">Execute</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="right-attach">4</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="others_x">
|
||||
<property name="label" translatable="yes">Execute</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="border-width">2</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="right-attach">4</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVSeparator" id="vseparator1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">4</property>
|
||||
<property name="right-attach">5</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options">GTK_FILL</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="label2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">_Permissions</property>
|
||||
<property name="use-underline">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
<property name="tab-fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">cancel_button</action-widget>
|
||||
<action-widget response="-5">ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Properties",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "context_menu",
|
||||
"pass_fm_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,244 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, time, pwd, grp
|
||||
from datetime import datetime
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gio
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Properties:
|
||||
file_uri: str = None
|
||||
file_name: str = None
|
||||
file_location: str = None
|
||||
file_target: str = None
|
||||
mime_type: str = None
|
||||
file_size: str = None
|
||||
mtime: int = None
|
||||
atime: int = None
|
||||
file_owner: str = None
|
||||
file_group: str = None
|
||||
chmod_stat: str = None
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self._GLADE_FILE = f"{self.path}/file_properties.glade"
|
||||
self.name = "Properties" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
self._properties_dialog = None
|
||||
self._file_name = None
|
||||
self._file_location = None
|
||||
self._file_target = None
|
||||
self._mime_type = None
|
||||
self._file_size = None
|
||||
self._mtime = None
|
||||
self._atime = None
|
||||
self._file_owner = None
|
||||
self._file_group = None
|
||||
|
||||
self._chmod_map: {} = {
|
||||
"7": "rwx",
|
||||
"6": "rw",
|
||||
"5": "rx",
|
||||
"4": "r",
|
||||
"3": "wx",
|
||||
"2": "w",
|
||||
"1": "x",
|
||||
"0": ""
|
||||
}
|
||||
|
||||
self._chmod_map_counter: {} = {
|
||||
"rwx": "7",
|
||||
"rw": "6",
|
||||
"rx": "5",
|
||||
"r": "4",
|
||||
"wx": "3",
|
||||
"w": "2",
|
||||
"x": "1",
|
||||
"": "0"
|
||||
}
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._show_properties_page)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
|
||||
self._properties_dialog = self._builder.get_object("file_properties_dialog")
|
||||
self._file_name = self._builder.get_object("file_name")
|
||||
self._file_location = self._builder.get_object("file_location")
|
||||
self._file_target = self._builder.get_object("file_target")
|
||||
self._mime_type = self._builder.get_object("mime_type")
|
||||
self._file_size = self._builder.get_object("file_size")
|
||||
self._mtime = self._builder.get_object("mtime")
|
||||
self._atime = self._builder.get_object("atime")
|
||||
self._file_owner = self._builder.get_object("file_owner")
|
||||
self._file_group = self._builder.get_object("file_group")
|
||||
|
||||
|
||||
|
||||
@threaded
|
||||
def _show_properties_page(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
state = self._event_message
|
||||
self._event_message = None
|
||||
|
||||
GLib.idle_add(self._process_changes, (state))
|
||||
|
||||
def _process_changes(self, state):
|
||||
if len(state.selected_files) == 1:
|
||||
uri = state.selected_files[0]
|
||||
path = state.tab.get_current_directory()
|
||||
|
||||
|
||||
properties = self._set_ui_data(uri, path)
|
||||
response = self._properties_dialog.run()
|
||||
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
|
||||
self._properties_dialog.hide()
|
||||
|
||||
self._update_file(properties)
|
||||
self._properties_dialog.hide()
|
||||
|
||||
|
||||
def _update_file(self, properties):
|
||||
chmod_stat = self._get_check_boxes()
|
||||
|
||||
if chmod_stat is not properties.chmod_stat:
|
||||
try:
|
||||
print("\nNew chmod flags...")
|
||||
print(f"Old: {''.join(properties.chmod_stat)}")
|
||||
print(f"New: {chmod_stat}")
|
||||
|
||||
command = ["chmod", f"{chmod_stat}", properties.file_uri]
|
||||
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
|
||||
result = proc.stdout.read().decode("UTF-8").strip()
|
||||
print(result)
|
||||
except Exception as e:
|
||||
print(f"Couldn't chmod\nFile: {properties.file_uri}")
|
||||
print( repr(e) )
|
||||
|
||||
|
||||
owner = self._file_owner.get_text()
|
||||
group = self._file_group.get_text()
|
||||
if owner is not properties.file_owner or group is not properties.file_group:
|
||||
try:
|
||||
print("\nNew owner/group flags...")
|
||||
print(f"Old:\n\tOwner: {properties.file_owner}\n\tGroup: {properties.file_group}")
|
||||
print(f"New:\n\tOwner: {owner}\n\tGroup: {group}")
|
||||
|
||||
uid = pwd.getpwnam(owner).pw_uid
|
||||
gid = grp.getgrnam(group).gr_gid
|
||||
os.chown(properties.file_uri, uid, gid)
|
||||
except Exception as e:
|
||||
print(f"Couldn't chmod\nFile: {properties.file_uri}")
|
||||
print( repr(e) )
|
||||
|
||||
|
||||
def _set_ui_data(self, uri, path):
|
||||
properties = Properties()
|
||||
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::*,owner::*,time::access,time::changed",
|
||||
flags=Gio.FileQueryInfoFlags.NONE,
|
||||
cancellable=None)
|
||||
|
||||
is_symlink = file_info.get_attribute_as_string("standard::is-symlink")
|
||||
properties.file_uri = uri
|
||||
properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink else ""
|
||||
properties.file_name = file_info.get_display_name()
|
||||
properties.file_location = path
|
||||
properties.mime_type = file_info.get_content_type()
|
||||
properties.file_size = self._sizeof_fmt(file_info.get_size())
|
||||
properties.mtime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::changed")) ).strftime("%A, %B %d, %Y %I:%M:%S")
|
||||
properties.atime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::access")) ).strftime("%A, %B %d, %Y %I:%M:%S")
|
||||
properties.file_owner = file_info.get_attribute_as_string("owner::user")
|
||||
properties.file_group = file_info.get_attribute_as_string("owner::group")
|
||||
|
||||
# NOTE: Read = 4, Write = 2, Exec = 1
|
||||
command = ["stat", "-c", "%a", uri]
|
||||
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
|
||||
properties.chmod_stat = list(proc.stdout.read().decode("UTF-8").strip())
|
||||
owner = self._chmod_map[f"{properties.chmod_stat[0]}"]
|
||||
group = self._chmod_map[f"{properties.chmod_stat[1]}"]
|
||||
others = self._chmod_map[f"{properties.chmod_stat[2]}"]
|
||||
|
||||
self._reset_check_boxes()
|
||||
self._set_check_boxes([["owner", owner], ["group", group], ["others", others]])
|
||||
|
||||
self._file_name.set_text(properties.file_name)
|
||||
self._file_location.set_text(properties.file_location)
|
||||
self._file_target.set_text(properties.file_target)
|
||||
self._mime_type.set_label(properties.mime_type)
|
||||
self._file_size.set_label(properties.file_size)
|
||||
self._mtime.set_text(properties.mtime)
|
||||
self._atime.set_text(properties.atime)
|
||||
self._file_owner.set_text(properties.file_owner)
|
||||
self._file_group.set_text(properties.file_group)
|
||||
|
||||
return properties
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_check_boxes(self):
|
||||
perms = [[], [], []]
|
||||
|
||||
for i, target in enumerate(["owner", "group", "others"]):
|
||||
for type in ["r", "w", "x"]:
|
||||
is_active = self._builder.get_object(f"{target}_{type}").get_active()
|
||||
if is_active:
|
||||
perms[i].append(type)
|
||||
|
||||
digits = []
|
||||
for perm in perms:
|
||||
digits.append(self._chmod_map_counter[ ''.join(perm) ])
|
||||
|
||||
return ''.join(digits)
|
||||
|
||||
def _set_check_boxes(self, targets):
|
||||
for name, target in targets:
|
||||
for type in list(target):
|
||||
obj = f"{name}_{type}"
|
||||
self._builder.get_object(obj).set_active(True)
|
||||
|
||||
def _reset_check_boxes(self):
|
||||
for target in ["owner", "group", "others"]:
|
||||
for type in ["r", "w", "x"]:
|
||||
self._builder.get_object(f"{target}_{type}").set_active(False)
|
||||
|
||||
def _sizeof_fmt(self, num, suffix="B"):
|
||||
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f} {unit}{suffix}"
|
||||
num /= 1024.0
|
||||
return f"{num:.1f} Yi{suffix}"
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Movie/TV Info",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "context_menu",
|
||||
"pass_fm_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,220 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkImage" id="image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-media-play</property>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="scrub_step_adjuster">
|
||||
<property name="lower">1</property>
|
||||
<property name="upper">100</property>
|
||||
<property name="value">65</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkTextBuffer" id="textbuffer"/>
|
||||
<object class="GtkDialog" id="info_dialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">6</property>
|
||||
<property name="title" translatable="yes">Movie / TV Info</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="default-width">420</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog_action_area">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="thumbnail_preview_img">
|
||||
<property name="height-request">320</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="overview_textview">
|
||||
<property name="height-request">120</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="wrap-mode">word</property>
|
||||
<property name="cursor-visible">False</property>
|
||||
<property name="buffer">textbuffer</property>
|
||||
<property name="overwrite">True</property>
|
||||
<property name="monospace">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTable" id="general_table">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="n-rows">3</property>
|
||||
<property name="n-columns">2</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLinkButton" id="trailer_link">
|
||||
<property name="label" translatable="yes">Trailer</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="image">image1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="right-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>File _Name:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Location:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_name">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_location">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-7">cancel_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
@@ -1,183 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, inspect, requests, shutil
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
from gi.repository import Gtk, GLib, GdkPixbuf
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
from .tmdbscraper import scraper
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self.name = "Movie/TV Info" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self._GLADE_FILE = f"{self.path}/movie_tv_info.glade"
|
||||
|
||||
self._dialog = None
|
||||
self._thumbnail_preview_img = None
|
||||
self._tmdb = scraper.get_tmdb_scraper()
|
||||
self._state = None
|
||||
self._overview = None
|
||||
self._file_name = None
|
||||
self._file_location = None
|
||||
self._trailer_link = None
|
||||
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._show_info_page)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
|
||||
classes = [self]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
self._builder.connect_signals(handlers)
|
||||
|
||||
self._thumbnailer_dialog = self._builder.get_object("info_dialog")
|
||||
self._overview = self._builder.get_object("textbuffer")
|
||||
self._file_name = self._builder.get_object("file_name")
|
||||
self._file_location = self._builder.get_object("file_location")
|
||||
self._thumbnail_preview_img = self._builder.get_object("thumbnail_preview_img")
|
||||
self._file_hash = self._builder.get_object("file_hash")
|
||||
self._trailer_link = self._builder.get_object("trailer_link")
|
||||
|
||||
@threaded
|
||||
def _show_info_page(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
state = self._event_message
|
||||
self._event_message = None
|
||||
|
||||
GLib.idle_add(self._process_changes, (state))
|
||||
|
||||
def _process_changes(self, state):
|
||||
self._state = None
|
||||
|
||||
if len(state.selected_files) == 1:
|
||||
self._state = state
|
||||
self._set_ui_data()
|
||||
response = self._thumbnailer_dialog.run()
|
||||
if response in [Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT]:
|
||||
self._thumbnailer_dialog.hide()
|
||||
|
||||
def _set_ui_data(self):
|
||||
title, path, trailer, video_data = self.get_video_data()
|
||||
keys = video_data.keys() if video_data else None
|
||||
|
||||
overview_text = video_data["overview"] if video_data else f"...NO {self.name.upper()} DATA..."
|
||||
|
||||
self.set_text_data(title, path, overview_text)
|
||||
self.set_thumbnail(video_data) if video_data else ...
|
||||
self.set_trailer_link(trailer)
|
||||
|
||||
print(video_data["videos"]) if not keys in ("", None) and "videos" in keys else ...
|
||||
|
||||
def get_video_data(self):
|
||||
uri = self._state.selected_files[0]
|
||||
path = self._state.tab.get_current_directory()
|
||||
parts = uri.split("/")
|
||||
_title = parts[ len(parts) - 1 ]
|
||||
|
||||
try:
|
||||
title = _title.split("(")[0].strip()
|
||||
startIndex = _title.index('(') + 1
|
||||
endIndex = _title.index(')')
|
||||
date = title[startIndex:endIndex]
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
title = _title
|
||||
date = None
|
||||
|
||||
try:
|
||||
|
||||
video_data = self._tmdb.search(title, date)[0]
|
||||
video_id = video_data["id"]
|
||||
try:
|
||||
results = self._tmdb.tmdbapi.get_movie(str(video_id), append_to_response="videos")["videos"]["results"]
|
||||
for result in results:
|
||||
if "YouTube" in result["site"]:
|
||||
trailer = result["key"]
|
||||
|
||||
if not trailer:
|
||||
raise Exception("No key found. Defering to none...")
|
||||
except Exception as e:
|
||||
print("No trailer found...")
|
||||
trailer = None
|
||||
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
video_data = None
|
||||
|
||||
return title, path, trailer, video_data
|
||||
|
||||
|
||||
def set_text_data(self, title, path, overview_text):
|
||||
self._file_name.set_text(title)
|
||||
self._file_location.set_text(path)
|
||||
self._overview.set_text(overview_text)
|
||||
|
||||
@threaded
|
||||
def set_thumbnail(self, video_data):
|
||||
background_url = video_data["backdrop_path"]
|
||||
# background_url = video_data["poster_path"]
|
||||
background_pth = "/tmp/sfm_mvtv_info.jpg"
|
||||
|
||||
try:
|
||||
os.remove(background_pth)
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
r = requests.get(background_url, stream = True)
|
||||
|
||||
if r.status_code == 200:
|
||||
r.raw.decode_content = True
|
||||
with open(background_pth,'wb') as f:
|
||||
shutil.copyfileobj(r.raw, f)
|
||||
|
||||
print('Cover Background Image sucessfully retreived...')
|
||||
preview_pixbuf = GdkPixbuf.Pixbuf.new_from_file(background_pth)
|
||||
self._thumbnail_preview_img.set_from_pixbuf(preview_pixbuf)
|
||||
else:
|
||||
print('Cover Background Image Couldn\'t be retreived...')
|
||||
|
||||
def set_trailer_link(self, trailer):
|
||||
if trailer:
|
||||
self._trailer_link.set_uri(f"https://www.youtube.com/watch?v={trailer}")
|
@@ -1,22 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from .lib.tmdbscraper.tmdb import TMDBMovieScraper
|
||||
from .lib.tmdbscraper.fanarttv import get_details as get_fanarttv_artwork
|
||||
from .lib.tmdbscraper.imdbratings import get_details as get_imdb_details
|
||||
from .lib.tmdbscraper.traktratings import get_trakt_ratinginfo
|
||||
from .scraper_datahelper import combine_scraped_details_info_and_ratings, \
|
||||
combine_scraped_details_available_artwork, find_uniqueids_in_text, get_params
|
||||
from .scraper_config import configure_scraped_details, PathSpecificSettings, \
|
||||
configure_tmdb_artwork, is_fanarttv_configured
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_tmdb_scraper():
|
||||
language = 'en-US'
|
||||
certcountry = 'us'
|
||||
ADDON_SETTINGS = None
|
||||
return TMDBMovieScraper(ADDON_SETTINGS, language, certcountry)
|
@@ -1,111 +0,0 @@
|
||||
def configure_scraped_details(details, settings):
|
||||
details = _configure_rating_prefix(details, settings)
|
||||
details = _configure_keeporiginaltitle(details, settings)
|
||||
details = _configure_trailer(details, settings)
|
||||
details = _configure_multiple_studios(details, settings)
|
||||
details = _configure_default_rating(details, settings)
|
||||
details = _configure_tags(details, settings)
|
||||
return details
|
||||
|
||||
def configure_tmdb_artwork(details, settings):
|
||||
if 'available_art' not in details:
|
||||
return details
|
||||
|
||||
art = details['available_art']
|
||||
fanart_enabled = settings.getSettingBool('fanart')
|
||||
if not fanart_enabled:
|
||||
if 'fanart' in art:
|
||||
del art['fanart']
|
||||
if 'set.fanart' in art:
|
||||
del art['set.fanart']
|
||||
if not settings.getSettingBool('landscape'):
|
||||
if 'landscape' in art:
|
||||
if fanart_enabled:
|
||||
art['fanart'] = art.get('fanart', []) + art['landscape']
|
||||
del art['landscape']
|
||||
if 'set.landscape' in art:
|
||||
if fanart_enabled:
|
||||
art['set.fanart'] = art.get('set.fanart', []) + art['set.landscape']
|
||||
del art['set.landscape']
|
||||
|
||||
return details
|
||||
|
||||
def is_fanarttv_configured(settings):
|
||||
return settings.getSettingBool('enable_fanarttv_artwork')
|
||||
|
||||
def _configure_rating_prefix(details, settings):
|
||||
if details['info'].get('mpaa'):
|
||||
details['info']['mpaa'] = settings.getSettingString('certprefix') + details['info']['mpaa']
|
||||
return details
|
||||
|
||||
def _configure_keeporiginaltitle(details, settings):
|
||||
if settings.getSettingBool('keeporiginaltitle'):
|
||||
details['info']['title'] = details['info']['originaltitle']
|
||||
return details
|
||||
|
||||
def _configure_trailer(details, settings):
|
||||
if details['info'].get('trailer') and not settings.getSettingBool('trailer'):
|
||||
del details['info']['trailer']
|
||||
return details
|
||||
|
||||
def _configure_multiple_studios(details, settings):
|
||||
if not settings.getSettingBool('multiple_studios'):
|
||||
details['info']['studio'] = details['info']['studio'][:1]
|
||||
return details
|
||||
|
||||
def _configure_default_rating(details, settings):
|
||||
imdb_default = bool(details['ratings'].get('imdb')) and settings.getSettingString('RatingS') == 'IMDb'
|
||||
trakt_default = bool(details['ratings'].get('trakt')) and settings.getSettingString('RatingS') == 'Trakt'
|
||||
default_rating = 'themoviedb'
|
||||
if imdb_default:
|
||||
default_rating = 'imdb'
|
||||
elif trakt_default:
|
||||
default_rating = 'trakt'
|
||||
if default_rating not in details['ratings']:
|
||||
default_rating = list(details['ratings'].keys())[0] if details['ratings'] else None
|
||||
for rating_type in details['ratings'].keys():
|
||||
details['ratings'][rating_type]['default'] = rating_type == default_rating
|
||||
return details
|
||||
|
||||
def _configure_tags(details, settings):
|
||||
if not settings.getSettingBool('add_tags'):
|
||||
del details['info']['tag']
|
||||
return details
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
try:
|
||||
basestring
|
||||
except NameError: # py2 / py3
|
||||
basestring = str
|
||||
|
||||
#pylint: disable=redefined-builtin
|
||||
class PathSpecificSettings(object):
|
||||
# read-only shim for typed `xbmcaddon.Addon().getSetting*` methods
|
||||
def __init__(self, settings_dict, log_fn):
|
||||
self.data = settings_dict
|
||||
self.log = log_fn
|
||||
|
||||
def getSettingBool(self, id):
|
||||
return self._inner_get_setting(id, bool, False)
|
||||
|
||||
def getSettingInt(self, id):
|
||||
return self._inner_get_setting(id, int, 0)
|
||||
|
||||
def getSettingNumber(self, id):
|
||||
return self._inner_get_setting(id, float, 0.0)
|
||||
|
||||
def getSettingString(self, id):
|
||||
return self._inner_get_setting(id, basestring, '')
|
||||
|
||||
def _inner_get_setting(self, setting_id, setting_type, default):
|
||||
value = self.data.get(setting_id)
|
||||
if isinstance(value, setting_type):
|
||||
return value
|
||||
self._log_bad_value(value, setting_id)
|
||||
return default
|
||||
|
||||
def _log_bad_value(self, value, setting_id):
|
||||
if value is None:
|
||||
self.log("requested setting ({0}) was not found.".format(setting_id))
|
||||
else:
|
||||
self.log('failed to load value "{0}" for setting {1}'.format(value, setting_id))
|
@@ -1,54 +0,0 @@
|
||||
import re
|
||||
try:
|
||||
from urlparse import parse_qsl
|
||||
except ImportError: # py2 / py3
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
# get addon params from the plugin path querystring
|
||||
def get_params(argv):
|
||||
result = {'handle': int(argv[0])}
|
||||
if len(argv) < 2 or not argv[1]:
|
||||
return result
|
||||
|
||||
result.update(parse_qsl(argv[1].lstrip('?')))
|
||||
return result
|
||||
|
||||
def combine_scraped_details_info_and_ratings(original_details, additional_details):
|
||||
def update_or_set(details, key, value):
|
||||
if key in details:
|
||||
details[key].update(value)
|
||||
else:
|
||||
details[key] = value
|
||||
|
||||
if additional_details:
|
||||
if additional_details.get('info'):
|
||||
update_or_set(original_details, 'info', additional_details['info'])
|
||||
if additional_details.get('ratings'):
|
||||
update_or_set(original_details, 'ratings', additional_details['ratings'])
|
||||
return original_details
|
||||
|
||||
def combine_scraped_details_available_artwork(original_details, additional_details):
|
||||
if additional_details and additional_details.get('available_art'):
|
||||
available_art = additional_details['available_art']
|
||||
if not original_details.get('available_art'):
|
||||
original_details['available_art'] = available_art
|
||||
else:
|
||||
for arttype, artlist in available_art.items():
|
||||
original_details['available_art'][arttype] = \
|
||||
artlist + original_details['available_art'].get(arttype, [])
|
||||
|
||||
return original_details
|
||||
|
||||
def find_uniqueids_in_text(input_text):
|
||||
result = {}
|
||||
res = re.search(r'(themoviedb.org/movie/)([0-9]+)', input_text)
|
||||
if (res):
|
||||
result['tmdb'] = res.group(2)
|
||||
res = re.search(r'imdb....?/title/tt([0-9]+)', input_text)
|
||||
if (res):
|
||||
result['imdb'] = 'tt' + res.group(1)
|
||||
else:
|
||||
res = re.search(r'imdb....?/Title\?t{0,2}([0-9]+)', input_text)
|
||||
if (res):
|
||||
result['imdb'] = 'tt' + res.group(1)
|
||||
return result
|
@@ -1,175 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from lib.tmdbscraper.tmdb import TMDBMovieScraper
|
||||
from lib.tmdbscraper.fanarttv import get_details as get_fanarttv_artwork
|
||||
from lib.tmdbscraper.imdbratings import get_details as get_imdb_details
|
||||
from lib.tmdbscraper.traktratings import get_trakt_ratinginfo
|
||||
from scraper_datahelper import combine_scraped_details_info_and_ratings, \
|
||||
combine_scraped_details_available_artwork, find_uniqueids_in_text, get_params
|
||||
from scraper_config import configure_scraped_details, PathSpecificSettings, \
|
||||
configure_tmdb_artwork, is_fanarttv_configured
|
||||
|
||||
ADDON_SETTINGS = xbmcaddon.Addon()
|
||||
ID = ADDON_SETTINGS.getAddonInfo('id')
|
||||
|
||||
def log(msg, level=xbmc.LOGDEBUG):
|
||||
xbmc.log(msg='[{addon}]: {msg}'.format(addon=ID, msg=msg), level=level)
|
||||
|
||||
def get_tmdb_scraper(settings):
|
||||
language = settings.getSettingString('language')
|
||||
certcountry = settings.getSettingString('tmdbcertcountry')
|
||||
return TMDBMovieScraper(ADDON_SETTINGS, language, certcountry)
|
||||
|
||||
def search_for_movie(title, year, handle, settings):
|
||||
log("Find movie with title '{title}' from year '{year}'".format(title=title, year=year), xbmc.LOGINFO)
|
||||
title = _strip_trailing_article(title)
|
||||
search_results = get_tmdb_scraper(settings).search(title, year)
|
||||
if not search_results:
|
||||
return
|
||||
if 'error' in search_results:
|
||||
header = "The Movie Database Python error searching with web service TMDB"
|
||||
xbmcgui.Dialog().notification(header, search_results['error'], xbmcgui.NOTIFICATION_WARNING)
|
||||
log(header + ': ' + search_results['error'], xbmc.LOGWARNING)
|
||||
return
|
||||
|
||||
for movie in search_results:
|
||||
listitem = _searchresult_to_listitem(movie)
|
||||
uniqueids = {'tmdb': str(movie['id'])}
|
||||
xbmcplugin.addDirectoryItem(handle=handle, url=build_lookup_string(uniqueids),
|
||||
listitem=listitem, isFolder=True)
|
||||
|
||||
_articles = [prefix + article for prefix in (', ', ' ') for article in ("the", "a", "an")]
|
||||
def _strip_trailing_article(title):
|
||||
title = title.lower()
|
||||
for article in _articles:
|
||||
if title.endswith(article):
|
||||
return title[:-len(article)]
|
||||
return title
|
||||
|
||||
def _searchresult_to_listitem(movie):
|
||||
movie_info = {'title': movie['title']}
|
||||
movie_label = movie['title']
|
||||
|
||||
movie_year = movie['release_date'].split('-')[0] if movie.get('release_date') else None
|
||||
if movie_year:
|
||||
movie_label += ' ({})'.format(movie_year)
|
||||
movie_info['year'] = movie_year
|
||||
|
||||
listitem = xbmcgui.ListItem(movie_label, offscreen=True)
|
||||
|
||||
listitem.setInfo('video', movie_info)
|
||||
if movie['poster_path']:
|
||||
listitem.setArt({'thumb': movie['poster_path']})
|
||||
|
||||
return listitem
|
||||
|
||||
# Low limit because a big list of artwork can cause trouble in some cases
|
||||
# (a column can be too large for the MySQL integration),
|
||||
# and how useful is a big list anyway? Not exactly rhetorical, this is an experiment.
|
||||
IMAGE_LIMIT = 10
|
||||
|
||||
def add_artworks(listitem, artworks):
|
||||
for arttype, artlist in artworks.items():
|
||||
if arttype == 'fanart':
|
||||
continue
|
||||
for image in artlist[:IMAGE_LIMIT]:
|
||||
listitem.addAvailableArtwork(image['url'], arttype)
|
||||
|
||||
fanart_to_set = [{'image': image['url'], 'preview': image['preview']}
|
||||
for image in artworks['fanart'][:IMAGE_LIMIT]]
|
||||
listitem.setAvailableFanart(fanart_to_set)
|
||||
|
||||
def get_details(input_uniqueids, handle, settings):
|
||||
if not input_uniqueids:
|
||||
return False
|
||||
details = get_tmdb_scraper(settings).get_details(input_uniqueids)
|
||||
if not details:
|
||||
return False
|
||||
if 'error' in details:
|
||||
header = "The Movie Database Python error with web service TMDB"
|
||||
xbmcgui.Dialog().notification(header, details['error'], xbmcgui.NOTIFICATION_WARNING)
|
||||
log(header + ': ' + details['error'], xbmc.LOGWARNING)
|
||||
return False
|
||||
|
||||
details = configure_tmdb_artwork(details, settings)
|
||||
|
||||
if settings.getSettingString('RatingS') == 'IMDb' or settings.getSettingBool('imdbanyway'):
|
||||
imdbinfo = get_imdb_details(details['uniqueids'])
|
||||
if 'error' in imdbinfo:
|
||||
header = "The Movie Database Python error with website IMDB"
|
||||
log(header + ': ' + imdbinfo['error'], xbmc.LOGWARNING)
|
||||
else:
|
||||
details = combine_scraped_details_info_and_ratings(details, imdbinfo)
|
||||
|
||||
if settings.getSettingString('RatingS') == 'Trakt' or settings.getSettingBool('traktanyway'):
|
||||
traktinfo = get_trakt_ratinginfo(details['uniqueids'])
|
||||
details = combine_scraped_details_info_and_ratings(details, traktinfo)
|
||||
|
||||
if is_fanarttv_configured(settings):
|
||||
fanarttv_info = get_fanarttv_artwork(details['uniqueids'],
|
||||
settings.getSettingString('fanarttv_clientkey'),
|
||||
settings.getSettingString('fanarttv_language'),
|
||||
details['_info']['set_tmdbid'])
|
||||
details = combine_scraped_details_available_artwork(details, fanarttv_info)
|
||||
|
||||
details = configure_scraped_details(details, settings)
|
||||
|
||||
listitem = xbmcgui.ListItem(details['info']['title'], offscreen=True)
|
||||
listitem.setInfo('video', details['info'])
|
||||
listitem.setCast(details['cast'])
|
||||
listitem.setUniqueIDs(details['uniqueids'], 'tmdb')
|
||||
add_artworks(listitem, details['available_art'])
|
||||
|
||||
for rating_type, value in details['ratings'].items():
|
||||
if 'votes' in value:
|
||||
listitem.setRating(rating_type, value['rating'], value['votes'], value['default'])
|
||||
else:
|
||||
listitem.setRating(rating_type, value['rating'], defaultt=value['default'])
|
||||
|
||||
xbmcplugin.setResolvedUrl(handle=handle, succeeded=True, listitem=listitem)
|
||||
return True
|
||||
|
||||
def find_uniqueids_in_nfo(nfo, handle):
|
||||
uniqueids = find_uniqueids_in_text(nfo)
|
||||
if uniqueids:
|
||||
listitem = xbmcgui.ListItem(offscreen=True)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=handle, url=build_lookup_string(uniqueids), listitem=listitem, isFolder=True)
|
||||
|
||||
def build_lookup_string(uniqueids):
|
||||
return json.dumps(uniqueids)
|
||||
|
||||
def parse_lookup_string(uniqueids):
|
||||
try:
|
||||
return json.loads(uniqueids)
|
||||
except ValueError:
|
||||
log("Can't parse this lookup string, is it from another add-on?\n" + uniqueids, xbmc.LOGWARNING)
|
||||
return None
|
||||
|
||||
def run():
|
||||
params = get_params(sys.argv[1:])
|
||||
enddir = True
|
||||
if 'action' in params:
|
||||
settings = ADDON_SETTINGS if not params.get('pathSettings') else \
|
||||
PathSpecificSettings(json.loads(params['pathSettings']), lambda msg: log(msg, xbmc.LOGWARNING))
|
||||
action = params["action"]
|
||||
if action == 'find' and 'title' in params:
|
||||
search_for_movie(params["title"], params.get("year"), params['handle'], settings)
|
||||
elif action == 'getdetails' and 'url' in params:
|
||||
enddir = not get_details(parse_lookup_string(params["url"]), params['handle'], settings)
|
||||
elif action == 'NfoUrl' and 'nfo' in params:
|
||||
find_uniqueids_in_nfo(params["nfo"], params['handle'])
|
||||
else:
|
||||
log("unhandled action: " + action, xbmc.LOGWARNING)
|
||||
else:
|
||||
log("No action in 'params' to act on", xbmc.LOGWARNING)
|
||||
if enddir:
|
||||
xbmcplugin.endOfDirectory(params['handle'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Search",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "context_menu",
|
||||
"pass_fm_events": "true",
|
||||
"bind_keys": ["Search||_show_grep_list_page:<Control>f"]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,231 +0,0 @@
|
||||
# Python imports
|
||||
import os, multiprocessing, threading, subprocess, inspect, time, json
|
||||
from multiprocessing import Manager, Process
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, GObject
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class FilePreviewWidget(Gtk.LinkButton):
|
||||
def __init__(self, path, file):
|
||||
super(FilePreviewWidget, self).__init__()
|
||||
self.set_label(file)
|
||||
self.set_uri(f"file://{path}")
|
||||
self.show_all()
|
||||
|
||||
|
||||
class GrepPreviewWidget(Gtk.Box):
|
||||
def __init__(self, path, sub_keys, data):
|
||||
super(GrepPreviewWidget, self).__init__()
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.line_color = "#e0cc64"
|
||||
|
||||
|
||||
_label = '/'.join( path.split("/")[-3:] )
|
||||
title = Gtk.LinkButton.new_with_label(uri=f"file://{path}", label=_label)
|
||||
|
||||
self.add(title)
|
||||
for key in sub_keys:
|
||||
line_num = key
|
||||
text = data[key]
|
||||
box = Gtk.Box()
|
||||
number_label = Gtk.Label()
|
||||
text_view = Gtk.Label(label=text[:-1])
|
||||
label_text = f"<span foreground='{self.line_color}'>{line_num}</span>"
|
||||
|
||||
number_label.set_markup(label_text)
|
||||
number_label.set_margin_left(15)
|
||||
number_label.set_margin_right(5)
|
||||
number_label.set_margin_top(5)
|
||||
number_label.set_margin_bottom(5)
|
||||
text_view.set_margin_top(5)
|
||||
text_view.set_margin_bottom(5)
|
||||
text_view.set_line_wrap(True)
|
||||
|
||||
box.add(number_label)
|
||||
box.add(text_view)
|
||||
self.add(box)
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
|
||||
manager = Manager()
|
||||
grep_result_set = manager.dict()
|
||||
file_result_set = manager.list()
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self.name = "Search" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self._GLADE_FILE = f"{self.path}/search_dialog.glade"
|
||||
|
||||
self._search_dialog = None
|
||||
self._active_path = None
|
||||
self._file_list = None
|
||||
self._grep_list = None
|
||||
self._grep_proc = None
|
||||
self._list_proc = None
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._show_grep_list_page)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
|
||||
classes = [self]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
self._builder.connect_signals(handlers)
|
||||
|
||||
self._search_dialog = self._builder.get_object("search_dialog")
|
||||
self._grep_list = self._builder.get_object("grep_list")
|
||||
self._file_list = self._builder.get_object("file_list")
|
||||
|
||||
GObject.signal_new("update-file-ui-signal", self._search_dialog, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
self._search_dialog.connect("update-file-ui-signal", self._load_file_ui)
|
||||
GObject.signal_new("update-grep-ui-signal", self._search_dialog, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
self._search_dialog.connect("update-grep-ui-signal", self._load_grep_ui)
|
||||
|
||||
|
||||
@daemon_threaded
|
||||
def _show_grep_list_page(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
state = self._event_message
|
||||
self._event_message = None
|
||||
|
||||
GLib.idle_add(self._process_queries, (state))
|
||||
|
||||
def _process_queries(self, state):
|
||||
self._active_path = state.tab.get_current_directory()
|
||||
response = self._search_dialog.run()
|
||||
self._search_dialog.hide()
|
||||
|
||||
|
||||
def _run_find_file_query(self, widget=None, eve=None):
|
||||
if self._list_proc:
|
||||
self._list_proc.terminate()
|
||||
self._list_proc = None
|
||||
time.sleep(.2)
|
||||
|
||||
del file_result_set[:]
|
||||
self.clear_children(self._file_list)
|
||||
|
||||
query = widget.get_text()
|
||||
if query:
|
||||
self._list_proc = multiprocessing.Process(self._do_list_search(self._active_path, query))
|
||||
self._list_proc.start()
|
||||
|
||||
def _do_list_search(self, path, query):
|
||||
self._file_traverse_path(path, query)
|
||||
for target, file in file_result_set:
|
||||
widget = FilePreviewWidget(target, file)
|
||||
self._search_dialog.emit("update-file-ui-signal", (widget))
|
||||
|
||||
def _load_file_ui(self, parent=None, widget=None):
|
||||
self._file_list.add(widget)
|
||||
|
||||
def _file_traverse_path(self, path, query):
|
||||
try:
|
||||
for file in os.listdir(path):
|
||||
target = os.path.join(path, file)
|
||||
if os.path.isdir(target):
|
||||
self._file_traverse_path(target, query)
|
||||
else:
|
||||
if query.lower() in file.lower():
|
||||
file_result_set.append([target, file])
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print("Couldn't traverse to path. Might be permissions related...")
|
||||
|
||||
|
||||
def _run_grep_query(self, widget=None, eve=None):
|
||||
if self._grep_proc:
|
||||
self._grep_proc.terminate()
|
||||
self._grep_proc = None
|
||||
time.sleep(.2)
|
||||
|
||||
grep_result_set.clear()
|
||||
self.clear_children(self._grep_list)
|
||||
|
||||
query = widget.get_text()
|
||||
if query:
|
||||
self._grep_proc = multiprocessing.Process(self._do_grep_search(self._active_path, query))
|
||||
self._grep_proc.start()
|
||||
|
||||
def _do_grep_search(self, path, query):
|
||||
self._grep_traverse_path(path, query)
|
||||
|
||||
keys = grep_result_set.keys()
|
||||
for key in keys:
|
||||
sub_keys = grep_result_set[key].keys()
|
||||
widget = GrepPreviewWidget(key, sub_keys, grep_result_set[key])
|
||||
self._search_dialog.emit("update-grep-ui-signal", (widget))
|
||||
|
||||
def _load_grep_ui(self, parent=None, widget=None):
|
||||
self._grep_list.add(widget)
|
||||
|
||||
def _grep_traverse_path(self, path, query):
|
||||
try:
|
||||
for file in os.listdir(path):
|
||||
target = os.path.join(path, file)
|
||||
if os.path.isdir(target):
|
||||
self._grep_traverse_path(target, query)
|
||||
else:
|
||||
self._search_for_string(target, query)
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print("Couldn't traverse to path. Might be permissions related...")
|
||||
|
||||
def _search_for_string(self, file, query):
|
||||
try:
|
||||
with open(file, 'r') as fp:
|
||||
for i, line in enumerate(fp):
|
||||
if query in line:
|
||||
if f"{file}" in grep_result_set.keys():
|
||||
grep_result_set[f"{file}"][f"{i+1}"] = line
|
||||
else:
|
||||
grep_result_set[f"{file}"] = {}
|
||||
grep_result_set[f"{file}"] = {f"{i+1}": line}
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print("Couldn't read file. Might be binary or other cause...")
|
@@ -1,227 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkDialog" id="search_dialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">6</property>
|
||||
<property name="title" translatable="yes">Search...</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="default-width">720</property>
|
||||
<property name="default-height">620</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog_action_area">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="ok_button">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkNotebook">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="show-border">False</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Query...</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Search for file...</property>
|
||||
<signal name="search-changed" handler="_run_find_file_query" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_list">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="baseline-position">top</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">File Search</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab-fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Query...</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Query string in file...</property>
|
||||
<signal name="search-changed" handler="_run_grep_query" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="grep_list">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="baseline-position">top</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Grep Search</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
<property name="tab-fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">cancel_button</action-widget>
|
||||
<action-widget response="-5">ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Example Plugin",
|
||||
"author": "John Doe",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "plugin_control_list",
|
||||
"pass_fm_events": "true",
|
||||
"bind_keys": ["Example Plugin||send_message:<Control>f"]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self.send_message)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
def send_message(self, widget=None, eve=None):
|
||||
message = "Hello, World!"
|
||||
self._event_system.push_gui_event([self.name, "display_message", ("warning", message, None)])
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "VOD Thumbnailer",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "context_menu",
|
||||
"pass_fm_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,137 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, time, inspect, hashlib
|
||||
from datetime import datetime
|
||||
|
||||
# Gtk imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
from gi.repository import Gtk, GLib, Gio, GdkPixbuf
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self._GLADE_FILE = f"{self.path}/re_thumbnailer.glade"
|
||||
self.name = "VOD Thumbnailer" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
self._thumbnailer_dialog = None
|
||||
self._thumbnail_preview_img = None
|
||||
self._scrub_step = None
|
||||
self._file_name = None
|
||||
self._file_location = None
|
||||
self._file_hash = None
|
||||
self._state = None
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._show_thumbnailer_page)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
|
||||
classes = [self]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
self._builder.connect_signals(handlers)
|
||||
|
||||
self._thumbnailer_dialog = self._builder.get_object("thumbnailer_dialog")
|
||||
self._scrub_step = self._builder.get_object("scrub_step")
|
||||
self._file_name = self._builder.get_object("file_name")
|
||||
self._file_location = self._builder.get_object("file_location")
|
||||
self._thumbnail_preview_img = self._builder.get_object("thumbnail_preview_img")
|
||||
self._file_hash = self._builder.get_object("file_hash")
|
||||
|
||||
|
||||
@threaded
|
||||
def _show_thumbnailer_page(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
state = self._event_message
|
||||
self._event_message = None
|
||||
|
||||
GLib.idle_add(self._process_changes, (state))
|
||||
|
||||
def _process_changes(self, state):
|
||||
self._state = None
|
||||
|
||||
if len(state.selected_files) == 1:
|
||||
if state.selected_files[0].lower().endswith(state.tab.fvideos):
|
||||
self._state = state
|
||||
self._set_ui_data()
|
||||
response = self._thumbnailer_dialog.run()
|
||||
if response in [Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT]:
|
||||
self._thumbnailer_dialog.hide()
|
||||
|
||||
|
||||
def _regenerate_thumbnail(self, widget=None, eve=None):
|
||||
scrub_percent = int(self._scrub_step.get_value())
|
||||
file = self._file_name.get_text()
|
||||
dir = self._file_location.get_text()
|
||||
file_hash = self._file_hash.get_text()
|
||||
hash_img_pth = f"{self._state.tab.ABS_THUMBS_PTH}/{file_hash}.jpg"
|
||||
|
||||
try:
|
||||
os.remove(hash_img_pth) if os.path.isfile(hash_img_pth) else ...
|
||||
|
||||
self._state.tab.create_thumbnail(dir, file, f"{scrub_percent}%")
|
||||
preview_pixbuf = GdkPixbuf.Pixbuf.new_from_file(hash_img_pth)
|
||||
self._thumbnail_preview_img.set_from_pixbuf(preview_pixbuf)
|
||||
|
||||
img_pixbuf = self._state.tab.create_scaled_image(hash_img_pth)
|
||||
tree_pth = self._state.icon_grid.get_selected_items()[0]
|
||||
itr = self._state.store.get_iter(tree_pth)
|
||||
pixbuff = self._state.store.get(itr, 0)[0]
|
||||
self._state.store.set(itr, 0, img_pixbuf)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
print("Couldn't regenerate thumbnail!")
|
||||
|
||||
|
||||
def _set_ui_data(self):
|
||||
uri = self._state.selected_files[0]
|
||||
path = self._state.tab.get_current_directory()
|
||||
parts = uri.split("/")
|
||||
|
||||
file_hash = hashlib.sha256(str.encode(uri)).hexdigest()
|
||||
hash_img_pth = f"{self._state.tab.ABS_THUMBS_PTH}/{file_hash}.jpg"
|
||||
preview_pixbuf = GdkPixbuf.Pixbuf.new_from_file(hash_img_pth)
|
||||
|
||||
self._thumbnail_preview_img.set_from_pixbuf(preview_pixbuf)
|
||||
self._file_name.set_text(parts[ len(parts) - 1 ])
|
||||
self._file_location.set_text(path)
|
||||
self._file_hash.set_text(file_hash)
|
@@ -1,239 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkAdjustment" id="scrub_step_adjuster">
|
||||
<property name="lower">1</property>
|
||||
<property name="upper">100</property>
|
||||
<property name="value">65</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkDialog" id="thumbnailer_dialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">6</property>
|
||||
<property name="title" translatable="yes">VOD Thumbnailer</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="default-width">420</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog_action_area">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-default">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="thumbnail_preview_img">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label" translatable="yes">New Thumbnail Scrub Step: </property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scrub_step">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="adjustment">scrub_step_adjuster</property>
|
||||
<property name="show-fill-level">True</property>
|
||||
<property name="round-digits">1</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="value-pos">right</property>
|
||||
<signal name="value-changed" handler="_regenerate_thumbnail" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTable" id="general_table">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="n-rows">3</property>
|
||||
<property name="n-columns">2</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>File _Name:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_name</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_name">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Location:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_location</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_location">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="hash">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>_Thumbnail Hash:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="mnemonic-widget">file_location</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="file_hash">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="right-attach">2</property>
|
||||
<property name="top-attach">2</property>
|
||||
<property name="bottom-attach">3</property>
|
||||
<property name="x-options">GTK_FILL</property>
|
||||
<property name="y-options"/>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-7">cancel_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# . CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
function main() {
|
||||
cd "$(dirname "")"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
LINK=`xclip -selection clipboard -o`
|
||||
yt-dlp --write-sub --embed-sub --sub-langs en -o "${1}/%(title)s.%(ext)s" "${LINK}"
|
||||
}
|
||||
main "$@";
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Youtube Download",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "plugin_control_list",
|
||||
"pass_fm_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, subprocess, time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self.name = "Youtube Download" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
|
||||
def get_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self._do_download)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
self._module_event_observer()
|
||||
|
||||
|
||||
@threaded
|
||||
def _do_download(self, widget=None, eve=None):
|
||||
self._event_system.push_gui_event([self.name, "get_current_state", ()])
|
||||
self.wait_for_fm_message()
|
||||
|
||||
state = self._event_message
|
||||
subprocess.Popen([f'{self.path}/download.sh' , state.tab.get_current_directory()])
|
||||
self._event_message = None
|
12
src/debs/clear_pycache_dirs.sh
Executable file
12
src/debs/clear_pycache_dirs.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
function main() {
|
||||
find . -name "__pycache__" -exec rm -rf $1 {} \;
|
||||
find . -name "*.pyc" -exec rm -rf $1 {} \;
|
||||
}
|
||||
main
|
0
src/debs/solarfm-0-0-1-x64/bin/solarfm
Normal file → Executable file
0
src/debs/solarfm-0-0-1-x64/bin/solarfm
Normal file → Executable file
@@ -4,26 +4,20 @@ import builtins
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from ipc_server import IPCServer
|
||||
from signal_classes.DBusControllerMixin import DBusControllerMixin
|
||||
|
||||
|
||||
|
||||
|
||||
class EventSystem(IPCServer):
|
||||
""" Inheret IPCServerMixin. Create an pub/sub systems. """
|
||||
class Builtins(DBusControllerMixin):
|
||||
"""Docstring for __builtins__ extender"""
|
||||
|
||||
def __init__(self):
|
||||
super(EventSystem, self).__init__()
|
||||
|
||||
# NOTE: The format used is list of [type, target, (data,)] Where:
|
||||
# type is useful context for control flow,
|
||||
# target is the method to call,
|
||||
# data is the method parameters to give
|
||||
# NOTE: The format used is list of [type, target, data]
|
||||
# Where data may be any kind of data
|
||||
self._gui_events = []
|
||||
self._module_events = []
|
||||
|
||||
|
||||
self._fm_events = []
|
||||
self.is_ipc_alive = False
|
||||
|
||||
# Makeshift fake "events" type system FIFO
|
||||
def _pop_gui_event(self):
|
||||
@@ -31,9 +25,9 @@ class EventSystem(IPCServer):
|
||||
return self._gui_events.pop(0)
|
||||
return None
|
||||
|
||||
def _pop_module_event(self):
|
||||
if len(self._module_events) > 0:
|
||||
return self._module_events.pop(0)
|
||||
def _pop_fm_event(self):
|
||||
if len(self._fm_events) > 0:
|
||||
return self._fm_events.pop(0)
|
||||
return None
|
||||
|
||||
|
||||
@@ -42,33 +36,31 @@ class EventSystem(IPCServer):
|
||||
self._gui_events.append(event)
|
||||
return None
|
||||
|
||||
raise Exception("Invald event format! Please do: [type, target, (data,)]")
|
||||
raise Exception("Invald event format! Please do: [type, target, data]")
|
||||
|
||||
def push_module_event(self, event):
|
||||
def push_fm_event(self, event):
|
||||
if len(event) == 3:
|
||||
self._module_events.append(event)
|
||||
self._fm_events.append(event)
|
||||
return None
|
||||
|
||||
raise Exception("Invald event format! Please do: [type, target, (data,)]")
|
||||
raise Exception("Invald event format! Please do: [type, target, data]")
|
||||
|
||||
def read_gui_event(self):
|
||||
return self._gui_events[0]
|
||||
|
||||
def read_module_event(self):
|
||||
return self._module_events[0]
|
||||
def read_fm_event(self):
|
||||
return self._fm_events[0]
|
||||
|
||||
def consume_gui_event(self):
|
||||
return self._pop_gui_event()
|
||||
|
||||
def consume_module_event(self):
|
||||
return self._pop_module_event()
|
||||
def consume_fm_event(self):
|
||||
return self._pop_fm_event()
|
||||
|
||||
|
||||
|
||||
# NOTE: Just reminding myself we can add to builtins two different ways...
|
||||
# __builtins__.update({"event_system": Builtins()})
|
||||
builtins.app_name = "SolarFM"
|
||||
builtins.event_system = EventSystem()
|
||||
builtins.event_sleep_time = 0.2
|
||||
builtins.event_system = Builtins()
|
||||
builtins.event_sleep_time = 0.5
|
||||
builtins.debug = False
|
||||
builtins.trace_debug = False
|
||||
|
@@ -1,3 +1,51 @@
|
||||
"""
|
||||
Base module
|
||||
"""
|
||||
# Python imports
|
||||
import os, inspect, time
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from utils import Settings
|
||||
from signal_classes import Controller
|
||||
from __builtins__ import Builtins
|
||||
|
||||
|
||||
|
||||
|
||||
class Main(Builtins):
|
||||
def __init__(self, args, unknownargs):
|
||||
event_system.create_ipc_server()
|
||||
time.sleep(0.5)
|
||||
if not event_system.is_ipc_alive:
|
||||
if unknownargs:
|
||||
for arg in unknownargs:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
if args.new_tab and os.path.isdir(args.new_tab):
|
||||
message = f"FILE|{args.new_tab}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
settings.createWindow()
|
||||
|
||||
controller = Controller(args, unknownargs, settings)
|
||||
if not controller:
|
||||
raise Exception("Controller exited and doesn't exist...")
|
||||
|
||||
# Gets the methods from the classes and sets to handler.
|
||||
# Then, builder connects to any signals it needs.
|
||||
classes = [controller]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
settings.builder.connect_signals(handlers)
|
||||
|
@@ -15,17 +15,15 @@ gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from app import Application
|
||||
from __init__ import Main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
""" Set process title, get arguments, and create GTK main thread. """
|
||||
|
||||
try:
|
||||
# import web_pdb
|
||||
# web_pdb.set_trace()
|
||||
|
||||
setproctitle('SolarFM')
|
||||
setproctitle('solarfm')
|
||||
faulthandler.enable() # For better debug info
|
||||
parser = argparse.ArgumentParser()
|
||||
# Add long and short arguments
|
||||
@@ -35,7 +33,7 @@ if __name__ == "__main__":
|
||||
# Read arguments (If any...)
|
||||
args, unknownargs = parser.parse_known_args()
|
||||
|
||||
Application(args, unknownargs)
|
||||
Main(args, unknownargs)
|
||||
Gtk.main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
@@ -1,55 +0,0 @@
|
||||
# Python imports
|
||||
import os, inspect, time
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from utils.settings import Settings
|
||||
from context.controller import Controller
|
||||
from __builtins__ import EventSystem
|
||||
|
||||
|
||||
|
||||
|
||||
class Application(EventSystem):
|
||||
""" Create Settings and Controller classes. Bind signal to Builder. Inherit from Builtins to bind global methods and classes. """
|
||||
|
||||
def __init__(self, args, unknownargs):
|
||||
if not trace_debug:
|
||||
event_system.create_ipc_server()
|
||||
time.sleep(0.1)
|
||||
|
||||
if not event_system.is_ipc_alive:
|
||||
if unknownargs:
|
||||
for arg in unknownargs:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
if args.new_tab and os.path.isdir(args.new_tab):
|
||||
message = f"FILE|{args.new_tab}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
settings.create_window()
|
||||
|
||||
controller = Controller(args, unknownargs, settings)
|
||||
if not controller:
|
||||
raise Exception("Controller exited and doesn't exist...")
|
||||
|
||||
# Gets the methods from the classes and sets to handler.
|
||||
# Then, builder connects to any signals it needs.
|
||||
classes = [controller]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
settings.builder.connect_signals(handlers)
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Gtk Bound Signal Module
|
||||
"""
|
@@ -1,171 +0,0 @@
|
||||
# Python imports
|
||||
import os, gc, threading, time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
# Application imports
|
||||
from .mixins.exception_hook_mixin import ExceptionHookMixin
|
||||
from .mixins.ui_mixin import UIMixin
|
||||
from .signals.ipc_signals_mixin import IPCSignalsMixin
|
||||
from .signals.keyboard_signals_mixin import KeyboardSignalsMixin
|
||||
from .controller_data import Controller_Data
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, ExceptionHookMixin, Controller_Data):
|
||||
""" Controller coordinates the mixins and is somewhat the root hub of it all. """
|
||||
def __init__(self, args, unknownargs, _settings):
|
||||
self.setup_controller_data(_settings)
|
||||
self.window.show()
|
||||
|
||||
self.generate_windows(self.state)
|
||||
self.plugins.launch_plugins()
|
||||
|
||||
if debug:
|
||||
self.window.set_interactive_debugging(True)
|
||||
|
||||
if not trace_debug:
|
||||
self.gui_event_observer()
|
||||
|
||||
if unknownargs:
|
||||
for arg in unknownargs:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
if args.new_tab and os.path.isdir(args.new_tab):
|
||||
message = f"FILE|{args.new_tab}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
|
||||
def tear_down(self, widget=None, eve=None):
|
||||
event_system.send_ipc_message("close server")
|
||||
self.fm_controller.save_state()
|
||||
time.sleep(event_sleep_time)
|
||||
Gtk.main_quit()
|
||||
|
||||
|
||||
@threaded
|
||||
def gui_event_observer(self):
|
||||
while True:
|
||||
time.sleep(event_sleep_time)
|
||||
event = event_system.consume_gui_event()
|
||||
if event:
|
||||
try:
|
||||
type, target, data = event
|
||||
if type:
|
||||
method = getattr(self.__class__, "handle_gui_event_and_set_message")
|
||||
GLib.idle_add(method, *(self, type, target, data))
|
||||
else:
|
||||
method = getattr(self.__class__, target)
|
||||
GLib.idle_add(method, *(self, *data,))
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
def handle_gui_event_and_set_message(self, type, target, parameters):
|
||||
method = getattr(self.__class__, f"{target}")
|
||||
data = method(*(self, *parameters))
|
||||
self.plugins.send_message_to_plugin(type, data)
|
||||
|
||||
def open_terminal(self, widget=None, eve=None):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
tab.execute(f"{tab.terminal_app}", dir)
|
||||
|
||||
def save_load_session(self, action="save_session"):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
save_load_dialog = self.builder.get_object("save_load_dialog")
|
||||
|
||||
if action == "save_session":
|
||||
self.fm_controller.save_state()
|
||||
return
|
||||
elif action == "save_session_as":
|
||||
save_load_dialog.set_action(Gtk.FileChooserAction.SAVE)
|
||||
elif action == "load_session":
|
||||
save_load_dialog.set_action(Gtk.FileChooserAction.OPEN)
|
||||
else:
|
||||
raise Exception(f"Unknown action given: {action}")
|
||||
|
||||
save_load_dialog.set_current_folder(tab.get_current_directory())
|
||||
save_load_dialog.set_current_name("session.json")
|
||||
response = save_load_dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
if action == "save_session_as":
|
||||
path = f"{save_load_dialog.get_current_folder()}/{save_load_dialog.get_current_name()}"
|
||||
self.fm_controller.save_state(path)
|
||||
elif action == "load_session":
|
||||
path = f"{save_load_dialog.get_file().get_path()}"
|
||||
session_json = self.fm_controller.load_state(path)
|
||||
self.load_session(session_json)
|
||||
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
|
||||
pass
|
||||
|
||||
save_load_dialog.hide()
|
||||
|
||||
def load_session(self, session_json):
|
||||
if debug:
|
||||
print(f"Session Data: {session_json}")
|
||||
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
for notebook in self.notebooks:
|
||||
self.clear_children(notebook)
|
||||
|
||||
self.fm_controller.unload_tabs_and_windows()
|
||||
self.generate_windows(session_json)
|
||||
gc.collect()
|
||||
|
||||
|
||||
def do_action_from_menu_controls(self, widget, event_button):
|
||||
action = widget.get_name()
|
||||
self.hide_context_menu()
|
||||
self.hide_new_file_menu()
|
||||
self.hide_edit_file_menu()
|
||||
|
||||
if action == "open":
|
||||
self.open_files()
|
||||
if action == "open_with":
|
||||
self.show_appchooser_menu()
|
||||
if action == "execute":
|
||||
self.execute_files()
|
||||
if action == "execute_in_terminal":
|
||||
self.execute_files(in_terminal=True)
|
||||
if action == "rename":
|
||||
self.rename_files()
|
||||
if action == "cut":
|
||||
self.to_copy_files.clear()
|
||||
self.cut_files()
|
||||
if action == "copy":
|
||||
self.to_cut_files.clear()
|
||||
self.copy_files()
|
||||
if action == "paste":
|
||||
self.paste_files()
|
||||
if action == "archive":
|
||||
self.show_archiver_dialogue()
|
||||
if action == "delete":
|
||||
self.delete_files()
|
||||
if action == "trash":
|
||||
self.trash_files()
|
||||
if action == "go_to_trash":
|
||||
self.path_entry.set_text(self.trash_files_path)
|
||||
if action == "restore_from_trash":
|
||||
self.restore_trash_files()
|
||||
if action == "empty_trash":
|
||||
self.empty_trash()
|
||||
if action == "create":
|
||||
self.show_new_file_menu()
|
||||
if action in ["save_session", "save_session_as", "load_session"]:
|
||||
self.save_load_session(action)
|
@@ -1,157 +0,0 @@
|
||||
# Python imports
|
||||
import sys, os, signal
|
||||
|
||||
# Lib imports
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from trasher.xdgtrash import XDGTrash
|
||||
from shellfm.windows.controller import WindowController
|
||||
from plugins.plugins import Plugins
|
||||
|
||||
|
||||
|
||||
|
||||
class Controller_Data:
|
||||
""" Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """
|
||||
|
||||
def setup_controller_data(self, _settings):
|
||||
self.trashman = XDGTrash()
|
||||
self.fm_controller = WindowController()
|
||||
self.plugins = Plugins(_settings)
|
||||
self.state = self.fm_controller.load_state()
|
||||
self.trashman.regenerate()
|
||||
|
||||
self.settings = _settings
|
||||
self.builder = self.settings.get_builder()
|
||||
self.logger = self.settings.get_logger()
|
||||
|
||||
self.window = self.settings.get_main_window()
|
||||
self.window1 = self.builder.get_object("window_1")
|
||||
self.window2 = self.builder.get_object("window_2")
|
||||
self.window3 = self.builder.get_object("window_3")
|
||||
self.window4 = self.builder.get_object("window_4")
|
||||
self.message_popup_widget = self.builder.get_object("message_popup_widget")
|
||||
self.message_text_view = self.builder.get_object("message_text_view")
|
||||
self.message_buffer = self.builder.get_object("message_buffer")
|
||||
self.arc_command_buffer = self.builder.get_object("arc_command_buffer")
|
||||
|
||||
self.exists_file_rename_bttn = self.builder.get_object("exists_file_rename_bttn")
|
||||
self.warning_alert = self.builder.get_object("warning_alert")
|
||||
self.edit_file_menu = self.builder.get_object("edit_file_menu")
|
||||
self.file_exists_dialog = self.builder.get_object("file_exists_dialog")
|
||||
self.exists_file_label = self.builder.get_object("exists_file_label")
|
||||
self.exists_file_field = self.builder.get_object("exists_file_field")
|
||||
self.path_menu = self.builder.get_object("path_menu")
|
||||
self.path_entry = self.builder.get_object("path_entry")
|
||||
|
||||
self.bottom_size_label = self.builder.get_object("bottom_size_label")
|
||||
self.bottom_file_count_label = self.builder.get_object("bottom_file_count_label")
|
||||
self.bottom_path_label = self.builder.get_object("bottom_path_label")
|
||||
|
||||
self.trash_files_path = GLib.get_user_data_dir() + "/Trash/files"
|
||||
self.trash_info_path = GLib.get_user_data_dir() + "/Trash/info"
|
||||
|
||||
# In compress commands:
|
||||
# %n: First selected filename/dir to archive
|
||||
# %N: All selected filenames/dirs to archive, or (with %O) a single filename
|
||||
# %o: Resulting single archive file
|
||||
# %O: Resulting archive per source file/directory (use changes %N meaning)
|
||||
#
|
||||
# In extract commands:
|
||||
# %x: Archive file to extract
|
||||
# %g: Unique extraction target filename with optional subfolder
|
||||
# %G: Unique extraction target filename, never with subfolder
|
||||
#
|
||||
# In list commands:
|
||||
# %x: Archive to list
|
||||
#
|
||||
# Plus standard bash variables are accepted.
|
||||
self.arc_commands = [ '$(which 7za || echo 7zr) a %o %N',
|
||||
'zip -r %o %N',
|
||||
'rar a -r %o %N',
|
||||
'tar -cvf %o %N',
|
||||
'tar -cvjf %o %N',
|
||||
'tar -cvzf %o %N',
|
||||
'tar -cvJf %o %N',
|
||||
'gzip -c %N > %O',
|
||||
'xz -cz %N > %O'
|
||||
]
|
||||
|
||||
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
|
||||
self.selected_files = []
|
||||
self.to_copy_files = []
|
||||
self.to_cut_files = []
|
||||
self.soft_update_lock = {}
|
||||
|
||||
self.single_click_open = False
|
||||
self.is_pane1_hidden = False
|
||||
self.is_pane2_hidden = False
|
||||
self.is_pane3_hidden = False
|
||||
self.is_pane4_hidden = False
|
||||
|
||||
self.override_drop_dest = None
|
||||
self.is_searching = False
|
||||
self.search_icon_grid = None
|
||||
self.search_tab = None
|
||||
|
||||
self.skip_edit = False
|
||||
self.cancel_edit = False
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
|
||||
self.success_color = self.settings.get_success_color()
|
||||
self.warning_color = self.settings.get_warning_color()
|
||||
self.error_color = self.settings.get_error_color()
|
||||
|
||||
sys.excepthook = self.custom_except_hook
|
||||
self.window.connect("delete-event", self.tear_down)
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
|
||||
|
||||
def get_current_state(self):
|
||||
'''
|
||||
Returns the state info most useful for any given context and action intent.
|
||||
|
||||
Parameters:
|
||||
a (obj): self
|
||||
|
||||
Returns:
|
||||
wid, tid, tab, icon_grid, store
|
||||
'''
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
|
||||
store = icon_grid.get_model()
|
||||
return wid, tid, tab, icon_grid, store
|
||||
|
||||
|
||||
def clear_console(self):
|
||||
''' Clears the terminal screen. '''
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def call_method(self, _method_name, data = None):
|
||||
'''
|
||||
Calls a method from scope of class.
|
||||
|
||||
Parameters:
|
||||
a (obj): self
|
||||
b (str): method name to be called
|
||||
c (*): Data (if any) to be passed to the method.
|
||||
Note: It must be structured according to the given methods requirements.
|
||||
|
||||
Returns:
|
||||
Return data is that which the calling method gives.
|
||||
'''
|
||||
method_name = str(_method_name)
|
||||
method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}")
|
||||
return method(data) if data else method()
|
||||
|
||||
def has_method(self, obj, name):
|
||||
''' Checks if a given method exists. '''
|
||||
return callable(getattr(obj, name, None))
|
||||
|
||||
def clear_children(self, widget):
|
||||
''' Clear children of a gtk widget. '''
|
||||
for child in widget.get_children():
|
||||
widget.remove(child)
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Mixins module
|
||||
"""
|
@@ -1,62 +0,0 @@
|
||||
# Python imports
|
||||
import traceback, threading, time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
class ExceptionHookMixin:
|
||||
""" ExceptionHookMixin custom exception hook to reroute to a Gtk text area. """
|
||||
|
||||
def custom_except_hook(self, exec_type, value, _traceback):
|
||||
trace = ''.join(traceback.format_tb(_traceback))
|
||||
data = f"Exec Type: {exec_type} <--> Value: {value}\n\n{trace}\n\n\n\n"
|
||||
start_itr = self.message_buffer.get_start_iter()
|
||||
self.message_buffer.place_cursor(start_itr)
|
||||
self.display_message(self.error, data)
|
||||
|
||||
def display_message(self, type, text, seconds=None):
|
||||
self.message_buffer.insert_at_cursor(text)
|
||||
self.message_popup_widget.popup()
|
||||
if seconds:
|
||||
self.hide_message_timeout(seconds)
|
||||
|
||||
@threaded
|
||||
def hide_message_timeout(self, seconds=3):
|
||||
time.sleep(seconds)
|
||||
GLib.idle_add(self.message_popup_widget.popdown)
|
||||
|
||||
def save_debug_alerts(self, widget=None, eve=None):
|
||||
start_itr, end_itr = self.message_buffer.get_bounds()
|
||||
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", self.window, \
|
||||
action = Gtk.FileChooserAction.SAVE, \
|
||||
buttons = (Gtk.STOCK_CANCEL, \
|
||||
Gtk.ResponseType.CANCEL, \
|
||||
Gtk.STOCK_SAVE, \
|
||||
Gtk.ResponseType.OK))
|
||||
|
||||
text = self.message_buffer.get_text(start_itr, end_itr, False)
|
||||
resp = save_location_prompt.run()
|
||||
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
|
||||
pass
|
||||
elif resp == Gtk.ResponseType.OK:
|
||||
target = save_location_prompt.get_filename();
|
||||
with open(target, "w") as f:
|
||||
f.write(text)
|
||||
|
||||
save_location_prompt.destroy()
|
||||
|
||||
|
||||
def set_arc_buffer_text(self, widget=None, eve=None):
|
||||
sid = widget.get_active_id()
|
||||
self.arc_command_buffer.set_text(self.arc_commands[int(sid)])
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
UI module
|
||||
"""
|
@@ -1,202 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from .widget_mixin import WidgetMixin
|
||||
|
||||
|
||||
|
||||
|
||||
class TabMixin(WidgetMixin):
|
||||
"""docstring for TabMixin"""
|
||||
|
||||
def create_tab(self, wid, path=None):
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
path_entry = self.builder.get_object(f"path_entry")
|
||||
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
|
||||
tab.logger = self.logger
|
||||
|
||||
tab.set_wid(wid)
|
||||
if path: tab.set_path(path)
|
||||
|
||||
tab_widget = self.create_tab_widget(tab)
|
||||
scroll, store = self.create_icon_grid_widget(tab, wid)
|
||||
# TODO: Fix global logic to make the below work too
|
||||
# scroll, store = self.create_icon_tree_widget(tab, wid)
|
||||
index = notebook.append_page(scroll, tab_widget)
|
||||
|
||||
self.fm_controller.set__wid_and_tid(wid, tab.get_id())
|
||||
path_entry.set_text(tab.get_current_directory())
|
||||
notebook.show_all()
|
||||
notebook.set_current_page(index)
|
||||
|
||||
ctx = notebook.get_style_context()
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
notebook.set_tab_reorderable(scroll, True)
|
||||
self.load_store(tab, store)
|
||||
self.set_window_title()
|
||||
self.set_file_watcher(tab)
|
||||
|
||||
|
||||
|
||||
|
||||
def close_tab(self, button, eve=None):
|
||||
notebook = button.get_parent().get_parent()
|
||||
wid = int(notebook.get_name()[-1])
|
||||
tid = self.get_id_from_tab_box(button.get_parent())
|
||||
scroll = self.builder.get_object(f"{wid}|{tid}")
|
||||
page = notebook.page_num(scroll)
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
watcher = tab.get_dir_watcher()
|
||||
|
||||
watcher.cancel()
|
||||
self.get_fm_window(wid).delete_tab_by_id(tid)
|
||||
notebook.remove_page(page)
|
||||
self.fm_controller.save_state()
|
||||
self.set_window_title()
|
||||
|
||||
def on_tab_reorder(self, child, page_num, new_index):
|
||||
wid, tid = page_num.get_name().split("|")
|
||||
window = self.get_fm_window(wid)
|
||||
tab = None
|
||||
|
||||
for i, tab in enumerate(window.get_all_tabs()):
|
||||
if tab.get_id() == tid:
|
||||
_tab = window.get_tab_by_id(tid)
|
||||
watcher = _tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
|
||||
|
||||
tab = window.get_tab_by_id(tid)
|
||||
self.set_file_watcher(tab)
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def on_tab_switch_update(self, notebook, content=None, index=None):
|
||||
self.selected_files.clear()
|
||||
wid, tid = content.get_children()[0].get_name().split("|")
|
||||
self.fm_controller.set__wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
def get_id_from_tab_box(self, tab_box):
|
||||
return tab_box.get_children()[2].get_text()
|
||||
|
||||
def get_tab_label(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
|
||||
|
||||
def get_tab_close(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
|
||||
|
||||
def get_tab_icon_grid_from_notebook(self, notebook):
|
||||
return notebook.get_children()[1].get_children()[0]
|
||||
|
||||
def refresh_tab(data=None):
|
||||
wid, tid, tab, icon_grid, store = self.get_current_state()
|
||||
tab.load_directory()
|
||||
self.load_store(tab, store)
|
||||
|
||||
def update_tab(self, tab_label, tab, store, wid, tid):
|
||||
self.load_store(tab, store)
|
||||
self.set_path_text(wid, tid)
|
||||
|
||||
char_width = len(tab.get_end_of_path())
|
||||
tab_label.set_width_chars(char_width)
|
||||
tab_label.set_label(tab.get_end_of_path())
|
||||
self.set_window_title()
|
||||
self.set_file_watcher(tab)
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def do_action_from_bar_controls(self, widget, eve=None):
|
||||
action = widget.get_name()
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
|
||||
if action == "create_tab":
|
||||
dir = tab.get_current_directory()
|
||||
self.create_tab(wid, dir)
|
||||
self.fm_controller.save_state()
|
||||
return
|
||||
if action == "go_up":
|
||||
tab.pop_from_path()
|
||||
if action == "go_home":
|
||||
tab.set_to_home()
|
||||
if action == "refresh_tab":
|
||||
tab.load_directory()
|
||||
if action == "path_entry":
|
||||
focused_obj = self.window.get_focus()
|
||||
dir = f"{tab.get_current_directory()}/"
|
||||
path = widget.get_text()
|
||||
|
||||
if isinstance(focused_obj, Gtk.Entry):
|
||||
path_menu_buttons = self.builder.get_object("path_menu_buttons")
|
||||
query = widget.get_text().replace(dir, "")
|
||||
files = tab.get_files() + tab.get_hidden()
|
||||
|
||||
self.clear_children(path_menu_buttons)
|
||||
show_path_menu = False
|
||||
for file, hash in files:
|
||||
if os.path.isdir(f"{dir}{file}"):
|
||||
if query.lower() in file.lower():
|
||||
button = Gtk.Button(label=file)
|
||||
button.show()
|
||||
button.connect("clicked", self.set_path_entry)
|
||||
path_menu_buttons.add(button)
|
||||
show_path_menu = True
|
||||
|
||||
if not show_path_menu:
|
||||
self.path_menu.popdown()
|
||||
else:
|
||||
self.path_menu.popup()
|
||||
widget.grab_focus_without_selecting()
|
||||
widget.set_position(-1)
|
||||
|
||||
if path.endswith(".") or path == dir:
|
||||
return
|
||||
|
||||
if not tab.set_path(path):
|
||||
return
|
||||
|
||||
self.update_tab(tab_label, tab, store, wid, tid)
|
||||
|
||||
try:
|
||||
widget.grab_focus_without_selecting()
|
||||
widget.set_position(-1)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def set_path_entry(self, button=None, eve=None):
|
||||
wid, tid, tab, icon_grid, store = self.get_current_state()
|
||||
path = f"{tab.get_current_directory()}/{button.get_label()}"
|
||||
path_entry = self.builder.get_object("path_entry")
|
||||
path_entry.set_text(path)
|
||||
path_entry.grab_focus_without_selecting()
|
||||
path_entry.set_position(-1)
|
||||
self.path_menu.popdown()
|
||||
|
||||
def keyboard_close_tab(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
scroll = self.builder.get_object(f"{wid}|{tid}")
|
||||
page = notebook.page_num(scroll)
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
watcher = tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
|
||||
self.get_fm_window(wid).delete_tab_by_id(tid)
|
||||
notebook.remove_page(page)
|
||||
self.fm_controller.save_state()
|
||||
self.set_window_title()
|
||||
|
||||
def show_hide_hidden_files(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.set_hiding_hidden(not tab.is_hiding_hidden())
|
||||
tab.load_directory()
|
||||
self.builder.get_object("refresh_tab").released()
|
@@ -1,256 +0,0 @@
|
||||
# Python imports
|
||||
import copy
|
||||
from os.path import isdir, isfile
|
||||
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk, Gio
|
||||
|
||||
# Application imports
|
||||
from .tab_mixin import TabMixin
|
||||
|
||||
|
||||
class WindowMixin(TabMixin):
|
||||
"""docstring for WindowMixin"""
|
||||
|
||||
def generate_windows(self, session_json = None):
|
||||
if session_json:
|
||||
for j, value in enumerate(session_json):
|
||||
i = j + 1
|
||||
notebook_tggl_button = self.builder.get_object(f"tggl_notebook_{i}")
|
||||
is_hidden = True if value[0]["window"]["isHidden"] == "True" else False
|
||||
tabs = value[0]["window"]["tabs"]
|
||||
self.fm_controller.create_window()
|
||||
notebook_tggl_button.set_active(True)
|
||||
|
||||
for tab in tabs:
|
||||
self.create_new_tab_notebook(None, i, tab)
|
||||
|
||||
if is_hidden:
|
||||
self.toggle_notebook_pane(notebook_tggl_button)
|
||||
|
||||
try:
|
||||
if not self.is_pane4_hidden:
|
||||
icon_grid = self.window4.get_children()[1].get_children()[0]
|
||||
elif not self.is_pane3_hidden:
|
||||
icon_grid = self.window3.get_children()[1].get_children()[0]
|
||||
elif not self.is_pane2_hidden:
|
||||
icon_grid = self.window2.get_children()[1].get_children()[0]
|
||||
elif not self.is_pane1_hidden:
|
||||
icon_grid = self.window1.get_children()[1].get_children()[0]
|
||||
|
||||
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
|
||||
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
|
||||
except Exception as e:
|
||||
print("\n: The saved session might be missing window data! :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n")
|
||||
print(repr(e))
|
||||
else:
|
||||
for j in range(0, 4):
|
||||
i = j + 1
|
||||
self.fm_controller.create_window()
|
||||
self.create_new_tab_notebook(None, i, None)
|
||||
|
||||
|
||||
def get_fm_window(self, wid):
|
||||
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
|
||||
|
||||
def format_to_uris(self, store, wid, tid, treePaths, use_just_path=False):
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
uris = []
|
||||
|
||||
for path in treePaths:
|
||||
itr = store.get_iter(path)
|
||||
file = store.get(itr, 1)[0]
|
||||
fpath = ""
|
||||
|
||||
if not use_just_path:
|
||||
fpath = f"file://{dir}/{file}"
|
||||
else:
|
||||
fpath = f"{dir}/{file}"
|
||||
|
||||
uris.append(fpath)
|
||||
|
||||
return uris
|
||||
|
||||
|
||||
def set_bottom_labels(self, tab):
|
||||
_wid, _tid, _tab, icon_grid, store = self.get_current_state()
|
||||
selected_files = icon_grid.get_selected_items()
|
||||
current_directory = tab.get_current_directory()
|
||||
path_file = Gio.File.new_for_path(current_directory)
|
||||
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
|
||||
formatted_mount_free = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
|
||||
formatted_mount_size = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) )
|
||||
|
||||
if self.trash_files_path == current_directory:
|
||||
self.builder.get_object("restore_from_trash").show()
|
||||
self.builder.get_object("empty_trash").show()
|
||||
else:
|
||||
self.builder.get_object("restore_from_trash").hide()
|
||||
self.builder.get_object("empty_trash").hide()
|
||||
|
||||
# If something selected
|
||||
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
|
||||
self.bottom_path_label.set_label(tab.get_current_directory())
|
||||
if selected_files:
|
||||
uris = self.format_to_uris(store, _wid, _tid, selected_files, True)
|
||||
combined_size = 0
|
||||
for uri in uris:
|
||||
try:
|
||||
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::size",
|
||||
flags=Gio.FileQueryInfoFlags.NONE,
|
||||
cancellable=None)
|
||||
file_size = file_info.get_size()
|
||||
combined_size += file_size
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(repr(e))
|
||||
|
||||
|
||||
formatted_size = self.sizeof_fmt(combined_size)
|
||||
if tab.is_hiding_hidden():
|
||||
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_files_count()} ({formatted_size})")
|
||||
else:
|
||||
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_not_hidden_count()} ({formatted_size})")
|
||||
|
||||
return
|
||||
|
||||
# If nothing selected
|
||||
if tab.get_hidden():
|
||||
if tab.get_hidden_count() > 0:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_not_hidden_count()} visible ({tab.get_hidden_count()} hidden)")
|
||||
else:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
|
||||
else:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
|
||||
|
||||
|
||||
|
||||
def set_window_title(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
|
||||
for _notebook in self.notebooks:
|
||||
ctx = _notebook.get_style_context()
|
||||
ctx.remove_class("notebook-selected-focus")
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
|
||||
ctx = notebook.get_style_context()
|
||||
ctx.remove_class("notebook-unselected-focus")
|
||||
ctx.add_class("notebook-selected-focus")
|
||||
|
||||
self.window.set_title(f"SolarFM ~ {dir}")
|
||||
self.set_bottom_labels(tab)
|
||||
|
||||
def set_path_text(self, wid, tid):
|
||||
path_entry = self.builder.get_object("path_entry")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
path_entry.set_text(tab.get_current_directory())
|
||||
|
||||
def grid_set_selected_items(self, icons_grid):
|
||||
self.selected_files = icons_grid.get_selected_items()
|
||||
|
||||
def grid_cursor_toggled(self, icons_grid):
|
||||
print("wat...")
|
||||
|
||||
def grid_icon_single_click(self, icons_grid, eve):
|
||||
try:
|
||||
self.path_menu.popdown()
|
||||
wid, tid = icons_grid.get_name().split("|")
|
||||
self.fm_controller.set__wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
|
||||
if self.single_click_open: # FIXME: need to find a way to pass the model index
|
||||
self.grid_icon_double_click(icons_grid)
|
||||
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
|
||||
self.show_context_menu()
|
||||
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
self.display_message(self.error_color, f"{repr(e)}")
|
||||
|
||||
def grid_icon_double_click(self, icons_grid, item, data=None):
|
||||
try:
|
||||
if self.ctrl_down and self.shift_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files(in_terminal=True)
|
||||
return
|
||||
elif self.ctrl_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files()
|
||||
return
|
||||
|
||||
|
||||
wid, tid, tab, _icons_grid, store = self.get_current_state()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
tab_label = self.get_tab_label(notebook, icons_grid)
|
||||
|
||||
fileName = store[item][1]
|
||||
dir = tab.get_current_directory()
|
||||
file = f"{dir}/{fileName}"
|
||||
|
||||
if isdir(file):
|
||||
tab.set_path(file)
|
||||
self.update_tab(tab_label, tab, store, wid, tid)
|
||||
else:
|
||||
self.open_files()
|
||||
except Exception as e:
|
||||
self.display_message(self.error_color, f"{repr(e)}")
|
||||
|
||||
|
||||
|
||||
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
|
||||
action = icons_grid.get_name()
|
||||
wid, tid = action.split("|")
|
||||
store = icons_grid.get_model()
|
||||
treePaths = icons_grid.get_selected_items()
|
||||
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
|
||||
# further down call chain when doing internal fm stuff.
|
||||
uris = self.format_to_uris(store, wid, tid, treePaths)
|
||||
uris_text = '\n'.join(uris)
|
||||
|
||||
data.set_uris(uris)
|
||||
data.set_text(uris_text, -1)
|
||||
|
||||
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
|
||||
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
|
||||
target = icons_grid.get_name()
|
||||
wid, tid = target.split("|")
|
||||
store = icons_grid.get_model()
|
||||
treePath = icons_grid.get_drag_dest_item().path
|
||||
|
||||
if treePath:
|
||||
uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "")
|
||||
self.override_drop_dest = uri if isdir(uri) else None
|
||||
|
||||
if target not in current:
|
||||
self.fm_controller.set__wid_and_tid(wid, tid)
|
||||
|
||||
|
||||
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
||||
if info == 80:
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
|
||||
uris = data.get_uris()
|
||||
dest = f"{tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
|
||||
if len(uris) == 0:
|
||||
uris = data.get_text().split("\n")
|
||||
|
||||
from_uri = '/'.join(uris[0].replace("file://", "").split("/")[:-1])
|
||||
if from_uri != dest:
|
||||
self.move_files(uris, dest)
|
||||
|
||||
|
||||
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
|
||||
self.create_tab(wid, path)
|
@@ -1,14 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Gtk imports
|
||||
|
||||
# Application imports
|
||||
from .show_hide_mixin import ShowHideMixin
|
||||
from .ui.widget_file_action_mixin import WidgetFileActionMixin
|
||||
from .ui.pane_mixin import PaneMixin
|
||||
from .ui.window_mixin import WindowMixin
|
||||
from .show_hide_mixin import ShowHideMixin
|
||||
|
||||
|
||||
class UIMixin(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin):
|
||||
pass
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Signals module
|
||||
"""
|
@@ -1,29 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class IPCSignalsMixin:
|
||||
""" IPCSignalsMixin handle messages from another starting solarfm process. """
|
||||
|
||||
def print_to_console(self, message=None):
|
||||
print(self)
|
||||
print(message)
|
||||
|
||||
def handle_file_from_ipc(self, path):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
if notebook.is_visible():
|
||||
self.create_tab(wid, path)
|
||||
return
|
||||
|
||||
if not self.is_pane4_hidden:
|
||||
self.create_tab(4, path)
|
||||
elif not self.is_pane3_hidden:
|
||||
self.create_tab(3, path)
|
||||
elif not self.is_pane2_hidden:
|
||||
self.create_tab(2, path)
|
||||
elif not self.is_pane1_hidden:
|
||||
self.create_tab(1, path)
|
@@ -1,114 +0,0 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
|
||||
|
||||
|
||||
class KeyboardSignalsMixin:
|
||||
""" KeyboardSignalsMixin keyboard hooks controller. """
|
||||
|
||||
def unset_keys_and_data(self, widget=None, eve=None):
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
self.is_searching = False
|
||||
|
||||
def global_key_press_controller(self, eve, user_data):
|
||||
keyname = Gdk.keyval_name(user_data.keyval).lower()
|
||||
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
|
||||
if "control" in keyname:
|
||||
self.ctrl_down = True
|
||||
if "shift" in keyname:
|
||||
self.shift_down = True
|
||||
if "alt" in keyname:
|
||||
self.alt_down = True
|
||||
|
||||
# NOTE: Yes, this should actually be mapped to some key controller setting
|
||||
# file or something. Sue me.
|
||||
def global_key_release_controller(self, eve, user_data):
|
||||
keyname = Gdk.keyval_name(user_data.keyval).lower()
|
||||
if debug:
|
||||
print(f"global_key_release_controller > key > {keyname}")
|
||||
|
||||
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
|
||||
if "control" in keyname:
|
||||
self.ctrl_down = False
|
||||
if "shift" in keyname:
|
||||
self.shift_down = False
|
||||
if "alt" in keyname:
|
||||
self.alt_down = False
|
||||
|
||||
if self.ctrl_down and self.shift_down and keyname == "t":
|
||||
self.unset_keys_and_data()
|
||||
self.trash_files()
|
||||
|
||||
if self.ctrl_down:
|
||||
if keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
|
||||
self.builder.get_object(f"tggl_notebook_{keyname.strip('kp_')}").released()
|
||||
if keyname == "q":
|
||||
self.tear_down()
|
||||
if keyname == "slash" or keyname == "home":
|
||||
self.builder.get_object("go_home").released()
|
||||
if keyname == "r" or keyname == "f5":
|
||||
self.builder.get_object("refresh_tab").released()
|
||||
if keyname == "up" or keyname == "u":
|
||||
self.builder.get_object("go_up").released()
|
||||
if keyname == "l":
|
||||
self.unset_keys_and_data()
|
||||
self.builder.get_object("path_entry").grab_focus()
|
||||
if keyname == "t":
|
||||
self.builder.get_object("create_tab").released()
|
||||
if keyname == "o":
|
||||
self.unset_keys_and_data()
|
||||
self.open_files()
|
||||
if keyname == "w":
|
||||
self.keyboard_close_tab()
|
||||
if keyname == "h":
|
||||
self.show_hide_hidden_files()
|
||||
if keyname == "e":
|
||||
self.unset_keys_and_data()
|
||||
self.rename_files()
|
||||
if keyname == "c":
|
||||
self.copy_files()
|
||||
self.to_cut_files.clear()
|
||||
if keyname == "x":
|
||||
self.to_copy_files.clear()
|
||||
self.cut_files()
|
||||
if keyname == "v":
|
||||
self.paste_files()
|
||||
if keyname == "n":
|
||||
self.unset_keys_and_data()
|
||||
self.show_new_file_menu()
|
||||
|
||||
if keyname == "delete":
|
||||
self.unset_keys_and_data()
|
||||
self.delete_files()
|
||||
if keyname == "f2":
|
||||
self.unset_keys_and_data()
|
||||
self.rename_files()
|
||||
if keyname == "f4":
|
||||
self.unset_keys_and_data()
|
||||
self.open_terminal()
|
||||
if keyname in ["alt_l", "alt_r"]:
|
||||
top_main_menubar = self.builder.get_object("top_main_menubar")
|
||||
top_main_menubar.hide() if top_main_menubar.is_visible() else top_main_menubar.show()
|
||||
|
||||
if re.fullmatch(valid_keyvalue_pat, keyname):
|
||||
if not self.is_searching and not self.ctrl_down \
|
||||
and not self.shift_down and not self.alt_down:
|
||||
focused_obj = self.window.get_focus()
|
||||
if isinstance(focused_obj, Gtk.IconView):
|
||||
self.is_searching = True
|
||||
wid, tid, self.search_tab, self.search_icon_grid, store = self.get_current_state()
|
||||
self.unset_keys_and_data()
|
||||
self.popup_search_files(wid, keyname)
|
||||
return
|
@@ -1,90 +0,0 @@
|
||||
# Python imports
|
||||
import os, threading, time
|
||||
from multiprocessing.connection import Listener, Client
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class IPCServer:
|
||||
""" Create a listener so that other SolarFM instances send requests back to existing instance. """
|
||||
def __init__(self, conn_type="socket"):
|
||||
self.is_ipc_alive = False
|
||||
self._conn_type = conn_type
|
||||
self.ipc_authkey = b'solarfm-ipc'
|
||||
self.ipc_timeout = 15.0
|
||||
|
||||
if conn_type == "socket":
|
||||
self.ipc_address = '/tmp/solarfm-ipc.sock'
|
||||
else:
|
||||
self.ipc_address = '127.0.0.1'
|
||||
self.ipc_port = 4848
|
||||
|
||||
|
||||
@threaded
|
||||
def create_ipc_server(self):
|
||||
if self._conn_type == "socket":
|
||||
if os.path.exists(self.ipc_address):
|
||||
return
|
||||
|
||||
listener = Listener(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey)
|
||||
else:
|
||||
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
|
||||
|
||||
|
||||
self.is_ipc_alive = True
|
||||
while True:
|
||||
conn = listener.accept()
|
||||
start_time = time.time()
|
||||
|
||||
print(f"New Connection: {listener.last_accepted}")
|
||||
while True:
|
||||
msg = conn.recv()
|
||||
if debug:
|
||||
print(msg)
|
||||
|
||||
if "FILE|" in msg:
|
||||
file = msg.split("FILE|")[1].strip()
|
||||
if file:
|
||||
event_system.push_gui_event([None, "handle_file_from_ipc", (file,)])
|
||||
|
||||
conn.close()
|
||||
break
|
||||
|
||||
|
||||
if msg == 'close connection':
|
||||
conn.close()
|
||||
break
|
||||
if msg == 'close server':
|
||||
conn.close()
|
||||
break
|
||||
|
||||
# NOTE: Not perfect but insures we don't lock up the connection for too long.
|
||||
end_time = time.time()
|
||||
if (end - start) > self.ipc_timeout:
|
||||
conn.close()
|
||||
|
||||
listener.close()
|
||||
|
||||
|
||||
def send_ipc_message(self, message="Empty Data..."):
|
||||
try:
|
||||
if self._conn_type == "socket":
|
||||
conn = Client(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey)
|
||||
else:
|
||||
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
|
||||
|
||||
|
||||
conn.send(message)
|
||||
conn.send('close connection')
|
||||
except Exception as e:
|
||||
print(repr(e))
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Gtk Bound Plugins Module
|
||||
"""
|
@@ -1,83 +0,0 @@
|
||||
# Python imports
|
||||
import os, sys, importlib, traceback
|
||||
from os.path import join, isdir
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gio
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class Plugin:
|
||||
name = None
|
||||
module = None
|
||||
reference = None
|
||||
|
||||
|
||||
class Plugins:
|
||||
"""Plugins controller"""
|
||||
|
||||
def __init__(self, settings):
|
||||
self._settings = settings
|
||||
self._builder = self._settings.get_builder()
|
||||
self._plugins_path = self._settings.get_plugins_path()
|
||||
self._plugins_dir_watcher = None
|
||||
self._plugin_collection = []
|
||||
|
||||
|
||||
def launch_plugins(self):
|
||||
self._set_plugins_watcher()
|
||||
self.load_plugins()
|
||||
|
||||
def _set_plugins_watcher(self):
|
||||
self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \
|
||||
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
|
||||
self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ())
|
||||
|
||||
def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None):
|
||||
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
|
||||
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
|
||||
Gio.FileMonitorEvent.MOVED_OUT]:
|
||||
self.reload_plugins(file)
|
||||
|
||||
# @threaded
|
||||
def load_plugins(self, file=None):
|
||||
print(f"Loading plugins...")
|
||||
parent_path = os.getcwd()
|
||||
|
||||
for file in os.listdir(self._plugins_path):
|
||||
try:
|
||||
path = join(self._plugins_path, file)
|
||||
if isdir(path):
|
||||
os.chdir(path)
|
||||
|
||||
sys.path.insert(0, path)
|
||||
spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py"))
|
||||
app = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(app)
|
||||
|
||||
plugin_reference = app.Plugin(self._builder, event_system)
|
||||
plugin = Plugin()
|
||||
plugin.name = plugin_reference.get_plugin_name()
|
||||
plugin.module = path
|
||||
plugin.reference = plugin_reference
|
||||
|
||||
self._plugin_collection.append(plugin)
|
||||
except Exception as e:
|
||||
print("Malformed plugin! Not loading!")
|
||||
traceback.print_exc()
|
||||
|
||||
os.chdir(parent_path)
|
||||
|
||||
|
||||
def reload_plugins(self, file=None):
|
||||
print(f"Reloading plugins... stub.")
|
||||
|
||||
def send_message_to_plugin(self, type, data):
|
||||
print("Trying to send message to plugin...")
|
||||
for plugin in self._plugin_collection:
|
||||
if type in plugin.name:
|
||||
print('Found plugin; posting message...')
|
||||
plugin.reference.set_message(data)
|
@@ -1,3 +1 @@
|
||||
"""
|
||||
Root of ShellFM
|
||||
"""
|
||||
from .windows import WindowController
|
||||
|
@@ -0,0 +1,66 @@
|
||||
# Python imports
|
||||
from random import randint
|
||||
|
||||
|
||||
# Lib imports
|
||||
|
||||
|
||||
# Application imports
|
||||
from .view import View
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self):
|
||||
self.id_length = 10
|
||||
self.id = ""
|
||||
self.name = ""
|
||||
self.nickname = ""
|
||||
self.isHidden = False
|
||||
self.views = []
|
||||
|
||||
self.generate_id()
|
||||
|
||||
|
||||
def random_with_N_digits(self, n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def generate_id(self):
|
||||
self.id = str(self.random_with_N_digits(self.id_length))
|
||||
|
||||
def get_window_id(self):
|
||||
return self.id
|
||||
|
||||
def create_view(self):
|
||||
view = View()
|
||||
self.views.append(view)
|
||||
return view
|
||||
|
||||
def pop_view(self):
|
||||
self.views.pop()
|
||||
|
||||
def delete_view_by_id(self, vid):
|
||||
for view in self.views:
|
||||
if view.id == vid:
|
||||
self.views.remove(view)
|
||||
break
|
||||
|
||||
|
||||
def get_view_by_id(self, vid):
|
||||
for view in self.views:
|
||||
if view.id == vid:
|
||||
return view
|
||||
|
||||
def get_view_by_index(self, index):
|
||||
return self.views[index]
|
||||
|
||||
def get_views_count(self):
|
||||
return len(self.views)
|
||||
|
||||
def get_all_views(self):
|
||||
return self.views
|
||||
|
||||
def list_files_from_views(self):
|
||||
for view in self.views:
|
||||
print(view.files)
|
@@ -0,0 +1,179 @@
|
||||
# Python imports
|
||||
import threading, subprocess, time, json
|
||||
from os import path
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from . import Window
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
class WindowController:
|
||||
def __init__(self):
|
||||
USER_HOME = path.expanduser('~')
|
||||
CONFIG_PATH = USER_HOME + "/.config/solarfm"
|
||||
self.session_file = CONFIG_PATH + "/session.json"
|
||||
|
||||
self._event_sleep_time = 1
|
||||
self.active_window_id = ""
|
||||
self.active_tab_id = ""
|
||||
self.windows = []
|
||||
self.fm_event_observer()
|
||||
|
||||
@threaded
|
||||
def fm_event_observer(self):
|
||||
while True:
|
||||
time.sleep(event_sleep_time)
|
||||
event = event_system.consume_fm_event()
|
||||
if event:
|
||||
print(event)
|
||||
|
||||
def set_active_data(self, wid, tid):
|
||||
self.active_window_id = str(wid)
|
||||
self.active_tab_id = str(tid)
|
||||
|
||||
def get_active_data(self):
|
||||
return self.active_window_id, self.active_tab_id
|
||||
|
||||
def create_window(self):
|
||||
window = Window()
|
||||
window.name = "window_" + window.id
|
||||
window.nickname = "window_" + str(len(self.windows) + 1)
|
||||
|
||||
self.windows.append(window)
|
||||
return window
|
||||
|
||||
|
||||
def add_view_for_window(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
return window.create_view()
|
||||
|
||||
def add_view_for_window_by_name(self, name):
|
||||
for window in self.windows:
|
||||
if window.name == name:
|
||||
return window.create_view()
|
||||
|
||||
def add_view_for_window_by_nickname(self, nickname):
|
||||
for window in self.windows:
|
||||
if window.nickname == nickname:
|
||||
return window.create_view()
|
||||
|
||||
def pop_window(self):
|
||||
self.windows.pop()
|
||||
|
||||
def delete_window_by_id(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
self.windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_name(self, name):
|
||||
for window in self.windows:
|
||||
if window.name == name:
|
||||
self.windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_nickname(self, nickname):
|
||||
for window in self.windows:
|
||||
if window.nickname == nickname:
|
||||
self.windows.remove(window)
|
||||
break
|
||||
|
||||
def get_window_by_id(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
return window
|
||||
|
||||
raise(f"No Window by ID {win_id} found!")
|
||||
|
||||
def get_window_by_name(self, name):
|
||||
for window in self.windows:
|
||||
if window.name == name:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Name {name} found!")
|
||||
|
||||
def get_window_by_nickname(self, nickname):
|
||||
for window in self.windows:
|
||||
if window.nickname == nickname:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Nickname {nickname} found!")
|
||||
|
||||
def get_window_by_index(self, index):
|
||||
return self.windows[index]
|
||||
|
||||
def get_all_windows(self):
|
||||
return self.windows
|
||||
|
||||
def set_window_nickname(self, win_id = None, nickname = ""):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
window.nickname = nickname
|
||||
|
||||
def list_windows(self):
|
||||
print("\n[ ---- Windows ---- ]\n")
|
||||
for window in self.windows:
|
||||
print(f"\nID: {window.id}")
|
||||
print(f"Name: {window.name}")
|
||||
print(f"Nickname: {window.nickname}")
|
||||
print(f"Is Hidden: {window.isHidden}")
|
||||
print(f"View Count: {window.get_views_count()}")
|
||||
print("\n-------------------------\n")
|
||||
|
||||
|
||||
|
||||
def list_files_from_views_of_window(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
window.list_files_from_views()
|
||||
break
|
||||
|
||||
def get_views_count(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
return window.get_views_count()
|
||||
|
||||
def get_views_from_window(self, win_id):
|
||||
for window in self.windows:
|
||||
if window.id == win_id:
|
||||
return window.get_all_views()
|
||||
|
||||
|
||||
|
||||
|
||||
def save_state(self):
|
||||
windows = []
|
||||
for window in self.windows:
|
||||
views = []
|
||||
for view in window.views:
|
||||
views.append(view.get_current_directory())
|
||||
|
||||
windows.append(
|
||||
[
|
||||
{
|
||||
'window':{
|
||||
"ID": window.id,
|
||||
"Name": window.name,
|
||||
"Nickname": window.nickname,
|
||||
"isHidden": f"{window.isHidden}",
|
||||
'views': views
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with open(self.session_file, 'w') as outfile:
|
||||
json.dump(windows, outfile, separators=(',', ':'), indent=4)
|
||||
|
||||
def load_state(self):
|
||||
if path.isfile(self.session_file):
|
||||
with open(self.session_file) as infile:
|
||||
return json.load(infile)
|
@@ -1,3 +1,2 @@
|
||||
"""
|
||||
Window module
|
||||
"""
|
||||
from .Window import Window
|
||||
from .WindowController import WindowController
|
||||
|
@@ -1,185 +0,0 @@
|
||||
# Python imports
|
||||
import threading, json
|
||||
from os import path
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .window import Window
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
class WindowController:
|
||||
def __init__(self):
|
||||
USER_HOME = path.expanduser('~')
|
||||
CONFIG_PATH = USER_HOME + "/.config/solarfm"
|
||||
self._session_file = CONFIG_PATH + "/session.json"
|
||||
|
||||
self._event_sleep_time = 1
|
||||
self._active_window_id = ""
|
||||
self._active_tab_id = ""
|
||||
self._windows = []
|
||||
|
||||
|
||||
def set__wid_and_tid(self, wid, tid):
|
||||
self._active_window_id = str(wid)
|
||||
self._active_tab_id = str(tid)
|
||||
|
||||
def get_active_wid_and_tid(self):
|
||||
return self._active_window_id, self._active_tab_id
|
||||
|
||||
def create_window(self):
|
||||
window = Window()
|
||||
window.set_nickname(f"window_{str(len(self._windows) + 1)}")
|
||||
self._windows.append(window)
|
||||
return window
|
||||
|
||||
|
||||
def add_tab_for_window(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.create_tab()
|
||||
|
||||
def add_tab_for_window_by_name(self, name):
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
return window.create_tab()
|
||||
|
||||
def add_tab_for_window_by_nickname(self, nickname):
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
return window.create_tab()
|
||||
|
||||
def pop_window(self):
|
||||
self._windows.pop()
|
||||
|
||||
def delete_window_by_id(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_name(self, name):
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_nickname(self, nickname):
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def get_window_by_id(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window
|
||||
|
||||
raise(f"No Window by ID {win_id} found!")
|
||||
|
||||
def get_window_by_name(self, name):
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Name {name} found!")
|
||||
|
||||
def get_window_by_nickname(self, nickname):
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Nickname {nickname} found!")
|
||||
|
||||
def get_window_by_index(self, index):
|
||||
return self._windows[index]
|
||||
|
||||
def get_all_windows(self):
|
||||
return self._windows
|
||||
|
||||
|
||||
def set_window_nickname(self, win_id = None, nickname = ""):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
window.set_nickname(nickname)
|
||||
|
||||
def list_windows(self):
|
||||
print("\n[ ---- Windows ---- ]\n")
|
||||
for window in self._windows:
|
||||
print(f"\nID: {window.get_id()}")
|
||||
print(f"Name: {window.get_name()}")
|
||||
print(f"Nickname: {window.get_nickname()}")
|
||||
print(f"Is Hidden: {window.is_hidden()}")
|
||||
print(f"Tab Count: {window.get_tabs_count()}")
|
||||
print("\n-------------------------\n")
|
||||
|
||||
|
||||
|
||||
def list_files_from_tabs_of_window(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
window.list_files_from_tabs()
|
||||
break
|
||||
|
||||
def get_tabs_count(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.get_tabs_count()
|
||||
|
||||
def get_tabs_from_window(self, win_id):
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.get_all_tabs()
|
||||
|
||||
|
||||
|
||||
|
||||
def unload_tabs_and_windows(self):
|
||||
for window in self._windows:
|
||||
window.get_all_tabs().clear()
|
||||
|
||||
self._windows.clear()
|
||||
|
||||
def save_state(self, session_file = None):
|
||||
if not session_file:
|
||||
session_file = self._session_file
|
||||
|
||||
if len(self._windows) > 0:
|
||||
windows = []
|
||||
for window in self._windows:
|
||||
tabs = []
|
||||
for tab in window.get_all_tabs():
|
||||
tabs.append(tab.get_current_directory())
|
||||
|
||||
windows.append(
|
||||
[
|
||||
{
|
||||
'window':{
|
||||
"ID": window.get_id(),
|
||||
"Name": window.get_name(),
|
||||
"Nickname": window.get_nickname(),
|
||||
"isHidden": f"{window.is_hidden()}",
|
||||
'tabs': tabs
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with open(session_file, 'w') as outfile:
|
||||
json.dump(windows, outfile, separators=(',', ':'), indent=4)
|
||||
else:
|
||||
raise Exception("Window data corrupted! Can not save session!")
|
||||
|
||||
def load_state(self, session_file = None):
|
||||
if not session_file:
|
||||
session_file = self._session_file
|
||||
|
||||
if path.isfile(session_file):
|
||||
with open(session_file) as infile:
|
||||
return json.load(infile)
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Tabs module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Icons module
|
||||
"""
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Icons mixins module
|
||||
"""
|
@@ -1,246 +0,0 @@
|
||||
# Python imports
|
||||
import hashlib, re
|
||||
from os import listdir
|
||||
from os.path import isdir, isfile, join
|
||||
|
||||
from random import randint
|
||||
|
||||
|
||||
# Lib imports
|
||||
|
||||
|
||||
# Application imports
|
||||
from .utils.settings import Settings
|
||||
from .utils.launcher import Launcher
|
||||
from .utils.filehandler import FileHandler
|
||||
|
||||
from .icons.icon import Icon
|
||||
from .path import Path
|
||||
|
||||
|
||||
class Tab(Settings, FileHandler, Launcher, Icon, Path):
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self._id_length = 10
|
||||
|
||||
self._id = ""
|
||||
self._wid = None
|
||||
self._dir_watcher = None
|
||||
self._hide_hidden = self.HIDE_HIDDEN_FILES
|
||||
self._files = []
|
||||
self._dirs = []
|
||||
self._vids = []
|
||||
self._images = []
|
||||
self._desktop = []
|
||||
self._ungrouped = []
|
||||
self._hidden = []
|
||||
|
||||
self._generate_id()
|
||||
self.set_to_home()
|
||||
|
||||
def load_directory(self):
|
||||
path = self.get_path()
|
||||
self._dirs = []
|
||||
self._vids = []
|
||||
self._images = []
|
||||
self._desktop = []
|
||||
self._ungrouped = []
|
||||
self._hidden = []
|
||||
self._files = []
|
||||
|
||||
if not isdir(path):
|
||||
self.set_to_home()
|
||||
return ""
|
||||
|
||||
for f in listdir(path):
|
||||
file = join(path, f)
|
||||
if self._hide_hidden:
|
||||
if f.startswith('.'):
|
||||
self._hidden.append(f)
|
||||
continue
|
||||
|
||||
if isfile(file):
|
||||
lowerName = file.lower()
|
||||
if lowerName.endswith(self.fvideos):
|
||||
self._vids.append(f)
|
||||
elif lowerName.endswith(self.fimages):
|
||||
self._images.append(f)
|
||||
elif lowerName.endswith((".desktop",)):
|
||||
self._desktop.append(f)
|
||||
else:
|
||||
self._ungrouped.append(f)
|
||||
else:
|
||||
self._dirs.append(f)
|
||||
|
||||
self._dirs.sort(key=self._natural_keys)
|
||||
self._vids.sort(key=self._natural_keys)
|
||||
self._images.sort(key=self._natural_keys)
|
||||
self._desktop.sort(key=self._natural_keys)
|
||||
self._ungrouped.sort(key=self._natural_keys)
|
||||
|
||||
self._files = self._dirs + self._vids + self._images + self._desktop + self._ungrouped
|
||||
|
||||
def is_folder_locked(self, hash):
|
||||
if self.lock_folder:
|
||||
path_parts = self.get_path().split('/')
|
||||
file = self.get_path_part_from_hash(hash)
|
||||
|
||||
# Insure chilren folders are locked too.
|
||||
lockedFolderInPath = False
|
||||
for folder in self.locked_folders:
|
||||
if folder in path_parts:
|
||||
lockedFolderInPath = True
|
||||
break
|
||||
|
||||
return (file in self.locked_folders or lockedFolderInPath)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_not_hidden_count(self):
|
||||
return len(self._files) + \
|
||||
len(self._dirs) + \
|
||||
len(self._vids) + \
|
||||
len(self._images) + \
|
||||
len(self._desktop) + \
|
||||
len(self._ungrouped)
|
||||
|
||||
def get_hidden_count(self):
|
||||
return len(self._hidden)
|
||||
|
||||
def get_files_count(self):
|
||||
return len(self._files)
|
||||
|
||||
def get_path_part_from_hash(self, hash):
|
||||
files = self.get_files()
|
||||
file = None
|
||||
|
||||
for f in files:
|
||||
if hash == f[1]:
|
||||
file = f[0]
|
||||
break
|
||||
|
||||
return file
|
||||
|
||||
def get_files_formatted(self):
|
||||
files = self._hash_set(self._files),
|
||||
dirs = self._hash_set(self._dirs),
|
||||
videos = self.get_videos(),
|
||||
images = self._hash_set(self._images),
|
||||
desktops = self._hash_set(self._desktop),
|
||||
ungrouped = self._hash_set(self._ungrouped)
|
||||
hidden = self._hash_set(self._hidden)
|
||||
|
||||
return {
|
||||
'path_head': self.get_path(),
|
||||
'list': {
|
||||
'files': files,
|
||||
'dirs': dirs,
|
||||
'videos': videos,
|
||||
'images': images,
|
||||
'desktops': desktops,
|
||||
'ungrouped': ungrouped,
|
||||
'hidden': hidden
|
||||
}
|
||||
}
|
||||
|
||||
def get_pixbuf_icon_str_combo(self):
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self._files:
|
||||
icon = self.create_icon(dir, file).get_pixbuf()
|
||||
data.append([icon, file])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_gtk_icon_str_combo(self):
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self._files:
|
||||
icon = self.create_icon(dir, file)
|
||||
data.append([icon, file[0]])
|
||||
|
||||
return data
|
||||
|
||||
def get_current_directory(self):
|
||||
return self.get_path()
|
||||
|
||||
def get_current_sub_path(self):
|
||||
path = self.get_path()
|
||||
home = f"{self.get_home()}/"
|
||||
return path.replace(home, "")
|
||||
|
||||
def get_end_of_path(self):
|
||||
parts = self.get_current_directory().split("/")
|
||||
size = len(parts)
|
||||
return parts[size - 1]
|
||||
|
||||
|
||||
def set_hiding_hidden(self, state):
|
||||
self._hide_hidden = state
|
||||
|
||||
def is_hiding_hidden(self):
|
||||
return self._hide_hidden
|
||||
|
||||
def get_dot_dots(self):
|
||||
return self._hash_set(['.', '..'])
|
||||
|
||||
def get_files(self):
|
||||
return self._hash_set(self._files)
|
||||
|
||||
def get_dirs(self):
|
||||
return self._hash_set(self._dirs)
|
||||
|
||||
def get_videos(self):
|
||||
return self._hash_set(self._vids)
|
||||
|
||||
def get_images(self):
|
||||
return self._hash_set(self._images)
|
||||
|
||||
def get_desktops(self):
|
||||
return self._hash_set(self._desktop)
|
||||
|
||||
def get_ungrouped(self):
|
||||
return self._hash_set(self._ungrouped)
|
||||
|
||||
def get_hidden(self):
|
||||
return self._hash_set(self._hidden)
|
||||
|
||||
def get_id(self):
|
||||
return self._id
|
||||
|
||||
def set_wid(self, _wid):
|
||||
self._wid = _wid
|
||||
|
||||
def get_wid(self):
|
||||
return self._wid
|
||||
|
||||
def set_dir_watcher(self, watcher):
|
||||
self._dir_watcher = watcher
|
||||
|
||||
def get_dir_watcher(self):
|
||||
return self._dir_watcher
|
||||
|
||||
def _atoi(self, text):
|
||||
return int(text) if text.isdigit() else text
|
||||
|
||||
def _natural_keys(self, text):
|
||||
return [ self._atoi(c) for c in re.split('(\d+)',text) ]
|
||||
|
||||
def _hash_text(self, text):
|
||||
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
|
||||
|
||||
def _hash_set(self, arry):
|
||||
data = []
|
||||
for arr in arry:
|
||||
data.append([arr, self._hash_text(arr)])
|
||||
return data
|
||||
|
||||
def _random_with_N_digits(self, n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def _generate_id(self):
|
||||
self._id = str(self._random_with_N_digits(self._id_length))
|
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Utils module
|
||||
"""
|
@@ -11,7 +11,7 @@ class Path:
|
||||
return os.path.expanduser("~") + self.subpath
|
||||
|
||||
def get_path(self):
|
||||
return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
|
||||
return "/" + "/".join(self.path)
|
||||
|
||||
def get_path_list(self):
|
||||
return self.path
|
||||
@@ -21,7 +21,7 @@ class Path:
|
||||
self.load_directory()
|
||||
|
||||
def pop_from_path(self):
|
||||
try:
|
||||
if len(self.path) > 1:
|
||||
self.path.pop()
|
||||
|
||||
if not self.go_past_home:
|
||||
@@ -29,8 +29,6 @@ class Path:
|
||||
self.set_to_home()
|
||||
|
||||
self.load_directory()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def set_path(self, path):
|
||||
if path == self.get_path():
|
@@ -0,0 +1,229 @@
|
||||
# Python imports
|
||||
import hashlib
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import isdir, isfile, join
|
||||
|
||||
from random import randint
|
||||
|
||||
|
||||
# Lib imports
|
||||
|
||||
|
||||
# Application imports
|
||||
from .utils import Settings, Launcher, FileHandler
|
||||
from .icons import Icon
|
||||
from . import Path
|
||||
|
||||
|
||||
class View(Settings, FileHandler, Launcher, Icon, Path):
|
||||
def __init__(self):
|
||||
self. logger = None
|
||||
self.id_length = 10
|
||||
|
||||
self.id = ""
|
||||
self.wid = None
|
||||
self.dir_watcher = None
|
||||
self.hide_hidden = self.HIDE_HIDDEN_FILES
|
||||
self.files = []
|
||||
self.dirs = []
|
||||
self.vids = []
|
||||
self.images = []
|
||||
self.desktop = []
|
||||
self.ungrouped = []
|
||||
self.hidden = []
|
||||
|
||||
self.generate_id()
|
||||
self.set_to_home()
|
||||
|
||||
|
||||
def random_with_N_digits(self, n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def generate_id(self):
|
||||
self.id = str(self.random_with_N_digits(self.id_length))
|
||||
|
||||
def get_tab_id(self):
|
||||
return self.id
|
||||
|
||||
def set_wid(self, _wid):
|
||||
self.wid = _wid
|
||||
|
||||
def get_wid(self):
|
||||
return self.wid
|
||||
|
||||
def set_dir_watcher(self, watcher):
|
||||
self.dir_watcher = watcher
|
||||
|
||||
def get_dir_watcher(self):
|
||||
return self.dir_watcher
|
||||
|
||||
def load_directory(self):
|
||||
path = self.get_path()
|
||||
self.dirs = []
|
||||
self.vids = []
|
||||
self.images = []
|
||||
self.desktop = []
|
||||
self.ungrouped = []
|
||||
self.hidden = []
|
||||
self.files = []
|
||||
|
||||
if not isdir(path):
|
||||
self.set_to_home()
|
||||
return ""
|
||||
|
||||
for f in listdir(path):
|
||||
file = join(path, f)
|
||||
if self.hide_hidden:
|
||||
if f.startswith('.'):
|
||||
self.hidden.append(f)
|
||||
continue
|
||||
|
||||
if isfile(file):
|
||||
lowerName = file.lower()
|
||||
if lowerName.endswith(self.fvideos):
|
||||
self.vids.append(f)
|
||||
elif lowerName.endswith(self.fimages):
|
||||
self.images.append(f)
|
||||
elif lowerName.endswith((".desktop",)):
|
||||
self.desktop.append(f)
|
||||
else:
|
||||
self.ungrouped.append(f)
|
||||
else:
|
||||
self.dirs.append(f)
|
||||
|
||||
self.dirs.sort()
|
||||
self.vids.sort()
|
||||
self.images.sort()
|
||||
self.desktop.sort()
|
||||
self.ungrouped.sort()
|
||||
|
||||
self.files = self.dirs + self.vids + self.images + self.desktop + self.ungrouped
|
||||
|
||||
def hash_text(self, text):
|
||||
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
|
||||
|
||||
def hash_set(self, arry):
|
||||
data = []
|
||||
for arr in arry:
|
||||
data.append([arr, self.hash_text(arr)])
|
||||
return data
|
||||
|
||||
def is_folder_locked(self, hash):
|
||||
if self.lock_folder:
|
||||
path_parts = self.get_path().split('/')
|
||||
file = self.get_path_part_from_hash(hash)
|
||||
|
||||
# Insure chilren folders are locked too.
|
||||
lockedFolderInPath = False
|
||||
for folder in self.locked_folders:
|
||||
if folder in path_parts:
|
||||
lockedFolderInPath = True
|
||||
break
|
||||
|
||||
return (file in self.locked_folders or lockedFolderInPath)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_not_hidden_count(self):
|
||||
return len(self.files) + \
|
||||
len(self.dirs) + \
|
||||
len(self.vids) + \
|
||||
len(self.images) + \
|
||||
len(self.desktop) + \
|
||||
len(self.ungrouped)
|
||||
|
||||
def get_hidden_count(self):
|
||||
return len(self.hidden)
|
||||
|
||||
def get_files_count(self):
|
||||
return len(self.files)
|
||||
|
||||
def get_path_part_from_hash(self, hash):
|
||||
files = self.get_files()
|
||||
file = None
|
||||
|
||||
for f in files:
|
||||
if hash == f[1]:
|
||||
file = f[0]
|
||||
break
|
||||
|
||||
return file
|
||||
|
||||
def get_files_formatted(self):
|
||||
files = self.hash_set(self.files),
|
||||
dirs = self.hash_set(self.dirs),
|
||||
videos = self.get_videos(),
|
||||
images = self.hash_set(self.images),
|
||||
desktops = self.hash_set(self.desktop),
|
||||
ungrouped = self.hash_set(self.ungrouped)
|
||||
hidden = self.hash_set(self.hidden)
|
||||
|
||||
return {
|
||||
'path_head': self.get_path(),
|
||||
'list': {
|
||||
'files': files,
|
||||
'dirs': dirs,
|
||||
'videos': videos,
|
||||
'images': images,
|
||||
'desktops': desktops,
|
||||
'ungrouped': ungrouped,
|
||||
'hidden': hidden
|
||||
}
|
||||
}
|
||||
|
||||
def get_pixbuf_icon_str_combo(self):
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self.files:
|
||||
icon = self.create_icon(dir, file).get_pixbuf()
|
||||
data.append([icon, file])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_gtk_icon_str_combo(self):
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self.files:
|
||||
icon = self.create_icon(dir, file)
|
||||
data.append([icon, file[0]])
|
||||
|
||||
return data
|
||||
|
||||
def get_current_directory(self):
|
||||
return self.get_path()
|
||||
|
||||
def get_current_sub_path(self):
|
||||
path = self.get_path()
|
||||
home = self.get_home() + "/"
|
||||
return path.replace(home, "")
|
||||
|
||||
def get_end_of_path(self):
|
||||
parts = self.get_current_directory().split("/")
|
||||
size = len(parts)
|
||||
return parts[size - 1]
|
||||
|
||||
def get_dot_dots(self):
|
||||
return self.hash_set(['.', '..'])
|
||||
|
||||
def get_files(self):
|
||||
return self.hash_set(self.files)
|
||||
|
||||
def get_dirs(self):
|
||||
return self.hash_set(self.dirs)
|
||||
|
||||
def get_videos(self):
|
||||
return self.hash_set(self.vids)
|
||||
|
||||
def get_images(self):
|
||||
return self.hash_set(self.images)
|
||||
|
||||
def get_desktops(self):
|
||||
return self.hash_set(self.desktop)
|
||||
|
||||
def get_ungrouped(self):
|
||||
return self.hash_set(self.ungrouped)
|
@@ -0,0 +1,5 @@
|
||||
from .utils import *
|
||||
from .icons import *
|
||||
|
||||
from .Path import Path
|
||||
from .View import View
|
@@ -3,13 +3,10 @@ import os, subprocess, threading, hashlib
|
||||
from os.path import isfile
|
||||
|
||||
# Gtk imports
|
||||
import gi
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
# Application imports
|
||||
from .mixins.desktopiconmixin import DesktopIconMixin
|
||||
from .mixins.videoiconmixin import VideoIconMixin
|
||||
from .mixins import *
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
@@ -20,7 +17,7 @@ def threaded(fn):
|
||||
|
||||
class Icon(DesktopIconMixin, VideoIconMixin):
|
||||
def create_icon(self, dir, file):
|
||||
full_path = f"{dir}/{file}"
|
||||
full_path = dir + "/" + file
|
||||
return self.get_icon_image(dir, file, full_path)
|
||||
|
||||
def get_icon_image(self, dir, file, full_path):
|
||||
@@ -39,32 +36,29 @@ class Icon(DesktopIconMixin, VideoIconMixin):
|
||||
return None
|
||||
|
||||
def create_thumbnail(self, dir, file):
|
||||
full_path = f"{dir}/{file}"
|
||||
full_path = dir + "/" + file
|
||||
try:
|
||||
file_hash = hashlib.sha256(str.encode(full_path)).hexdigest()
|
||||
hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg"
|
||||
hash_img_pth = self.ABS_THUMBS_PTH + "/" + file_hash + ".jpg"
|
||||
if isfile(hash_img_pth) == False:
|
||||
self.generate_video_thumbnail(full_path, hash_img_pth)
|
||||
|
||||
thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
|
||||
if thumbnl == None: # If no icon whatsoever, return internal default
|
||||
thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
|
||||
thumbnl = GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
|
||||
|
||||
return thumbnl
|
||||
except Exception as e:
|
||||
print("Thumbnail generation issue:")
|
||||
print( repr(e) )
|
||||
return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
|
||||
return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
|
||||
|
||||
|
||||
def create_scaled_image(self, path, wxh):
|
||||
try:
|
||||
if path.lower().endswith(".gif"):
|
||||
return GdkPixbuf.PixbufAnimation.new_from_file(path) \
|
||||
.get_static_image() \
|
||||
.scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
|
||||
else:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
|
||||
scaled_pixbuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default
|
||||
return scaled_pixbuf
|
||||
except Exception as e:
|
||||
print("Image Scaling Issue:")
|
||||
print( repr(e) )
|
@@ -0,0 +1,4 @@
|
||||
from .mixins import DesktopIconMixin
|
||||
from .mixins import VideoIconMixin
|
||||
|
||||
from .Icon import Icon
|
@@ -3,6 +3,9 @@ import os, subprocess, hashlib
|
||||
from os.path import isfile
|
||||
|
||||
# Gtk imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from .xdg.DesktopEntry import DesktopEntry
|
@@ -0,0 +1,4 @@
|
||||
from . import xdg
|
||||
|
||||
from .VideoIconMixin import VideoIconMixin
|
||||
from .DesktopIconMixin import DesktopIconMixin
|
@@ -1,5 +1,5 @@
|
||||
# Python imports
|
||||
import os, shutil
|
||||
import os, shutil, subprocess, threading
|
||||
|
||||
# Lib imports
|
||||
|
@@ -59,7 +59,7 @@ class Settings:
|
||||
subpath = settings["base_of_home"]
|
||||
HIDE_HIDDEN_FILES = True if settings["hide_hidden_files"] == "true" else False
|
||||
FFMPG_THUMBNLR = FFMPG_THUMBNLR if settings["thumbnailer_path"] == "" else settings["thumbnailer_path"]
|
||||
go_past_home = True if settings["go_past_home"] == "" else settings["go_past_home"]
|
||||
go_past_home = True if settings["go_past_home"] == "true" else False
|
||||
lock_folder = True if settings["lock_folder"] == "true" else False
|
||||
locked_folders = settings["locked_folders"].split("::::")
|
||||
mplayer_options = settings["mplayer_options"].split()
|
@@ -0,0 +1,3 @@
|
||||
from .Settings import Settings
|
||||
from .Launcher import Launcher
|
||||
from .FileHandler import FileHandler
|
@@ -1,87 +0,0 @@
|
||||
# Python imports
|
||||
from random import randint
|
||||
|
||||
|
||||
# Lib imports
|
||||
|
||||
|
||||
# Application imports
|
||||
from .tabs.tab import Tab
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self):
|
||||
self._id_length = 10
|
||||
self._id = ""
|
||||
self._name = ""
|
||||
self._nickname = ""
|
||||
self._isHidden = False
|
||||
self._tabs = []
|
||||
|
||||
self._generate_id()
|
||||
self._set_name()
|
||||
|
||||
|
||||
def create_tab(self):
|
||||
tab = Tab()
|
||||
self._tabs.append(tab)
|
||||
return tab
|
||||
|
||||
def pop_tab(self):
|
||||
self._tabs.pop()
|
||||
|
||||
def delete_tab_by_id(self, vid):
|
||||
for tab in self._tabs:
|
||||
if tab.get_id() == vid:
|
||||
self._tabs.remove(tab)
|
||||
break
|
||||
|
||||
|
||||
def get_tab_by_id(self, vid):
|
||||
for tab in self._tabs:
|
||||
if tab.get_id() == vid:
|
||||
return tab
|
||||
|
||||
def get_tab_by_index(self, index):
|
||||
return self._tabs[index]
|
||||
|
||||
def get_tabs_count(self):
|
||||
return len(self._tabs)
|
||||
|
||||
def get_all_tabs(self):
|
||||
return self._tabs
|
||||
|
||||
def get_id(self):
|
||||
return self._id
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def get_nickname(self):
|
||||
return self._nickname
|
||||
|
||||
def is_hidden(self):
|
||||
return self._isHidden
|
||||
|
||||
def list_files_from_tabs(self):
|
||||
for tab in self._tabs:
|
||||
print(tab.get_files())
|
||||
|
||||
|
||||
def set_nickname(self, nickname):
|
||||
self._nickname = f"{nickname}"
|
||||
|
||||
def set_is_hidden(self, state):
|
||||
self._isHidden = f"{state}"
|
||||
|
||||
def _set_name(self):
|
||||
self._name = "window_" + self.get_id()
|
||||
|
||||
|
||||
def _random_with_N_digits(self, n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def _generate_id(self):
|
||||
self._id = str(self._random_with_N_digits(self._id_length))
|
@@ -0,0 +1,166 @@
|
||||
# Python imports
|
||||
import sys, traceback, threading, signal, inspect, os, time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
# Application imports
|
||||
from .mixins import *
|
||||
from . import ShowHideMixin, KeyboardSignalsMixin, Controller_Data
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Controller(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin, \
|
||||
KeyboardSignalsMixin, Controller_Data):
|
||||
def __init__(self, args, unknownargs, _settings):
|
||||
# sys.excepthook = self.custom_except_hook
|
||||
self.settings = _settings
|
||||
self.setup_controller_data()
|
||||
|
||||
self.window.show()
|
||||
self.generate_windows(self.state)
|
||||
|
||||
self.window.connect("delete-event", self.tear_down)
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
|
||||
self.gui_event_observer()
|
||||
|
||||
if unknownargs:
|
||||
for arg in unknownargs:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
if args.new_tab and os.path.isdir(args.new_tab):
|
||||
message = f"FILE|{args.new_tab}"
|
||||
event_system.send_ipc_message(message)
|
||||
|
||||
|
||||
def tear_down(self, widget=None, eve=None):
|
||||
event_system.send_ipc_message("close server")
|
||||
self.window_controller.save_state()
|
||||
time.sleep(event_sleep_time)
|
||||
Gtk.main_quit()
|
||||
|
||||
|
||||
@threaded
|
||||
def gui_event_observer(self):
|
||||
while True:
|
||||
time.sleep(event_sleep_time)
|
||||
event = event_system.consume_gui_event()
|
||||
if event:
|
||||
try:
|
||||
type, target, data = event
|
||||
method = getattr(self.__class__, type)
|
||||
GLib.idle_add(method, (self, data,))
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
|
||||
def custom_except_hook(self, exctype, value, _traceback):
|
||||
trace = ''.join(traceback.format_tb(_traceback))
|
||||
data = f"Exectype: {exctype} <--> Value: {value}\n\n{trace}\n\n\n\n"
|
||||
start_itr = self.message_buffer.get_start_iter()
|
||||
self.message_buffer.place_cursor(start_itr)
|
||||
self.display_message(self.error, data)
|
||||
|
||||
def display_message(self, type, text, seconds=None):
|
||||
self.message_buffer.insert_at_cursor(text)
|
||||
self.message_widget.popup()
|
||||
if seconds:
|
||||
self.hide_message_timeout(seconds)
|
||||
|
||||
@threaded
|
||||
def hide_message_timeout(self, seconds=3):
|
||||
time.sleep(seconds)
|
||||
GLib.idle_add(self.message_widget.popdown)
|
||||
|
||||
def save_debug_alerts(self, widget=None, eve=None):
|
||||
start_itr, end_itr = self.message_buffer.get_bounds()
|
||||
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", self.window, \
|
||||
action = Gtk.FileChooserAction.SAVE, \
|
||||
buttons = (Gtk.STOCK_CANCEL, \
|
||||
Gtk.ResponseType.CANCEL, \
|
||||
Gtk.STOCK_SAVE, \
|
||||
Gtk.ResponseType.OK))
|
||||
|
||||
text = self.message_buffer.get_text(start_itr, end_itr, False)
|
||||
resp = save_location_prompt.run()
|
||||
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
|
||||
pass
|
||||
elif resp == Gtk.ResponseType.OK:
|
||||
target = save_location_prompt.get_filename();
|
||||
with open(target, "w") as f:
|
||||
f.write(text)
|
||||
|
||||
save_location_prompt.destroy()
|
||||
|
||||
|
||||
def set_arc_buffer_text(self, widget=None, eve=None):
|
||||
id = widget.get_active_id()
|
||||
self.arc_command_buffer.set_text(self.arc_commands[int(id)])
|
||||
|
||||
|
||||
def clear_children(self, widget):
|
||||
for child in widget.get_children():
|
||||
widget.remove(child)
|
||||
|
||||
def get_current_state(self):
|
||||
wid, tid = self.window_controller.get_active_data()
|
||||
view = self.get_fm_window(wid).get_view_by_id(tid)
|
||||
iconview = self.builder.get_object(f"{wid}|{tid}|iconview")
|
||||
store = iconview.get_model()
|
||||
return wid, tid, view, iconview, store
|
||||
|
||||
def do_action_from_menu_controls(self, widget, eventbutton):
|
||||
action = widget.get_name()
|
||||
self.ctrlDown = True
|
||||
self.hide_context_menu()
|
||||
self.hide_new_file_menu()
|
||||
self.hide_edit_file_menu()
|
||||
|
||||
if action == "open":
|
||||
self.open_files()
|
||||
if action == "open_with":
|
||||
self.show_appchooser_menu()
|
||||
if action == "execute":
|
||||
self.execute_files()
|
||||
if action == "execute_in_terminal":
|
||||
self.execute_files(in_terminal=True)
|
||||
if action == "rename":
|
||||
self.rename_files()
|
||||
if action == "cut":
|
||||
self.to_copy_files.clear()
|
||||
self.cut_files()
|
||||
if action == "copy":
|
||||
self.to_cut_files.clear()
|
||||
self.copy_files()
|
||||
if action == "paste":
|
||||
self.paste_files()
|
||||
if action == "archive":
|
||||
self.show_archiver_dialogue()
|
||||
if action == "delete":
|
||||
self.delete_files()
|
||||
if action == "trash":
|
||||
self.trash_files()
|
||||
if action == "go_to_trash":
|
||||
self.builder.get_object("path_entry").set_text(self.trash_files_path)
|
||||
if action == "restore_from_trash":
|
||||
self.restore_trash_files()
|
||||
if action == "empty_trash":
|
||||
self.empty_trash()
|
||||
|
||||
|
||||
if action == "create":
|
||||
self.create_files()
|
||||
self.hide_new_file_menu()
|
||||
|
||||
self.ctrlDown = False
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user