diff --git a/.gitignore b/.gitignore index b90e25b..64c3b0b 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,5 @@ dmypy.json # Cython debug symbols cython_debug/ + +/subprojects/blueprint-compiler diff --git a/build-aux/arch/PKGBUILD b/build-aux/arch/PKGBUILD index eda45e2..1d3b959 100644 --- a/build-aux/arch/PKGBUILD +++ b/build-aux/arch/PKGBUILD @@ -1,4 +1,5 @@ # Maintainer: Roshan R Chandar +# TODO: update the PKGBUILD pkgver=1.0 pkgname=PyDrop diff --git a/build-aux/meson/postinstall.py b/build-aux/meson/postinstall.py index 6a3ea97..8df8881 100755 --- a/build-aux/meson/postinstall.py +++ b/build-aux/meson/postinstall.py @@ -3,19 +3,17 @@ from os import environ, path from subprocess import call -prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') -datadir = path.join(prefix, 'share') -destdir = environ.get('DESTDIR', '') +prefix = environ.get("MESON_INSTALL_PREFIX", "/usr/local") +datadir = path.join(prefix, "share") +destdir = environ.get("DESTDIR", "") # Package managers set this so we don't need to run if not destdir: - print('Updating icon cache...') - call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) - - print('Updating desktop database...') - call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) - - print('Compiling GSettings schemas...') - call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')]) + print("Updating icon cache...") + call(["gtk-update-icon-cache", "-qtf", path.join(datadir, "icons", "hicolor")]) + print("Updating desktop database...") + call(["update-desktop-database", "-q", path.join(datadir, "applications")]) + print("Compiling GSettings schemas...") + call(["glib-compile-schemas", path.join(datadir, "glib-2.0", "schemas")]) diff --git a/com.github.Roshan_R.PyDrop.json b/com.github.Roshan_R.PyDrop.json index adfbdd5..817476c 100644 --- a/com.github.Roshan_R.PyDrop.json +++ b/com.github.Roshan_R.PyDrop.json @@ -1,7 +1,7 @@ { "app-id" : "com.github.Roshan_R.PyDrop", "runtime" : "org.gnome.Platform", - "runtime-version" : "40", + "runtime-version" : "48", "sdk" : "org.gnome.Sdk", "command" : "pydrop", "finish-args" : [ diff --git a/data/meson.build b/data/meson.build index f3576d9..1b49e30 100644 --- a/data/meson.build +++ b/data/meson.build @@ -16,6 +16,14 @@ if desktop_utils.found() ) endif +blueprints = custom_target('blueprints', + input: files( + 'ui/window.blp', + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) + gnome = import('gnome') pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) @@ -25,6 +33,7 @@ gnome.compile_resources('pydrop', gresource_bundle: true, install: true, install_dir: pkgdatadir, + dependencies: blueprints, ) appstream_file = i18n.merge_file( @@ -51,4 +60,4 @@ if compile_schemas.found() test('Validate schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()] ) -endif +endif \ No newline at end of file diff --git a/data/ui/window.blp b/data/ui/window.blp new file mode 100644 index 0000000..dbc9d1a --- /dev/null +++ b/data/ui/window.blp @@ -0,0 +1,114 @@ +using Gtk 4.0; +using Adw 1; + +template $PydropWindow : Adw.ApplicationWindow { + title: _("PyDrop"); + // TODO: don't hard-code the values + default-width: 250; + default-height: 250; + width-request: 250; + resizable: false; + + Box droparea { + orientation: vertical; + + Adw.HeaderBar headerbar { + decoration-layout: ":close"; + title-widget: + Adw.WindowTitle { + title: _("PyDrop"); + }; + + MenuButton menubutton { + icon-name: "open-menu-symbolic"; + menu-model: menu; + } + + styles [ + "flat", + ] + } + + Box drag_source { + orientation: vertical; + + Stack stack { + transition-duration: 100; + + Box initial_stack { + orientation: vertical; + margin-bottom: 24; + vexpand: true; + + Image { + pixel-size: 50; + valign: center; + vexpand: true; + icon-name: "go-jump-symbolic"; + } + } + + Box eventbox { + halign: center; + valign: center; + orientation: vertical; + + Image preview_image { + pixel-size: 150; + use-fallback: true; + } + + MenuButton button { + label: "button"; + name: "button"; + halign: center; + margin-bottom: 10; + popover: plain_popover; + } + } + + Box spinner_box { + orientation: vertical; + margin-bottom: 24; + vexpand: true; + + Adw.Spinner spinner { + halign: center; + valign: center; + width-request: 48; + height-request: 48; + vexpand: true; + } + Label{ + label: _("Downloading"); + use-markup: true; + } + } + } + } + + } +} + +Popover plain_popover { + has-arrow: true; + name: "popover"; + height-request: 400; + child: ScrolledWindow{ + propagate-natural-width: true; + min-content-height: 170; + GridView grid_view{ + enable-rubberband: true; + max-columns: 3; + } + }; +} + +menu menu { + section { + item { + label: "About"; + action: "app.about"; + } + } +} diff --git a/data/ui/window.ui b/data/ui/window.ui deleted file mode 100644 index fb69a9f..0000000 --- a/data/ui/window.ui +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - -
- - About - app.about - -
-
-
diff --git a/python3-modules.json b/python3-modules.json index 8d6e163..50ba973 100644 --- a/python3-modules.json +++ b/python3-modules.json @@ -3,20 +3,6 @@ "buildsystem": "simple", "build-commands": [], "modules": [ - { - "name": "python3-python-magic", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"python-magic\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/3a/70/76b185393fecf78f81c12f9dc7b1df814df785f6acb545fc92b016e75a7e/python-magic-0.4.24.tar.gz", - "sha256": "de800df9fb50f8ec5974761054a708af6e4246b03b4bdaee993f948947b0ebcf" - } - ] - }, { "name": "python3-validators", "buildsystem": "simple", @@ -26,75 +12,8 @@ "sources": [ { "type": "file", - "url": "https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz", - "sha256": "72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/33/1a/4e4c12982b093796c1ceaff49cbc5998fb3a7866da755f8e7a1a40b8fda4/validators-0.18.2.tar.gz", - "sha256": "37cd9a9213278538ad09b5b9f9134266e7c226ab1fede1d500e29e0a8fbb9ea6" - } - ] - }, - { - "name": "python3-requests", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"requests\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/94/40/c396b5b212533716949a4d295f91a4c100d51ba95ea9e2d96b6b0517e5a5/urllib3-1.26.5.tar.gz", - "sha256": "a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz", - "sha256": "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/ee/2d/9cdc2b527e127b4c9db64b86647d567985940ac3698eeabc7ffaccb4ea61/chardet-4.0.0.tar.gz", - "sha256": "0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz", - "sha256": "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/6b/47/c14abc08432ab22dc18b9892252efaf005ab44066de871e72a38d6af464b/requests-2.25.1.tar.gz", - "sha256": "27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804" - } - ] - }, - { - "name": "python3-decorator", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"decorator\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz", - "sha256": "72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5" - } - ] - }, - { - "name": "python3-Pillow", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"Pillow\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/21/23/af6bac2a601be6670064a817273d4190b79df6f74d8012926a39bc7aa77f/Pillow-8.2.0.tar.gz", - "sha256": "a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1" + "url": "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", + "sha256": "e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd" } ] } diff --git a/src/dropped_item.py b/src/dropped_item.py new file mode 100644 index 0000000..6fe8899 --- /dev/null +++ b/src/dropped_item.py @@ -0,0 +1,61 @@ +import gi + +gi.require_version("Gtk", "4.0") # noqa +gi.require_version("Gdk", "4.0") # noqa + +from gi.repository import Gio, Gdk, GObject +from .utils.tools import get_paintable_from_gicon, pixbuf_size +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository.Gdk import Texture + + +class DroppedItem(GObject.Object): + def __init__( + self, + file_path: str, + file_name: str, + paintable: None | Gdk.Paintable = None, + mime_type: str | None = None, + ): + self.file_path = file_path + self.file_name = file_name + if paintable: + self.paintable = paintable + else: + if mime_type: + self.mime_type = mime_type + self.generate_paintable_from_mimetype() + else: + self.generate_paintable_from_file_path() + + super().__init__() + + def generate_paintable_from_mimetype(self): + icon = Gio.content_type_get_icon(self.mime_type) + self.paintable = get_paintable_from_gicon(icon) + + def generate_paintable_from_file_path(self): + file = Gio.File.new_for_path(self.file_path) + info = file.query_info( + ",".join( + [ + Gio.FILE_ATTRIBUTE_STANDARD_ICON, + Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + ] + ), + Gio.FileQueryInfoFlags.NONE, + None, + ) + content_type = info.get_content_type() + + # TODO: does not work in case of some files, like psd + # Create a fallback icon + if content_type.split("/")[0] == "image": + pixbuf = Pixbuf.new_from_file_at_scale( + self.file_path, pixbuf_size, pixbuf_size, True + ) + texture = Texture.new_for_pixbuf(pixbuf) + self.paintable = texture.get_current_image() + else: + icon = info.get_attribute_object(Gio.FILE_ATTRIBUTE_STANDARD_ICON) + self.paintable = get_paintable_from_gicon(icon) diff --git a/src/main.py b/src/main.py index 22ad5cc..4bbad99 100644 --- a/src/main.py +++ b/src/main.py @@ -15,26 +15,42 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .window import PydropWindow import sys import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Handy', '1') +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") -from gi.repository import Gtk, Gio, Gdk, GLib, Handy +from gi.repository import Gtk, Gio, Adw, Gdk # noqa -from .window import PydropWindow -class Application(Gtk.Application): - def __init__(self): - super().__init__(application_id='com.github.Roshan_R.PyDrop', - flags=Gio.ApplicationFlags.FLAGS_NONE) +def apply_custom_css(): + css = b""" + gridview { + background-color: @popover_bg_color; + } +""" + provider = Gtk.CssProvider() + provider.load_from_data(css) - css_provider = Gtk.CssProvider() - css_provider.load_from_resource('/com/github/Roshan_R/PyDrop/css/style.css') - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + display = Gdk.Display.get_default() + Gtk.StyleContext.add_provider_for_display( + display, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER + ) - self.setup_actions() + +class Application(Adw.Application): + def __init__(self): + super().__init__( + application_id="com.github.Roshan_R.PyDrop", + flags=Gio.ApplicationFlags.DEFAULT_FLAGS, + resource_base_path="/com/github/Roshan_R/PyDrop/", + ) + apply_custom_css() + action = Gio.SimpleAction(name="about") + action.connect("activate", self.show_about_dialog) + self.add_action(action) def do_activate(self): win = self.props.active_window @@ -42,11 +58,6 @@ def do_activate(self): win = PydropWindow(application=self) win.present() - def setup_actions(self): - action = Gio.SimpleAction(name="about") - action.connect("activate", self.show_about_dialog) - self.add_action(action) - def show_about_dialog(self, action, param): about = Gtk.AboutDialog() about.set_transient_for(self.get_active_window()) @@ -65,6 +76,7 @@ def show_about_dialog(self, action, param): about.set_website("https://github.com/Roshan-R/PyDrop") about.present() + def main(version): app = Application() return app.run(sys.argv) diff --git a/src/meson.build b/src/meson.build index 94e43a4..4626100 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,5 @@ -pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) -moduledir = join_paths(pkgdatadir, 'pydrop') +pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() +moduledir = pkgdatadir / 'pydrop' gnome = import('gnome') subdir('utils') @@ -9,7 +9,7 @@ python = import('python') conf = configuration_data() conf.set('PYTHON', python.find_installation('python3').path()) conf.set('VERSION', meson.project_version()) -conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) +conf.set('localedir', get_option('prefix') / get_option('localedir')) conf.set('pkgdatadir', pkgdatadir) configure_file( @@ -17,7 +17,8 @@ configure_file( output: 'pydrop', configuration: conf, install: true, - install_dir: get_option('bindir') + install_dir: get_option('bindir'), + install_mode: 'r-xr-xr-x' ) pydrop_sources = [ @@ -25,6 +26,7 @@ pydrop_sources = [ 'main.py', 'window.py', 'parsedata.py', + 'dropped_item.py' ] install_data(pydrop_sources, install_dir: moduledir) diff --git a/src/parsedata.py b/src/parsedata.py index f993b81..2898979 100644 --- a/src/parsedata.py +++ b/src/parsedata.py @@ -1,75 +1,136 @@ -(TARGET_OCTECT_STREAM, TARGET_PNG,TARGET_URI_LIST, TARGET_PLAIN) = range(4) - - -import magic from urllib.parse import unquote from .utils import tools +from .dropped_item import DroppedItem import re -from PIL import Image -import io +import gi + +gi.require_version("Soup", "3.0") +from gi.repository import Gdk, GObject, GLib, Soup # noqa google_re = re.compile( - "[http|https]:\/\/www.google.com\/imgres\?imgurl=(.*)\&imgrefurl" + r"[http|https]:\/\/www.google.com\/imgres\?imgurl=(.*)\&imgrefurl" +) + +BASE_DIR = GLib.get_user_cache_dir() + "/pydrop" +chunk_size = 4096 + + +class ParseData(GObject.Object): + def __init__(self, toggle_download_func): + self.toggle_download_func = toggle_download_func + self.soup = Soup.Session() + super().__init__() + + def parse(self, value, dropped_items, count, callback): + self.dropped_items = dropped_items + self.callback = callback + self.count = count + + match value: + case Gdk.FileList(): + self.handle_file_list(value, callback) + case Gdk.MemoryTexture(): + self.handle_memory_texture(value, callback) + case str(): + # TODO: better count handling + self.count += 1 + self.handle_text(value, callback) + case _: + print("Default", type(value), value) + + def handle_file_list(self, file_list: Gdk.FileList, callback): + for file in file_list.get_files(): + # TODO: error handling if file path did not run correctly + file_path = file.get_path() + file_name = file.get_basename() + self.dropped_items.append(DroppedItem(file_path, file_name)) + self.count += 1 + callback(self.count) + + def handle_memory_texture(self, memory_texture, callback): + file_path = tools.generate_file_path(self.count, "png") + memory_texture.save_to_png(file_path) + paintable = memory_texture.get_current_image() + self.dropped_items.append(DroppedItem(file_path, str(self.count), paintable)) + self.count += 1 + callback(self.count) + + def handle_text(self, text, callback): + if tools.is_link(text): + self.handle_link(text) + else: + self.write_to_text_file(text, callback) + + def write_to_text_file(self, text, callback): + first_word = text.split()[0] + file_path = tools.generate_file_path(first_word, "txt") + with open(f"{file_path}", "w+") as f: + f.write(text) + self.dropped_items.append(DroppedItem(file_path, first_word)) + callback(self.count) + + def handle_link(self, link): + self.link = link + x = google_re.findall(self.link) + if x: + self.link = unquote(x[0]) + print("this is a google image : ", self.link) + print("Google image link : ", self.link) + # self.download_image(self.link, link_stack, count, callback) + # TODO: check if this will work + self.download_if_image_else_create_desktop_file() + else: + self.download_if_image_else_create_desktop_file() + + def download_if_image_else_create_desktop_file( + self, + ): + self.message = Soup.Message.new("GET", self.link) + self.message.get_request_headers().append( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ) + self.soup.send_async( + self.message, GLib.PRIORITY_DEFAULT, None, self.on_reponse_headers + ) + + def on_reponse_headers(self, session, task): + # TODO: fail when the response is zero + headers = self.message.get_response_headers() + content_type, _ = headers.get_content_type() + if content_type not in ["image/png", "image/jpeg", "image/jpg"]: + self.handle_normal_link() + return + self.extension = content_type.split("/")[-1] + input_stream = session.send_finish(task) + self.toggle_download_func() + buffer = bytearray() + input_stream.read_bytes_async( + chunk_size, GLib.PRIORITY_DEFAULT, None, self.on_read_callback, buffer + ) + + def on_read_callback(self, input_stream, task, buffer): + data = input_stream.read_bytes_finish(task) + if data.get_size(): + buffer.extend(data.get_data()) + input_stream.read_bytes_async( + chunk_size, GLib.PRIORITY_DEFAULT, None, self.on_read_callback, buffer + ) + else: + file_path = tools.generate_file_path(self.count, self.extension) + with open(file_path, "wb") as f: + f.write(bytes(buffer)) + # TODO: generate paintable from here itself + self.dropped_items.append(DroppedItem(file_path, str(self.count))) + self.toggle_download_func() + self.callback(self.count) -class ParseData: - def parse(self, data, info, link_stack, count): - - if info == TARGET_URI_LIST: - for uri in data.get_uris(): - link_stack.append(uri) - count += 1 - mime = magic.Magic(mime=True) - try: - a = mime.from_file(unquote(uri[7:])) - except IsADirectoryError: - a = "inode/directory" - - # Application/Octect stream : Image from Chromuim browsers - if info == TARGET_OCTECT_STREAM or info == TARGET_PNG: - print("Got Image") - image = Image.open(io.BytesIO(data.get_data())) - format = image.format.lower() - image.save(f"/tmp/pydrop/{count}.{format}") - link_stack.append(f"file:///tmp/pydrop/{count}.{format}") - count += 1 - a = "image" - - - elif info == TARGET_PLAIN: - - text = data.get_text() - print(text) - - if tools.is_link(text): - link = text - x = google_re.findall(link) - if x: - link = unquote(x[0]) - print("this is a google image : ", link) - print("Google image link : ", link) - tools.download_image(link, link_stack, count) - a = "image" - - elif tools.link_is_image(link): - tools.download_image(link. link_stack, count) - a = "image" - else: - # TODO: handle link better, preferably make a file that contains the link? - # investigate on which filetype to use - file_path = f'/tmp/pydrop/{count}.desktop' - with open(file_path, 'w+') as f: - f.write(tools.get_desktop(link)) - link_stack.append(f'file://{file_path}') - a = "text/html" - else: - print("Got text") - file_name = f'{text.split()[0]}.txt' - file_path = f'/tmp/pydrop/{file_name}' - with open(f'{file_path}', 'w+') as f: - f.write(text) - link_stack.append(f'file://{file_path}') - a = "text/plain" - - count += 1 - return count, a + def handle_normal_link(self): + file_path = f"{BASE_DIR}/{self.count}.desktop" + with open(file_path, "w+") as f: + f.write(tools.get_desktop(self.link)) + self.dropped_items.append( + DroppedItem(file_path, str(self.count) + ".desktop") + ) + mime = "text/html" + self.callback(self.count, mime) diff --git a/src/utils/tools.py b/src/utils/tools.py index 5911ac8..cc2c700 100644 --- a/src/utils/tools.py +++ b/src/utils/tools.py @@ -1,85 +1,75 @@ import validators -import requests -import os -from urllib.parse import unquote - import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gio , Gtk -from gi.repository.GdkPixbuf import Pixbuf, PixbufLoader - -def link_is_image(link): - """" - returns True is link is an image - else False - - https://stackoverflow.com/questions/10543940/check-if-a-url-to-an-image-is-up-and-exists-in-python - """ - link = link.strip() - image_formats = ["image/png", "image/jpeg", "image/jpg"] - r = requests.head(link) - if r.headers["content-type"] in image_formats: - return True - return False + +gi.require_version("Gtk", "4.0") # noqa +from gi.repository import Gtk, Gdk, GLib, Gsk # noqa +from gi.repository.Graphene import Point + + +# Instead of hardcoding the value, get it from the image_widget +pixbuf_size = 150 + def is_link(text): return validators.url(text) -def get_thumbnail(filename,size): - """ - returns path to a valid icon file - https://stackoverflow.com/questions/9203251/how-can-i-get-an-icon-or-thumbnail-for-a-specific-file/9212476 - """ - final_filename = "" - if os.path.exists(filename): - file = Gio.File.new_for_path(filename) - info = file.query_info('standard::icon' , 0 , Gio.Cancellable()) - icon = info.get_icon().get_names()[0] - - icon_theme = Gtk.IconTheme.get_default() - icon_file = icon_theme.lookup_icon(icon , size , 0) - if icon_file != None: - final_filename = icon_file.get_filename() - return final_filename +def get_paintable_from_gicon(gicon): + display = Gdk.Display.get_default() + icon_theme = Gtk.IconTheme.get_for_display(display) + paintable = icon_theme.lookup_by_gicon( + gicon, 150, 1, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.NONE + ) + return paintable + def get_desktop(link): return f"[Desktop Entry]\nEncoding=UTF-8\nType=Link\nURL={link}\nIcon=text-html" -pixbuf_size = 80 - -def set_image(link_stack, icon, a): - file_path = unquote(link_stack[-1][7:]) - icon_path = get_thumbnail(file_path, 512) - print(file_path, icon_path, a) - if not icon_path: - print("Did not get themed icon") - if "image" in a: - print("Got image") - try: - pixbuf = Pixbuf.new_from_file_at_scale(file_path, pixbuf_size, pixbuf_size, True) - icon.set_from_pixbuf(pixbuf) - except : - icon.set_from_gicon(Gio.content_type_get_icon(a), 512) - else: - gicon = Gio.content_type_get_icon(a) - icon.set_from_gicon(gicon, 512) - else: - print("Normal") - pixbuf = Pixbuf.new_from_file_at_scale(icon_path, pixbuf_size, pixbuf_size, True) - icon.set_from_pixbuf(pixbuf) - -def download_image(link, link_stack, count): - - # TODO: make the download another thread - - print(link) - print("Starting download...") - r = requests.get(link) - - extension = r.headers["content-type"].split('/')[1] - file_path = f"/tmp/pydrop/{count}.{extension}" - - with open(file_path, "wb") as f: - f.write(r.content) - link_stack.append(f"file://{file_path}") + +def create_overlayed_paintable(dropped_items): + # FIXME: normal images get stretched + width, height = pixbuf_size, pixbuf_size + snapshot = Gtk.Snapshot.new() + shadow = Gsk.Shadow() + shadow.color = Gdk.RGBA(0, 0, 0, 0.4) # semi-transparent black + shadow.dx = 2 + shadow.dy = 2 + shadow.radius = 6 + + items_count = len(dropped_items) + snapshot.push_shadow([shadow]) + + # TODO: document how this variable came to + rotation_iterable = iter(range((items_count - 1) * 2, -1, -2)) + for index, dropped_item in enumerate(dropped_items): + paintable = dropped_item.paintable + width = paintable.get_intrinsic_width() + height = paintable.get_intrinsic_height() + + # Rotate the paintable + rotation_value = -1 * next(rotation_iterable) + snapshot.save() + snapshot.translate(Point().alloc().init(75, 75)) + snapshot.rotate(rotation_value) + snapshot.translate(Point().alloc().init(-75, -75)) + paintable.snapshot(snapshot, width, height) + snapshot.restore() + + # Remove the shadow related thing + snapshot.pop() + return snapshot.to_paintable() + + +def set_image(dropped_items, image_widget): + """ + Sets an appropriate image or icon on the provided image widget based on + the MIME type or file content. + """ + final_paintable = create_overlayed_paintable(dropped_items) + image_widget.set_from_paintable(final_paintable) + + +def generate_file_path(count, extension): + BASE_DIR = GLib.get_user_cache_dir() + "/pydrop" + return f"{BASE_DIR}/{count}.{extension}" diff --git a/src/window.py b/src/window.py index 4a5181d..5bf785f 100644 --- a/src/window.py +++ b/src/window.py @@ -15,113 +15,194 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .dropped_item import DroppedItem +from .utils import tools +from .parsedata import ParseData, BASE_DIR import gi import os +import shutil -gi.require_version('Handy', '1') -from gi.repository import Gtk, Gdk, Gio, Handy -from gi.repository.GdkPixbuf import Pixbuf, PixbufLoader - -from .utils import tools -from .parsedata import ParseData +gi.require_version("Adw", "1") +gi.require_version("Gtk", "4.0") +gi.require_version("Gdk", "4.0") -(TARGET_OCTECT_STREAM, TARGET_PNG,TARGET_URI_LIST, TARGET_PLAIN) = range(4) +from gi.repository import Gtk, Gdk, Adw, GObject, GLib, Gio # noqa @Gtk.Template(resource_path="/com/github/Roshan_R/PyDrop/ui/window.ui") -class PydropWindow(Handy.Window): +class PydropWindow(Adw.ApplicationWindow): __gtype_name__ = "PydropWindow" + Adw.init() - Handy.init() - icon = Gtk.Template.Child() + preview_image = Gtk.Template.Child() droparea = Gtk.Template.Child() button = Gtk.Template.Child() drag_source = Gtk.Template.Child() stack = Gtk.Template.Child() - spinner = Gtk.Template.Child() + spinner_box = Gtk.Template.Child() eventbox = Gtk.Template.Child() - # TODO : make iconview - #iconview = Gtk.Template.Child() initial_stack = Gtk.Template.Child() + grid_view = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) self.setup_variables() self.setup_signals() + self.setup_gridview() def setup_variables(self): - self.button.hide() self.count = 0 - self.link_stack = [] - self.initial = 1 - self.stick() - self.set_keep_above(True) - self.parser = ParseData() + self.dropped_items: list[DroppedItem] = [] + self.initial = True + self.parser = ParseData(self.on_download) - # TODO : better temporary directory? - if not os.path.exists('/tmp/pydrop'): - os.mkdir("/tmp/pydrop") + if os.path.exists(BASE_DIR): + shutil.rmtree(BASE_DIR) + os.makedirs(BASE_DIR, exist_ok=True) def setup_signals(self): - - # Drop Target - enforce_target = [ - Gtk.TargetEntry.new("application/octet-stream", Gtk.TargetFlags(4), TARGET_OCTECT_STREAM), - Gtk.TargetEntry.new("image/png", Gtk.TargetFlags(4), TARGET_PNG), - - Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags(4), TARGET_URI_LIST), - Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags(4), TARGET_PLAIN), - ] - - self.droparea.drag_dest_set( - Gtk.DestDefaults.ALL, enforce_target, Gdk.DragAction.COPY + target = Gtk.DropTarget(actions=Gdk.DragAction.COPY) + target.set_gtypes([Gdk.Texture, Gdk.FileList, GObject.TYPE_STRING]) + target.connect("drop", self.on_drop) + target.connect("accept", self.on_accept) + self.droparea.add_controller(target) + + self.drag_source = Gtk.DragSource() + self.drag_source.connect("prepare", self.on_drag_prepare) + self.drag_source.connect("drag-begin", self.on_drag_begin) + self.drag_source.connect("drag-end", self.on_drag_end) + + event_controller_key = Gtk.EventControllerKey() + event_controller_key.connect("key-released", self.on_key_release) + self.add_controller(event_controller_key) + + def setup_gridview(self): + self.list_store = Gio.ListStore() + # TODO: might have to change this + ss = Gtk.SingleSelection() + ss.set_model(self.list_store) + self.grid_view.set_model(ss) + + factory = Gtk.SignalListItemFactory() + factory.connect("setup", self._on_factory_setup) + factory.connect("bind", self._on_factory_bind) + + self.grid_view.set_factory(factory) + + def _on_factory_setup(self, fact, item): + """ + Gtk.Overlay + ├── Gtk.Box (Vertical) + │ ├── Gtk.Image + │ └── Gtk.Label + └── Gtk.Button (Overlay: top-right, close icon) + """ + box = Gtk.Box() + box.set_orientation(Gtk.Orientation.VERTICAL) + image = Gtk.Image() + image.set_pixel_size(150) + box.append(image) + label = Gtk.Label() + box.append(label) + + button = Gtk.Button.new_from_icon_name("window-close-symbolic") + button.set_halign(Gtk.Align.END) + button.set_valign(Gtk.Align.START) + button.add_css_class("destructive-action") + button.connect("clicked", self._remove_item_on_button_click) + + overlay = Gtk.Overlay() + overlay.set_child(box) + overlay.add_overlay(button) + item.set_child(overlay) + + def _on_factory_bind(self, fact, item): + overlay = item.get_child() + box = overlay.get_child() + image = box.get_first_child() + label = box.get_last_child() + + dropped_item = item.get_item() + image.set_from_paintable(dropped_item.paintable) + label.set_label(dropped_item.file_name) + + # Set the model item on the button for later access + button = overlay.get_last_child() + button.data = dropped_item + + def _remove_item_on_button_click(self, button): + got_it, position = self.list_store.find(button.data) + if not got_it: + raise Exception("Cannot find the element for deletion") + self.list_store.remove(position) + self.dropped_items.remove(button.data) + self._refresh_ui_on_dropped_items_change() + + def _refresh_ui_on_dropped_items_change(self): + # TODO: change this logic to be better + self.count = len(self.dropped_items) + if self.count == 0: + self.stack.set_visible_child(self.initial_stack) + else: + self.stack.set_visible_child(self.eventbox) + + self.button.set_label(f"{self.count} Files") + tools.set_image(self.dropped_items, self.preview_image) + + + def on_download(self): + match self.stack.get_visible_child().get_buildable_id(): + case "initial_stack" | "eventbox": + self.stack.set_visible_child(self.spinner_box) + case "spinner_box": + self.stack.set_visible_child(self.eventbox) + + def on_drop(self, target, value, x, y): + self.parser.parse(value, self.dropped_items, self.count, self.on_parse_complete) + + def on_parse_complete(self, count, mime_type=None): + # Only add controller to Droparea once something is DnD'd to the app. + if self.initial: + self.initial = False + self.eventbox.add_controller(self.drag_source) + self.stack.set_visible_child(self.eventbox) + + self.list_store.remove_all() + for item in self.dropped_items: + self.list_store.append(item) + + self._refresh_ui_on_dropped_items_change() + + def on_accept(self, target, drop): + drag = drop.get_drag() + # Do no accept DnD operations from the app itself + if drag and drag.get_surface() == self.get_surface(): + return False + return True + + def on_drag_prepare(self, source, x, y): + # TODO: just working with files right now + # Took from: https://github.com/mijorus/collector/blob/master/src/window.py + uri_list = "\n".join([f"file://{f.file_path}" for f in self.dropped_items]) + return Gdk.ContentProvider.new_union( + [ + Gdk.ContentProvider.new_for_bytes( + "text/uri-list", GLib.Bytes.new(uri_list.encode()) ) - self.droparea.connect("drag-data-received", self.on_drag_data_received) - - self.connect("key-press-event", self.key_press_event) + ] + ) + def on_drag_begin(self, drag_source, widget): + if self.preview_image.get_gicon(): + paintable = tools.get_paintable_from_gicon(self.preview_image.get_gicon()) + else: + paintable = self.preview_image.get_paintable() + drag_source.set_icon(paintable, 75, 75) - def connect_drag_source(self): - source_targets = [ - Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags(4), TARGET_URI_LIST), - Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags(4), TARGET_PLAIN), - ] - self.eventbox.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, source_targets, Gdk.DragAction.COPY - ) - self.eventbox.connect("drag-begin", self.change_drag_icon) - self.eventbox.connect("drag-data-get", self.on_drag_data_get) - self.initial = 0 - self.button.show() - - def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): - count, a = self.parser.parse(data, info, self.link_stack, self.count) - print(self.link_stack) - self.count = count - tools.set_image(self.link_stack, self.icon, a) - self.stack.set_visible_child(self.eventbox) - self.button.set_label(str(self.count) + " Files") - - if self.initial == 1: - self.connect_drag_source() - - def on_drag_data_get(self, widget, drag_context, data, info, time): - drag_context.connect("dnd-finished", self.finished) - data.set_uris(self.link_stack) - - def finished(self, _a): + def on_drag_end(self, drag_source, drag, delete_data): self.close() - def change_drag_icon(self, widget, data): - self.dropped = 0 - if self.icon.get_pixbuf(): - Gtk.drag_set_icon_pixbuf(data, self.icon.get_pixbuf(), 0, 0) - else: - Gtk.drag_set_icon_gicon(data, self.icon.get_gicon()[0], 0, 0) - if self.initial != 1: - pass - #self.icon.clear() - - def key_press_event(self, _a, event_key): - if event_key.keyval == Gdk.KEY_Escape: + # TODO: document this behaviour somewhere + def on_key_release(self, event_controller_key, keycode, *args): + if keycode == Gdk.KEY_Escape: self.close() diff --git a/subprojects/blueprint-compiler.wrap b/subprojects/blueprint-compiler.wrap new file mode 100644 index 0000000..6e6ee32 --- /dev/null +++ b/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = main +depth = 1 + +[provide] +program_names = blueprint-compiler \ No newline at end of file