[preview] Add audio widget
This commit is contained in:
committed by
wurstsalat
parent
774a9b06a5
commit
fc46ceaa1a
@@ -56,6 +56,7 @@ MIME_TYPES = (
|
|||||||
'audio/opus',
|
'audio/opus',
|
||||||
'audio/wav',
|
'audio/wav',
|
||||||
'audio/x-flac',
|
'audio/x-flac',
|
||||||
|
'audio/x-m4a',
|
||||||
'audio/x-matroska',
|
'audio/x-matroska',
|
||||||
# font/*
|
# font/*
|
||||||
'font/ttf',
|
'font/ttf',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated with glade 3.22.2 -->
|
<!-- Generated with glade 3.36.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.20"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<object class="GtkBox" id="preview_box">
|
<object class="GtkBox" id="preview_box">
|
||||||
@@ -156,7 +156,8 @@
|
|||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">1</property>
|
<property name="pack_type">end</property>
|
||||||
|
<property name="position">2</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ from gi.repository import GdkPixbuf
|
|||||||
from gi.repository import Gio
|
from gi.repository import Gio
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from gi.repository import Soup
|
from gi.repository import Soup
|
||||||
|
try:
|
||||||
|
from gi.repository import Gst
|
||||||
|
from gi.repository import GstPbutils
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
from gajim.common import app
|
from gajim.common import app
|
||||||
from gajim.common import configpaths
|
from gajim.common import configpaths
|
||||||
@@ -94,11 +100,19 @@ def get_previewable_mime_types():
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def change_cursor(widget, event):
|
||||||
|
if event.type == Gdk.EventType.ENTER_NOTIFY:
|
||||||
|
widget.get_window().set_cursor(get_cursor('default'))
|
||||||
|
else:
|
||||||
|
widget.get_window().set_cursor(get_cursor('text'))
|
||||||
|
|
||||||
|
|
||||||
PREVIEWABLE_MIME_TYPES = get_previewable_mime_types()
|
PREVIEWABLE_MIME_TYPES = get_previewable_mime_types()
|
||||||
mime_types = set(MIME_TYPES)
|
mime_types = set(MIME_TYPES)
|
||||||
# Merge both: if it’s a previewable image, it should be allowed
|
# Merge both: if it’s a previewable image, it should be allowed
|
||||||
ALLOWED_MIME_TYPES = mime_types.union(PREVIEWABLE_MIME_TYPES)
|
ALLOWED_MIME_TYPES = mime_types.union(PREVIEWABLE_MIME_TYPES)
|
||||||
|
|
||||||
|
|
||||||
class UrlImagePreviewPlugin(GajimPlugin):
|
class UrlImagePreviewPlugin(GajimPlugin):
|
||||||
def init(self):
|
def init(self):
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
@@ -434,8 +448,9 @@ class UrlImagePreviewPlugin(GajimPlugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if file_size == 0 or file_size > int(self.config['MAX_FILE_SIZE']):
|
if file_size == 0 or file_size > int(self.config['MAX_FILE_SIZE']):
|
||||||
log.info('File size (%s) too big or unknown (zero) for URL: \'%s\'',
|
log.info(
|
||||||
file_size, uri)
|
'File size (%s) too big or unknown (zero) for URL: \'%s\'',
|
||||||
|
file_size, uri)
|
||||||
if not force:
|
if not force:
|
||||||
session.cancel_message(message, Soup.Status.CANCELLED)
|
session.cancel_message(message, Soup.Status.CANCELLED)
|
||||||
|
|
||||||
@@ -486,10 +501,10 @@ class UrlImagePreviewPlugin(GajimPlugin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pixbuf = pixbuf_from_data(preview.thumbnail)
|
pixbuf = pixbuf_from_data(preview.thumbnail)
|
||||||
except Exception as error:
|
except Exception as err:
|
||||||
log.error('Unable to load: %s, %s',
|
log.error('Unable to load: %s, %s',
|
||||||
preview.thumb_path.name,
|
preview.thumb_path.name,
|
||||||
error)
|
err)
|
||||||
return
|
return
|
||||||
self._update_textview(preview, pixbuf)
|
self._update_textview(preview, pixbuf)
|
||||||
|
|
||||||
@@ -541,28 +556,22 @@ class UrlImagePreviewPlugin(GajimPlugin):
|
|||||||
def _on_realize(box):
|
def _on_realize(box):
|
||||||
box.get_window().set_cursor(get_cursor('pointer'))
|
box.get_window().set_cursor(get_cursor('pointer'))
|
||||||
|
|
||||||
def _on_enter_leave(button, event):
|
|
||||||
if event.type == Gdk.EventType.ENTER_NOTIFY:
|
|
||||||
button.get_window().set_cursor(get_cursor('default'))
|
|
||||||
else:
|
|
||||||
button.get_window().set_cursor(get_cursor('text'))
|
|
||||||
|
|
||||||
path = self.local_file_path('preview.ui')
|
path = self.local_file_path('preview.ui')
|
||||||
ui = get_builder(path)
|
ui = get_builder(path)
|
||||||
|
|
||||||
ui.download_button.set_no_show_all(True)
|
ui.download_button.set_no_show_all(True)
|
||||||
ui.download_button.connect('enter-notify-event', _on_enter_leave)
|
ui.download_button.connect('enter-notify-event', change_cursor)
|
||||||
ui.download_button.connect('leave-notify-event', _on_enter_leave)
|
ui.download_button.connect('leave-notify-event', change_cursor)
|
||||||
ui.download_button.connect('clicked', self._on_download, preview)
|
ui.download_button.connect('clicked', self._on_download, preview)
|
||||||
|
|
||||||
ui.save_as_button.set_no_show_all(True)
|
ui.save_as_button.set_no_show_all(True)
|
||||||
ui.save_as_button.connect('enter-notify-event', _on_enter_leave)
|
ui.save_as_button.connect('enter-notify-event', change_cursor)
|
||||||
ui.save_as_button.connect('leave-notify-event', _on_enter_leave)
|
ui.save_as_button.connect('leave-notify-event', change_cursor)
|
||||||
ui.save_as_button.connect('clicked', self._on_save_as, preview)
|
ui.save_as_button.connect('clicked', self._on_save_as, preview)
|
||||||
|
|
||||||
ui.open_folder_button.set_no_show_all(True)
|
ui.open_folder_button.set_no_show_all(True)
|
||||||
ui.open_folder_button.connect('enter-notify-event', _on_enter_leave)
|
ui.open_folder_button.connect('enter-notify-event', change_cursor)
|
||||||
ui.open_folder_button.connect('leave-notify-event', _on_enter_leave)
|
ui.open_folder_button.connect('leave-notify-event', change_cursor)
|
||||||
ui.open_folder_button.connect('clicked', self._on_open_folder, preview)
|
ui.open_folder_button.connect('clicked', self._on_open_folder, preview)
|
||||||
|
|
||||||
ui.event_box.set_tooltip_text(preview.filename)
|
ui.event_box.set_tooltip_text(preview.filename)
|
||||||
@@ -595,6 +604,10 @@ class UrlImagePreviewPlugin(GajimPlugin):
|
|||||||
|
|
||||||
if preview.orig_exists():
|
if preview.orig_exists():
|
||||||
ui.download_button.hide()
|
ui.download_button.hide()
|
||||||
|
if (preview.is_audio and app.is_installed('GST') and
|
||||||
|
self._contains_audio_streams(preview.orig_path)):
|
||||||
|
audio_widget = AudioWidget(preview.orig_path)
|
||||||
|
ui.preview_box.pack_start(audio_widget, True, True, 0)
|
||||||
else:
|
else:
|
||||||
ui.save_as_button.hide()
|
ui.save_as_button.hide()
|
||||||
ui.open_folder_button.hide()
|
ui.open_folder_button.hide()
|
||||||
@@ -722,6 +735,17 @@ class UrlImagePreviewPlugin(GajimPlugin):
|
|||||||
menu = self._get_context_menu(preview)
|
menu = self._get_context_menu(preview)
|
||||||
menu.popup_at_pointer(event)
|
menu.popup_at_pointer(event)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _contains_audio_streams(file_path):
|
||||||
|
# Check if it is really an audio file
|
||||||
|
discoverer = GstPbutils.Discoverer()
|
||||||
|
info = discoverer.discover_uri(f'file://{str(file_path)}')
|
||||||
|
has_audio = bool(info.get_audio_streams())
|
||||||
|
if not has_audio:
|
||||||
|
log.warning('File does not contain audio stream: %s',
|
||||||
|
str(file_path))
|
||||||
|
return has_audio
|
||||||
|
|
||||||
|
|
||||||
class Preview:
|
class Preview:
|
||||||
def __init__(self, uri, urlparts, orig_path, thumb_path,
|
def __init__(self, uri, urlparts, orig_path, thumb_path,
|
||||||
@@ -758,6 +782,11 @@ class Preview:
|
|||||||
def is_previewable(self):
|
def is_previewable(self):
|
||||||
return self.mime_type in PREVIEWABLE_MIME_TYPES
|
return self.mime_type in PREVIEWABLE_MIME_TYPES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_audio(self):
|
||||||
|
is_allowed = bool(self.mime_type in ALLOWED_MIME_TYPES)
|
||||||
|
return is_allowed and self.mime_type.startswith('audio/')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uri(self):
|
def uri(self):
|
||||||
return self._uri
|
return self._uri
|
||||||
@@ -792,3 +821,126 @@ class Preview:
|
|||||||
log.warning('Creating thumbnail failed for: %s', self.orig_path)
|
log.warning('Creating thumbnail failed for: %s', self.orig_path)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AudioWidget(Gtk.Box):
|
||||||
|
def __init__(self, file_path):
|
||||||
|
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL,
|
||||||
|
spacing=6)
|
||||||
|
self._playbin = None
|
||||||
|
self._query = None
|
||||||
|
self._has_timeout = False
|
||||||
|
|
||||||
|
self._build_audio_widget()
|
||||||
|
self._setup_audio_player(file_path)
|
||||||
|
|
||||||
|
def _build_audio_widget(self):
|
||||||
|
play_button = Gtk.Button()
|
||||||
|
play_button.get_style_context().add_class('flat')
|
||||||
|
play_button.get_style_context().add_class('preview-button')
|
||||||
|
play_button.set_tooltip_text(_('Start/stop playback'))
|
||||||
|
self._play_icon = Gtk.Image.new_from_icon_name(
|
||||||
|
'media-playback-start-symbolic',
|
||||||
|
Gtk.IconSize.BUTTON)
|
||||||
|
play_button.add(self._play_icon)
|
||||||
|
self._seek_bar = Gtk.Scale(
|
||||||
|
orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
self._seek_bar.set_range(0.0, 1.0)
|
||||||
|
self._seek_bar.set_hexpand(True)
|
||||||
|
self._seek_bar.set_value_pos(Gtk.PositionType.RIGHT)
|
||||||
|
self._seek_bar.connect('enter-notify-event', change_cursor)
|
||||||
|
self._seek_bar.connect('leave-notify-event', change_cursor)
|
||||||
|
self._seek_bar.connect('change-value', self._on_seek)
|
||||||
|
self._seek_bar.connect(
|
||||||
|
'format-value', self._format_audio_timestamp)
|
||||||
|
play_button.connect('enter-notify-event', change_cursor)
|
||||||
|
play_button.connect('leave-notify-event', change_cursor)
|
||||||
|
play_button.connect('clicked', self._on_play_clicked)
|
||||||
|
|
||||||
|
self.add(play_button)
|
||||||
|
self.add(self._seek_bar)
|
||||||
|
self.connect('destroy', self._on_destroy)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def _setup_audio_player(self, file_path):
|
||||||
|
self._playbin = Gst.ElementFactory.make('playbin', 'bin')
|
||||||
|
if self._playbin is None:
|
||||||
|
return
|
||||||
|
self._playbin.set_property(
|
||||||
|
'uri', f'file://{str(file_path)}')
|
||||||
|
state_return = self._playbin.set_state(Gst.State.PAUSED)
|
||||||
|
if state_return == Gst.StateChangeReturn.FAILURE:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._query = Gst.Query.new_position(Gst.Format.TIME)
|
||||||
|
bus = self._playbin.get_bus()
|
||||||
|
bus.add_signal_watch()
|
||||||
|
bus.connect('message', self._on_bus_message)
|
||||||
|
|
||||||
|
def _on_bus_message(self, _bus, message):
|
||||||
|
if message.type == Gst.MessageType.EOS:
|
||||||
|
self._set_pause(True)
|
||||||
|
self._playbin.seek_simple(
|
||||||
|
Gst.Format.TIME, Gst.SeekFlags.FLUSH, 0)
|
||||||
|
elif message.type == Gst.MessageType.STATE_CHANGED:
|
||||||
|
_success, duration = self._playbin.query_duration(
|
||||||
|
Gst.Format.TIME)
|
||||||
|
if duration > 0:
|
||||||
|
self._seek_bar.set_range(0.0, duration)
|
||||||
|
|
||||||
|
is_paused = self._get_paused()
|
||||||
|
if (duration > 0 and not is_paused and
|
||||||
|
not self._has_timeout):
|
||||||
|
GLib.timeout_add(500, self._update_seek_bar)
|
||||||
|
self._has_timeout = True
|
||||||
|
|
||||||
|
def _on_seek(self, _range, _scroll, value):
|
||||||
|
self._playbin.seek_simple(
|
||||||
|
Gst.Format.TIME, Gst.SeekFlags.FLUSH, value)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_play_clicked(self, _button):
|
||||||
|
self._set_pause(not self._get_paused())
|
||||||
|
|
||||||
|
def _on_destroy(self, _widget):
|
||||||
|
self._playbin.set_state(Gst.State.NULL)
|
||||||
|
|
||||||
|
def _get_paused(self):
|
||||||
|
_, state, _ = self._playbin.get_state(20)
|
||||||
|
return state == Gst.State.PAUSED
|
||||||
|
|
||||||
|
def _set_pause(self, paused):
|
||||||
|
if paused:
|
||||||
|
self._playbin.set_state(Gst.State.PAUSED)
|
||||||
|
self._play_icon.set_from_icon_name(
|
||||||
|
'media-playback-start-symbolic',
|
||||||
|
Gtk.IconSize.BUTTON)
|
||||||
|
else:
|
||||||
|
self._playbin.set_state(Gst.State.PLAYING)
|
||||||
|
self._play_icon.set_from_icon_name(
|
||||||
|
'media-playback-pause-symbolic',
|
||||||
|
Gtk.IconSize.BUTTON)
|
||||||
|
|
||||||
|
def _update_seek_bar(self):
|
||||||
|
if self._get_paused():
|
||||||
|
self._has_timeout = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._playbin.query(self._query):
|
||||||
|
_fmt, cur_pos = self._query.parse_position()
|
||||||
|
self._seek_bar.set_value(cur_pos)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_audio_timestamp(_widget, ns):
|
||||||
|
seconds = ns / 1000000000
|
||||||
|
minutes = seconds / 60
|
||||||
|
hours = minutes / 60
|
||||||
|
|
||||||
|
i_seconds = int(seconds)
|
||||||
|
i_minutes = int(minutes)
|
||||||
|
i_hours = int(hours)
|
||||||
|
|
||||||
|
if i_hours > 0:
|
||||||
|
return f'{i_hours:d}:{i_minutes:02d}:{i_seconds:02d}'
|
||||||
|
return f'{i_minutes:d}:{i_seconds:02d}'
|
||||||
|
|||||||
Reference in New Issue
Block a user