From b233f30268928b09de5bc5b1723272993162d5aa Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Mon, 26 Aug 2024 03:39:49 +0200
Subject: [PATCH] To browser, add directory navigation.

---
 browser.py | 121 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 89 insertions(+), 32 deletions(-)

diff --git a/browser.py b/browser.py
index a3e0979..9fbe217 100755
--- a/browser.py
+++ b/browser.py
@@ -21,11 +21,26 @@ SORT_KEY_RANDOM = 'randomize'
 
 class FileItem(GObject.GObject):
 
-    def __init__(self, path, info, cache):
+    def __init__(self, path, info):
         super().__init__()
         self.name = info.get_name()
         self.last_mod_time = info.get_modification_date_time().format_iso8601()
         self.full_path = path_join(path, self.name)
+
+
+class DirItem(FileItem):
+
+    def __init__(self, path, info, is_parent=False):
+        super().__init__(path, info)
+        self.name = '  ..' if is_parent else f'  {self.name}/'
+        if is_parent:
+            self.full_path = path
+
+
+class ImgItem(FileItem):
+
+    def __init__(self, path, info, cache):
+        super().__init__(path, info)
         for param_name in GEN_PARAMS:
             if param_name in GEN_PARAMS_STR:
                 setattr(self, param_name.lower(), '')
@@ -38,7 +53,7 @@ class FileItem(GObject.GObject):
                     setattr(self, k, cached[k])
 
     def set_metadata(self, et, cache):
-        for d in et.get_tags([self.name], ['Comment']):
+        for d in et.get_tags([self.full_path], ['Comment']):
             for k, v in d.items():
                 if k.endswith('Comment'):
                     gen_params = GenParams.from_str(v)
@@ -97,7 +112,6 @@ class Window(Gtk.ApplicationWindow):
         self.dir_box.append(self.sorter)
 
         self.img_dir_absolute = abspath(IMG_DIR)
-        self.dir = Gio.File.new_for_path(self.img_dir_absolute)
         self.list_store = Gio.ListStore(item_type=FileItem)
         self.selection = Gtk.SingleSelection.new(self.list_store)
         factory = Gtk.SignalListItemFactory()
@@ -105,6 +119,7 @@ class Window(Gtk.ApplicationWindow):
         factory.connect('bind',
                         lambda _, i: i.props.child.set_text(i.props.item.name))
         self.selector = Gtk.ListView(model=self.selection, factory=factory)
+        self.selector.connect('activate', self.on_selector_activate)
         scrolled = Gtk.ScrolledWindow(child=self.selector, vexpand=True,
                                       propagate_natural_width=True)
         self.dir_box.append(scrolled)
@@ -115,9 +130,19 @@ class Window(Gtk.ApplicationWindow):
         box_outer.append(self.viewer)
         self.props.child = box_outer
 
-        self.item = None
+        self.item_img, self.item_dir = None, None
+        self.unsorted_dirs, self.unsorted_files = [], []
         self.sort_order = ['last_mod_time']
         self.reload_dir()
+        self.selection.props.selected = self.max_index
+
+    def on_selector_activate(self, _, __):
+        if isinstance(self.selection.props.selected_item, DirItem):
+            self.item_dir = self.selection.props.selected_item
+            self.img_dir_absolute = self.item_dir.full_path
+            self.item_img, self.item_dir = None, None
+            self.unsorted_dirs, self.unsorted_files = [], []
+            self.reload_dir()
 
     def toggle_folder_view(self, _):
         self.dir_box.props.visible = not self.dir_box.props.visible
@@ -139,32 +164,41 @@ class Window(Gtk.ApplicationWindow):
         self.list_store.remove_all()
         for key in self.sort_order:
             if SORT_KEY_RANDOM == key:
-                shuffle(self.unsorted)
+                shuffle(self.unsorted_files)
+                shuffle(self.unsorted_dirs)
                 continue
-            self.unsorted.sort(key=attrgetter(key))
-        for file_item in self.unsorted:
+            self.unsorted_files.sort(key=attrgetter(key))
+            if key in {'name', 'last_mod_time'}:
+                self.unsorted_dirs.sort(key=attrgetter(key))
+        for file_item in [self.parent_dir_item]\
+                + self.unsorted_dirs + self.unsorted_files:
             self.list_store.append(file_item)
-        if self.item:
-            for pos, item in enumerate(self.list_store):
-                if item.full_path == self.item.full_path:
-                    self.selection.set_selected(pos)
-        else:
-            self.update_selected()
+        for self_item in (self.item_dir, self.item_img):
+            if self_item:
+                for pos, item in enumerate(self.list_store):
+                    if item.full_path == self_item.full_path:
+                        self.selection.set_selected(pos)
+                return
+        self.update_selected()
 
     def update_selected(self, *_args):
-        self.item = self.selection.props.selected_item
-        self.reload_image()
+        if isinstance(self.selection.props.selected_item, ImgItem):
+            self.item_img = self.selection.props.selected_item
+            self.item_dir = None
+            self.reload_image()
+        else:
+            self.item_dir = self.selection.props.selected_item
         self.selector.scroll_to(self.selection.props.selected,
                                 Gtk.ListScrollFlags.NONE, None)
 
     def reload_image(self):
         self.viewer.remove(self.viewer.get_last_child())
-        if self.item:
-            params_strs = [f'{k}: ' + str(getattr(self.item, k.lower()))
+        if self.item_img:
+            params_strs = [f'{k}: ' + str(getattr(self.item_img, k.lower()))
                            for k in GEN_PARAMS]
-            self.metadata.props.label = '\n'.join([self.item.full_path]
+            self.metadata.props.label = '\n'.join([self.item_img.full_path]
                                                   + params_strs)
-            pic = Gtk.Picture.new_for_filename(self.item.name)
+            pic = Gtk.Picture.new_for_filename(self.item_img.full_path)
             pic.props.halign = Gtk.Align.START
             self.viewer.append(pic)
         else:
@@ -173,38 +207,61 @@ class Window(Gtk.ApplicationWindow):
 
     def move_selection(self, increment, absolute_position):
         cur_index = self.selection.props.selected
+        if len(self.unsorted_files) > 2:
+            self.min_index = len(self.unsorted_dirs) + 1
+        else:
+            self.min_index = 0
         if 0 == absolute_position:
-            self.selection.props.selected = 0
+            self.selection.props.selected = self.min_index
         elif -1 == absolute_position:
             self.selection.props.selected = self.max_index
         elif (1 == increment and cur_index < self.max_index)\
-                or (-1 == increment and cur_index > 0):
+                or (-1 == increment and cur_index > self.min_index):
             self.selection.props.selected = cur_index + increment
 
     def reload_dir(self):
-        old_item_path = self.item.full_path if self.item else ''
+        self.dir = Gio.File.new_for_path(self.img_dir_absolute)
+        old_dir_path = self.item_dir.full_path if self.item_dir else ''
+        old_img_path = self.item_img.full_path if self.item_img else ''
         if not path_exists(CACHE_PATH):
             with open(CACHE_PATH, 'w', encoding='utf8') as f:
                 json_dump({}, f)
         with open(CACHE_PATH, 'r', encoding='utf8') as f:
             cache = json_load(f)
         self.max_index = 0
-        self.item = None
+        query_attrs = 'standard::name,standard::type,time::*'
+        self.item_img, self.item_dir = None, None
         self.selection.connect('selection-changed', self.update_selected)
-        query_attrs = 'standard::name,standard::content-type,time::*'
+        parent_path = abspath(path_join(self.img_dir_absolute, '..'))
+        parent_dir = self.dir.get_parent()
+        parent_dir_info = parent_dir.query_info(
+                query_attrs, Gio.FileQueryInfoFlags.NONE, None)
+        self.parent_dir_item = DirItem(
+                parent_path, parent_dir_info, is_parent=True)
+        self.unsorted_dirs, self.unsorted_files = [], []
         enumerator = self.dir.enumerate_children(
                 query_attrs, Gio.FileQueryInfoFlags.NONE, None)
-        self.unsorted = []
         for info in [info for info in enumerator
-                     if info.get_content_type().startswith('image/')]:
-            item = FileItem(self.img_dir_absolute, info, cache)
-            if old_item_path == item.full_path:
-                self.item = item
-            self.unsorted += [item]
+                     if info.get_file_type() == Gio.FileType.DIRECTORY]:
+            item = DirItem(self.img_dir_absolute, info)
+            if old_dir_path == item.full_path:
+                self.item_dir = item
+            self.unsorted_dirs += [item]
+        enumerator = self.dir.enumerate_children(
+                query_attrs + ',standard::content-type',
+                Gio.FileQueryInfoFlags.NONE, None)
+        for info in [info for info in enumerator
+                     if info.get_file_type() == Gio.FileType.REGULAR
+                     and info.get_content_type().startswith('image/')]:
+            item = ImgItem(self.img_dir_absolute, info, cache)
+            if old_img_path == item.full_path:
+                self.item_img = item
+            self.unsorted_files += [item]
         with ExifToolHelper() as et:
-            for item in [item for item in self.unsorted if '' == item.model]:
+            for item in [item for item
+                         in self.unsorted_files if '' == item.model]:
                 item.set_metadata(et, cache)
-        self.max_index = len(self.unsorted) - 1
+        self.max_index = len(self.unsorted_files + self.unsorted_dirs)
         self.sort()
         with open(CACHE_PATH, 'w', encoding='utf8') as f:
             json_dump(cache, f)
-- 
2.30.2