Merge lp:~artem-anufrij/audience/library-view into lp:~audience-members/audience/trunk

Proposed by Danielle Foré
Status: Merged
Approved by: Felipe Escoto
Approved revision: 710
Merged at revision: 657
Proposed branch: lp:~artem-anufrij/audience/library-view
Merge into: lp:~audience-members/audience/trunk
Diff against target: 1071 lines (+826/-16)
13 files modified
data/org.pantheon.audience.gschema.xml (+10/-0)
src/Audience.vala (+18/-1)
src/CMakeLists.txt (+6/-0)
src/Objects/Video.vala (+196/-0)
src/Services/LibraryManager.vala (+183/-0)
src/Services/Thubnailer.vala (+52/-0)
src/Settings.vala (+3/-1)
src/Widgets/LibraryItem.vala (+100/-0)
src/Widgets/LibraryPage.vala (+120/-0)
src/Widgets/NavigationButton.vala (+35/-0)
src/Widgets/PlayerPage.vala (+8/-0)
src/Widgets/WelcomePage.vala (+25/-2)
src/Window.vala (+70/-12)
To merge this branch: bzr merge lp:~artem-anufrij/audience/library-view
Reviewer Review Type Date Requested Status
Felipe Escoto (community) Approve
Danielle Foré ux Approve
Cody Garver Needs Fixing
Review via email: mp+306018@code.launchpad.net

Commit message

Create a library view

To post a comment you must log in.
Revision history for this message
Danielle Foré (danrabbit) wrote :

* I don't think the words "Back to" are necessary. On other back buttons we just put the thing you're going back to. So "Library" and "Welcome Screen" are probably fine.

* The window title doesn't seem to update when you change screens. It should show "Videos" on the Library and Welcome pages

* I think these pages should be added to a Gtk.Stack so we can have a nice swipe animation when moving back like you see in Switchboard

* There should probably be some kind of placeholder for when Videos is unable to fetch a poster image

* The shortcut "alt + left" should activate the back button

Revision history for this message
Danielle Foré (danrabbit) wrote :

* Instead of the magnifying glass icon, use "folder-videos"

* re: question in slack about if library >0, I think that should be the eventual behavior, but I think that would require moving the "resume last video" (and maybe "open file") items into the library so that they remain accessible even if you have a library.

Revision history for this message
Artem Anufrij (artem-anufrij) wrote :

DONE: I don't think the words "Back to" are necessary. On other back buttons we just put the thing you're going back to. So "Library" and "Welcome Screen" are probably fine.

Revision history for this message
Artem Anufrij (artem-anufrij) wrote :

DONE: The window title doesn't seem to update when you change screens. It should show "Videos" on the Library and Welcome pages

Revision history for this message
Danielle Foré (danrabbit) wrote :

If I select "Resume last video" and then use the back button to return to the Welcome, the video will continue to play in the background (I can hear it). This doesn't happen when choosing a video from the library

Revision history for this message
Adam Bieńkowski (donadigo) wrote :

I commented on some things in the code, mostly code style issues, but also some different ones about using native functions.

Revision history for this message
Artem Anufrij (artem-anufrij) wrote :

@Daniel: Done

@Adam: Done

thanks for your feedback....

Revision history for this message
Danielle Foré (danrabbit) wrote :

Please don't add any more features until we debug the existing ones ;)

* Keyboard navigation doesn't seem to work

* I now have a "TV Shows" heading even though I don't have any TV shows

review: Needs Fixing
Revision history for this message
Danielle Foré (danrabbit) wrote :

* Please remove the h4 from video titles. This style class is usually used for category or section headers
* Add 12px row spacing to the library item grid
* You shouldn't have more than one h1 on a single screen, so for section titles, h2 is probably more appropriate

Revision history for this message
Danielle Foré (danrabbit) wrote :

I really don't like this "tv-shows-indicator". This seems like a super hacky and unreliable way to figure out if something is a TV Show

Revision history for this message
Danielle Foré (danrabbit) wrote :

* Can we add the "card" class to cover images? this gives them a nice little box shadow
* It looks like your flowbox has valign set to fill. This causes a funny looking selection when you have a small library. this should probably be set to either CENTER or START

Revision history for this message
Danielle Foré (danrabbit) wrote :

* Should probably also add row and column spacing of 12 to the flowbox to make sure item labels don't crash into each other.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Re-posted outstanding issues since Launchpad doesn't have cool checkboxes like GitHub:

* No arrow key navigation in the library
* Alt + left arrow should activate the back button

Revision history for this message
Danielle Foré (danrabbit) wrote :

Rico wants us to be using GObject-style construction, I think we should make an effort to do this for new code https://chebizarro.gitbooks.io/the-vala-tutorial/content/gobject-style-construction.html

Revision history for this message
Danielle Foré (danrabbit) wrote :

If I rename a file in Files, Audience won't update until I close it. Since it can't find the file, clicking the item can't play it. We should probably just autoremove missing files

Revision history for this message
Danielle Foré (danrabbit) wrote :

It looks like the keyboard navigation not working is a problem with other views and isn't just in this branch. Tracking it separately here: https://bugs.launchpad.net/audience/+bug/1624940

That shouldn't be a blocker on this branch

Revision history for this message
Danielle Foré (danrabbit) wrote :

Let's change "Open Library" to "Browse Library" for clarity so we're not implying that a new window will appear :)

Revision history for this message
Danielle Foré (danrabbit) wrote :

Getting this error:

[FATAL 14:59:35.076098] Video.vala:124: Failed to open '/home/daniel/.cache/audience/30436b5461215ef466cac2b7d320089c.jpg' for writing: No such file or directory

I don't have ~/.cache/audience. If the folder doesn't exist, we should create it

Revision history for this message
Danielle Foré (danrabbit) wrote :

Hm it seems that the item is only removed when clicked. Can we use GLib.FileMonitor or something instead so that it updates live?

Revision history for this message
Danielle Foré (danrabbit) wrote :

If I delete a file, it doesn't seem to clear the associated cover image from .cache

Revision history for this message
Danielle Foré (danrabbit) wrote :

Made some diff comments

