Commit a6511c1e authored by Harish Fulara's avatar Harish Fulara Committed by Alexandru Băluț

greeter: Add project thumbnails

parent 5d6392fb
Pipeline #20368 failed with stage
in 41 minutes and 18 seconds
......@@ -21,7 +21,38 @@
<object class="GtkBox" id="project_info_vbox">
<object class="GtkBox">
<property name="name">project_thumbnail_box</property>
<property name="width_request">96</property>
<property name="height_request">54</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_right">12</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<object class="GtkImage" id="project_thumbnail">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="vexpand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
......@@ -70,7 +101,9 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="margin_right">200</property>
<property name="vexpand">True</property>
<property name="expand">False</property>
......@@ -82,7 +115,7 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">2</property>
......@@ -151,7 +151,7 @@ class Pitivi(Gtk.Application, Loggable):
"new-project-loading", self._newProjectLoadingCb)
"new-project-loaded", self._newProjectLoaded)
self.project_manager.connect("project-closed", self._projectClosed)
self.project_manager.connect_after("project-closed", self._projectClosed)
self.project_manager.connect("project-saved", self.__project_saved_cb)
......@@ -29,6 +29,7 @@ from gi.repository import Gtk
from pitivi.configure import get_ui_dir
from pitivi.dialogs.browseprojects import BrowseProjectsDialog
from pitivi.perspective import Perspective
from pitivi.project import Project
from pitivi.utils.ui import beautify_last_updated_timestamp
from pitivi.utils.ui import beautify_project_path
from pitivi.utils.ui import fix_infobar
......@@ -57,6 +58,7 @@ class ProjectInfoRow(Gtk.ListBoxRow):
# show it during projects removal screen.
......@@ -252,7 +252,20 @@ class AssetThumbnail(Loggable):
return small_thumb, large_thumb
def get_thumbnails_from_xdg_cache(real_uri):
def get_asset_thumbnails_path(real_uri):
"""Gets normal & large thumbnail path for the asset in the XDG cache.
List[str]: The path of normal thumbnail and large thumbnail.
quoted_uri = quote_uri(real_uri)
thumbnail_hash = md5(quoted_uri.encode()).hexdigest()
thumb_dir = os.path.join(GLib.get_user_cache_dir(), "thumbnails")
return os.path.join(thumb_dir, "normal", thumbnail_hash + ".png"),\
os.path.join(thumb_dir, "large", thumbnail_hash + ".png")
def get_thumbnails_from_xdg_cache(cls, real_uri):
"""Gets pixbufs for the specified thumbnail from the user's cache dir.
Looks for thumbnails according to the [Thumbnail Managing Standard](
......@@ -264,10 +277,7 @@ class AssetThumbnail(Loggable):
List[GdkPixbuf.Pixbuf]: The small thumbnail and the large thumbnail,
if available in the user's cache directory, otherwise (None, None).
quoted_uri = quote_uri(real_uri)
thumbnail_hash = md5(quoted_uri.encode()).hexdigest()
thumb_dir = os.path.join(GLib.get_user_cache_dir(), "thumbnails")
path_128 = os.path.join(thumb_dir, "normal", thumbnail_hash + ".png")
path_128, path_256 = cls.get_asset_thumbnails_path(real_uri)
interpolation = GdkPixbuf.InterpType.BILINEAR
# The cache dirs might have resolutions of 256 and/or 128,
......@@ -280,7 +290,6 @@ class AssetThumbnail(Loggable):
return small_thumb, large_thumb
except GLib.GError:
# path_128 doesn't exist, try the 256 version.
path_256 = os.path.join(thumb_dir, "large", thumbnail_hash + ".png")
thumb_256 = GdkPixbuf.Pixbuf.new_from_file(path_256)
w, h = thumb_256.get_width(), thumb_256.get_height()
......@@ -649,8 +658,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
rows = [, path)
for path in paths]
with"remove asset from media library",
with"assets-removal", toplevel=True):
for row in rows:
asset = model[row.get_path()][COL_ASSET]
target = asset.get_proxy_target()
......@@ -21,11 +21,14 @@
import datetime
import os
import pwd
import shutil
import tarfile
import time
import uuid
from gettext import gettext as _
from hashlib import md5
from gi.repository import GdkPixbuf
from gi.repository import GES
from gi.repository import GLib
from gi.repository import GObject
......@@ -35,9 +38,12 @@ from gi.repository import GstVideo
from gi.repository import Gtk
from pitivi.configure import get_ui_dir
from pitivi.medialibrary import AssetThumbnail
from pitivi.preset import AudioPresetManager
from pitivi.preset import VideoPresetManager
from pitivi.render import Encoders
from pitivi.settings import get_dir
from pitivi.settings import xdg_cache_home
from pitivi.undo.project import AssetAddedIntention
from pitivi.undo.project import AssetProxiedIntention
from pitivi.utils.loggable import Loggable
......@@ -45,6 +51,7 @@ from pitivi.utils.misc import fixate_caps_with_default_values
from pitivi.utils.misc import isWritable
from pitivi.utils.misc import path_from_uri
from pitivi.utils.misc import quote_uri
from pitivi.utils.misc import scale_pixbuf
from pitivi.utils.misc import unicode_error_dialog
from pitivi.utils.pipeline import Pipeline
from pitivi.utils.ripple_update_group import RippleUpdateGroup
......@@ -80,6 +87,11 @@ for i in range(2, GLib.MAXINT):
# a project.
IGNORED_PROPS = ["name", "parent"]
class ProjectManager(GObject.Object, Loggable):
"""The project manager.
......@@ -477,6 +489,7 @@ class ProjectManager(GObject.Object, Loggable):
project = self.current_project
self.current_project = None
self.emit("project-closed", project)
# We should never choke on silly stuff like disconnecting signals
# that were already disconnected. It blocks the UI for nothing.
......@@ -783,6 +796,103 @@ class Project(Loggable, GES.Project):
self.set_meta("author", author)
def get_thumb_path(uri, resolution):
"""Returns path of thumbnail of specified resolution in the cache."""
thumb_hash = md5(quote_uri(uri).encode()).hexdigest()
thumbs_cache_dir = get_dir(os.path.join(xdg_cache_home(),
"project_thumbs", resolution))
return os.path.join(thumbs_cache_dir, thumb_hash) + ".png"
def get_thumb(cls, uri):
"""Gets the project thumb, if exists, else the default thumb or None."""
thumb = GdkPixbuf.Pixbuf.new_from_file(cls.get_thumb_path(uri, SCALED_THUMB_DIR))
except GLib.Error:
# Try to get the default thumb.
thumb = Gtk.IconTheme.get_default().load_icon("video-x-generic", 128, 0)
except GLib.Error:
return None
thumb = scale_pixbuf(thumb, SCALED_THUMB_WIDTH, SCALED_THUMB_HEIGHT)
return thumb
def __create_scaled_thumb(self):
"""Creates scaled thumbnail from the original thumbnail."""
thumb = GdkPixbuf.Pixbuf.new_from_file(self.get_thumb_path(self.uri, ORIGINAL_THUMB_DIR))
thumb = scale_pixbuf(thumb, SCALED_THUMB_WIDTH, SCALED_THUMB_HEIGHT)
thumb.savev(self.get_thumb_path(self.uri, SCALED_THUMB_DIR), "png", [], [])
except GLib.Error as e:
self.warning("Failed to create scaled project thumbnail: %s", e)
def __remove_thumbs(self):
"""Removes existing project thumbnails."""
os.remove(self.get_thumb_path(self.uri, thumb_dir))
except FileNotFoundError:
def create_thumb(self):
"""Creates project thumbnails."""
thumb_path = self.get_thumb_path(self.uri, ORIGINAL_THUMB_DIR)
if os.path.exists(thumb_path) and not
# The project thumbnail already exists and the assets are the same.
# Project Thumbnail Generation Approach: Out of thumbnails of all
# the assets in the current project, the one with maximum file size
# will be our project thumbnail -
assets_uri = [ for asset in self.listSources()]
normal_thumb_path = None
large_thumb_path = None
normal_thumb_size = 0
large_thumb_size = 0
n_normal_thumbs = 0
n_large_thumbs = 0
for uri in assets_uri:
path_128, path_256 = AssetThumbnail.get_asset_thumbnails_path(uri)
thumb_size = os.stat(path_128).st_size
if thumb_size > normal_thumb_size:
normal_thumb_path = path_128
normal_thumb_size = thumb_size
n_normal_thumbs += 1
except FileNotFoundError:
# The asset is missing the normal thumbnail.
thumb_size = os.stat(path_256).st_size
if thumb_size > large_thumb_size:
large_thumb_path = path_256
large_thumb_size = thumb_size
n_large_thumbs += 1
except FileNotFoundError:
# The asset is missing the large thumbnail.
if normal_thumb_path or large_thumb_path:
# Use the category for which we found the max number of
# thumbnails to find the most complex thumbnail, because
# we can't compare the small with the large.
if n_normal_thumbs > n_large_thumbs:
shutil.copyfile(normal_thumb_path, thumb_path)
shutil.copyfile(large_thumb_path, thumb_path)
# No asset thumbs available, so remove the existing
# project thumbnails, if any.
def set_rendering(self, rendering):
"""Sets the a/v restrictions for rendering or for editing."""
......@@ -1470,7 +1580,7 @@ class Project(Loggable, GES.Project):
uris (List[str]): The URIs of the assets.
with"Adding assets"):
for uri in uris:
if self.create_asset(quote_uri(uri), GES.UriClip):
# The asset was not already part of the project.
......@@ -358,6 +358,13 @@ class UndoableActionLog(GObject.Object, Loggable):
"""Gets whether currently recording an operation."""
return bool(self.stacks)
def has_assets_operations(self):
"""Checks whether user added/removed assets while working on the project."""
for stack in self.undo_stacks:
if stack.action_group_name in ["assets-addition", "assets-removal"]:
return True
return False
class MetaChangedAction(UndoableAction):
......@@ -28,6 +28,7 @@ from urllib.parse import unquote
from urllib.parse import urlparse
from urllib.parse import urlsplit
from gi.repository import GdkPixbuf
from gi.repository import GES
from gi.repository import GLib
from gi.repository import Gst
......@@ -40,6 +41,22 @@ from pitivi.configure import APPNAME
from pitivi.utils.threads import Thread
def scale_pixbuf(pixbuf, width, height):
"""Scales the given pixbuf preserving the original aspect ratio."""
pixbuf_width = pixbuf.props.width
pixbuf_height = pixbuf.props.height
if pixbuf_width > width:
pixbuf_height = width * pixbuf_height / pixbuf_width
pixbuf_width = width
if pixbuf_height > height:
pixbuf_width = height * pixbuf_width / pixbuf_height
pixbuf_height = height
return pixbuf.scale_simple(pixbuf_width, pixbuf_height, GdkPixbuf.InterpType.BILINEAR)
# Work around
def disconnectAllByFunc(obj, func):
i = 0
......@@ -149,6 +149,10 @@ GREETER_PERSPECTIVE_CSS = """
#recent_projects_label {
font-weight: bold;
#project_thumbnail_box {
background-color: #181818;
......@@ -19,13 +19,45 @@
"""Tests for the utils.misc module."""
# pylint: disable=protected-access,no-self-use
import os
from unittest import mock
from gi.repository import GdkPixbuf
from gi.repository import Gst
from pitivi.utils.misc import PathWalker
from pitivi.utils.misc import scale_pixbuf
from tests import common
class MiscMethodsTest(common.TestCase):
"""Tests methods in utils.misc module."""
# pylint: disable=too-many-arguments
def check_pixbuf_scaling(self, pixbuf_width, pixbuf_height,
width, height,
expected_width, expected_height):
"""Checks pixbuf scaling."""
pixbuf = mock.Mock()
pixbuf.props.width = pixbuf_width
pixbuf.props.height = pixbuf_height
_ = scale_pixbuf(pixbuf, width, height)
pixbuf.scale_simple.assert_called_once_with(expected_width, expected_height, GdkPixbuf.InterpType.BILINEAR)
def test_scale_pixbuf(self):
"""Tests pixbuf scaling."""
# Larger, same aspect ratio.
self.check_pixbuf_scaling(200, 100, 20, 10, 20, 10)
# Larger, wider aspect ratio.
self.check_pixbuf_scaling(200, 50, 20, 10, 20, 5)
# Larger, taller aspect ratio.
self.check_pixbuf_scaling(100, 200, 20, 10, 5, 10)
# Smaller.
self.check_pixbuf_scaling(1, 1, 20, 10, 1, 1)
self.check_pixbuf_scaling(20, 1, 20, 10, 20, 1)
self.check_pixbuf_scaling(1, 10, 20, 10, 1, 10)
class PathWalkerTest(common.TestCase):
"""Tests for the `PathWalker` class."""
......@@ -63,10 +63,13 @@ class TestProjectUndo(common.TestCase):
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 0)
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
def test_use_proxy(self):
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment