ui.py 21 KB

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