From e864cce741d79b73faa5fc01bb2ea069103f7f96 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 24 Apr 2021 16:43:45 -0500 Subject: [PATCH] Updated Icon generation and Settings --- src/shellfm/windows/view/Icon.py | 162 +++++++++++------- src/shellfm/windows/view/View.py | 12 +- src/shellfm/windows/view/utils/Launcher.py | 55 +----- src/shellfm/windows/view/utils/Settings.py | 46 ++++- src/user_config/pyfm/ffmpegthumbnailer | Bin 0 -> 23152 bytes .../pyfm}/icons/archive.png | Bin .../pyfm}/icons/audio.png | Bin .../utils => user_config/pyfm}/icons/bin.png | Bin .../utils => user_config/pyfm}/icons/dir.png | Bin .../utils => user_config/pyfm}/icons/doc.png | Bin .../utils => user_config/pyfm}/icons/pdf.png | Bin .../pyfm}/icons/presentation.png | Bin .../pyfm}/icons/spreadsheet.png | Bin .../utils => user_config/pyfm}/icons/text.png | Bin .../pyfm}/icons/trash.png | Bin .../pyfm}/icons/video.png | Bin .../utils => user_config/pyfm}/icons/web.png | Bin 17 files changed, 143 insertions(+), 132 deletions(-) create mode 100755 src/user_config/pyfm/ffmpegthumbnailer rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/archive.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/audio.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/bin.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/dir.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/doc.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/pdf.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/presentation.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/spreadsheet.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/text.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/trash.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/video.png (100%) rename src/{shellfm/windows/view/utils => user_config/pyfm}/icons/web.png (100%) diff --git a/src/shellfm/windows/view/Icon.py b/src/shellfm/windows/view/Icon.py index c82aba9..b6cf648 100644 --- a/src/shellfm/windows/view/Icon.py +++ b/src/shellfm/windows/view/Icon.py @@ -1,7 +1,6 @@ # Python Imports -import os, subprocess, hashlib, threading -from os.path import isdir, isfile, join - +import os, subprocess, threading, hashlib +from os.path import isfile, join # Gtk imports import gi @@ -24,118 +23,111 @@ def threaded(fn): return wrapper class Icon: - def __init__(self): - self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" - self.INTERNAL_ICON_PTH = self.SCRIPT_PTH + "./utils/icons/text.png" + def create_icon(self, dir, file): + full_path = dir + "/" + file + return self.get_icon_image(file, full_path) - - def createIcon(self, dir, file): - fullPath = dir + "/" + file - return self.getIconImage(file, fullPath) - - def createThumbnail(self, dir, file): - fullPath = dir + "/" + file + def create_thumbnail(self, dir, file): + full_path = dir + "/" + file try: - fileHash = hashlib.sha256(str.encode(fullPath)).hexdigest() - hashImgPth = self.get_home() + "/.thumbnails/normal/" + fileHash + ".png" + file_hash = hashlib.sha256(str.encode(full_path)).hexdigest() + hash_img_pth = ABS_THUMBS_PTH + "/" + file_hash + ".jpg" + if isfile(hash_img_pth) == False: + self.generate_video_thumbnail(full_path, hash_img_pth) - thumbnl = self.createScaledImage(hashImgPth, self.viIconWH) + thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) if thumbnl == None: # If no icon whatsoever, return internal default - thumbnl = Gtk.Image.new_from_file(self.SCRIPT_PTH + "./utils/icons/video.png") + thumbnl = Gtk.Image.new_from_file(self.DEFAULT_ICONS + "/video.png") return thumbnl except Exception as e: print("Thumbnail generation issue:") print( repr(e) ) - return Gtk.Image.new_from_file(self.SCRIPT_PTH + "./utils/icons/video.png") + return Gtk.Image.new_from_file(self.DEFAULT_ICONS + "/video.png") - def getIconImage(self, file, fullPath): + def get_icon_image(self, file, full_path): try: thumbnl = None # Video icon if file.lower().endswith(self.fvideos): - thumbnl = Gtk.Image.new_from_file(self.SCRIPT_PTH + "./utils/icons/video.png") + thumbnl = Gtk.Image.new_from_file(self.DEFAULT_ICONS + "/video.png") # Image Icon elif file.lower().endswith(self.fimages): - thumbnl = self.createScaledImage(fullPath, self.viIconWH) + thumbnl = self.create_scaled_image(full_path, self.VIDEO_ICON_WH) # .desktop file parsing - elif fullPath.lower().endswith( ('.desktop',) ): - thumbnl = self.parseDesktopFiles(fullPath) + elif full_path.lower().endswith( ('.desktop',) ): + thumbnl = self.parse_desktop_files(full_path) # System icons else: - thumbnl = self.getSystemThumbnail(fullPath, self.systemIconImageWH[0]) + thumbnl = self.get_system_thumbnail(full_path, self.SYS_ICON_WH[0]) if thumbnl == None: # If no icon whatsoever, return internal default - thumbnl = Gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) + thumbnl = Gtk.Image.new_from_file(self.DEFAULT_ICON) return thumbnl except Exception as e: print("Icon generation issue:") print( repr(e) ) - return Gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) + return Gtk.Image.new_from_file(self.DEFAULT_ICON) - def parseDesktopFiles(self, fullPath): + def parse_desktop_files(self, full_path): try: - xdgObj = DesktopEntry(fullPath) + xdgObj = DesktopEntry(full_path) icon = xdgObj.getIcon() - altIconPath = "" + alt_icon_path = "" if "steam" in icon: - steamIconsDir = self.get_home() + "/.thumbnails/steam_icons/" - name = xdgObj.getName() - fileHash = hashlib.sha256(str.encode(name)).hexdigest() + name = xdgObj.getName() + file_hash = hashlib.sha256(str.encode(name)).hexdigest() + hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg" - if isdir(steamIconsDir) == False: - os.mkdir(steamIconsDir) - - hashImgPth = steamIconsDir + fileHash + ".jpg" - if isfile(hashImgPth) == True: + if isfile(hash_img_pth) == True: # Use video sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) - execStr = xdgObj.getExec() - parts = execStr.split("steam://rungameid/") + exec_str = xdgObj.getExec() + parts = exec_str.split("steam://rungameid/") id = parts[len(parts) - 1] - imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg" - proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink]) + imageLink = self.STEAM_BASE_URL + id + "/header.jpg" + proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink]) proc.wait() # Use video thumbnail sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) elif os.path.exists(icon): - return self.createScaledImage(icon, self.systemIconImageWH) + return self.create_scaled_image(icon, self.SYS_ICON_WH) else: - iconsDirs = ["/usr/share/pixmaps", "/usr/share/icons", self.get_home() + "/.icons" ,] - altIconPath = "" + alt_icon_path = "" - for iconsDir in iconsDirs: - altIconPath = self.traverseIconsFolder(iconsDir, icon) - if altIconPath is not "": + for dir in self.ICON_DIRS: + alt_icon_path = self.traverse_icons_folder(dir, icon) + if alt_icon_path is not "": break - return self.createScaledImage(altIconPath, self.systemIconImageWH) + return self.create_scaled_image(alt_icon_path, self.SYS_ICON_WH) except Exception as e: + print(self.DEFAULT_ICON) print(".desktop icon generation issue:") print( repr(e) ) return None - def traverseIconsFolder(self, path, icon): - altIconPath = "" + def traverse_icons_folder(self, path, icon): + alt_icon_path = "" for (dirpath, dirnames, filenames) in os.walk(path): for file in filenames: appNM = "application-x-" + icon if icon in file or appNM in file: - altIconPath = dirpath + "/" + file + alt_icon_path = dirpath + "/" + file break - return altIconPath + return alt_icon_path - def getSystemThumbnail(self, filename, size): + def get_system_thumbnail(self, filename, size): try: if os.path.exists(filename): gioFile = Gio.File.new_for_path(filename) @@ -156,17 +148,63 @@ class Icon: return None - def createScaledImage(self, path, wxh): + def generate_video_thumbnail(self, full_path, hash_img_pth): try: - pixbuf = Gtk.Image.new_from_file(path).get_pixbuf() - scaledPixBuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default - return Gtk.Image.new_from_pixbuf(scaledPixBuf) + proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_pth]) + proc.wait() + except Exception as e: + self.logger.debug(repr(e)) + self.ffprobe_generate_video_thumbnail(full_path, hash_img_pth) + + + def ffprobe_generate_video_thumbnail(self, full_path, hash_img_pth): + proc = None + try: + # Stream duration + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Stream duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Get frame roughly 35% through video + grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) + command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_pth] + proc = subprocess.Popen(command, stdout=subprocess.PIPE) + proc.wait() + except Exception as e: + print("Video thumbnail generation issue in thread:") + print( repr(e) ) + self.logger.debug(repr(e)) + + + def create_scaled_image(self, path, wxh): + try: + pixbuf = Gtk.Image.new_from_file(path).get_pixbuf() + scaled_pixbuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default + return Gtk.Image.new_from_pixbuf(scaled_pixbuf) except Exception as e: print("Image Scaling Issue:") print( repr(e) ) return None - def createFromFile(self, path): + def create_from_file(self, path): try: return Gtk.Image.new_from_file(path) except Exception as e: @@ -174,5 +212,5 @@ class Icon: print( repr(e) ) return None - def returnGenericIcon(self): - return Gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) + def return_generic_icon(self): + return Gtk.Image.new_from_file(self.DEFAULT_ICON) diff --git a/src/shellfm/windows/view/View.py b/src/shellfm/windows/view/View.py index 913b7e0..37552b4 100644 --- a/src/shellfm/windows/view/View.py +++ b/src/shellfm/windows/view/View.py @@ -17,6 +17,7 @@ from . import Path, Icon class View(Settings, Launcher, Icon, Path): def __init__(self): self.id = "" + self. logger = None self.files = [] self.dirs = [] self.vids = [] @@ -157,16 +158,7 @@ class View(Settings, Launcher, Icon, Path): return self.hashSet(self.dirs) def get_videos(self): - videos_set = self.hashSet(self.vids) - current_directory = self.get_current_directory() - for video in videos_set: - hashImgPth = join(self.ABS_THUMBS_PTH, video[1]) + ".jpg" - if not os.path.exists(hashImgPth) : - fullPath = join(current_directory, video[0]) - self.logger.debug(f"Hash Path: {hashImgPth}\nFile Path: {fullPath}") - self.generate_video_thumbnail(fullPath, hashImgPth) - - return videos_set + return self.hashSet(self.vids) def get_images(self): return self.hashSet(self.images) diff --git a/src/shellfm/windows/view/utils/Launcher.py b/src/shellfm/windows/view/utils/Launcher.py index f4403a6..ba50e47 100644 --- a/src/shellfm/windows/view/utils/Launcher.py +++ b/src/shellfm/windows/view/utils/Launcher.py @@ -9,7 +9,7 @@ import os, subprocess, threading class Launcher: - def openFilelocally(self, file): + def open_file_locally(self, file): lowerName = file.lower() command = [] @@ -38,7 +38,7 @@ class Launcher: subprocess.Popen(command, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True) - def remuxVideo(self, hash, file): + def remux_video(self, hash, file): remux_vid_pth = self.REMUX_FOLDER + "/" + hash + ".mp4" self.logger.debug(remux_vid_pth) @@ -66,53 +66,6 @@ class Launcher: return True - - def generate_video_thumbnail(self, fullPath, hashImgPth): - try: - proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", fullPath, "-o", hashImgPth]) - proc.wait() - except Exception as e: - self.logger.debug(repr(e)) - self.ffprobe_generate_video_thumbnail(fullPath, hashImgPth) - - - def generate_video_thumbnail(self, fullPath, hashImgPth): - proc = None - try: - # Stream duration - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Stream duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Get frame roughly 35% through video - grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) - command = ["ffmpeg", "-ss", grabTime, "-an", "-i", fullPath, "-s", "320x180", "-vframes", "1", hashImgPth] - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - proc.wait() - except Exception as e: - print("Video thumbnail generation issue in thread:") - print( repr(e) ) - self.logger.debug(repr(e)) - - def check_remux_space(self): limit = self.remux_folder_max_disk_usage try: @@ -121,7 +74,7 @@ class Launcher: self.logger.debug(e) return - usage = self.getRemuxFolderUsage(self.REMUX_FOLDER) + usage = self.get_remux_folder_usage(self.REMUX_FOLDER) if usage > limit: files = os.listdir(self.REMUX_FOLDER) for file in files: @@ -129,7 +82,7 @@ class Launcher: os.unlink(fp) - def getRemuxFolderUsage(self, start_path = "."): + def get_remux_folder_usage(self, start_path = "."): total_size = 0 for dirpath, dirnames, filenames in os.walk(start_path): for f in filenames: diff --git a/src/shellfm/windows/view/utils/Settings.py b/src/shellfm/windows/view/utils/Settings.py index f2dfc94..12cae7d 100644 --- a/src/shellfm/windows/view/utils/Settings.py +++ b/src/shellfm/windows/view/utils/Settings.py @@ -1,7 +1,7 @@ # System import +import os from os import path - # Lib imports @@ -11,19 +11,33 @@ from os import path class Settings: logger = None - GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) + lock_folder = False + go_past_home = True + + GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) ABS_THUMBS_PTH = None # Used for thumbnail generation and is set by passing in REMUX_FOLDER = None # Used for Remuxed files and is set by passing in FFMPG_THUMBNLR = None # Used for thumbnail generator binary and is set by passing in HIDE_HIDDEN_FILES = True - lock_folder = False - go_past_home = True - iconContainerWH = [128, 128] - systemIconImageWH = [56, 56] - viIconWH = [256, 128] + USER_HOME = path.expanduser('~') + CONFIG_PATH = USER_HOME + "/.config/pyfm" + DEFAULT_ICONS = CONFIG_PATH + "/icons" + DEFAULT_ICON = DEFAULT_ICONS + "/text.png" + FFMPG_THUMBNLR = CONFIG_PATH + "/ffmpegthumbnailer" # Thumbnail generator binary + REMUX_FOLDER = USER_HOME + "/.remuxs" # Remuxed files folder - subpath = "" # modify 'home' folder path + STEAM_BASE_URL = "https://steamcdn-a.akamaihd.net/steam/apps/" + ICON_DIRS = ["/usr/share/pixmaps", "/usr/share/icons", USER_HOME + "/.icons" ,] + BASE_THUMBS_PTH = USER_HOME + "/.thumbnails" # Used for thumbnail generation + ABS_THUMBS_PTH = BASE_THUMBS_PTH + "/normal" # Used for thumbnail generation + STEAM_ICONS_PTH = BASE_THUMBS_PTH + "/steam_icons" + CONTAINER_ICON_WH = [128, 128] + VIDEO_ICON_WH = [256, 128] + SYS_ICON_WH = [56, 56] + + subpath = "/LazyShare/Movies-TV-Music/TV/Anime/Barakamon" # modify 'home' folder path + # subpath = "/Desktop" # modify 'home' folder path locked_folders = "venv::::flasks".split("::::") mplayer_options = "-quiet -really-quiet -xy 1600 -geometry 50%:50%".split() music_app = "/opt/deadbeef/bin/deadbeef" @@ -35,10 +49,24 @@ class Settings: file_manager_app = "spacefm" remux_folder_max_disk_usage = "8589934592" - + # Filters fvideos = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm') foffice = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') fimages = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga') ftext = ('.txt', '.text', '.sh', '.cfg', '.conf') fmusic = ('.psf', '.mp3', '.ogg', '.flac', '.m4a') fpdf = ('.pdf') + + + # Dire structure check + if path.isdir(REMUX_FOLDER) == False: + os.mkdir(REMUX_FOLDER) + + if path.isdir(BASE_THUMBS_PTH) == False: + os.mkdir(BASE_THUMBS_PTH) + + if path.isdir(ABS_THUMBS_PTH) == False: + os.mkdir(ABS_THUMBS_PTH) + + if path.isdir(STEAM_ICONS_PTH) == False: + os.mkdir(STEAM_ICONS_PTH) diff --git a/src/user_config/pyfm/ffmpegthumbnailer b/src/user_config/pyfm/ffmpegthumbnailer new file mode 100755 index 0000000000000000000000000000000000000000..0b6e6e2a3f7a9a9f54d4b2bdc0c939cfc80cd36a GIT binary patch literal 23152 zcmeHvdw3L8ns=p>hH&eShz^QM$pj6K#O?qgiK3l^Km~)eBrq(9ola6oS~}frcLf5X z6HTJ@M0O(MD8pPH#@$buQAa-4$H9z-C`w!%cj66E9AN~s+K9v(qKx+b-c#pvDouKK z#{Hh}**`paD(`vU^WM&Tu2prae^genz>=0GiDZ$kln85`?qVe4za$MVWhkc=NMj_I zbb)lfGzt_eE*C2yXygPu$yE!O$@$U&GjJ7H3_^j$#WDh(XOJZUg@**m-cWM!tN}CU z8>oPiBr^iZk*~WJGZe6zWds}~hmf`s6H7AtPwo+EKbPCjgF1k?<&B%qMH7IF#KInRFuvUuTAZZB0_6z;!( zq8%>t`)cxMU*@ly>GuViTV}Qt5Xof`?}=COK{e&Stt%`#?5#8C!DK2O3$A*XWJ z)7$Rf-m>5N^cff5HMo6lQK<6vZ8K1ohl_YArqd>c(*E_rjEtOgDN7ok5tVYJjPY{D zI7!a5W@X$a6YnfsvvJMAm5*yKu0mWQ6(MuEKmf16HIL(00`ib^$`Ei3Wu+3HF9n>B zs|?pdP7|P%U*_{PaH-ukXO-IBhfF$ zU>k}4Av)Vg^lUWPNc4Bn0Y;*qjm8~`ei_`7NAUA#3V+^8;m_gGBkTVLx<|4bPNDzY z6mfH=u$!ME4%81us@JI$e*P$h-G5ENe^&~B&QIY_B!&JlDfAyop}#i;|CAK(6ncJ~!XJAIJy)d|U)d@6m!xQ~U!|bGl)|5t zDdPW9ia6{~p?`adIJ;7e>$g+zPfI~x1phCTrbxZlR>Lz1lf02UV`#jbD{bX;PL?hQ zeWGNOS_@r_MC%Kse8e*g{5z8P4}<>u$sb2mHNz(x zWa%%=?l{SDo{Liquz~v{&o$@=(cYAfu=ce{+jCtEU&ZO?gMJ3|_j5mivUD}qG>_G5%4zi~=X5vsle0@s&?!ElsfM1n5noDWOut=f=k+zHlJqLKo5krWFIWY+ ziInGh1pjLAQ=F6A&pZmWDainxTZ};BwT?`lhYB|5yZ(hO+b|6j^|G}lzjFQ9Y zzvA_h94H%a%DBcvv{1hp`7T4c$@`&0rzJx$sY;(n)>>tklLU%HaFD- zygt7grftn^$+Kv!XQ|rYi)d=N)bEW%)QIFMuUhP>Q^TBGwYbzD45(G!8ow%eJPl33 zfG47P!)TUJmP^sMvMse!Uu$+Pkr=iJl$apo^ad9Z58 zL+9Lxs#U7$8n+s*RRfy0K`rzB-O{DbGG9%NujcO-DD>9VE%5mfh%)yzm7clf97(C5 zQVaV64NF35*sJ-1fk>f0xDK9rBWl^wYbv$59#3sci__`MtMNvBwTKz9m)BNmPG@bS zH|){EUY{00a4Nm{S5^i?Rm(>V%^bMWMCL;OolmF5(8*(1tM)Ycg25V3Q z6b&`eir>;?-t2@1HC*0=majrTDho6>RTg;M&N;=)7X-siUad43(9{+!WfLWbW4^O& zEscgczp<;Y^Vj-=5ml=5N7gste=wv50Mu|e7$&}mR#*Fj9}sUo*{IYCe8GqZ?WxW# z5BM~u4nv})#e-fD2?o4=pSIq!*1?#AvqH_9(cjHO!8yyb*cb~QA9TPWrn%7QCD1=o z7b%-l>sP(uGIX9iPdR+Rr6JIU>_6Jo@-fhT4S_QE)ifIG{BHPw&g%8lV9W9p32Ofk~Zxd<}QN!AZUM~gbETdwayS&O%% zSI+YInYP(#pw9mfE1WY~XV?1zb!D|pyn-+-DvWyN&GvY-#&B?*C)licg7u!T zH_)JZ>YFsgY6C1*l~-zo=2)OpIGz4r14cOOHb$S|Au?3ec*#UWV=etHJf2${Yzle9 zDsL}X9NrBv)$vX=%oC8m{eb@#ZW@6_oSG0@1YmY80>VQ!%rR=Xf4tqC4vd?y8uEK< z)v~50vI9kdb)q3zteIhFp=d-?yBK}FQu9a9g2^***$5rAwhG;J4!SAE2pb&rVRiU0 z_!gBYL>+1|YBWs7i7=t7v5*Ak)cczwjm8w!P>UslW*(>HSr%A_K8uAq(%hudyi{vU zNtoMdbh33B(?RVTPi^BGPd(Om31d?WGT_IQW>f%HTYQ?-plZR8CN-%|wW0MAvfgk5 zI7lsD*qgjQ$f$K*&5MGXNW`Fn6sTi6k^hJPJS&~EwD9_6VIL;*wU|i0?b?}FP^V(? zZt?}x#EP*X9AxWepyAt&1S2f9k!BC34cdJ;^Fmn1wdI)aF&WatNnsKJTIqDK;$;!6 zSu`kTIY#yh3%MHbHmUz}zi1J`Mr;XOsKeyl98v3}g{7sQS-B2rVMTdKsV6TtFLzEN zKRc0g^6ZKgV4j^@XcTbF=^TdUIi=DSD~e0XJ@sx|U=d@KG zjXbuv_-{d;sAHtDz=%$NR%smL8iSk#WmIlOsgWDcctB+Q^xpzXTH;US$0R6JW>Brj z(K9>44$&XQ-kI*hWL)lx*2C~P=@T-9lm@T=%*$QCo6q&3WQ_C`{*%lkpKk&^xf?as z!0*269#n6Xlm(np8uD|o2PfVTuIP_uAQa=JDV*PX@?PL$r5PN*k>R7I0*=4>$QlI4 zDlO!=He(CImLVZKCro!-`gz=)7qdz1l>l9xhU& zi9TKs0YfG_I$9#NnCQ5xPo!28-JYOI(k2s~`l3i(COW(S}y;F486w-64p8T_(EIL|58A%v3t9PtB4grK4A~>QgYQC~ePX?lG9+a~?%e zeEMVfZ^tvE_;kV>sW5gZjx_yugwxa&>u30VgwvE2>t*;h!l6`(?PT~C!fA?%ZD;t8 z38$$k*2VDK2&XA2*2?e=gi|+*g%}Q3Am*M9UekS2N89tG4no?rh89tV9no43_47U(YQ%J0p;a}wer>P?rV)!S7)07db zX7~q$)07Z%GyGk`X)1`h82%RFQwVo3{1w7!=*MJ+zd$$*`B)aipCO!vdQ4(?H{mqI zV}oB){U0SV|WvF!~1G2t|1V_git zjc^*Ou~vp}Ae@G1EX43I;WRX3)eOIZa2k>^H^XZPr=b{gF?=QAKOo$}@TG*)kc`O; zUqm3#rpQKBD359;~o32i6i+L>*-x9#NF_B)P4K|K38ojBSy z!hL(k)v(b`Fw&9FdEig3lDBwAjbw{l}Q3V)G1Um6+RJh*v}( z%HB?O={TWHRH82{(J!36pZs3=;$>xXl`T&Cs8~OW2~Qx-S9<8pweF?XK`e-&qVTw8 zr$(Htzi^7k$SG~TY4&z%GI;mdWiYsTW45fXB-d!@E1mP+0kxaj%6URBK@gxIGdABS zd&X3Dkx@22mV>h5-K^T@GqyRu;EJpDG*Ud;pmuPoOCNw}>|+!qx9w6C7;U=(IlU6m zX?rmqyTGtM-(-CUY6Q^5AYy?-inpj7z9F1Gzsk*WLmjb*o0n-u_$4qvF*) zN6@YGUqV+$Z*#8F_M|Z!n!n#&K^jjcG(G_q!?E4S#X7kHmwpJdenQUP@h=ov(yYIK zrX<-PugX4iKH@&UEc#`qmYpe@tJwbde-yXckq2P=j3N?2W#qZ-KDVaZDSlbe3UQ9gn*cCiD}Ki@l5b!|)^_rni0- zj}Po5v=y%EPotq@%h3OJ(`eN{VYUnlN}nWQyeZMWO7uDXBxzd(M*ST#PjrtGeO>P) zQuLq_{Xl8^AkE(K2ZZlE=TBk~E2wxqJW-#3!?rI zkXQz_In<*ww?cOK!2XKq9zL+&<1~bbCOS5VU*R;_G_ZS!hWTd{Gr32<5^!Lv{)aEn zk7!Bmc};k!??I8_-@hZLPr|>LpPD|}7n{fiEf3Y^yV*2I{uaJrZ(m4pS`ItL&_-q7 zB!u<~wBMoq(e84*jMHy}#_sLcR!cpE?xq(_v`>nC0QUM^`_y*$gMoOgJo@tWO7vZ& z?Z}|Js=Tk4-e@X)Pt%JyrLT|P-|3^F>a%+L)cH8S^awOAQ)cb}P})w~l<3j5Q+AOi z+K!#EdN&os4}saxzj_aOHDs@c%9km(e#8_!{Y^R)DA7UXY5giCZLjk3H`-YVZR3PC z`&4Q59>ad?yfboT+&-VN41#(9wO+l~`pryPO8b;q+TFjH9J@+twhsIhatUQ0lR&(G zb!-DpYbuCv_%~f5PLe(1?qJm3Is$PW(itsN;)#Y5(F0N`H z*%_gNcP3QLHmTagRk_Vxo#d)IQ>ZFVs5-}_Y93c*B~|mdSMJM=Sh-TDIspYn3x0$B zm}tRIJ~1lT4cMK-RdsPypOc*tD)?YR)nSvW2e_&rshZ1G^>S6eOQEVdq3U^)s_VF_ zY*Hn2RVTSBPYP9MB~(3ZQgsGb_2$P=bv{?s%Dd>K6slf;0wY#iOse)m1uZt{5UW;$$SnD2bC|$! zHfhDC0IBby8Dsw-`7;84shK}N!M`IF|Mh16YZLq}src8M`TYrgB^CeOX8t=8{F77h zKVs&;FTt-LPf_0&%=}L!_;;n^KWgURpWxq=il1&!X(vgGM(ooB|B6)nQ_TD}AVz!T zq~gyv^Up}|pZq9AeXlk1&j+I43|91_q5T2wliNOAp>)n2wlz?U8)K}{pD5`+L^pq5 ziDqQ$kI-5heP8c`gFU}yBjy9L}u1F!Ab z6|0P$Q_o%4F_O+osDTnYyW<#mOpK?VdwKaRR<0e|odpH@(PQw0Zog>T2$PQP9=OwU zAM*(}?0N>d%I*a)z!nmBCvg+2;oYa8bK6u<7k5Cw*^4TdsQ!QOrgd65*jZt`)+vrkFck%VV=a%J4J znL^nfd;8M}K(c2|q^#qZ{b$cC?l@xqS#SF@_V#k(qy3KynV#va{SWHt2y9P*fs{x0 z>ePNcNO%hepwUZZX2u z9>Q***jSg=LM3+1Wu=&OU_-xT0JlpYAauq)9vfkd--G&3KVXq2@U+-Y@ zwb;N+wdl&NeH59_b#mK(r0I3k3Om@dBkPJ0IZBA*gu#(NB8N;IpRw2?j0bf$g?5B} z(Oc|sg>kPHdkF|uDC)^F^MBN7+nhf^CCZIucK49}!S_ejKh~`OYw**pYrLQJ;y!!( z&(Q+Z%D;g)8-P)a)i0eVD1=XeRuP2&CWv@Hj8YDRH}+dJAu()ZLVRyUadkUl5tMaL zp?3Xjo_MVG7^@2(-K+=RN0bpqcgHj+&|i6v_5LBMmP$MkK){IGszqI7c=Rxz;9)14OKUo3ze+8O(g zTR+@HEs(SYX)(4Sve<%P<-&G^rtulr3DByTrC$Y~mCoz%Xm)euDW&i&`yHoPTkccR zwd|~J+!E3=iwtrO%i%_>*!Bglk`jlK_KE&itSF4y(}nP|cz5(Zg;6Utms1cNcw`JY zw!%Mm)Cf!#>5`2d#Q$Zqifk<7;t{uQ?|6ue-uf(630s^lrL%&$_cx{R1^XR$Q(@F_ zPnq$uQurq6t0phpihbdW>o7N7LQW%OqU(yRrA6=kz~-7cTqb z$71COs1kj%=O*ean=ql!e&TS#?J8rh#A@@!5n?^4UjW}>l`oG>w zVl)y49vpaQ;DLevLs5deRj2{Z3(XKfn8nE!97EpO|&zfRYWy$h4*w-<>0U`6x;tTgkXAwKDCrEO1| zQaI3zN2kwbnwF}rA(Nl(p*{d@;6N5LI%ZKM;~*$brr{iPQ4yVl!>MU4++3^4fo8v7 z4g@uMEsoIEF>XmZ?TomL^I>%Asz}bub>uo7oR1y0Dw0oUUxmYMbTWIj!6?f!BUc2j z#1dH~Cr-l45#I(?&cU%M*&){l!*Vd}!y#C&pB2oIbL!N3Z?j)3lAU=4##?Xb(XI$= zj9f{2aIRX`f^r1s)CuO`ATU3y4o$MRCKB{FYhWS1#>S$irlLq>m{!MhL#re~?~TGg zA02v@Z)|2Kisc-KJWs|k(o1D0%c&8~tYgTpT2v0HNDd-YVH~KJ4MnIJj!VlqH{c@x z@+%Z*Fe^x$QLB?{*30$$Y?{e0uh5HQ!@;mBdm|wg&Bji?%ichpEKYvQ5%>&-M0^o? z)T>^|8;2iduiO+|>r-Xw3?b&M-@BeSC-JWn{Of|@HQsQr876#@NV6J|eE~V>uM=G% zA_wbv%ahz%A;(VP2_|%}rW!RY^e>jR6k+|-9@rtX?mnc_IN_Uh@7*o z(O27u8pzW(G|!N)guE{@^GfP^IcSGrrFap>&UCWjKwLKWtP3^1853Bd4fOYIQhT3R_9NAfT#s5n`x80|*0hj+)z$ zTO`k{Wu1fcEipz<5e3$F1-d(REMEYp+F2Bc928_2GSeGHJJk6iII+8)RakCR{UHfm zPbv?van3s@eIqjD2%T?jz%`S`IL^O17$I1513KJrnlCtn549=pVbqA!k~V9eTo2gmj%=o8gl5j?BfL5!e(FSqGpm-PID2E<|g5SerXc@m$Jb>f0_XW-+u>l zIsf-L9GxUT!0pr}*;~nRH-~9c(l6H}NumQMJKv7Sdjb0o#^cpkCU#|^<(YB6K z+J>c4T1!^il<}i8yV6Ey5&b+|3T$3N3_Oj@kGP|p`1J$vcoyllWi7BxUSuD)F0)m- z>fFmOnUy__RFM39Tp_$2<1P@Heb0Rz;C|S9h6p9LtUE2mw#m1r&$r2K84GO=i+`+5 z2Bp}RRbtC5F-{MX&AV}h5R;oo4yA1ti)*Yca|v-=it904{U~SOsG zPzefDmW`9Dz)M#v_EG2K1!~4g@S|=&v6R>*|1;HXb4H0R=T7Sao8$IT#kPXB(G|9x zY3cV_#@Y&sZH{7FPKiwhFJw^bg+{FnKiC!z&pGHD#EeL(*^h;`?P-=*6ONROBOl(z z)%cfqoW2pv*a7|_KVXCV0h`>9O509L`kiSb`cW+Wm;{}Z(5D8mLAnTe3Bg=#%VceQ zDd=1Agh(a&kgc+WEro0;ROcl&S7@aAvi_n$PcQTU3`_BNntUnhGJE|`EYcfk)_oS~ zNSgIgi}YiQ6_gzol!0g6OiT>-rCDE}ARSM$_D_(GrAO1g9xr`nE%|zabYzt4(FxLP zqg@9lNRMQ$0sRkStf0I(#tO=*F;?(&j-7`5AIDBZeqii0l>U6&5mK>Z0!%zSf%spX zUU5cl#PFwOT%RgJ9W2Np4>u<+NA7|_&F8K!+ceJp=W)`#_jCg~I4-=#)}N(hNZ-Iw5_I|o&y3SIe`cJ%@uM`}0Eyo{ zq5Dxv;&(*OG0?>Cs!ic|7Kh?nqvvrvkdnBc%4C=%ef~{6NZG9x7Xf_PXQZrgE|$Uh zFqZbGyBMUiaFm*OM%;HyHpr6sTPq?Z?kU~Xj6>Wrv}Q0galgx}D}B!(OFO^kVmU$2 zoP=9(hTn3QQO?ExzXS0*cFBh8&olg5%i--D z-oxSj96rwBUJmzh_#THRI2?tWWlCprcoB#B94_E+IfwNeuI2D{4)5Xcehwe!a4(1Z zIDC)86C94ZfZONrA`bI8T)^RS4(mBw%i--D-oxSj96rwBUJmzh_#THRI2?tO9F)%H z@FEWLIb6Wuat;}@>0hn0(Cf|jI_4JC6*%V9(odz#Ep*m9ypCCOXL;v3=FBR{FZAZt zsWr_FB(}Bat1VJ)q!G{5G;fWRi_fsta7fC^uZuKnWGZ^Qehb4L0MS!!}$6D&xQONnZ(D|xya@=1d(AsK9bA6 z49^V*+4u6fY9k&6;>&JNV;z(T4BGKQzwyHn9(a#$_L(w|Hyp;3Z>B@!Z>WVJ411e= zwU7&HWDpek96uLd<1*3<=2=0( zmjn1Jdq}>@;sSk-M*nwl(s3uqJ*u^x)wA#?DKC)Qc8u$0{X^y0`Y z@8_6HI4Gysm3;yK#|D4N<_7lGc*TvcX0Wxr2Zo(1dWUp@<}eT0wGW9gE=Mh5-x0;`QGOCCnX6aq0)Z>c2D42 literal 0 HcmV?d00001 diff --git a/src/shellfm/windows/view/utils/icons/archive.png b/src/user_config/pyfm/icons/archive.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/archive.png rename to src/user_config/pyfm/icons/archive.png diff --git a/src/shellfm/windows/view/utils/icons/audio.png b/src/user_config/pyfm/icons/audio.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/audio.png rename to src/user_config/pyfm/icons/audio.png diff --git a/src/shellfm/windows/view/utils/icons/bin.png b/src/user_config/pyfm/icons/bin.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/bin.png rename to src/user_config/pyfm/icons/bin.png diff --git a/src/shellfm/windows/view/utils/icons/dir.png b/src/user_config/pyfm/icons/dir.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/dir.png rename to src/user_config/pyfm/icons/dir.png diff --git a/src/shellfm/windows/view/utils/icons/doc.png b/src/user_config/pyfm/icons/doc.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/doc.png rename to src/user_config/pyfm/icons/doc.png diff --git a/src/shellfm/windows/view/utils/icons/pdf.png b/src/user_config/pyfm/icons/pdf.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/pdf.png rename to src/user_config/pyfm/icons/pdf.png diff --git a/src/shellfm/windows/view/utils/icons/presentation.png b/src/user_config/pyfm/icons/presentation.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/presentation.png rename to src/user_config/pyfm/icons/presentation.png diff --git a/src/shellfm/windows/view/utils/icons/spreadsheet.png b/src/user_config/pyfm/icons/spreadsheet.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/spreadsheet.png rename to src/user_config/pyfm/icons/spreadsheet.png diff --git a/src/shellfm/windows/view/utils/icons/text.png b/src/user_config/pyfm/icons/text.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/text.png rename to src/user_config/pyfm/icons/text.png diff --git a/src/shellfm/windows/view/utils/icons/trash.png b/src/user_config/pyfm/icons/trash.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/trash.png rename to src/user_config/pyfm/icons/trash.png diff --git a/src/shellfm/windows/view/utils/icons/video.png b/src/user_config/pyfm/icons/video.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/video.png rename to src/user_config/pyfm/icons/video.png diff --git a/src/shellfm/windows/view/utils/icons/web.png b/src/user_config/pyfm/icons/web.png similarity index 100% rename from src/shellfm/windows/view/utils/icons/web.png rename to src/user_config/pyfm/icons/web.png