Revision history for this message
Danielle Foré (danrabbit) wrote :

We should add margin_bottom = 12 to the label in a library item. That way the selection from flowbox will look nicer

Revision history for this message
Felipe Escoto (philip.scott) wrote :

Made some comments on the diff about code style, but also:

- The library shouldn't be loaded into memory unless the user requests it. This is currently causing a long startup time when you just click on a file to open ;)

Non-blockers, but would be nice on this MR:
- Load the library async: Even if we are not loading the library on startup, clicking on it would still take a bit of time where it would look like its' frozen. Loading it async should also fix that
- HD Thumbnails. Because high res is best res

review: Needs Fixing
Revision history for this message
Artem Anufrij (artem-anufrij) wrote :

Thank you guys for your feedback.

@ Felipe:
* how can I hide the library button (on welcome screen) if I don't check the files?

* library loads already async
public void begin_scan () {
   detect_video_files.begin (Audience.settings.library_folder);
}

poster check is async, too:
public async void initialize_poster ()

Revision history for this message
Djax (parnold-x) wrote :

async still blocks if there is no yield.
Maybe just do the checking for viedo files at startup and finish the initalization with poster and stuff on entering the library.

Revision history for this message
Artem Anufrij (artem-anufrij) wrote :

@Djax: Thats a good idea. I will do that.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Re-posting outstanding issues again:

* We should add margin_bottom = 12 to the library item. That way the selection from flowbox will look nicer and match the top

* If I delete a file, it doesn't seem to clear the associated cover image from .cache

* It seems that a missing item is only removed when clicked. Can we use GLib.FileMonitor or something instead so that it updates live?

Revision history for this message
Danielle Foré (danrabbit) wrote :

The spinner should probably be packed into a grid (or another stylable widget) and have the card class and set size request on that widget so the spinner itself can be a sane size like 32px

Revision history for this message
Danielle Foré (danrabbit) wrote :

I'm gonna give a UX approve. I can't really find anything else to complain about right now ;) Looks good. Thank you for your hard work!

I do have some diff comments for code style :)

review: Approve (ux)
Revision history for this message
Danielle Foré (danrabbit) wrote :

Oops found an issue:
1. Have an empty videos folder
2. Open Audience
3. see that browse library is not available
4. Add a movie to videos folder

Expectation: The Browse Library button appears

Reality: Nothing happens

Revision history for this message
Cody Garver (codygarver) wrote :

I found 2 issues:

* If you start playing a video from the library, then return to the library by clicking "Back", then hit the Play key on the keyboard, it plays the audio from the video you exited. Maybe it's not actually closing the video?

* If you start playing a video, then return to the library by clicking "Back", then play the same video, it momentarily remembers where you were at in the video but then it starts playing it from the beginning. It should always remember the playback progress.

review: Needs Fixing
Revision history for this message
Danielle Foré (danrabbit) :
review: Needs Fixing
Revision history for this message
Danielle Foré (danrabbit) wrote :

1. Start with something in your videos folder
2. Open Videos; See that the "Browse Library" option is available
3. Remove all items from your videos folder

Expectation: The "Browse Library" option goes away
Reality: It stays and clicking it shoes an empty library

704. By Artem Anufrij

hide welcome button if all video files were deleted

Revision history for this message
Danielle Foré (danrabbit) wrote :

Alright, that seems pretty damn robust. Really having trouble finding new and exciting ways to break things. Looks good :)

review: Approve (ux)
Revision history for this message
Felipe Escoto (philip.scott) wrote :

Thank you for doing my requested fixes :) Now i just have one blocker before merging:

If you clear your cache on ~/.cache/thumbnails/large (As if it was on a first try), the app takes 100% CPU, as if it was trying to make all thumbnails at once... The thumbnails are created fine, and next time you load it they will all be there, but on that first run it just stays stuck trying to load them all.

review: Needs Fixing
Revision history for this message
Felipe Escoto (philip.scott) wrote :

Also made some comments regarding code style, but overall it looks very nice! Great job :)

705. By Artem Anufrij

code style

706. By Artem Anufrij

improve multithreading

707. By Artem Anufrij

removed unused code

708. By Artem Anufrij

build_path -> build_filename

709. By Artem Anufrij

fixed: Replay button shows last played video

710. By Artem Anufrij

fixed last played video button

Revision history for this message
Felipe Escoto (philip.scott) wrote :

