FastLBRY-GTK-master/flbry/ui.py
auser e2cdd0e5fc Add commit 6a34355fda files minus lbrynet binary
Add files from 6a34355fda commit of FastLBRY-GTK notabug.org repo
by jyamihud without the flbry/lbrynet binary
2026-09-29 20:07:22 +00:00

649 lines
21 KiB
Python

#####################################################################
# #
# THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
# LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
# FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
# WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
# IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
# OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
# #
# ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
# (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
# YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
# THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
# VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
# WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
# #
# THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
# NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
# THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
# SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
# THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
# #
# THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
# FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
# THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
# TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
# #
#####################################################################
# This file will contain elements used a lot with in the application.
# They are all built upon GTK, so it's implementable without using these
# but it may simplify your modification. Since the elements in will be
# specific to making something like this application easier.
import os
import time
import urllib.request
import threading
import json
from subprocess import *
from gi.repository import Gtk
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Pango
from gi.repository import GdkPixbuf
from PIL import Image, ImageSequence
from flbry import url
def icon( win, name, f="png"):
# This function returns a fitting icon of the current theme,
# or if not available from another theme on the system.
# If a custom icon theme is set, it returns the icon from the corresponding folder.
if Gtk.IconTheme.get_default().has_icon(name) and win.settings["GTK_icon_theme"] == "System Theme":
return Gtk.Image.new_from_icon_name(name, Gtk.IconSize.DND)
else:
# Real GTK Spinner for loading ? Why not?
if name == "loading":
s = Gtk.Spinner()
s.set_size_request(32,32)
s.start()
return s
return Gtk.Image.new_from_file("icons/"+win.settings["GTK_icon_theme"]+"/"+name+"."+f)
def resize_gif(filename, new_file, size):
# This function will resize a gif
gif = Image.open(filename)
layers = ImageSequence.Iterator(gif)
def rs(l):
for i in l:
rsv = i.copy()
rsv.thumbnail(size, Image.ANTIALIAS)
yield rsv
layers = rs(layers)
# Overwrite the original gif
f = next(layers)
f.info = gif.info
f.save(new_file, save_all=True, append_images=list(layers))
def load(win, calculation_function, render_function, *args, wait=True):
# This function will load widgets that take time to load.
# Due to the peculiarities of the GTK main thread. I need
# to separate the computation and the rendering part of
# the job into two distingt functions.
# One will do all the job to get the file or resolve the url
# or whatever it needs to do, which does not require GTK to
# be done. The second will be the GTK commands. The rendering,
# done with in the GTK main thread.
wbox = Gtk.HBox()
widget = icon(win, "loading", "gif")
wbox.pack_start(widget, True, False, False)
widget.loaddo = True
def resolve_widget_thread(widget, wbox, wf, rf, *args):
calculations = wf(*args)
def gtk_schedule(calculations, rf):
# It seems to be important to edit GTK only
# in the main thread. This will schedule it.
new_widget = rf(calculations)
widget.destroy()
wbox.pack_start(new_widget, True, True, True)
wbox.show_all()
GLib.idle_add(gtk_schedule, calculations, rf)
def load_event(w,e):
if w.loaddo:
load_thread = threading.Thread(target=resolve_widget_thread, args=(widget, wbox, calculation_function, render_function, *args))
load_thread.setDaemon(True)
load_thread.start()
w.loaddo = False
if wait:
widget.connect("draw", load_event)
else:
load_event(widget, False)
return wbox
image_cache = "/tmp/FastLBRY_GTK_image_cashe/"
def image_save_name(url):
save_as = ""
good = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOOPASDFGHJKLZXCVBNM_1234567890"
for i in url:
if i in good:
save_as = save_as + i
else:
save_as = save_as + "_"
try:
os.mkdir(image_cache)
except:
pass
save_as = image_cache+save_as
return save_as
def clean_image_cache():
for i in os.listdir(image_cache):
os.remove(image_cache+i)
def net_image_calculation( url, size_width, size_height, save_as=False, allow_gif=False):
ret = ["file", save_as]
# This is when we want to load a file
if save_as == "FORCELOAD":
try:
open(url)
save_as = url
except:
save_as = ""
if not save_as:
save_as = image_save_name(url)
# This function will load the image in a separate thread.
try:
open(save_as) # In case it's already been saved
except Exception as e:
pass
try:
urllib.request.urlretrieve(url, save_as)
except Exception as e:
f = open(save_as, "w")
f.close()
if size_width:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as, size_width, size_height)
ret = ["pixbuf", pixbuf] #Gtk.Image.new_from_pixbuf(pixbuf)
except Exception as e:
if "image file format" in str(e):
try:
PILImage = Image.open(save_as).convert("RGBA")
PILImage.save(save_as+".png", "png")
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(save_as+".png", size_width, size_height)
ret = ["pixbuf", pixbuf]
except:
ret = ["file", save_as]
else:
try:
os.rename(save_as, save_as+".gif")
resize_gif(save_as+".gif", save_as+"_2.gif", [size_width,size_height])
ret = ["file", save_as+"_2.gif"] # Gtk.Image.new_from_file(save_as+"_2.gif")
os.remove(save_as+"_2.gif")
os.rename(save_as+".gif", save_as)
except Exception as e:
if allow_gif:
ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
else:
ret = ["file", save_as] #Gtk.Image.new_from_file(save_as)
return ret
def net_image_render(calc):
# This will make the image itself.
ret = Gtk.Image()
if calc[0] == "file":
ret = Gtk.Image.new_from_file(calc[1])
elif calc[0] == "pixbuf":
ret = Gtk.Image.new_from_pixbuf(calc[1])
return ret
# Converts seconds to time format (H:MM:SS or M:SS)
def seconds_to_time(seconds=0):
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
if h:
return '{:d}:{:02d}:{:02d}'.format(h, m, s)
else:
return '{:d}:{:02d}'.format(m, s)
def search_item(win, data, channel_load=True, upcoming_streams=True):
# This will generate a little item for the claim_search
box = Gtk.VBox()
repost = False
if "reposted_claim" in data:
repost = True
data = data["reposted_claim"]
try:
title = data["value"]["title"]
except:
title = data['name']
if upcoming_streams and int(data.get("value",[]).get("release_time", 0)) > time.time():
title = "[ UPCOMING "+time.ctime( int(data.get("value",[]).get("release_time", 0)))+" ] \n" + title
try:
link = data["canonical_url"]
except:
link = data["permanent_url"]
def public_resolve(w):
win.url.set_text(link)
win.url.activate()
def new_tab(w, e):
if e.get_button()[1] == 2:
print("MMB")
win.resolve_tab = "new_tab"
win.url.set_text(link)
win.url.activate()
namebutton = Gtk.Button()
namebutton.set_tooltip_text(link)
namebutton.connect("clicked", public_resolve)
namebutton.connect("button-press-event", new_tab)
namebutton.set_relief(Gtk.ReliefStyle.NONE)
namebutton_box = Gtk.VBox()
namebutton.add(namebutton_box)
try:
# Trying to get the thumb
namebutton_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 200, 113, "", False)
namebutton_thumb.set_size_request(200, 113)
except:
try:
# Trying to get a thumb by referencing the mimetype
namebutton_thumb = icon(win,data["value"]["source"]["media_type"].replace("/", "-"))
namebutton_thumb.set_size_request(200, 113)
except:
namebutton_thumb = icon(win,"none")
namebutton_thumb.set_size_request(200, 113)
# For overlay info on top of thumbnail image
thumbnail_overlay = Gtk.Overlay()
overlay_box = Gtk.HBox(spacing=5)
thumbnail_overlay.add(namebutton_thumb)
thumbnail_overlay.add_overlay(overlay_box)
thumbnail_overlay.set_overlay_pass_through(overlay_box, True)
overlay_box.set_margin_end(25)
overlay_box.set_margin_bottom(5)
overlay_box.set_halign(Gtk.Align.END)
overlay_box.set_valign(Gtk.Align.END)
# Prepare info for overlay
overlay_items_count = 0
seconds_length = data.get("value", {}) \
.get("video", {}) \
.get("duration", "0")
downloaded_file = url.get_downloaded_file(data.get("claim_id", ""))
# Show time
try:
if seconds_length != "0":
overlay_items_count += 1
time_label = Gtk.Label()
# For some reason setting text color with CssProvider does not
# work when loading thumbnail list for a second time. So we use
# set_markup for text color.
time_label.set_markup("<span color=\"#EFEFEF\" fgalpha=\"70%\">"+seconds_to_time(seconds_length)+"</span>")
overlay_box.pack_start(time_label, False, False, 0)
except:
pass
# Show repost indicator
try:
if repost:
overlay_items_count += 1
repost_icon = Gtk.Image.new_from_file("icons/repost.svg")
repost_icon.set_size_request(16, 16)
overlay_box.pack_start(repost_icon, False, False, 0)
except:
pass
# Show downloaded indicator
try:
if downloaded_file:
overlay_items_count += 1
downloaded_icon = Gtk.Image.new_from_file("icons/downloaded.svg")
downloaded_icon.set_size_request(16, 16)
overlay_box.pack_start(downloaded_icon, False, False, 0)
except:
pass
# Finishing up with thumbnail overlay
thumbnail_overlay.show_all()
namebutton_box.pack_start(thumbnail_overlay, False, False, 0)
if overlay_items_count > 0:
# Add "thumbnail_overlay" class only when there is something inside
# the box. Meaning don't add it when empty because padding set on
# CssProvider makes the empty box show, which is unnecessary.
overlay_box.get_style_context().add_class("thumbnail_overlay")
try:
title_label = Gtk.Label(title)
title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
title_label.set_line_wrap(True)
title_label.set_max_width_chars(20)
title_label.set_xalign(0.0)
title_label.set_margin_top(5)
namebutton_box.pack_start(title_label, True,False,0)
except:
pass
box.pack_start(namebutton, False, False, False)
if "signing_channel" in data and (channel_load or repost):
box.pack_start(go_to_channel(win, data["signing_channel"]), False, False, False)
##### DRAGING IT OUT THE WINDOW ####
def on_drag(widget, drag_context, send, info, time):
# TODO: swap to FastLBRY HTML instance when ready
librarian = link.replace("lbry://", win.settings["librarian_instance"])
librarian = librarian.replace("#", ":")
send.set_text(librarian, -1)
namebutton.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
namebutton.drag_source_add_text_targets()
namebutton.connect("drag-data-get", on_drag)
return box
def go_to_channel(win, data, resolve=True):
try:
try:
channel_name = data["value"]["title"]
except:
channel_name = data["name"]
try:
channel_url = data["canonical_url"]
except:
channel_url = data["name"]
try:
channel_url = channel_url + "#" + data["claim_id"]
except:
pass
channel_button = Gtk.Button()
if resolve:
def channel_resolve(w):
win.url.set_text(channel_url)
win.url.activate()
try:
channel_button.set_tooltip_text(data["canonical_url"])
except:
channel_button.set_tooltip_text(data["name"])
channel_button.connect("clicked", channel_resolve)
def new_tab(w, e):
if e.get_button()[1] == 2:
print("MMB")
win.resolve_tab = "new_tab"
win.url.set_text(channel_url)
win.url.activate()
channel_button.connect("button-press-event", new_tab)
channel_button.set_relief(Gtk.ReliefStyle.NONE)
channel_button_box = Gtk.HBox()
channel_button.add(channel_button_box)
# If channel thumbnail exists.
try:
channel_thumb = load(win, net_image_calculation, net_image_render, data["value"]["thumbnail"]["url"], 40, 40, "", False)
channel_button_box.pack_start(channel_thumb, False,False,False)
except:
channel_button_box.pack_start(icon(win, "system-users"), False,False,False)
title_label = Gtk.Label(" "+channel_name+" ")
title_label.set_line_wrap_mode( Gtk.WrapMode.WORD )
title_label.set_line_wrap(True)
title_label.set_max_width_chars(20)
channel_button_box.pack_start(title_label, False, False, False)
def on_drag(widget, drag_context, send, info, time):
# TODO: swap to FastLBRY HTML instance when ready
librarian = channel_url.replace("lbry://", win.settings["librarian_instance"])
librarian = librarian.replace("#", ":")
send.set_text(librarian, -1)
channel_button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
channel_button.drag_source_add_text_targets()
channel_button.connect("drag-data-get", on_drag)
return channel_button
except Exception as e:
print("GO TO CHANNEL:", e)
return Gtk.Label("[anonymous]")
def notify(win, text, subtext="", force=False):
# This function will send a notify send thingy if
# notifications are set to True
enabled = win.settings["notifications"]
if enabled and (( not win.is_active()) or force ):
Popen(["notify-send",
"-i", os.getcwd()+"/icon.png",
"-a", "FastLBRY GTK", text, subtext])
def select_file(was="", filter=[]):
# This is a simple file_chooser_dialog.
dialog = Gtk.FileChooserDialog("Choose a file",
None,
Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
# Filter
if filter:
filter_sup = Gtk.FileFilter()
filter_sup.set_name("Supported files")
for i in filter:
filter_sup.add_pattern(i)
dialog.add_filter(filter_sup)
filter_any = Gtk.FileFilter()
filter_any.set_name("All files")
filter_any.add_pattern("*")
dialog.add_filter(filter_any)
############### PREVIEW CODE ###################
# TODO:
# Perhaps more mime-types could be added to it
# for example .blender could be previewed using
# blender-thumbnailer.py in each Blender install.
# Videos by Totem.
preview_image= Gtk.Image()
dialog.set_preview_widget(preview_image)
def update_preview(dialog):
path= dialog.get_preview_filename()
try:
pixbuf= GdkPixbuf.Pixbuf.new_from_file(path)
except Exception:
dialog.set_preview_widget_active(False)
else:
maxwidth, maxheight= 300, 700
width, height= pixbuf.get_width(), pixbuf.get_height()
scale= min(maxwidth/width, maxheight/height)
if scale<1:
width, height= int(width*scale), int(height*scale)
pixbuf= pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
preview_image.set_from_pixbuf(pixbuf)
dialog.set_preview_widget_active(True)
dialog.connect('update-preview', update_preview)
response = dialog.run()
if response == Gtk.ResponseType.OK:
ret = dialog.get_filename()
dialog.destroy()
return ret
else:
dialog.destroy()
return was
def tags_editor(win, data, return_edit_functions=False):
tagscont = Gtk.HBox()
tagscrl = Gtk.ScrolledWindow()
tagscrl.set_size_request(40,40)
tagscont.pack_start(tagscrl, True, True, 0)
tagsbox = Gtk.HBox()
tagscrl.add_with_viewport(tagsbox)
def add_tag(tag):
if not tag:
return
if tag not in data:
data.append(tag)
tagb = Gtk.HBox()
tagb.pack_start(Gtk.Label(" "+tag+" "), False, False, 0)
def kill(w):
tagb.destroy()
data.remove(tag)
tagk = Gtk.Button()
tagk.connect("clicked", kill)
tagk.set_relief(Gtk.ReliefStyle.NONE)
tagk.add(icon(win, "edit-delete"))
tagb.pack_start(tagk, False, False, 0)
tagb.pack_start(Gtk.VSeparator(), False, False, 5)
tagsbox.pack_start(tagb, False, False, 0)
tagsbox.show_all()
# Scroll to the last
def later():
time.sleep(0.1)
def now():
a = tagscrl.get_hadjustment()
a.set_value(a.get_upper())
GLib.idle_add(now)
load_thread = threading.Thread(target=later)
load_thread.start()
# The threading is needed, since we want to wait
# while GTK will update the UI and only then move
# the adjustent. Becuase else, it will move to the
# last previous, not to the last last.
addt = Gtk.Button()
addt.set_relief(Gtk.ReliefStyle.NONE)
addt.add(icon(win, "list-add"))
tagscont.pack_end(addt, False, False, 0)
def on_entry(w):
add_tag(tagentry.get_text())
tagentry.set_text("")
tagentry = Gtk.Entry()
tagentry.connect("activate", on_entry)
addt.connect("clicked", on_entry)
tagscont.pack_end(tagentry, False, False, False)
for tag in data:
add_tag(tag)
if not return_edit_functions:
return tagscont
else:
return tagscont, tagsbox, add_tag
def password_entry(win):
# This function will create a basic entry for passwords
# with a button to reveal text.
box = Gtk.HBox()
entry = Gtk.Entry()
entry.set_visibility(False)
def vis(w, e):
e.set_visibility(w.get_active())
button = Gtk.ToggleButton()
button.set_relief(Gtk.ReliefStyle.NONE)
button.add(icon(win, "video-display"))
button.connect("clicked", vis, entry)
box.pack_start(entry, 1,1,2)
box.pack_end(button, 0,0,1)
return box, entry
# Show a simple message box with only OK button
def simple_message_box(primary_text, secondary_text):
dialog = Gtk.MessageDialog(
transient_for=None,
flags=0,
message_type=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.OK,
text=primary_text,
)
dialog.format_secondary_text(secondary_text)
dialog.run()
dialog.destroy()