ui.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. #####################################################################
  2. # #
  3. # THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
  4. # LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
  5. # FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
  6. # WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
  7. # IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
  8. # OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
  9. # #
  10. # ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
  11. # (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
  12. # YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
  13. # THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
  14. # VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
  15. # WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
  16. # #
  17. # THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
  18. # NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
  19. # THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
  20. # SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
  21. # THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
  22. # #
  23. # THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
  24. # FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
  25. # THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
  26. # TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
  27. # #
  28. #####################################################################
  29. # This file will contain elements used a lot with in the application.
  30. # They are all built upon GTK, so it's implementable without using these
  31. # but it may simplify your modification. Since the elements in will be
  32. # specific to making something like this application easier.
  33. import os
  34. import urllib.request
  35. import threading
  36. import json
  37. from subprocess import *
  38. from gi.repository import Gtk
  39. from gi.repository import Gio
  40. from gi.repository import Gdk
  41. from gi.repository import GLib
  42. from gi.repository import Pango
  43. from gi.repository import GdkPixbuf
  44. from PIL import Image, ImageSequence
  45. def icon( win, name, f="png"):
  46. # This function returns an icon of the current setting, or a
  47. # system default icon if there is no such icon in the set icon theme.
  48. # For this, of course, we will need to store names of what those
  49. # icons are called in the system. Not everthing is the same.
  50. # I could name the icons in the custom theme the same way, but
  51. # this would require using some hilarious names.
  52. system_names = {
  53. "settings":"application-menu",
  54. "connect":"network-connect",
  55. "disconnect":"network-disconnect",
  56. "loading":"accept_time_event",
  57. "launch":"practice-start"
  58. }
  59. try_icon = "icons/"+win.settings["GTK_icon_theme"]+"/"+name+"."+f
  60. if os.path.exists(try_icon):
  61. return Gtk.Image.new_from_file(try_icon)
  62. else:
  63. # Real GTK Spinner for loading ? Why not?
  64. if name == "loading":
  65. s = Gtk.Spinner()
  66. s.set_size_request(64,64)
  67. s.start()
  68. return s
  69. except_icon = Gio.Icon.new_for_string(system_names.get(name, name))
  70. return Gtk.Image.new_from_gicon(except_icon, Gtk.IconSize.DND)
  71. def resize_gif(filename, new_file, size):
  72. # This function will resize a gif
  73. gif = Image.open(filename)
  74. layers = ImageSequence.Iterator(gif)
  75. def rs(l):
  76. for i in l:
  77. rsv = i.copy()
  78. rsv.thumbnail(size, Image.ANTIALIAS)
  79. yield rsv
  80. layers = rs(layers)
  81. # Overwrite the original gif
  82. f = next(layers)
  83. f.info = gif.info
  84. f.save(new_file, save_all=True, append_images=list(layers))
  85. def load(win, calculation_function, render_function, *args, wait=True):
  86. # This function will load widgets that take time to load.
  87. # Due to the peculiarities of the GTK main thread. I need
  88. # to separate the computation and the rendering part of
  89. # the job into two distingt functions.
  90. # One will do all the job to get the file or resolve the url
  91. # or whatever it needs to do, which does not require GTK to
  92. # be done. The second will be the GTK commands. The rendering,
  93. # done with in the GTK main thread.
  94. wbox = Gtk.HBox()
  95. widget = icon(win, "loading", "gif")
  96. wbox.pack_start(widget, True, False, False)
  97. widget.loaddo = True
  98. def resolve_widget_thread(widget, wbox, wf, rf, *args):
  99. calculations = wf(*args)
  100. def gtk_schedule(calculations, rf):
  101. # It seems to be important to edit GTK only
  102. # in the main thread. This will schedule it.
  103. new_widget = rf(calculations)
  104. widget.destroy()
  105. wbox.pack_start(new_widget, True, True, True)
  106. wbox.show_all()
  107. GLib.idle_add(gtk_schedule, calculations, rf)
  108. def load_event(w,e):
  109. if w.loaddo:
  110. load_thread = threading.Thread(target=resolve_widget_thread, args=(widget, wbox, calculation_function, render_function, *args))
  111. load_thread.setDaemon(True)
  112. load_thread.start()
  113. w.loaddo = False
  114. if wait:
  115. widget.connect("draw", load_event)
  116. else:
  117. load_event(widget, False)
  118. return wbox
  119. image_cache = "/tmp/FastLBRY_GTK_image_cashe/"
  120. def image_save_name(url):
  121. save_as = ""
  122. good = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOOPASDFGHJKLZXCVBNM_"
  123. for i in url:
  124. if i in good:
  125. save_as = save_as + i
  126. else:
  127. save_as = save_as + "_"
  128. try:
  129. os.mkdir(image_cache)
  130. except:
  131. pass
  132. save_as = image_cache+save_as
  133. return save_as
  134. def clean_image_cache():
  135. for i in os.listdir(image_cache):
  136. os.remove(image_cache+i)
  137. def net_image_calculation( url, size, save_as=False, allow_gif=False):
  138. ret = ["file", save_as]
  139. print("save_as", save_as)
  140. # This is when we want to load a file
  141. if save_as == "FORCELOAD":
  142. try:
  143. open(url)
  144. save_as = url
  145. except:
  146. save_as = ""
  147. if not save_as:
  148. save_as = image_save_name(url)
  149. # This function will load the image in a separate thread.
  150. try:
  151. open(save_as) # In case it's already been saved
  152. except Exception as e:
  153. print("F", e)
  154. try:
  155. urllib.request.urlretrieve(url, save_as)
  156. except Exception as e:
  157. f = open(save_as, "w")
  158. f.close()
  159. if size:
  160. try:
  161. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as, size, size)
  162. ret = ["pixbuf", pixbuf] #Gtk.Image.new_from_pixbuf(pixbuf)
  163. except Exception as e:
  164. if "image file format" in str(e):
  165. try:
  166. PILImage = Image.open(save_as).convert("RGBA")
  167. PILImage.save(save_as+".png", "png")
  168. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as+".png", size, size)
  169. ret = ["pixbuf", pixbuf]
  170. except:
  171. ret = ["file", save_as]
  172. else:
  173. try:
  174. os.rename(save_as, save_as+".gif")
  175. resize_gif(save_as+".gif", save_as+"_2.gif", [size,size])
  176. ret = ["file", save_as+"_2.gif"] # Gtk.Image.new_from_file(save_as+"_2.gif")
  177. os.remove(save_as+"_2.gif")
  178. os.rename(save_as+".gif", save_as)
  179. except Exception as e:
  180. if allow_gif:
  181. ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
  182. else:
  183. ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
  184. return ret
  185. def net_image_render(calc):
  186. # This will make the image itself.
  187. ret = Gtk.Image()
  188. if calc[0] == "file":
  189. ret = Gtk.Image.new_from_file(calc[1])
  190. elif calc[0] == "pixbuf":
  191. ret = Gtk.Image.new_from_pixbuf(calc[1])
  192. return ret
  193. def search_item(win, data, channel_load=True):
  194. # This will generate a little item for the claim_search
  195. box = Gtk.VBox()
  196. repost = ""
  197. if "reposted_claim" in data:
  198. repost = "Reposted:"
  199. data = data["reposted_claim"]
  200. try:
  201. title = data["value"]["title"]
  202. except:
  203. title = data['name']
  204. def public_resolve(w):
  205. win.url.set_text(data["canonical_url"])
  206. win.url.activate()
  207. namebutton = Gtk.Button()
  208. namebutton.set_tooltip_text(data["canonical_url"])
  209. namebutton.connect("clicked", public_resolve)
  210. namebutton.set_relief(Gtk.ReliefStyle.NONE)
  211. namebutton_box = Gtk.VBox()
  212. namebutton.add(namebutton_box)
  213. if repost:
  214. namebutton_box.pack_start(Gtk.Label(repost), True,False,0)
  215. try:
  216. # Trying to get the thumb
  217. namebutton_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 200 , "", False)
  218. namebutton_thumb.set_size_request(200,200)
  219. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  220. except:
  221. try:
  222. # Trying to get a thumb by referencing the mimetype
  223. namebutton_thumb = icon(win,data["value"]["source"]["media_type"].replace("/", "-"))
  224. namebutton_thumb.set_size_request(200,200)
  225. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  226. except:
  227. namebutton_thumb = icon(win,"none")
  228. namebutton_thumb.set_size_request(200,200)
  229. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  230. try:
  231. title_label = Gtk.Label(title)
  232. title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
  233. title_label.set_line_wrap(True)
  234. title_label.set_max_width_chars(20)
  235. namebutton_box.pack_start(title_label, True,False,0)
  236. except:
  237. pass
  238. box.pack_start(namebutton, False, False, False)
  239. if "signing_channel" in data and (channel_load or repost):
  240. box.pack_start(go_to_channel(win, data["signing_channel"]), False, False, False)
  241. ##### DRAGING IT OUT THE WINDOW ####
  242. def on_drag(widget, drag_context, send, info, time):
  243. # TODO: swap to FastLBRY HTML instance when ready
  244. librarian = data["canonical_url"].replace("lbry://", win.settings["librarian_instance"])
  245. librarian = librarian.replace("#", ":")
  246. send.set_text(librarian, -1)
  247. namebutton.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
  248. namebutton.drag_source_add_text_targets()
  249. namebutton.connect("drag-data-get", on_drag)
  250. return box
  251. def go_to_channel(win, data, resolve=True):
  252. try:
  253. try:
  254. channel_name = data["value"]["title"]
  255. except:
  256. channel_name = data["name"]
  257. try:
  258. channel_url = data["canonical_url"]
  259. except:
  260. channel_url = data["name"]
  261. try:
  262. channel_url = channel_url + "#" + data["claim_id"]
  263. except:
  264. pass
  265. channel_button = Gtk.Button()
  266. if resolve:
  267. def channel_resolve(w):
  268. win.url.set_text(channel_url)
  269. win.url.activate()
  270. try:
  271. channel_button.set_tooltip_text(data["canonical_url"])
  272. except:
  273. channel_button.set_tooltip_text(data["name"])
  274. channel_button.connect("clicked", channel_resolve)
  275. channel_button.set_relief(Gtk.ReliefStyle.NONE)
  276. channel_button_box = Gtk.HBox()
  277. channel_button.add(channel_button_box)
  278. # If channel thumbnail exists.
  279. try:
  280. channel_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 40 , "", False)
  281. channel_button_box.pack_start(channel_thumb, False,False,False)
  282. except:
  283. channel_button_box.pack_start(icon(win, "actor"), False,False,False)
  284. title_label = Gtk.Label(" "+channel_name+" ")
  285. title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
  286. title_label.set_line_wrap(True)
  287. title_label.set_max_width_chars(20)
  288. channel_button_box.pack_start(title_label, False, False, False)
  289. return channel_button
  290. except Exception as e:
  291. print("GO TO CHANNEL:", e)
  292. return Gtk.Label("[anonymous]")
  293. def notify(win, text, subtext="", force=False):
  294. # This function will send a notify send thingy if
  295. # notifications are set to True
  296. enabled = win.settings["notifications"]
  297. if enabled and (( not win.is_active()) or force ):
  298. Popen(["notify-send",
  299. "-i", os.getcwd()+"/icon.png",
  300. "-a", "FastLBRY GTK", text, subtext])
  301. def select_file(was="", filter=[]):
  302. # This is a simple file_chooser_dialog.
  303. dialog = Gtk.FileChooserDialog("Choose a file",
  304. None,
  305. Gtk.FileChooserAction.OPEN,
  306. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  307. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  308. # Filter
  309. if filter:
  310. filter_sup = Gtk.FileFilter()
  311. filter_sup.set_name("Supported files")
  312. for i in filter:
  313. filter_sup.add_pattern(i)
  314. dialog.add_filter(filter_sup)
  315. filter_any = Gtk.FileFilter()
  316. filter_any.set_name("All files")
  317. filter_any.add_pattern("*")
  318. dialog.add_filter(filter_any)
  319. ############### PREVIEW CODE ###################
  320. # TODO:
  321. # Perhaps more mime-types could be added to it
  322. # for example .blender could be previewed using
  323. # blender-thumbnailer.py in each Blender install.
  324. # Videos by Totem.
  325. preview_image= Gtk.Image()
  326. dialog.set_preview_widget(preview_image)
  327. def update_preview(dialog):
  328. path= dialog.get_preview_filename()
  329. try:
  330. pixbuf= GdkPixbuf.Pixbuf.new_from_file(path)
  331. except Exception:
  332. dialog.set_preview_widget_active(False)
  333. else:
  334. maxwidth, maxheight= 300, 700
  335. width, height= pixbuf.get_width(), pixbuf.get_height()
  336. scale= min(maxwidth/width, maxheight/height)
  337. if scale<1:
  338. width, height= int(width*scale), int(height*scale)
  339. pixbuf= pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
  340. preview_image.set_from_pixbuf(pixbuf)
  341. dialog.set_preview_widget_active(True)
  342. dialog.connect('update-preview', update_preview)
  343. response = dialog.run()
  344. if response == Gtk.ResponseType.OK:
  345. ret = dialog.get_filename()
  346. dialog.destroy()
  347. return ret
  348. else:
  349. dialog.destroy()
  350. return was