Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Guido Gunther
Stream
Commits
45e037ce
Verified
Commit
45e037ce
authored
Apr 09, 2021
by
Todd Weaver
Browse files
Adding process for getting strongest instances into fallback array
Adding slider time/viewed/duration Adding player UI opacity
parent
424e544b
Changes
8
Hide whitespace changes
Inline
Side-by-side
src/instances.py
0 → 100644
View file @
45e037ce
# instances.py
#
# Copyright 2021 Todd Weaver
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import
gi
gi
.
require_version
(
'Soup'
,
'2.4'
)
from
gi.repository
import
Soup
import
json
class
Instances
:
def
__init__
(
self
,
**
kwargs
):
self
.
app_window
=
kwargs
.
get
(
'app_window'
,
None
)
def
get_strong_instances
(
self
):
# this processes a series of tests
# 1. check if instance api and search are valid
# 2. check if the instance returns a valid video page result
# 3. check if the instance returns valid video url
# then sets it as a strong instance
# get urls of instances from api.invidious.io
uri
=
f
"https://api.invidious.io/instances.json?sort_by=health"
print
(
"0: "
+
uri
)
session
=
Soup
.
Session
.
new
()
session
.
set_property
(
"timeout"
,
10
)
message
=
Soup
.
Message
.
new
(
"GET"
,
uri
)
session
.
queue_message
(
message
,
self
.
get_strong_instances_cb
,
None
)
def
get_strong_instances_cb
(
self
,
session
,
results
,
user_data
):
if
results
.
status_code
!=
200
:
self
.
app_window
.
show_error_box
(
"Service Failure"
,
"No instances found, cannot complete search."
)
return
False
try
:
instances_json
=
json
.
loads
(
results
.
response_body
.
data
)
except
:
self
.
app_window
.
show_error_box
(
"Service Failure"
,
"Instances are malformed, cannot complete search."
)
return
False
for
instance
in
instances_json
:
instance_uri
=
instance
[
1
][
'uri'
].
rstrip
(
'/'
)
# remove non http gettable urls
if
(
not
instance_uri
.
endswith
(
'.onion'
)
and
not
instance_uri
.
endswith
(
'.i2p'
)):
self
.
check_query_api_valid
(
instance
[
1
][
'uri'
])
# check the instance can run a query on the API
def
check_query_api_valid
(
self
,
uri
):
# /api/v1/search?q=query
# /api/v1/search?q=Librem%205;fields=type
search_uri
=
f
"
{
uri
}
/api/v1/search?q=Librem%205;fields=type"
print
(
"1: "
+
search_uri
)
session
=
Soup
.
Session
.
new
()
session
.
set_property
(
"timeout"
,
2
)
message
=
Soup
.
Message
.
new
(
"GET"
,
search_uri
)
session
.
queue_message
(
message
,
self
.
check_query_valid_cb
,
uri
)
def
check_query_valid_cb
(
self
,
session
,
results
,
uri
):
if
results
.
status_code
!=
200
:
return
False
try
:
instance_json
=
json
.
loads
(
results
.
response_body
.
data
)
except
:
return
False
# only need to check if 'type' appears
for
r
in
instance_json
:
if
'type'
in
r
:
self
.
check_video_api_valid
(
uri
)
break
# check the instances return a valid API
def
check_video_api_valid
(
self
,
uri
):
# api urls to confirm are strong (some throw forbidden)
# /api/v1/videos/{videoId}
# /api/v1/videos/cAUNrY_qPCg?fields=type
fs_uri
=
f
"
{
uri
}
/api/v1/videos/cAUNrY_qPCg?fields=formatStreams"
print
(
"2: "
+
fs_uri
)
session
=
Soup
.
Session
.
new
()
session
.
set_property
(
"timeout"
,
2
)
message
=
Soup
.
Message
.
new
(
"GET"
,
fs_uri
)
session
.
queue_message
(
message
,
self
.
check_video_api_valid_cb
,
uri
)
def
check_video_api_valid_cb
(
self
,
session
,
results
,
uri
):
if
results
.
status_code
!=
200
:
return
False
try
:
instance_json
=
json
.
loads
(
results
.
response_body
.
data
)
except
:
return
False
# verify json has key 'formatStreams' (not 'error')
if
'formatStreams'
in
instance_json
:
fs
=
instance_json
[
'formatStreams'
][
0
]
if
fs
[
'type'
].
startswith
(
'video/mp4'
):
confirm_video
=
fs
[
'url'
]
self
.
check_video_valid
(
uri
,
confirm_video
)
def
check_video_valid
(
self
,
uri
,
confirm_video
):
session
=
Soup
.
Session
.
new
()
session
.
set_property
(
"timeout"
,
2
)
message
=
Soup
.
Message
.
new
(
"HEAD"
,
confirm_video
)
session
.
queue_message
(
message
,
self
.
check_video_valid_cb
,
uri
)
def
check_video_valid_cb
(
self
,
session
,
results
,
uri
):
print
(
results
.
status_code
)
if
results
.
status_code
!=
200
:
return
False
self
.
app_window
.
strong_instances
.
append
(
uri
)
src/meson.build
View file @
45e037ce
...
...
@@ -31,6 +31,7 @@ stream_sources = [
'window.py'
,
'results.py'
,
'search.py'
,
'instances.py'
,
]
install_data
(
stream_sources
,
install_dir
:
moduledir
)
src/results.py
View file @
45e037ce
...
...
@@ -41,6 +41,8 @@ class ResultsBox(Gtk.Box):
play
=
Gtk
.
Template
.
Child
()
pause
=
Gtk
.
Template
.
Child
()
slider
=
Gtk
.
Template
.
Child
()
time_viewed
=
Gtk
.
Template
.
Child
()
time_remaining
=
Gtk
.
Template
.
Child
()
audio_dl
=
Gtk
.
Template
.
Child
()
audio_dl_image
=
Gtk
.
Template
.
Child
()
...
...
@@ -74,6 +76,9 @@ class ResultsBox(Gtk.Box):
self
.
video_box_width
=
int
(
size
.
width
-
self
.
window_to_player_box_padding
)
self
.
video_box_height
=
int
(
self
.
video_box_width
/
1.77
)
self
.
player_box
.
set_size_request
(
self
.
video_box_width
,
self
.
video_box_height
)
self
.
poster_image
.
set_size_request
(
self
.
video_box_width
,
self
.
video_box_height
)
# init gstreamer player
self
.
player
=
Gst
.
ElementFactory
.
make
(
"playbin"
,
"player"
)
self
.
sink
=
Gst
.
ElementFactory
.
make
(
"gtksink"
)
...
...
@@ -83,13 +88,14 @@ class ResultsBox(Gtk.Box):
self
.
player_box
.
add
(
self
.
video_widget
)
def
get_
duration
(
self
,
seconds
):
def
get_
readable_seconds
(
self
,
seconds
):
m
,
s
=
divmod
(
seconds
,
60
)
h
,
m
=
divmod
(
m
,
60
)
if
seconds
>=
3600
:
self
.
video_duration
=
f
"
{
h
:
d
}
:
{
m
:
02
d
}
:
{
s
:
02
d
}
"
readable_seconds
=
f
"
{
h
:
d
}
:
{
m
:
02
d
}
:
{
s
:
02
d
}
"
else
:
self
.
video_duration
=
f
"
{
m
:
d
}
:
{
s
:
02
d
}
"
readable_seconds
=
f
"
{
m
:
d
}
:
{
s
:
02
d
}
"
return
readable_seconds
def
on_file_read
(
self
,
poster_file
,
async_res
,
user_data
):
stream
=
poster_file
.
read_finish
(
async_res
)
...
...
@@ -105,6 +111,7 @@ class ResultsBox(Gtk.Box):
self
.
poster_image
.
clear
()
self
.
poster_image
.
set_from_pixbuf
(
pixbuf
)
self
.
poster_image
.
set_visible
(
True
)
def
stream_at_scale_async
(
self
,
poster_file
):
stream
=
poster_file
.
read_async
(
GLib
.
PRIORITY_DEFAULT
,
None
,
...
...
@@ -112,13 +119,14 @@ class ResultsBox(Gtk.Box):
def
parse_video_results
(
self
,
session
,
result
,
message
):
if
message
.
status_code
!=
200
:
print
(
"video json details did not respond"
)
# remove unplayable video urls from list
self
.
set_visible
(
False
)
return
False
try
:
self
.
json
=
json
.
loads
(
message
.
response_body
.
data
)
except
:
print
(
"video json did not parse."
)
self
.
set_visible
(
False
)
return
False
self
.
video_uri
=
None
...
...
@@ -133,7 +141,6 @@ class ResultsBox(Gtk.Box):
# self.video_uri = format_stream['url']
if
not
self
.
video_uri
:
# remove unplayable video urls from list
self
.
set_visible
(
False
)
return
False
...
...
@@ -159,11 +166,11 @@ class ResultsBox(Gtk.Box):
self
.
video_title
=
video_meta
[
'title'
]
self
.
video_channel
=
video_meta
[
'author'
]
self
.
poster_uri
=
video_meta
[
'poster_uri'
]
self
.
get_duration
(
video_meta
[
'lengthSeconds'
])
video_duration
=
self
.
get_readable_seconds
(
video_meta
[
'lengthSeconds'
])
self
.
title
.
set_label
(
self
.
video_title
)
self
.
channel
.
set_label
(
self
.
video_channel
)
self
.
duration
.
set_label
(
self
.
video_duration
)
self
.
duration
.
set_label
(
video_duration
)
self
.
get_video_details
()
...
...
@@ -171,16 +178,27 @@ class ResultsBox(Gtk.Box):
if
not
self
.
is_playing
:
return
False
else
:
success
,
self
.
duration
=
self
.
player
.
query_duration
(
Gst
.
Format
.
TIME
)
success
,
duration
=
self
.
player
.
query_duration
(
Gst
.
Format
.
TIME
)
# GtkScale is set to 100%, calculate duration and position steps
self
.
percent
=
100
/
(
self
.
duration
/
Gst
.
SECOND
)
self
.
percent
=
100
/
(
duration
/
Gst
.
SECOND
)
# get current position (nanoseconds)
success
,
position
=
self
.
player
.
query_position
(
Gst
.
Format
.
TIME
)
position_value
=
float
(
position
)
/
Gst
.
SECOND
*
self
.
percent
if
int
(
duration
)
>
0
:
viewed_seconds
=
int
(
position
/
Gst
.
SECOND
)
remaining_seconds
=
int
((
duration
-
position
)
/
Gst
.
SECOND
)
viewed
=
self
.
get_readable_seconds
(
viewed_seconds
)
remaining
=
self
.
get_readable_seconds
(
remaining_seconds
)
print
(
"Viewed: "
+
str
(
viewed
))
print
(
"Remaining: -"
+
str
(
remaining
))
self
.
time_viewed
.
set_label
(
viewed
)
self
.
time_remaining
.
set_label
(
f
"-
{
remaining
}
"
)
# is negative number when not successful, so put it to 0
if
not
success
:
position_value
=
0
...
...
@@ -227,7 +245,10 @@ class ResultsBox(Gtk.Box):
dest_title
=
self
.
strictify_name
(
self
.
video_title
)
dest_path
=
f
"
{
dest_dir
}
/
{
dest_title
}
.
{
dest_ext
}
"
dest
=
Gio
.
File
.
new_for_path
(
dest_path
)
dl_stream
.
copy_async
(
dest
,
Gio
.
FileCopyFlags
.
OVERWRITE
,
flags
=
Gio
.
FileCopyFlags
dl_stream
.
copy_async
(
dest
,
# bitwise or (not tuple) for multiple flags
Gio
.
FileCopyFlags
.
OVERWRITE
|
Gio
.
FileCopyFlags
.
ALL_METADATA
|
Gio
.
FileCopyFlags
.
TARGET_DEFAULT_PERMS
,
GLib
.
PRIORITY_DEFAULT
,
None
,
self
.
progress_video_cb
,
(),
self
.
ready_video_cb
,
None
)
...
...
@@ -264,7 +285,7 @@ class ResultsBox(Gtk.Box):
src
.
copy_finish
(
async_res
)
except
GLib
.
Error
as
e
:
self
.
show_error_icon
(
'audio'
)
print
(
"Failed to write file stream
%s
"
,
e
.
message
)
print
(
"Failed to write file stream "
,
e
.
message
)
return
False
self
.
show_success_icon
(
'audio'
)
...
...
@@ -273,7 +294,7 @@ class ResultsBox(Gtk.Box):
src
.
copy_finish
(
async_res
)
except
GLib
.
Error
as
e
:
self
.
show_error_icon
(
'video'
)
print
(
"Failed to write file stream
%s
"
,
e
.
message
)
print
(
"Failed to write file stream "
,
e
.
message
)
return
False
self
.
show_success_icon
(
'video'
)
...
...
@@ -413,11 +434,8 @@ class ResultsBox(Gtk.Box):
# "container": "mp4",
# "qualityLabel": "720p"
video_quality
=
"720p"
last_bitrate
=
None
self
.
audio_dl_uri
=
None
self
.
video_dl_uri
=
None
for
af
in
self
.
json
[
'adaptiveFormats'
]:
if
af
[
'type'
].
startswith
(
'audio/mp4'
):
if
not
self
.
audio_dl_uri
:
...
...
@@ -428,18 +446,21 @@ class ResultsBox(Gtk.Box):
last_bitrate
=
af
[
'bitrate'
]
self
.
audio_dl_uri
=
af
[
'url'
]
elif
af
[
'type'
].
startswith
(
'video/mp4'
):
video_quality
=
"720p"
self
.
video_dl_uri
=
None
for
fs
in
self
.
json
[
'formatStreams'
]:
if
fs
[
'type'
].
startswith
(
'video/mp4'
):
# set it to something
if
not
self
.
video_dl_uri
:
self
.
video_dl_uri
=
a
f
[
'url'
]
if
'qualityLabel'
in
a
f
:
if
a
f
[
'qualityLabel'
]
==
"720p"
and
video_quality
==
"720p"
:
self
.
video_dl_uri
=
a
f
[
'url'
]
elif
a
f
[
'qualityLabel'
]
==
"480p"
and
video_quality
==
"480p"
:
self
.
video_dl_uri
=
a
f
[
'url'
]
elif
a
f
[
'qualityLabel'
]
==
"1080p"
and
video_quality
==
"1080p"
:
self
.
video_dl_uri
=
a
f
[
'url'
]
self
.
video_dl_uri
=
f
s
[
'url'
]
if
'qualityLabel'
in
f
s
:
if
f
s
[
'qualityLabel'
]
==
"720p"
and
video_quality
==
"720p"
:
self
.
video_dl_uri
=
f
s
[
'url'
]
elif
f
s
[
'qualityLabel'
]
==
"480p"
and
video_quality
==
"480p"
:
self
.
video_dl_uri
=
f
s
[
'url'
]
elif
f
s
[
'qualityLabel'
]
==
"1080p"
and
video_quality
==
"1080p"
:
self
.
video_dl_uri
=
f
s
[
'url'
]
if
self
.
audio_dl_uri
:
self
.
audio_dl
.
set_sensitive
(
True
)
...
...
src/search.py
View file @
45e037ce
...
...
@@ -28,58 +28,37 @@ class Search:
def
__init__
(
self
,
**
kwargs
):
self
.
app_window
=
kwargs
.
get
(
'app_window'
,
None
)
self
.
query
=
kwargs
.
get
(
'query'
,
None
)
self
.
do_search
()
def
get_strong_instance
(
self
):
# lookup instances, get a strong one, to be done (stubbed for now)
# get urls from api.invidious.io
# https://api.invidious.io/instances.json?sort_by=health
# api urls to confirm are strong (some throw forbidden)
# api/v1/videos/{videoId}
# api/v1/search/query
#self.instance = "https://ytprivate.com"
#self.instance = "https://vid.puffyan.us"
#self.instance = "https://iteroni.com/"
self
.
instance
=
"https://invidious.xyz"
def
clear_entries
(
self
):
children
=
self
.
app_window
.
results_list
.
get_children
()
for
child
in
children
:
child
.
destroy
()
def
do_search
(
self
):
self
.
get_strong_instance
()
def
do_search
(
self
,
query
):
esc_query
=
GLib
.
uri_escape_string
(
query
,
None
,
None
)
uri
=
f
"
{
self
.
app_window
.
strong_instances
[
0
]
}
/api/v1/search?q=
{
esc_query
}
;fields=title,videoId,author,lengthSeconds,videoThumbnails"
query
=
GLib
.
uri_escape_string
(
self
.
query
,
None
,
None
)
uri
=
f
"
{
self
.
instance
}
/api/v1/search?q=
{
query
}
;fields=title,videoId,author,lengthSeconds,videoThumbnails"
print
(
uri
)
self
.
session
=
Soup
.
Session
.
new
()
self
.
session
.
set_property
(
"timeout"
,
5
)
message
=
Soup
.
Message
.
new
(
"GET"
,
uri
)
self
.
session
.
queue_message
(
message
,
self
.
show_results
,
message
)
def
show_error_box
(
self
,
heading
,
text
):
self
.
app_window
.
error_box
.
set_visible
(
True
)
self
.
app_window
.
error_heading
.
set_label
(
heading
)
self
.
app_window
.
error_text
.
set_label
(
text
)
def
show_results
(
self
,
session
,
result
,
message
):
self
.
clear_entries
()
self
.
app_window
.
spinner
.
set_visible
(
False
)
if
message
.
status_code
!=
200
:
self
.
show_error_box
(
"Service Failure"
,
self
.
app_window
.
show_error_box
(
"Service Failure"
,
"There is no response from the streaming servers."
)
return
False
try
:
self
.
json
=
json
.
loads
(
message
.
response_body
.
data
)
except
:
self
.
show_error_box
(
"Service Failure"
,
self
.
app_window
.
show_error_box
(
"Service Failure"
,
"The streaming server response failed to parse results."
)
print
(
"json did not load from url results"
)
return
False
self
.
get_poster_url
()
...
...
@@ -96,7 +75,7 @@ class Search:
# tweak json with local poster url
for
video_meta
in
self
.
json
:
# append the strong instance for results to use
video_meta
[
'strong_instance'
]
=
self
.
instance
video_meta
[
'strong_instance'
]
=
self
.
app_window
.
strong_
instance
s
[
0
]
for
poster
in
video_meta
[
'videoThumbnails'
]:
if
poster
[
'quality'
]
==
'medium'
:
video_meta
[
'poster_uri'
]
=
poster
[
'url'
]
src/ui/results.ui
View file @
45e037ce
...
...
@@ -33,7 +33,6 @@
<object
class=
"GtkImage"
id=
"poster_image"
>
<property
name=
"width-request"
>
332
</property>
<property
name=
"height-request"
>
186
</property>
<property
name=
"visible"
>
True
</property>
<property
name=
"sensitive"
>
False
</property>
<property
name=
"can-focus"
>
False
</property>
<property
name=
"halign"
>
center
</property>
...
...
@@ -41,7 +40,7 @@
<property
name=
"resource"
>
/sm/puri/Stream/ui/video-placeholder-332x186.png
</property>
</object>
<packing>
<property
name=
"expand"
>
Fals
e
</property>
<property
name=
"expand"
>
Tru
e
</property>
<property
name=
"fill"
>
True
</property>
<property
name=
"position"
>
0
</property>
</packing>
...
...
@@ -94,6 +93,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Save Audio
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"audio_dl_button"
swapped=
"no"
/>
...
...
@@ -121,6 +121,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Save Video
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"video_dl_button"
swapped=
"no"
/>
...
...
@@ -164,6 +165,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Play
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"play_button"
swapped=
"no"
/>
...
...
@@ -190,6 +192,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Pause
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"pause_button"
swapped=
"no"
/>
...
...
@@ -237,6 +240,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Adjust Speed
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"speed_button"
swapped=
"no"
/>
...
...
@@ -272,6 +276,7 @@
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Fullscreen
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"fullscreen_button"
swapped=
"no"
/>
...
...
@@ -292,10 +297,10 @@
</child>
<child>
<object
class=
"GtkButton"
id=
"unfullscreen"
>
<property
name=
"visible"
>
True
</property>
<property
name=
"can-focus"
>
True
</property>
<property
name=
"receives-default"
>
True
</property>
<property
name=
"tooltip-text"
translatable=
"yes"
>
Unfullscreen
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
<signal
name=
"clicked"
handler=
"unfullscreen_button"
swapped=
"no"
/>
...
...
@@ -337,19 +342,78 @@
</packing>
</child>
<child>
<object
class=
"Gtk
Scale"
id=
"slider
"
>
<object
class=
"Gtk
Box
"
>
<property
name=
"visible"
>
True
</property>
<property
name=
"sensitive"
>
False
</property>
<property
name=
"can-focus"
>
True
</property>
<property
name=
"margin-top"
>
6
</property>
<property
name=
"margin-bottom"
>
6
</property>
<property
name=
"adjustment"
>
slider-duration
</property>
<property
name=
"show-fill-level"
>
True
</property>
<property
name=
"round-digits"
>
1
</property>
<property
name=
"draw-value"
>
False
</property>
<property
name=
"has-origin"
>
False
</property>
<signal
name=
"scroll-event"
handler=
"swallow_slider_scroll_event"
swapped=
"no"
/>
<signal
name=
"value-changed"
handler=
"seek_slider"
swapped=
"no"
/>
<property
name=
"can-focus"
>
False
</property>
<child>
<object
class=
"GtkLabel"
id=
"time_viewed"
>
<property
name=
"width-request"
>
24
</property>
<property
name=
"visible"
>
True
</property>
<property
name=
"can-focus"
>
False
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
end
</property>
<property
name=
"valign"
>
center
</property>
<property
name=
"margin-start"
>
6
</property>
<property
name=
"label"
translatable=
"yes"
>
...
</property>
<attributes>
<attribute
name=
"scale"
value=
"0.80000000000000004"
/>
</attributes>
<style>
<class
name=
"overlay-text"
/>
</style>
</object>
<packing>
<property
name=
"expand"
>
False
</property>
<property
name=
"fill"
>
True
</property>
<property
name=
"position"
>
0
</property>
</packing>
</child>
<child>
<object
class=
"GtkScale"
id=
"slider"
>
<property
name=
"visible"
>
True
</property>
<property
name=
"sensitive"
>
False
</property>
<property
name=
"can-focus"
>
True
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"margin-top"
>
6
</property>
<property
name=
"margin-bottom"
>
6
</property>
<property
name=
"hexpand"
>
True
</property>
<property
name=
"adjustment"
>
slider-duration
</property>
<property
name=
"show-fill-level"
>
True
</property>
<property
name=
"round-digits"
>
1
</property>
<property
name=
"draw-value"
>
False
</property>
<property
name=
"has-origin"
>
False
</property>
<signal
name=
"scroll-event"
handler=
"swallow_slider_scroll_event"
swapped=
"no"
/>
<signal
name=
"value-changed"
handler=
"seek_slider"
swapped=
"no"
/>
</object>
<packing>
<property
name=
"expand"
>
False
</property>
<property
name=
"fill"
>
True
</property>
<property
name=
"position"
>
1
</property>
</packing>
</child>
<child>
<object
class=
"GtkLabel"
id=
"time_remaining"
>
<property
name=
"width-request"
>
24
</property>
<property
name=
"visible"
>
True
</property>
<property
name=
"can-focus"
>
False
</property>
<property
name=
"opacity"
>
0.80
</property>
<property
name=
"halign"
>
start
</property>
<property
name=
"valign"
>
center
</property>
<property
name=
"margin-end"
>
6
</property>
<property
name=
"label"
translatable=
"yes"
>
...
</property>
<attributes>
<attribute
name=
"scale"
value=
"0.80000000000000004"
/>
</attributes>
<style>
<class
name=
"overlay-text"
/>
</style>
</object>
<packing>
<property
name=
"expand"
>
False
</property>
<property
name=
"fill"
>
True
</property>
<property
name=
"position"
>
2
</property>
</packing>
</child>
</object>
<packing>
<property
name=
"expand"
>
False
</property>
...
...
src/ui/stream.css
View file @
45e037ce
...
...
@@ -17,12 +17,22 @@
background-color
:
black
;
}
button
.rounded-button
{
padding
:
4px
;
.overlay-text
{
padding-top
:
2px
;
padding-left
:
6px
;
padding-right
:
6px
;
padding-bottom
:
2px
;
background-color
:
white
;
border-radius
:
9999px
;
-gtk-outline-radius
:
9999px
;
}
button
.rounded-button
{
padding
:
4px
;
min-width
:
24px
;
min-height
:
24px
;
border-radius
:
9999px
;
-gtk-outline-radius
:
9999px
;
}
button
.large
{
...
...
src/ui/window.ui
View file @
45e037ce
...
...
@@ -160,7 +160,7 @@
</child>
<child>
<object
class=
"GtkBox"
id=
"error_box"
>
<property
name=
"visible"
>
Tru
e
</property>
<property
name=
"visible"
>
Fals
e
</property>
<property
name=
"can-focus"
>
False
</property>
<property
name=
"halign"
>
center
</property>
<property
name=
"valign"
>
center
</property>
...
...
@@ -183,7 +183,7 @@
<property
name=
"visible"
>
True
</property>
<property
name=
"can-focus"
>
False
</property>
<property
name=
"margin-top"
>
12
</property>
<property
name=
"label"
translatable=
"yes"
>
...
</property>
<property
name=
"label"
translatable=
"yes"
>
Error
</property>
<attributes>
<attribute
name=
"weight"
value=
"bold"
/>
<attribute
name=
"scale"
value=
"1.2"
/>
...
...
src/window.py
View file @
45e037ce
...
...
@@ -23,7 +23,10 @@ from gi.repository import Gdk, Gtk, Handy
Handy
.
init
()
import
json
from
.search
import
Search
from
.instances
import
Instances
@
Gtk
.
Template
(
resource_path
=
'/sm/puri/Stream/ui/window.ui'
)
class
StreamWindow
(
Handy