Thank you for dealing with our requests! :) You rock man

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/org.pantheon.audience.gschema.xml'
2--- data/org.pantheon.audience.gschema.xml 2016-03-14 13:10:18 +0000
3+++ data/org.pantheon.audience.gschema.xml 2016-09-22 18:15:35 +0000
4@@ -49,5 +49,15 @@
5 <summary>Cause the audience window to stay on top by default when it's playing</summary>
6 <description>Set the option to keep the audience window above all other windows by default when audience is currently playing</description>
7 </key>
8+ <key name="library-folder" type="s">
9+ <default>""</default>
10+ <summary>Library folder</summary>
11+ <description></description>
12+ </key>
13+ <key name="poster-names" type="as">
14+ <default>['poster.jpg','Poster.jpg','cover.jpg','Cover.jpg']</default>
15+ <summary>Poster file names</summary>
16+ <description></description>
17+ </key>
18 </schema>
19 </schemalist>
20
21=== modified file 'src/Audience.vala'
22--- src/Audience.vala 2016-08-28 04:19:38 +0000
23+++ src/Audience.vala 2016-09-22 18:15:35 +0000
24@@ -65,7 +65,8 @@
25 translate_url = "https://translations.launchpad.net/audience";
26
27 about_authors = { "Cody Garver <cody@elementaryos.org>",
28- "Tom Beckmann <tom@elementaryos.org>" };
29+ "Tom Beckmann <tom@elementaryos.org>",
30+ "Artem Anufrij <artem.anufrij@live.de>" };
31 about_translators = _("translator-credits");
32 about_license_type = Gtk.License.GPL_3_0;
33 }
34@@ -89,6 +90,18 @@
35 if (settings.last_folder == "-1") {
36 settings.last_folder = Environment.get_user_special_dir (GLib.UserDirectory.VIDEOS);
37 }
38+ if (settings.library_folder == "") {
39+ settings.library_folder = GLib.Environment.get_user_special_dir (GLib.UserDirectory.VIDEOS);
40+ }
41+
42+ try {
43+ File cache = File.new_for_path (get_cache_directory ());
44+ if (!cache.query_exists ()) {
45+ cache.make_directory ();
46+ }
47+ } catch (Error e) {
48+ error (e.message);
49+ }
50
51 mainwindow = new Window ();
52 mainwindow.application = this;
53@@ -96,6 +109,10 @@
54 }
55 }
56
57+ public string get_cache_directory () {
58+ return GLib.Path.build_filename(GLib.Environment.get_user_cache_dir (), exec_name);
59+ }
60+
61 //the application was requested to open some files
62 public override void open (File[] files, string hint) {
63 activate ();
64
65=== modified file 'src/CMakeLists.txt'
66--- src/CMakeLists.txt 2016-08-01 18:46:23 +0000
67+++ src/CMakeLists.txt 2016-09-22 18:15:35 +0000
68@@ -57,7 +57,13 @@
69 Widgets/PlaylistPopover.vala
70 Widgets/WelcomePage.vala
71 Widgets/PlayerPage.vala
72+ Widgets/LibraryPage.vala
73+ Widgets/LibraryItem.vala
74+ Widgets/NavigationButton.vala
75 Services/Inhibitor.vala
76+ Services/LibraryManager.vala
77+ Services/Thubnailer.vala
78+ Objects/Video.vala
79 PACKAGES
80 ${VALA_DEPS}
81 OPTIONS
82
83=== added directory 'src/Objects'
84=== added file 'src/Objects/Video.vala'
85--- src/Objects/Video.vala 1970-01-01 00:00:00 +0000
86+++ src/Objects/Video.vala 2016-09-22 18:15:35 +0000
87@@ -0,0 +1,196 @@
88+/*-
89+ * Copyright (c) 2016-2016 elementary LLC.
90+ *
91+ * This program is free software: you can redistribute it and/or modify
92+ * it under the terms of the GNU General Public License as published by
93+ * the Free Software Foundation, either version 3 of the License, or
94+ * (at your option) any later version.
95+
96+ * This program is distributed in the hope that it will be useful,
97+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
98+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
99+ * GNU General Public License for more details.
100+
101+ * You should have received a copy of the GNU General Public License
102+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
103+ *
104+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
105+ *
106+ */
107+
108+namespace Audience.Objects {
109+ public class Video : Object {
110+ Audience.Services.LibraryManager manager;
111+
112+ public signal void poster_changed ();
113+ public signal void title_changed ();
114+
115+ public File video_file { get; private set; }
116+ public string directory { get; construct set; }
117+ public string file { get; construct set; }
118+
119+ public string title { get; private set; }
120+ public int year { get; private set; }
121+
122+ public Gdk.Pixbuf? poster { get; private set; }
123+
124+ public string mime_type { get; construct set; }
125+ public string poster_cache_file { get; private set; }
126+
127+ public string hash { get; construct set; }
128+
129+ public Video (string directory, string file, string mime_type) {
130+ Object (directory: directory, file: file, mime_type: mime_type);
131+ }
132+
133+ construct {
134+ manager = Audience.Services.LibraryManager.get_instance ();
135+ manager.thumbler.finished.connect (dbus_finished);
136+
137+ this.title = Audience.get_title (file);
138+
139+ this.extract_metadata ();
140+ video_file = File.new_for_path (this.get_path ());
141+
142+ hash = GLib.Checksum.compute_for_string (ChecksumType.MD5, this.get_path (), this.get_path ().length);
143+
144+ poster_cache_file = Path.build_filename (App.get_instance ().get_cache_directory (), hash + ".jpg");
145+
146+ notify["poster"].connect (() => {
147+ poster_changed ();
148+ });
149+ notify["title"].connect (() => {
150+ title_changed ();
151+ });
152+ }
153+
154+ private void extract_metadata () {
155+ // exclude YEAR from Title
156+ MatchInfo info;
157+ if (manager.regex_year.match (this.title, 0, out info)) {
158+ this.year = int.parse (info.fetch (0).substring (1, 4));
159+ this.title = this.title.replace (info.fetch (0) + ")", "");
160+ }
161+ }
162+
163+ public async void initialize_poster () {
164+ initialize_poster_thread.begin ((obj, res) => {
165+ this.poster = initialize_poster_thread.end (res);
166+ });
167+ }
168+
169+ public async Gdk.Pixbuf? initialize_poster_thread () {
170+ SourceFunc callback = initialize_poster_thread.callback;
171+ Gdk.Pixbuf? pixbuf = null;
172+
173+ ThreadFunc<void*> run = () => {
174+
175+ string poster_path = poster_cache_file;
176+ pixbuf = get_poster_from_file (poster_path);
177+
178+ // POSTER in Cache exists
179+ if (pixbuf != null) {
180+ Idle.add ((owned) callback);
181+ return null;
182+ }
183+
184+ // Try to find a POSTER in same folder of video file
185+ if (pixbuf == null) {
186+ poster_path = this.get_path () + ".jpg";
187+ pixbuf = get_poster_from_file (poster_path);
188+ }
189+
190+ if (pixbuf == null) {
191+ poster_path = Path.build_filename (this.directory, Audience.get_title (file) + ".jpg");
192+ pixbuf = get_poster_from_file (poster_path);
193+ }
194+
195+ foreach (string s in Audience.settings.poster_names) {
196+ if (pixbuf == null) {
197+ poster_path = Path.build_filename (this.directory, s);
198+ pixbuf = get_poster_from_file (poster_path);
199+ } else {
200+ break;
201+ }
202+ }
203+
204+ // POSTER found
205+ if (pixbuf != null) {
206+ try {
207+ pixbuf.save (poster_cache_file, "jpeg");
208+ } catch (Error e) {
209+ warning (e.message);
210+ }
211+ Idle.add ((owned) callback);
212+ return null;
213+ }
214+
215+ // Check if THUMBNAIL exists
216+ string? thumbnail_path = manager.get_thumbnail_path (video_file);
217+ if (thumbnail_path != null) {
218+ pixbuf = get_poster_from_file (thumbnail_path);
219+ Idle.add ((owned) callback);
220+ return null;
221+ }
222+
223+ // Call DBUS for create a new THUMBNAIL
224+ Gee.ArrayList<string> uris = new Gee.ArrayList<string> ();
225+ Gee.ArrayList<string> mimes = new Gee.ArrayList<string> ();
226+
227+ uris.add (video_file.get_uri ());
228+ mimes.add (mime_type);
229+
230+ manager.thumbler.Instand (uris, mimes);
231+
232+ Idle.add ((owned) callback);
233+ return null;
234+ };
235+
236+ try {
237+ new Thread<void*>.try (null, run);
238+ } catch (Error e) {
239+ error (e.message);
240+ }
241+
242+ yield;
243+
244+ return pixbuf;
245+ }
246+
247+ private void dbus_finished (uint heandle) {
248+ if (poster == null) {
249+ string? thumbnail_path = manager.get_thumbnail_path (video_file);
250+ if (thumbnail_path != null) {
251+ poster = get_poster_from_file (thumbnail_path);
252+ }
253+ }
254+ }
255+
256+ public string get_path () {
257+ return Path.build_filename (directory, file);
258+ }
259+
260+ public Gdk.Pixbuf? get_poster_from_file (string poster_path) {
261+ Gdk.Pixbuf pixbuf = null;
262+ if (File.new_for_path (poster_path).query_exists ()) {
263+ try {
264+ pixbuf = new Gdk.Pixbuf.from_file_at_scale (poster_path, -1, Audience.Services.POSTER_HEIGHT, true);
265+ } catch (Error e) {
266+ warning (e.message);
267+ }
268+
269+ if (pixbuf == null) {
270+ return null;
271+ }
272+ // Cut THUMBNAIL images
273+ int width = pixbuf.width;
274+ if (width > Audience.Services.POSTER_WIDTH) {
275+ int x_offset = (width - Audience.Services.POSTER_WIDTH) / 2;
276+ pixbuf = new Gdk.Pixbuf.subpixbuf (pixbuf, x_offset, 0, Audience.Services.POSTER_WIDTH, Audience.Services.POSTER_HEIGHT);
277+ }
278+ }
279+
280+ return pixbuf;
281+ }
282+ }
283+}
284
285=== added file 'src/Services/LibraryManager.vala'
286--- src/Services/LibraryManager.vala 1970-01-01 00:00:00 +0000
287+++ src/Services/LibraryManager.vala 2016-09-22 18:15:35 +0000
288@@ -0,0 +1,183 @@
289+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
290+/*-
291+ * Copyright (c) 2016-2016 elementary LLC.
292+ *
293+ * This program is free software: you can redistribute it and/or modify
294+ * it under the terms of the GNU General Public License as published by
295+ * the Free Software Foundation, either version 3 of the License, or
296+ * (at your option) any later version.
297+
298+ * This program is distributed in the hope that it will be useful,
299+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
300+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
301+ * GNU General Public License for more details.
302+
303+ * You should have received a copy of the GNU General Public License
304+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
305+ *
306+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
307+ *
308+ */
309+
310+namespace Audience.Services {
311+ public const int POSTER_WIDTH = 170;
312+ public const int POSTER_HEIGHT = 240;
313+
314+ public class LibraryManager : Object {
315+
316+ public signal void video_file_detected (Audience.Objects.Video video);
317+ public signal void video_file_deleted (string path);
318+ public signal void finished ();
319+
320+ public Regex regex_year { get; construct set; }
321+ public DbusThumbnailer thumbler { get; construct set; }
322+
323+ public bool has_items { get; private set; }
324+
325+ private Gee.ArrayList<string> poster_hash;
326+ private Gee.ArrayList<FileMonitor> monitoring_directories;
327+
328+ public static LibraryManager instance = null;
329+ public static LibraryManager get_instance () {
330+ if (instance == null) {
331+ instance = new LibraryManager ();
332+ }
333+
334+ return instance;
335+ }
336+
337+ private LibraryManager () {
338+ }
339+
340+ construct {
341+ poster_hash = new Gee.ArrayList<string> ();
342+ monitoring_directories = new Gee.ArrayList<FileMonitor> ();
343+ try {
344+ regex_year = new Regex ("\\(\\d\\d\\d\\d(?=(\\)$))");
345+ } catch (Error e) {
346+ error (e.message);
347+ }
348+ thumbler = new DbusThumbnailer ();
349+
350+ finished.connect (() => { clear_unused_cache_files.begin (); });
351+ }
352+
353+ public void begin_scan () {
354+ detect_video_files.begin (Audience.settings.library_folder);
355+ }
356+
357+ public async void detect_video_files (string source) throws GLib.Error {
358+ File directory = File.new_for_path (source);
359+
360+ FileMonitor monitor = directory.monitor (FileMonitorFlags.NONE, null);
361+ monitor.changed.connect ((src, dest, event) => {
362+ if (event == GLib.FileMonitorEvent.DELETED) {
363+ video_file_deleted (src.get_path ());
364+ }
365+ else if (event == GLib.FileMonitorEvent.CHANGES_DONE_HINT) {
366+ FileInfo file_info;
367+ try {
368+ file_info = src.query_info (FileAttribute.STANDARD_CONTENT_TYPE + "," + FileAttribute.STANDARD_IS_HIDDEN + "," + FileAttribute.STANDARD_TYPE, 0);
369+ } catch (Error e) {
370+ warning (e.message);
371+ return;
372+ }
373+ if (file_info.get_file_type () == FileType.DIRECTORY) {
374+ detect_video_files.begin (src.get_path ());
375+ } else if (is_file_valid (file_info)) {
376+ string src_path = src.get_path ();
377+ crate_video_object (file_info, Path.get_dirname (src_path), Path.get_basename (src_path));
378+ }
379+ }
380+ });
381+ monitoring_directories.add (monitor);
382+
383+ var children = directory.enumerate_children (FileAttribute.STANDARD_CONTENT_TYPE + "," + FileAttribute.STANDARD_IS_HIDDEN, 0);
384+
385+ if (children != null) {
386+ FileInfo file_info;
387+ while ((file_info = children.next_file ()) != null) {
388+ if (file_info.get_file_type () == FileType.DIRECTORY) {
389+ detect_video_files.begin (source + "/" + file_info.get_name ());
390+ continue;
391+ }
392+
393+ if (is_file_valid (file_info)) {
394+ crate_video_object (file_info, source);
395+ }
396+ }
397+ }
398+ if (directory.get_path () == Audience.settings.library_folder) {
399+ finished ();
400+ }
401+ }
402+
403+ private bool is_file_valid (FileInfo file_info) {
404+ string mime_type = file_info.get_content_type ();
405+ return !file_info.get_is_hidden () && mime_type.contains ("video");
406+ }
407+
408+ private void crate_video_object (FileInfo file_info, string source, string name = "") {
409+ if (name == "") {
410+ name = file_info.get_name ();
411+ }
412+ var video = new Audience.Objects.Video (source, name, file_info.get_content_type ());
413+ video_file_detected (video);
414+ poster_hash.add (video.hash + ".jpg");
415+ has_items = true;
416+ }
417+
418+ public string? get_thumbnail_path (File file) {
419+ if (!file.is_native ()) {
420+ return null;
421+ }
422+ string? path = null;
423+ try {
424+ var info = file.query_info (FileAttribute.THUMBNAIL_PATH + "," + FileAttribute.THUMBNAILING_FAILED, FileQueryInfoFlags.NONE);
425+ path = info.get_attribute_as_string (FileAttribute.THUMBNAIL_PATH);
426+ var failed = info.get_attribute_boolean (FileAttribute.THUMBNAILING_FAILED);
427+
428+ if (failed || path == null) {
429+ return null;
430+ }
431+
432+ path = path.replace ("normal", "large");
433+
434+ File large_thumbnail = File.new_for_path (path);
435+ if (!large_thumbnail.query_exists ()) {
436+ return null;
437+ }
438+ } catch (Error e) {
439+ warning (e.message);
440+ return null;
441+ }
442+
443+ return path;
444+ }
445+
446+ public void clear_cache (Audience.Objects.Video video) {
447+ File file = File.new_for_path (video.poster_cache_file);
448+ if (file.query_exists ()) {
449+ file.delete_async.begin (Priority.DEFAULT, null);
450+ }
451+ }
452+
453+ public async void clear_unused_cache_files () {
454+ File directory = File.new_for_path (App.get_instance ().get_cache_directory ());
455+ try {
456+ var children = directory.enumerate_children (FileAttribute.STANDARD_NAME, 0);
457+
458+ if (children != null) {
459+ FileInfo file_info;
460+ while ((file_info = children.next_file ()) != null) {
461+ if (!poster_hash.contains (file_info.get_name ())) {
462+ children.get_child (file_info).delete_async.begin ();
463+ }
464+ }
465+ }
466+ } catch (Error e) {
467+ warning (e.message);
468+ }
469+ }
470+ }
471+}
472
473=== added file 'src/Services/Thubnailer.vala'
474--- src/Services/Thubnailer.vala 1970-01-01 00:00:00 +0000
475+++ src/Services/Thubnailer.vala 2016-09-22 18:15:35 +0000
476@@ -0,0 +1,52 @@
477+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
478+/*-
479+ * Copyright (c) 2016-2016 elementary LLC.
480+ *
481+ * This program is free software: you can redistribute it and/or modify
482+ * it under the terms of the GNU General Public License as published by
483+ * the Free Software Foundation, either version 3 of the License, or
484+ * (at your option) any later version.
485+
486+ * This program is distributed in the hope that it will be useful,
487+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
488+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
489+ * GNU General Public License for more details.
490+
491+ * You should have received a copy of the GNU General Public License
492+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
493+ *
494+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
495+ *
496+ */
497+
498+namespace Audience.Services {
499+ [DBus (name = "org.freedesktop.thumbnails.Thumbnailer1")]
500+ private interface Tumbler : GLib.Object {
501+ public abstract async uint Queue (string[] uris, string[] mime_types, string flavor, string sheduler, uint handle_to_dequeue) throws GLib.IOError, GLib.DBusError;
502+ public signal void Finished (uint handle);
503+ }
504+
505+ public class DbusThumbnailer : GLib.Object {
506+ private Tumbler tumbler;
507+ private const string THUMBNAILER_IFACE = "org.freedesktop.thumbnails.Thumbnailer1";
508+ private const string THUMBNAILER_SERVICE = "/org/freedesktop/thumbnails/Thumbnailer1";
509+
510+ public signal void finished (uint handle);
511+
512+ public DbusThumbnailer () {
513+ }
514+
515+ construct {
516+ try {
517+ tumbler = Bus.get_proxy_sync (BusType.SESSION, THUMBNAILER_IFACE, THUMBNAILER_SERVICE);
518+ tumbler.Finished.connect ((handle) => { finished (handle); });
519+ } catch (Error e) {
520+ warning (e.message);
521+ }
522+ }
523+
524+ public void Instand (Gee.ArrayList<string> uris, Gee.ArrayList<string> mimes ){
525+ tumbler.Queue.begin (uris.to_array (), mimes.to_array (), "large", "default", 0);
526+ }
527+ }
528+}
529
530=== modified file 'src/Settings.vala'
531--- src/Settings.vala 2016-03-14 13:10:18 +0000
532+++ src/Settings.vala 2016-09-22 18:15:35 +0000
533@@ -28,7 +28,9 @@
534 public string last_folder {get; set;}
535 public bool playback_wait {get; set;}
536 public bool stay_on_top {get; set;}
537- public string subtitle_font {get; set; }
538+ public string subtitle_font {get; set;}
539+ public string library_folder {get; set;}
540+ public string[] poster_names {get; set;}
541
542 public Settings () {
543 base ("org.pantheon.audience");
544
545=== added file 'src/Widgets/LibraryItem.vala'
546--- src/Widgets/LibraryItem.vala 1970-01-01 00:00:00 +0000
547+++ src/Widgets/LibraryItem.vala 2016-09-22 18:15:35 +0000
548@@ -0,0 +1,100 @@
549+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
550+/*-
551+ * Copyright (c) 2016-2016 elementary LLC.
552+ *
553+ * This program is free software: you can redistribute it and/or modify
554+ * it under the terms of the GNU General Public License as published by
555+ * the Free Software Foundation, either version 3 of the License, or
556+ * (at your option) any later version.
557+
558+ * This program is distributed in the hope that it will be useful,
559+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
560+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
561+ * GNU General Public License for more details.
562+
563+ * You should have received a copy of the GNU General Public License
564+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
565+ *
566+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
567+ *
568+ */
569+
570+namespace Audience {
571+ public class LibraryItem : Gtk.FlowBoxChild {
572+
573+ Gtk.Grid grid;
574+ public Audience.Objects.Video video { get; construct set; }
575+
576+ Gtk.Image poster;
577+ Gtk.Label title;
578+ Gtk.Spinner spinner;
579+ Gtk.Grid spinner_container;
580+
581+ public LibraryItem (Audience.Objects.Video video) {
582+ Object (video: video);
583+ }
584+
585+ construct {
586+ margin_bottom = 12;
587+
588+ video.poster_changed.connect (() => {
589+ if (video.poster != null) {
590+ spinner.active = false;
591+ spinner_container.hide ();
592+ if (poster == null) {
593+ poster = new Gtk.Image ();
594+ poster.margin_top = poster.margin_left = poster.margin_right = 12;
595+ poster.get_style_context ().add_class ("card");
596+ grid.attach (poster, 0, 0, 1, 1);
597+ }
598+
599+ poster.pixbuf = video.poster;
600+ poster.show ();
601+ } else {
602+ spinner.active = true;
603+ spinner_container.show ();
604+ if (poster != null) {
605+ poster.hide ();
606+ }
607+ }
608+ });
609+
610+ video.title_changed.connect (() => {
611+ title.label = video.title;
612+ title.show ();
613+ });
614+
615+ spinner_container = new Gtk.Grid ();
616+ spinner_container.height_request = Audience.Services.POSTER_HEIGHT;
617+ spinner_container.width_request = Audience.Services.POSTER_WIDTH;
618+ spinner_container.margin_top = spinner_container.margin_left = spinner_container.margin_right = 12;
619+ spinner_container.get_style_context ().add_class ("card");
620+
621+ spinner = new Gtk.Spinner ();
622+ spinner.expand = true;
623+ spinner.active = true;
624+ spinner.valign = Gtk.Align.CENTER;
625+ spinner.halign = Gtk.Align.CENTER;
626+ spinner.height_request = 32;
627+ spinner.width_request = 32;
628+
629+ spinner_container.add (spinner);
630+
631+ grid = new Gtk.Grid ();
632+ grid.halign = Gtk.Align.CENTER;
633+ grid.valign = Gtk.Align.START;
634+ grid.row_spacing = 12;
635+
636+ title = new Gtk.Label (video.title);
637+ title.justify = Gtk.Justification.CENTER;
638+ title.set_line_wrap (true);
639+ title.max_width_chars = 0;
640+
641+
642+ grid.attach (spinner_container, 0, 0, 1, 1);
643+ grid.attach (title, 0, 1, 1 ,1);
644+
645+ this.add (grid);
646+ }
647+ }
648+}
649
650=== added file 'src/Widgets/LibraryPage.vala'
651--- src/Widgets/LibraryPage.vala 1970-01-01 00:00:00 +0000
652+++ src/Widgets/LibraryPage.vala 2016-09-22 18:15:35 +0000
653@@ -0,0 +1,120 @@
654+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
655+/*-
656+ * Copyright (c) 2016-2016 elementary LLC.
657+ *
658+ * This program is free software: you can redistribute it and/or modify
659+ * it under the terms of the GNU General Public License as published by
660+ * the Free Software Foundation, either version 3 of the License, or
661+ * (at your option) any later version.
662+
663+ * This program is distributed in the hope that it will be useful,
664+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
665+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
666+ * GNU General Public License for more details.
667+
668+ * You should have received a copy of the GNU General Public License
669+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
670+ *
671+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
672+ *
673+ */
674+
675+namespace Audience {
676+ public class LibraryPage : Gtk.ScrolledWindow {
677+
678+ Gtk.FlowBox view_movies;
679+
680+ Audience.Services.LibraryManager manager;
681+ bool poster_initialized = false;
682+ int items_counter;
683+ public bool has_items { get { return items_counter > 0; } }
684+
685+ public static LibraryPage instance = null;
686+ public static LibraryPage get_instance () {
687+ if (instance == null) {
688+ instance = new LibraryPage ();
689+ }
690+
691+ return instance;
692+ }
693+
694+ private LibraryPage () {
695+ }
696+
697+ construct {
698+ items_counter = 0;
699+ view_movies = new Gtk.FlowBox ();
700+ view_movies.margin = 24;
701+ view_movies.homogeneous = true;
702+ view_movies.row_spacing = 12;
703+ view_movies.column_spacing = 12;
704+ view_movies.valign = Gtk.Align.START;
705+ view_movies.selection_mode = Gtk.SelectionMode.NONE;
706+ view_movies.child_activated.connect (play_video);
707+
708+ view_movies.set_sort_func ((child1, child2) => {
709+ var item1 = child1 as LibraryItem;
710+ var item2 = child2 as LibraryItem;
711+ if (item1 != null && item2 != null) {
712+ return item1.video.file.collate (item2.video.file);
713+ }
714+ return 0;
715+ });
716+
717+ manager = Audience.Services.LibraryManager.get_instance ();
718+ manager.video_file_detected.connect (add_item);
719+ manager.video_file_deleted.connect (remove_item_from_path);
720+ manager.begin_scan ();
721+
722+ this.add (view_movies);
723+
724+ map.connect (() => {
725+ if (!poster_initialized) {
726+ poster_initialized = true;
727+ poster_initialisation.begin ();
728+ show_all ();
729+ }
730+ });
731+ }
732+
733+ private void add_item (Audience.Objects.Video video) {
734+ Audience.LibraryItem new_item = new Audience.LibraryItem (video);
735+ view_movies.add (new_item);
736+ if (poster_initialized) {
737+ new_item.show_all ();
738+ new_item.video.initialize_poster.begin ();
739+ }
740+ items_counter++;
741+ }
742+
743+ private void play_video (Gtk.FlowBoxChild item) {
744+ var selected = (item as Audience.LibraryItem);
745+ if (selected.video.video_file.query_exists ()) {
746+ bool from_beginning = selected.video.video_file.get_uri () != settings.current_video;
747+ App.get_instance ().mainwindow.play_file (selected.video.video_file.get_uri (), from_beginning);
748+ } else {
749+ remove_item.begin (selected);
750+ }
751+ }
752+
753+ private async void remove_item (LibraryItem item) {
754+ manager.clear_cache (item.video);
755+ item.dispose ();
756+ items_counter--;
757+ }
758+
759+ private async void remove_item_from_path (string path ) {
760+ foreach (var child in view_movies.get_children ()) {
761+ if ((child as LibraryItem).video.video_file.get_path ().has_prefix (path)) {
762+ remove_item.begin (child as LibraryItem);
763+ }
764+ }
765+ }
766+
767+ private async void poster_initialisation () {
768+ foreach (var child in view_movies.get_children ()) {
769+ (child as LibraryItem).video.initialize_poster.begin ();
770+ }
771+ }
772+ }
773+}
774
775=== added file 'src/Widgets/NavigationButton.vala'
776--- src/Widgets/NavigationButton.vala 1970-01-01 00:00:00 +0000
777+++ src/Widgets/NavigationButton.vala 2016-09-22 18:15:35 +0000
778@@ -0,0 +1,35 @@
779+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
780+/*-
781+ * Copyright (c) 2016-2016 elementary LLC.
782+ *
783+ * This program is free software: you can redistribute it and/or modify
784+ * it under the terms of the GNU General Public License as published by
785+ * the Free Software Foundation, either version 3 of the License, or
786+ * (at your option) any later version.
787+
788+ * This program is distributed in the hope that it will be useful,
789+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
790+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
791+ * GNU General Public License for more details.
792+
793+ * You should have received a copy of the GNU General Public License
794+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
795+ *
796+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
797+ *
798+ */
799+
800+namespace Audience {
801+ public class NavigationButton : Gtk.Button {
802+
803+ public NavigationButton () {
804+ }
805+
806+ construct {
807+ can_focus = false;
808+ valign = Gtk.Align.CENTER;
809+ vexpand = false;
810+ this.get_style_context ().add_class ("back-button");
811+ }
812+ }
813+}
814
815=== modified file 'src/Widgets/PlayerPage.vala'
816--- src/Widgets/PlayerPage.vala 2016-08-19 16:27:45 +0000
817+++ src/Widgets/PlayerPage.vala 2016-09-22 18:15:35 +0000
818@@ -264,9 +264,17 @@
819 settings.current_video = uri;
820 }
821
822+ public double get_progress () {
823+ return playback.progress;
824+ }
825+
826 public string get_played_uri () {
827 return playback.uri;
828 }
829+
830+ public void reset_played_uri () {
831+ playback.uri = null;
832+ }
833
834 public void next () {
835 get_playlist_widget ().next ();
836
837=== modified file 'src/Widgets/WelcomePage.vala'
838--- src/Widgets/WelcomePage.vala 2016-08-19 16:27:45 +0000
839+++ src/Widgets/WelcomePage.vala 2016-09-22 18:15:35 +0000
840@@ -1,6 +1,7 @@
841 namespace Audience {
842 public class WelcomePage : Granite.Widgets.Welcome {
843 private DiskManager disk_manager;
844+ private Services.LibraryManager library_manager;
845 public WelcomePage () {
846 base (_("No Videos Open"), _("Select a source to begin playing."));
847 }
848@@ -33,9 +34,22 @@
849 set_item_visible (2, disk_manager.has_media_volumes ());
850 });
851
852+ library_manager = Services.LibraryManager.get_instance ();
853+ library_manager.video_file_detected.connect ((vid) => {
854+ set_item_visible (3, true);
855+ this.show_all ();
856+ });
857+
858+ library_manager.video_file_deleted.connect ((vid) => {
859+ set_item_visible (3, LibraryPage.get_instance ().has_items);
860+ });
861+
862 append ("media-cdrom", _("Play from Disc"), _("Watch a DVD or open a file from disc"));
863 set_item_visible (2, disk_manager.has_media_volumes ());
864
865+ append ("folder-videos", _("Browse Library"), _("Watch a movie from your library"));
866+ set_item_visible (3, library_manager.has_items);
867+
868 activated.connect ((index) => {
869 var window = App.get_instance ().mainwindow;
870 switch (index) {
871@@ -49,6 +63,8 @@
872 case 2:
873 window.run_open_dvd ();
874 break;
875+ case 3:
876+ window.show_library ();
877 }
878 });
879 }
880@@ -60,9 +76,16 @@
881 var filename = settings.current_video;
882 var last_file = File.new_for_uri (filename);
883
884- replay_button.title = _("Replay last video");
885+ if (settings.last_stopped == 0.0) {
886+ replay_button.title = _("Replay last video");
887+ replay_button.icon.icon_name = ("media-playlist-repeat");
888+ } else {
889+ replay_button.title = _("Resume last video");
890+ replay_button.icon.icon_name = ("media-playback-start");
891+ }
892+
893 replay_button.description = get_title (last_file.get_basename ());
894- replay_button.icon.icon_name = ("media-playlist-repeat");
895+
896
897 bool show_last_file = settings.current_video != "";
898 if (last_file.query_exists () == false) {
899
900=== modified file 'src/Window.vala'
901--- src/Window.vala 2016-08-24 01:21:07 +0000
902+++ src/Window.vala 2016-09-22 18:15:35 +0000
903@@ -26,12 +26,18 @@
904 private Gtk.HeaderBar header;
905 private PlayerPage player_page;
906 private WelcomePage welcome_page;
907+ private LibraryPage library_page;
908+ private NavigationButton navigation_button;
909 private ZeitgeistManager zeitgeist_manager;
910
911+ // For better translation
912+ const string navigation_button_welcomescreen = N_("Back");
913+ const string navigation_button_library = N_("Library");
914+
915 public signal void media_volumes_changed ();
916
917 public Window () {
918-
919+
920 }
921
922 construct {
923@@ -43,8 +49,33 @@
924 header = new Gtk.HeaderBar ();
925 header.set_show_close_button (true);
926 header.get_style_context ().add_class ("compact");
927+
928+ navigation_button = new NavigationButton ();
929+ navigation_button.clicked.connect (() => {
930+ double progress = player_page.get_progress ();
931+ if (progress > 0) {
932+ settings.last_stopped = progress;
933+ }
934+ player_page.playing = false;
935+ player_page.reset_played_uri ();
936+ title = App.get_instance ().program_name;
937+ get_window ().set_cursor (null);
938+
939+ if (navigation_button.label == navigation_button_library) {
940+ navigation_button.label = navigation_button_welcomescreen;
941+ main_stack.set_visible_child_full ("library", Gtk.StackTransitionType.SLIDE_RIGHT);
942+ } else {
943+ navigation_button.hide ();
944+ main_stack.set_visible_child (welcome_page);
945+ }
946+
947+ welcome_page.refresh ();
948+ });
949+
950+ header.pack_start (navigation_button);
951 set_titlebar (header);
952
953+ library_page = LibraryPage.get_instance ();
954 welcome_page = new WelcomePage ();
955
956 player_page = new PlayerPage ();
957@@ -58,12 +89,17 @@
958 });
959
960 main_stack = new Gtk.Stack ();
961- main_stack.add (welcome_page);
962- main_stack.add (player_page);
963+ main_stack.expand = true;
964+ main_stack.add_named (welcome_page, "welcome");
965+ main_stack.add_named (player_page, "player");
966+ main_stack.add_named (library_page, "library");
967+ main_stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
968
969 add (main_stack);
970 show_all ();
971- main_stack.set_visible_child (welcome_page);
972+
973+ navigation_button.hide ();
974+ main_stack.set_visible_child_full ("welcome", Gtk.StackTransitionType.NONE);
975
976 Gtk.TargetEntry uris = {"text/uri-list", 0, 0};
977 Gtk.drag_dest_set (this, Gtk.DestDefaults.ALL, {uris}, Gdk.DragAction.MOVE);
978@@ -103,9 +139,9 @@
979 /*/ FIXME: Remove comments once gala bug is fixed: https://bugs.launchpad.net/gala/+bug/1602722
980 if (Gdk.WindowState.MAXIMIZED in e.changed_mask) {
981 bool currently_maximixed = Gdk.WindowState.MAXIMIZED in e.new_window_state;
982-
983+
984 if (main_stack.get_visible_child () == player_page && currently_maximixed) {
985- fullscreen ();
986+ fullscreen ();
987 }
988 }*/
989
990@@ -130,6 +166,12 @@
991
992 public override bool key_press_event (Gdk.EventKey e) {
993 uint keycode = e.hardware_keycode;
994+
995+ if ((e.state & Gdk.ModifierType.MOD1_MASK) != 0 && e.keyval == Gdk.Key.Left) {
996+ navigation_button.clicked ();
997+ return true;
998+ }
999+
1000 if ((e.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
1001 if (match_keycode (Gdk.Key.o, keycode)) {
1002 run_open_file ();
1003@@ -139,7 +181,7 @@
1004 return true;
1005 }
1006 }
1007-
1008+
1009 if (main_stack.get_visible_child () == player_page) {
1010 if (match_keycode (Gdk.Key.p, keycode) || match_keycode (Gdk.Key.space, keycode)) {
1011 player_page.playing = !player_page.playing;
1012@@ -225,7 +267,7 @@
1013 if (clear_playlist) {
1014 player_page.get_playlist_widget ().clear_items ();
1015 }
1016-
1017+
1018 string[] videos = {};
1019 foreach (var file in files) {
1020 if (file.query_file_type (0) == FileType.DIRECTORY) {
1021@@ -242,7 +284,7 @@
1022 if (videos.length == 0) {
1023 return;
1024 }
1025-
1026+
1027 if (force_play) {
1028 play_file (videos [0]);
1029 }
1030@@ -260,6 +302,13 @@
1031 read_first_disk.begin ();
1032 }
1033
1034+ public void show_library () {
1035+ navigation_button.label = navigation_button_welcomescreen;
1036+ navigation_button.show ();
1037+ main_stack.set_visible_child (library_page);
1038+ library_page.grab_focus ();
1039+ }
1040+
1041 public void run_open_file (bool clear_playlist = false, bool force_play = true) {
1042 var file = new Gtk.FileChooserDialog (_("Open"), this, Gtk.FileChooserAction.OPEN,
1043 _("_Cancel"), Gtk.ResponseType.CANCEL, _("_Open"), Gtk.ResponseType.ACCEPT);
1044@@ -328,15 +377,24 @@
1045 unfullscreen ();
1046 }
1047
1048- private void play_file (string uri, bool from_beginning = true) {
1049- main_stack.set_visible_child (player_page);
1050+ public void play_file (string uri, bool from_beginning = true) {
1051+ if (navigation_button.visible) {
1052+ navigation_button.label = navigation_button_library;
1053+ } else {
1054+ navigation_button.show ();
1055+ navigation_button.label = navigation_button_welcomescreen;
1056+ }
1057+
1058+ main_stack.set_visible_child_full ("player", Gtk.StackTransitionType.SLIDE_LEFT);
1059 player_page.play_file (uri, from_beginning);
1060 if (is_maximized) {
1061 fullscreen ();
1062 }
1063-
1064+
1065 if (settings.stay_on_top && !settings.playback_wait) {
1066 set_keep_above (true);
1067 }
1068+
1069+ welcome_page.refresh ();
1070 }
1071 }

Subscribers

People subscribed via source and target branches