ui.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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 time
  35. import urllib.request
  36. import threading
  37. import json
  38. from subprocess import *
  39. from gi.repository import Gtk
  40. from gi.repository import Gio
  41. from gi.repository import Gdk
  42. from gi.repository import GLib
  43. from gi.repository import Pango
  44. from gi.repository import GdkPixbuf
  45. from PIL import Image, ImageSequence
  46. def icon( win, name, f="png"):
  47. # This function returns a fitting icon of the current theme,
  48. # or if not available from another theme on the system.
  49. # If a custom icon theme is set, it returns the icon from the corresponding folder.
  50. if Gtk.IconTheme.get_default().has_icon(name) and win.settings["GTK_icon_theme"] == "System Theme":
  51. return Gtk.Image.new_from_icon_name(name, Gtk.IconSize.DND)
  52. else:
  53. # Real GTK Spinner for loading ? Why not?
  54. if name == "loading":
  55. s = Gtk.Spinner()
  56. s.set_size_request(32,32)
  57. s.start()
  58. return s
  59. return Gtk.Image.new_from_file("icons/"+win.settings["GTK_icon_theme"]+"/"+name+"."+f)
  60. def resize_gif(filename, new_file, size):
  61. # This function will resize a gif
  62. gif = Image.open(filename)
  63. layers = ImageSequence.Iterator(gif)
  64. def rs(l):
  65. for i in l:
  66. rsv = i.copy()
  67. rsv.thumbnail(size, Image.ANTIALIAS)
  68. yield rsv
  69. layers = rs(layers)
  70. # Overwrite the original gif
  71. f = next(layers)
  72. f.info = gif.info
  73. f.save(new_file, save_all=True, append_images=list(layers))
  74. def load(win, calculation_function, render_function, *args, wait=True):
  75. # This function will load widgets that take time to load.
  76. # Due to the peculiarities of the GTK main thread. I need
  77. # to separate the computation and the rendering part of
  78. # the job into two distingt functions.
  79. # One will do all the job to get the file or resolve the url
  80. # or whatever it needs to do, which does not require GTK to
  81. # be done. The second will be the GTK commands. The rendering,
  82. # done with in the GTK main thread.
  83. wbox = Gtk.HBox()
  84. widget = icon(win, "loading", "gif")
  85. wbox.pack_start(widget, True, False, False)
  86. widget.loaddo = True
  87. def resolve_widget_thread(widget, wbox, wf, rf, *args):
  88. calculations = wf(*args)
  89. def gtk_schedule(calculations, rf):
  90. # It seems to be important to edit GTK only
  91. # in the main thread. This will schedule it.
  92. new_widget = rf(calculations)
  93. widget.destroy()
  94. wbox.pack_start(new_widget, True, True, True)
  95. wbox.show_all()
  96. GLib.idle_add(gtk_schedule, calculations, rf)
  97. def load_event(w,e):
  98. if w.loaddo:
  99. load_thread = threading.Thread(target=resolve_widget_thread, args=(widget, wbox, calculation_function, render_function, *args))
  100. load_thread.setDaemon(True)
  101. load_thread.start()
  102. w.loaddo = False
  103. if wait:
  104. widget.connect("draw", load_event)
  105. else:
  106. load_event(widget, False)
  107. return wbox
  108. image_cache = "/tmp/FastLBRY_GTK_image_cashe/"
  109. def image_save_name(url):
  110. save_as = ""
  111. good = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOOPASDFGHJKLZXCVBNM_1234567890"
  112. for i in url:
  113. if i in good:
  114. save_as = save_as + i
  115. else:
  116. save_as = save_as + "_"
  117. try:
  118. os.mkdir(image_cache)
  119. except:
  120. pass
  121. save_as = image_cache+save_as
  122. return save_as
  123. def clean_image_cache():
  124. for i in os.listdir(image_cache):
  125. os.remove(image_cache+i)
  126. def net_image_calculation( url, size, save_as=False, allow_gif=False):
  127. ret = ["file", save_as]
  128. # This is when we want to load a file
  129. if save_as == "FORCELOAD":
  130. try:
  131. open(url)
  132. save_as = url
  133. except:
  134. save_as = ""
  135. if not save_as:
  136. save_as = image_save_name(url)
  137. # This function will load the image in a separate thread.
  138. try:
  139. open(save_as) # In case it's already been saved
  140. except Exception as e:
  141. pass
  142. try:
  143. urllib.request.urlretrieve(url, save_as)
  144. except Exception as e:
  145. f = open(save_as, "w")
  146. f.close()
  147. if size:
  148. try:
  149. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as, size, size)
  150. ret = ["pixbuf", pixbuf] #Gtk.Image.new_from_pixbuf(pixbuf)
  151. except Exception as e:
  152. if "image file format" in str(e):
  153. try:
  154. PILImage = Image.open(save_as).convert("RGBA")
  155. PILImage.save(save_as+".png", "png")
  156. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as+".png", size, size)
  157. ret = ["pixbuf", pixbuf]
  158. except:
  159. ret = ["file", save_as]
  160. else:
  161. try:
  162. os.rename(save_as, save_as+".gif")
  163. resize_gif(save_as+".gif", save_as+"_2.gif", [size,size])
  164. ret = ["file", save_as+"_2.gif"] # Gtk.Image.new_from_file(save_as+"_2.gif")
  165. os.remove(save_as+"_2.gif")
  166. os.rename(save_as+".gif", save_as)
  167. except Exception as e:
  168. if allow_gif:
  169. ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
  170. else:
  171. ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
  172. return ret
  173. def net_image_render(calc):
  174. # This will make the image itself.
  175. ret = Gtk.Image()
  176. if calc[0] == "file":
  177. ret = Gtk.Image.new_from_file(calc[1])
  178. elif calc[0] == "pixbuf":
  179. ret = Gtk.Image.new_from_pixbuf(calc[1])
  180. return ret
  181. def search_item(win, data, channel_load=True, upcoming_streams=True):
  182. # This will generate a little item for the claim_search
  183. box = Gtk.VBox()
  184. repost = ""
  185. if "reposted_claim" in data:
  186. repost = "Reposted:"
  187. data = data["reposted_claim"]
  188. try:
  189. title = data["value"]["title"]
  190. except:
  191. title = data['name']
  192. if upcoming_streams and int(data.get("value",[]).get("release_time", 0)) > time.time():
  193. title = "[ UPCOMING "+time.ctime( int(data.get("value",[]).get("release_time", 0)))+" ] \n" + title
  194. try:
  195. link = data["canonical_url"]
  196. except:
  197. link = data["permanent_url"]
  198. def public_resolve(w):
  199. win.url.set_text(link)
  200. win.url.activate()
  201. def new_tab(w, e):
  202. if e.get_button()[1] == 2:
  203. print("MMB")
  204. win.resolve_tab = "new_tab"
  205. win.url.set_text(link)
  206. win.url.activate()
  207. namebutton = Gtk.Button()
  208. namebutton.set_tooltip_text(link)
  209. namebutton.connect("clicked", public_resolve)
  210. namebutton.connect("button-press-event", new_tab)
  211. namebutton.set_relief(Gtk.ReliefStyle.NONE)
  212. namebutton_box = Gtk.VBox()
  213. namebutton.add(namebutton_box)
  214. if repost:
  215. namebutton_box.pack_start(Gtk.Label(repost), True,False,0)
  216. try:
  217. # Trying to get the thumb
  218. namebutton_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 200 , "", False)
  219. namebutton_thumb.set_size_request(200,200)
  220. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  221. except:
  222. try:
  223. # Trying to get a thumb by referencing the mimetype
  224. namebutton_thumb = icon(win,data["value"]["source"]["media_type"].replace("/", "-"))
  225. namebutton_thumb.set_size_request(200,200)
  226. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  227. except:
  228. namebutton_thumb = icon(win,"none")
  229. namebutton_thumb.set_size_request(200,200)
  230. namebutton_box.pack_start(namebutton_thumb, False,False,0)
  231. try:
  232. title_label = Gtk.Label(title)
  233. title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
  234. title_label.set_line_wrap(True)
  235. title_label.set_max_width_chars(20)
  236. namebutton_box.pack_start(title_label, True,False,0)
  237. except:
  238. pass
  239. box.pack_start(namebutton, False, False, False)
  240. if "signing_channel" in data and (channel_load or repost):
  241. box.pack_start(go_to_channel(win, data["signing_channel"]), False, False, False)
  242. ##### DRAGING IT OUT THE WINDOW ####
  243. def on_drag(widget, drag_context, send, info, time):
  244. # TODO: swap to FastLBRY HTML instance when ready
  245. librarian = link.replace("lbry://", win.settings["librarian_instance"])
  246. librarian = librarian.replace("#", ":")
  247. send.set_text(librarian, -1)
  248. namebutton.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
  249. namebutton.drag_source_add_text_targets()
  250. namebutton.connect("drag-data-get", on_drag)
  251. return box
  252. def go_to_channel(win, data, resolve=True):
  253. try:
  254. try:
  255. channel_name = data["value"]["title"]
  256. except:
  257. channel_name = data["name"]
  258. try:
  259. channel_url = data["canonical_url"]
  260. except:
  261. channel_url = data["name"]
  262. try:
  263. channel_url = channel_url + "#" + data["claim_id"]
  264. except:
  265. pass
  266. channel_button = Gtk.Button()
  267. if resolve:
  268. def channel_resolve(w):
  269. win.url.set_text(channel_url)
  270. win.url.activate()
  271. try:
  272. channel_button.set_tooltip_text(data["canonical_url"])
  273. except:
  274. channel_button.set_tooltip_text(data["name"])
  275. channel_button.connect("clicked", channel_resolve)
  276. def new_tab(w, e):
  277. if e.get_button()[1] == 2:
  278. print("MMB")
  279. win.resolve_tab = "new_tab"
  280. win.url.set_text(channel_url)
  281. win.url.activate()
  282. channel_button.connect("button-press-event", new_tab)
  283. channel_button.set_relief(Gtk.ReliefStyle.NONE)
  284. channel_button_box = Gtk.HBox()
  285. channel_button.add(channel_button_box)
  286. # If channel thumbnail exists.
  287. try:
  288. channel_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 40 , "", False)
  289. channel_button_box.pack_start(channel_thumb, False,False,False)
  290. except:
  291. channel_button_box.pack_start(icon(win, "system-users"), False,False,False)
  292. title_label = Gtk.Label(" "+channel_name+" ")
  293. title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
  294. title_label.set_line_wrap(True)
  295. title_label.set_max_width_chars(20)
  296. channel_button_box.pack_start(title_label, False, False, False)
  297. def on_drag(widget, drag_context, send, info, time):
  298. # TODO: swap to FastLBRY HTML instance when ready
  299. librarian = channel_url.replace("lbry://", win.settings["librarian_instance"])
  300. librarian = librarian.replace("#", ":")
  301. send.set_text(librarian, -1)
  302. channel_button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
  303. channel_button.drag_source_add_text_targets()
  304. channel_button.connect("drag-data-get", on_drag)
  305. return channel_button
  306. except Exception as e:
  307. print("GO TO CHANNEL:", e)
  308. return Gtk.Label("[anonymous]")
  309. def notify(win, text, subtext="", force=False):
  310. # This function will send a notify send thingy if
  311. # notifications are set to True
  312. enabled = win.settings["notifications"]
  313. if enabled and (( not win.is_active()) or force ):
  314. Popen(["notify-send",
  315. "-i", os.getcwd()+"/icon.png",
  316. "-a", "FastLBRY GTK", text, subtext])
  317. def select_file(was="", filter=[]):
  318. # This is a simple file_chooser_dialog.
  319. dialog = Gtk.FileChooserDialog("Choose a file",
  320. None,
  321. Gtk.FileChooserAction.OPEN,
  322. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  323. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  324. # Filter
  325. if filter:
  326. filter_sup = Gtk.FileFilter()
  327. filter_sup.set_name("Supported files")
  328. for i in filter:
  329. filter_sup.add_pattern(i)
  330. dialog.add_filter(filter_sup)
  331. filter_any = Gtk.FileFilter()
  332. filter_any.set_name("All files")
  333. filter_any.add_pattern("*")
  334. dialog.add_filter(filter_any)
  335. ############### PREVIEW CODE ###################
  336. # TODO:
  337. # Perhaps more mime-types could be added to it
  338. # for example .blender could be previewed using
  339. # blender-thumbnailer.py in each Blender install.
  340. # Videos by Totem.
  341. preview_image= Gtk.Image()
  342. dialog.set_preview_widget(preview_image)
  343. def update_preview(dialog):
  344. path= dialog.get_preview_filename()
  345. try:
  346. pixbuf= GdkPixbuf.Pixbuf.new_from_file(path)
  347. except Exception:
  348. dialog.set_preview_widget_active(False)
  349. else:
  350. maxwidth, maxheight= 300, 700
  351. width, height= pixbuf.get_width(), pixbuf.get_height()
  352. scale= min(maxwidth/width, maxheight/height)
  353. if scale<1:
  354. width, height= int(width*scale), int(height*scale)
  355. pixbuf= pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
  356. preview_image.set_from_pixbuf(pixbuf)
  357. dialog.set_preview_widget_active(True)
  358. dialog.connect('update-preview', update_preview)
  359. response = dialog.run()
  360. if response == Gtk.ResponseType.OK:
  361. ret = dialog.get_filename()
  362. dialog.destroy()
  363. return ret
  364. else:
  365. dialog.destroy()
  366. return was
  367. def tags_editor(win, data, return_edit_functions=False):
  368. tagscont = Gtk.HBox()
  369. tagscrl = Gtk.ScrolledWindow()
  370. tagscrl.set_size_request(40,40)
  371. tagscont.pack_start(tagscrl, True, True, 0)
  372. tagsbox = Gtk.HBox()
  373. tagscrl.add_with_viewport(tagsbox)
  374. def add_tag(tag):
  375. if not tag:
  376. return
  377. if tag not in data:
  378. data.append(tag)
  379. tagb = Gtk.HBox()
  380. tagb.pack_start(Gtk.Label(" "+tag+" "), False, False, 0)
  381. def kill(w):
  382. tagb.destroy()
  383. data.remove(tag)
  384. tagk = Gtk.Button()
  385. tagk.connect("clicked", kill)
  386. tagk.set_relief(Gtk.ReliefStyle.NONE)
  387. tagk.add(icon(win, "edit-delete"))
  388. tagb.pack_start(tagk, False, False, 0)
  389. tagb.pack_start(Gtk.VSeparator(), False, False, 5)
  390. tagsbox.pack_start(tagb, False, False, 0)
  391. tagsbox.show_all()
  392. # Scroll to the last
  393. def later():
  394. time.sleep(0.1)
  395. def now():
  396. a = tagscrl.get_hadjustment()
  397. a.set_value(a.get_upper())
  398. GLib.idle_add(now)
  399. load_thread = threading.Thread(target=later)
  400. load_thread.start()
  401. # The threading is needed, since we want to wait
  402. # while GTK will update the UI and only then move
  403. # the adjustent. Becuase else, it will move to the
  404. # last previous, not to the last last.
  405. addt = Gtk.Button()
  406. addt.set_relief(Gtk.ReliefStyle.NONE)
  407. addt.add(icon(win, "list-add"))
  408. tagscont.pack_end(addt, False, False, 0)
  409. def on_entry(w):
  410. add_tag(tagentry.get_text())
  411. tagentry.set_text("")
  412. tagentry = Gtk.Entry()
  413. tagentry.connect("activate", on_entry)
  414. addt.connect("clicked", on_entry)
  415. tagscont.pack_end(tagentry, False, False, False)
  416. for tag in data:
  417. add_tag(tag)
  418. if not return_edit_functions:
  419. return tagscont
  420. else:
  421. return tagscont, tagsbox, add_tag
  422. def password_entry(win):
  423. # This function will create a basic entry for passwords
  424. # with a button to reveal text.
  425. box = Gtk.HBox()
  426. entry = Gtk.Entry()
  427. entry.set_visibility(False)
  428. def vis(w, e):
  429. e.set_visibility(w.get_active())
  430. button = Gtk.ToggleButton()
  431. button.set_relief(Gtk.ReliefStyle.NONE)
  432. button.add(icon(win, "video-display"))
  433. button.connect("clicked", vis, entry)
  434. box.pack_start(entry, 1,1,2)
  435. box.pack_end(button, 0,0,1)
  436. return box, entry