update
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
import gi
|
||||
|
||||
import config.data as data
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import cairo
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
import modules.icons as icons
|
||||
|
||||
SAVE_FILE = os.path.expanduser("~/.pins.json")
|
||||
|
||||
icon_size = 80
|
||||
if data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Left", "Right"] or data.PANEL_POSITION in ["Start", "End"]:
|
||||
icon_size = 36
|
||||
|
||||
def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
|
||||
alloc = widget.get_allocation()
|
||||
surface = cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height)
|
||||
cr = cairo.Context(surface)
|
||||
cr.set_source_rgba(1, 1, 1, 0)
|
||||
cr.rectangle(0, 0, alloc.width, alloc.height)
|
||||
cr.fill()
|
||||
widget.draw(cr)
|
||||
return surface
|
||||
|
||||
def open_file(filepath):
|
||||
try:
|
||||
subprocess.Popen(["xdg-open", filepath])
|
||||
except Exception as e:
|
||||
print("Error opening file:", e)
|
||||
|
||||
def open_url(url):
|
||||
try:
|
||||
subprocess.Popen(["xdg-open", url])
|
||||
except Exception as e:
|
||||
print("Error opening URL:", e)
|
||||
|
||||
def is_url(text):
|
||||
|
||||
url_pattern = re.compile(
|
||||
r'^(https?|ftp)://'
|
||||
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
|
||||
r'localhost|'
|
||||
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
||||
r'(?::\d+)?'
|
||||
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
||||
return bool(url_pattern.match(text))
|
||||
|
||||
def get_favicon_url(url):
|
||||
"""Extract the base domain from a URL and construct a favicon URL."""
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
return f"{base_url}/favicon.ico"
|
||||
|
||||
def download_favicon(url, callback):
|
||||
"""Download a favicon asynchronously and call the callback with the result."""
|
||||
favicon_url = get_favicon_url(url)
|
||||
|
||||
def do_download():
|
||||
temp_file = None
|
||||
try:
|
||||
temp_fd, temp_path = tempfile.mkstemp(suffix='.ico')
|
||||
os.close(temp_fd)
|
||||
temp_file = temp_path
|
||||
|
||||
urllib.request.urlretrieve(favicon_url, temp_path)
|
||||
|
||||
GLib.idle_add(callback, temp_path)
|
||||
except Exception as e:
|
||||
print(f"Error downloading favicon: {e}")
|
||||
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
except:
|
||||
pass
|
||||
GLib.idle_add(callback, None)
|
||||
|
||||
GLib.Thread.new("favicon-download", do_download, None)
|
||||
|
||||
class FileChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def on_any_event(self, event):
|
||||
if event.is_directory:
|
||||
return
|
||||
|
||||
for cell in self.app.cells:
|
||||
if cell.content_type == 'file' and cell.content:
|
||||
try:
|
||||
cell_real = os.path.realpath(cell.content)
|
||||
src_real = os.path.realpath(event.src_path)
|
||||
dest_real = os.path.realpath(getattr(event, 'dest_path', ''))
|
||||
if cell_real == src_real or (dest_real and cell_real == dest_real):
|
||||
GLib.idle_add(self.handle_file_event, cell, event)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def handle_file_event(self, cell, event):
|
||||
if event.event_type == 'deleted':
|
||||
cell.clear_cell()
|
||||
self.app.save_state()
|
||||
elif event.event_type == 'moved':
|
||||
if hasattr(event, 'dest_path') and os.path.exists(event.dest_path):
|
||||
cell.content = event.dest_path
|
||||
cell.update_display()
|
||||
self.app.save_state()
|
||||
self.app.add_monitor_for_path(os.path.dirname(event.dest_path))
|
||||
|
||||
class Cell(Gtk.EventBox):
|
||||
def __init__(self, app, content=None, content_type=None):
|
||||
super().__init__(name="pin-cell")
|
||||
self.app = app
|
||||
self.content = content
|
||||
self.content_type = content_type
|
||||
self.box = Box(name="pin-cell-box", orientation="v", spacing=4)
|
||||
self.add(self.box)
|
||||
|
||||
|
||||
self.favicon_temp_path = None
|
||||
|
||||
target_dest = Gtk.TargetEntry.new("text/uri-list", 0, 0)
|
||||
self.drag_dest_set(Gtk.DestDefaults.ALL, [target_dest], Gdk.DragAction.COPY)
|
||||
self.connect("drag-data-received", self.on_drag_data_received)
|
||||
|
||||
targets = [
|
||||
Gtk.TargetEntry.new("text/uri-list", 0, 0),
|
||||
Gtk.TargetEntry.new("text/plain", 0, 1)
|
||||
]
|
||||
self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY)
|
||||
self.connect("drag-data-get", self.on_drag_data_get)
|
||||
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
|
||||
self.connect("drag-begin", self.on_drag_begin)
|
||||
|
||||
self.update_display()
|
||||
|
||||
def update_display(self):
|
||||
|
||||
if self.favicon_temp_path and os.path.exists(self.favicon_temp_path):
|
||||
try:
|
||||
os.remove(self.favicon_temp_path)
|
||||
self.favicon_temp_path = None
|
||||
except Exception as e:
|
||||
print(f"Error removing temp favicon: {e}")
|
||||
|
||||
for child in self.box.get_children():
|
||||
self.box.remove(child)
|
||||
|
||||
if self.content is None:
|
||||
label = Label(name="pin-add", markup=icons.paperclip)
|
||||
self.box.pack_start(label, True, True, 0)
|
||||
else:
|
||||
if self.content_type == 'file':
|
||||
widget = self.get_file_preview(self.content)
|
||||
self.box.pack_start(widget, True, True, 0)
|
||||
label = Label(name="pin-file", label=os.path.basename(self.content), justification="center", ellipsization="middle")
|
||||
self.box.pack_start(label, False, False, 0)
|
||||
elif self.content_type == 'text':
|
||||
if is_url(self.content):
|
||||
|
||||
icon_container = Box(name="pin-icon-container", orientation="v")
|
||||
self.box.pack_start(icon_container, True, True, 0)
|
||||
|
||||
|
||||
url_icon = Label(name="pin-url-icon", markup=icons.world, style=f"font-size: {icon_size}px;")
|
||||
icon_container.pack_start(url_icon, True, True, 0)
|
||||
|
||||
|
||||
domain = re.sub(r'^https?://', '', self.content)
|
||||
domain = domain.split('/')[0]
|
||||
label = Label(name="pin-url", label=domain, justification="center", ellipsization="end")
|
||||
self.box.pack_start(label, False, False, 0)
|
||||
|
||||
|
||||
download_favicon(
|
||||
self.content,
|
||||
lambda path: self.update_favicon(icon_container, url_icon, path)
|
||||
)
|
||||
else:
|
||||
|
||||
label = Label(name="pin-text", label=self.content.split('\n')[0], justification="center", ellipsization="end", line_wrap="word-char")
|
||||
self.box.pack_start(label, True, True, 0)
|
||||
self.box.show_all()
|
||||
if not self.app.loading_state:
|
||||
self.app.save_state()
|
||||
|
||||
def update_favicon(self, container, icon_widget, favicon_path):
|
||||
"""Update the icon with the downloaded favicon or keep the default."""
|
||||
if not favicon_path or not os.path.exists(favicon_path):
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
self.favicon_temp_path = favicon_path
|
||||
|
||||
|
||||
if data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Left", "Right"]:
|
||||
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
favicon_path, width=36, height=36, preserve_aspect_ratio=True)
|
||||
else:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
favicon_path, width=48, height=48, preserve_aspect_ratio=True)
|
||||
|
||||
|
||||
container.remove(icon_widget)
|
||||
|
||||
|
||||
img = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
img.set_name("pin-favicon")
|
||||
container.pack_start(img, True, True, 0)
|
||||
|
||||
|
||||
container.show_all()
|
||||
except Exception as e:
|
||||
print(f"Error setting favicon: {e}")
|
||||
|
||||
|
||||
def get_file_preview(self, filepath):
|
||||
try:
|
||||
file = Gio.File.new_for_path(filepath)
|
||||
info = file.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
|
||||
content_type = info.get_content_type()
|
||||
except Exception:
|
||||
content_type = None
|
||||
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
|
||||
if content_type == "inode/directory":
|
||||
try:
|
||||
pixbuf = icon_theme.load_icon("default-folder", icon_size, 0)
|
||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
except Exception:
|
||||
print("Error loading folder icon")
|
||||
return Gtk.Image.new_from_icon_name("default-folder", Gtk.IconSize.DIALOG)
|
||||
|
||||
if content_type and content_type.startswith("image/"):
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
filepath, width=icon_size, height=icon_size, preserve_aspect_ratio=True)
|
||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
except Exception as e:
|
||||
print("Error loading image preview:", e)
|
||||
|
||||
elif content_type and content_type.startswith("video/"):
|
||||
try:
|
||||
pixbuf = icon_theme.load_icon("video-x-generic", icon_size, 0)
|
||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
except Exception:
|
||||
print("Error loading video icon")
|
||||
return Gtk.Image.new_from_icon_name("video-x-generic", Gtk.IconSize.DIALOG)
|
||||
else:
|
||||
icon_name = "text-x-generic"
|
||||
if content_type:
|
||||
themed_icon = Gio.content_type_get_icon(content_type)
|
||||
if hasattr(themed_icon, 'get_names'):
|
||||
names = themed_icon.get_names()
|
||||
if names:
|
||||
icon_name = names[0]
|
||||
try:
|
||||
pixbuf = icon_theme.load_icon(icon_name, icon_size, 0)
|
||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
except Exception:
|
||||
print("Error loading icon", icon_name)
|
||||
return Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
|
||||
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
||||
if self.content is None and data.get_length() >= 0:
|
||||
uris = data.get_uris()
|
||||
if uris:
|
||||
try:
|
||||
filepath, _ = GLib.filename_from_uri(uris[0])
|
||||
self.content = filepath
|
||||
self.content_type = 'file'
|
||||
self.update_display()
|
||||
except Exception as e:
|
||||
print("Error getting file from URI:", e)
|
||||
drag_context.finish(True, False, time)
|
||||
|
||||
def on_drag_data_get(self, widget, drag_context, data, info, time):
|
||||
if self.content is None:
|
||||
return
|
||||
if info == 0 and self.content_type == 'file':
|
||||
uri = GLib.filename_to_uri(self.content)
|
||||
data.set_uris([uri])
|
||||
elif info == 1 and self.content_type == 'text':
|
||||
data.set_text(self.content, -1)
|
||||
|
||||
def on_drag_begin(self, widget, context):
|
||||
|
||||
if self.content_type == 'file':
|
||||
surface = createSurfaceFromWidget(self)
|
||||
Gtk.drag_set_icon_surface(context, surface)
|
||||
|
||||
def on_button_press(self, widget, event):
|
||||
if self.content is None:
|
||||
if event.button == 1:
|
||||
self.select_file()
|
||||
elif event.button == 2:
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
text = clipboard.wait_for_text()
|
||||
if text:
|
||||
self.content = text
|
||||
self.content_type = 'text'
|
||||
self.update_display()
|
||||
else:
|
||||
if self.content_type == 'file':
|
||||
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
|
||||
open_file(self.content)
|
||||
elif event.button == 3:
|
||||
self.clear_cell()
|
||||
elif self.content_type == 'text':
|
||||
if event.button == 1:
|
||||
|
||||
if is_url(self.content):
|
||||
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(self.content, -1)
|
||||
|
||||
|
||||
if not (event.state & Gdk.ModifierType.CONTROL_MASK):
|
||||
open_url(self.content)
|
||||
else:
|
||||
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(self.content, -1)
|
||||
elif event.button == 3:
|
||||
self.clear_cell()
|
||||
return True
|
||||
|
||||
def select_file(self):
|
||||
dialog = Gtk.FileChooserDialog(
|
||||
title="Select File",
|
||||
parent=self.get_toplevel(),
|
||||
action=Gtk.FileChooserAction.OPEN
|
||||
)
|
||||
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
filepath = dialog.get_filename()
|
||||
self.content = filepath
|
||||
self.content_type = 'file'
|
||||
self.update_display()
|
||||
dialog.destroy()
|
||||
|
||||
def clear_cell(self):
|
||||
|
||||
if self.favicon_temp_path and os.path.exists(self.favicon_temp_path):
|
||||
try:
|
||||
os.remove(self.favicon_temp_path)
|
||||
self.favicon_temp_path = None
|
||||
except Exception as e:
|
||||
print(f"Error removing temp favicon: {e}")
|
||||
|
||||
self.content = None
|
||||
self.content_type = None
|
||||
self.update_display()
|
||||
|
||||
class Pins(Gtk.Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
self.loading_state = True
|
||||
self.monitored_paths = set()
|
||||
self.observer = Observer()
|
||||
self.event_handler = FileChangeHandler(self)
|
||||
|
||||
self.cells = []
|
||||
|
||||
|
||||
grid = Gtk.Grid(row_spacing=8, column_spacing=8, name="pin-grid")
|
||||
grid.set_column_homogeneous(True)
|
||||
grid.set_row_homogeneous(True)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
scrolled_window = ScrolledWindow(child=grid, name="scrolled-window", style_classes="pins", propagate_width=False, propagate_height=False)
|
||||
scrolled_window.set_hexpand(True)
|
||||
scrolled_window.set_vexpand(True)
|
||||
scrolled_window.set_halign(Gtk.Align.FILL)
|
||||
scrolled_window.set_valign(Gtk.Align.FILL)
|
||||
self.pack_start(scrolled_window, True, True, 0)
|
||||
|
||||
|
||||
if data.PANEL_THEME == "Panel" and (data.BAR_POSITION in ["Left", "Right"] or data.PANEL_POSITION in ["Start", "End"]):
|
||||
for row in range(10):
|
||||
for col in range(3):
|
||||
cell = Cell(self)
|
||||
self.cells.append(cell)
|
||||
grid.attach(cell, col, row, 1, 1)
|
||||
else:
|
||||
for row in range(6):
|
||||
for col in range(5):
|
||||
cell = Cell(self)
|
||||
self.cells.append(cell)
|
||||
grid.attach(cell, col, row, 1, 1)
|
||||
|
||||
self.load_state()
|
||||
self.loading_state = False
|
||||
self.start_file_monitoring()
|
||||
|
||||
self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self.connect("drag-data-received", self.on_drag_data_received)
|
||||
|
||||
def start_file_monitoring(self):
|
||||
for cell in self.cells:
|
||||
if cell.content_type == 'file' and cell.content:
|
||||
dir_path = os.path.dirname(cell.content)
|
||||
if os.path.exists(dir_path) and dir_path not in self.monitored_paths:
|
||||
self.observer.schedule(self.event_handler, dir_path, recursive=False)
|
||||
self.monitored_paths.add(dir_path)
|
||||
self.observer.start()
|
||||
|
||||
def add_monitor_for_path(self, path):
|
||||
if path not in self.monitored_paths and os.path.exists(path):
|
||||
self.observer.schedule(self.event_handler, path, recursive=False)
|
||||
self.monitored_paths.add(path)
|
||||
|
||||
def save_state(self):
|
||||
state = []
|
||||
for cell in self.cells:
|
||||
state.append({
|
||||
'content_type': cell.content_type,
|
||||
'content': cell.content
|
||||
})
|
||||
try:
|
||||
with open(SAVE_FILE, 'w') as f:
|
||||
json.dump(state, f)
|
||||
except Exception as e:
|
||||
print("Error saving state:", e)
|
||||
|
||||
def load_state(self):
|
||||
if not os.path.exists(SAVE_FILE):
|
||||
return
|
||||
try:
|
||||
with open(SAVE_FILE, 'r') as f:
|
||||
state = json.load(f)
|
||||
for i, cell_data in enumerate(state):
|
||||
if i < len(self.cells):
|
||||
content = cell_data.get('content')
|
||||
content_type = cell_data.get('content_type')
|
||||
self.cells[i].content = content
|
||||
self.cells[i].content_type = content_type
|
||||
self.cells[i].update_display()
|
||||
except Exception as e:
|
||||
print("Error loading state:", e)
|
||||
|
||||
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
||||
if data.get_length() >= 0:
|
||||
uris = data.get_uris()
|
||||
for uri in uris:
|
||||
try:
|
||||
filepath, _ = GLib.filename_from_uri(uri)
|
||||
for cell in self.cells:
|
||||
if cell.content is None:
|
||||
cell.content = filepath
|
||||
cell.content_type = 'file'
|
||||
cell.update_display()
|
||||
break
|
||||
except Exception as e:
|
||||
print("Error getting file from URI:", e)
|
||||
drag_context.finish(True, False, time)
|
||||
|
||||
def stop_monitoring(self):
|
||||
|
||||
for cell in self.cells:
|
||||
if hasattr(cell, 'favicon_temp_path') and cell.favicon_temp_path and os.path.exists(cell.favicon_temp_path):
|
||||
try:
|
||||
os.remove(cell.favicon_temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
Reference in New Issue
Block a user