diff --git a/data/com.github.rafostar.Clapper.gschema.xml b/data/com.github.rafostar.Clapper.gschema.xml index f0789201..93c3e91a 100644 --- a/data/com.github.rafostar.Clapper.gschema.xml +++ b/data/com.github.rafostar.Clapper.gschema.xml @@ -32,6 +32,14 @@ "second" Unit to use with seeking value + + true + Ask to resume unfinished video + + + '[]' + Data storing unfinished videos resume info + diff --git a/src/dialogs.js b/src/dialogs.js index 4c153d70..10ce82da 100644 --- a/src/dialogs.js +++ b/src/dialogs.js @@ -162,6 +162,45 @@ class ClapperUriDialog extends Gtk.Dialog } }); +var ResumeDialog = GObject.registerClass( +class ClapperResumeDialog extends Gtk.MessageDialog +{ + _init(window, resumeInfo) + { + const percentage = Math.round((resumeInfo.time / resumeInfo.duration) * 100); + + const msg = [ + `Title: ${resumeInfo.title}`, + `Completed: ${percentage}%` + ].join('\n'); + + super._init({ + transient_for: window, + modal: true, + message_type: Gtk.MessageType.QUESTION, + buttons: Gtk.ButtonsType.YES_NO, + text: 'Resume playback?', + secondary_use_markup: true, + secondary_text: msg, + }); + + this.resumeInfo = resumeInfo; + this.connect('response', this._onResponse.bind(this)); + + this.show(); + } + + _onResponse(dialog, respId) + { + const { player } = this.transient_for.child; + + if(respId === Gtk.ResponseType.YES) + player.seek_seconds(this.resumeInfo.time); + + this.destroy(); + } +}); + var PrefsDialog = GObject.registerClass( class ClapperPrefsDialog extends Gtk.Dialog { diff --git a/src/player.js b/src/player.js index f6092ea0..c57597ac 100644 --- a/src/player.js +++ b/src/player.js @@ -372,6 +372,27 @@ class ClapperPlayer extends PlayerBase if(size[0] > 0 && size[1] > 0) clapperWidget._saveWindowSize(size); } + /* If "quitOnStop" is set here it means that we are in middle of autoclosing */ + if(this.state !== GstClapper.ClapperState.STOPPED && !this.quitOnStop) { + let resumeInfo = {}; + if(settings.get_boolean('resume-enabled')) { + const resumeTime = Math.floor(this.position / 1000000000); + const resumeDuration = this.duration / 1000000000; + + /* Do not save resume info when video is short, just started or almost finished */ + if(resumeDuration > 60 && resumeTime > 15 && resumeDuration - resumeTime > 20) { + resumeInfo.title = this.playlistWidget.getActiveFilename(); + resumeInfo.time = resumeTime; + resumeInfo.duration = resumeDuration; + + debug(`saving resume info for: ${resumeInfo.title}`); + debug(`resume time: ${resumeInfo.time}, duration: ${resumeInfo.duration}`); + } + else + debug('resume info is not worth saving'); + } + settings.set_string('resume-database', JSON.stringify([resumeInfo])); + } settings.set_double('volume-last', this.volume); clapperWidget.controls._onCloseRequest(); @@ -427,8 +448,8 @@ class ClapperPlayer extends PlayerBase if(settings.get_boolean('close-auto')) { /* Stop will be automatically called soon afterwards */ - this._performCloseCleanup(this.widget.get_root()); this.quitOnStop = true; + this._performCloseCleanup(this.widget.get_root()); } } diff --git a/src/prefs.js b/src/prefs.js index 1e516d15..c780dba7 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -70,6 +70,9 @@ class ClapperBehaviourPage extends PrefsBase.Grid ['percentage', "Percentage"], ], 'seeking-unit'); this.addSpinButton('Value', 1, 99, 'seeking-value'); + + this.addTitle('Resume'); + this.addCheckButton('Ask to resume last unfinished video', 'resume-enabled'); } }); diff --git a/src/widget.js b/src/widget.js index 2298b9ec..8224066a 100644 --- a/src/widget.js +++ b/src/widget.js @@ -1,6 +1,7 @@ const { Gdk, GLib, GObject, GstClapper, Gtk } = imports.gi; const { Controls } = imports.src.controls; const Debug = imports.src.debug; +const Dialogs = imports.src.dialogs; const Misc = imports.src.misc; const { Player } = imports.src.player; const Revealers = imports.src.revealers; @@ -487,21 +488,45 @@ class ClapperWidget extends Gtk.Grid this.revealerTop.endTime.set_visible(isNotStopped); } - _onPlayerDurationChanged(player) + _onPlayerDurationChanged(player, duration) { - const duration = Math.floor(player.get_duration() / 1000000000); + const durationSeconds = duration / 1000000000; + const durationFloor = Math.floor(durationSeconds); /* Sometimes GstPlayer might re-emit * duration changed during playback */ - if(this.controls.currentDuration === duration) + if(this.controls.currentDuration === durationFloor) return; - this.controls.currentDuration = duration; - this.controls.showHours = (duration >= 3600); + this.controls.currentDuration = durationFloor; + this.controls.showHours = (durationFloor >= 3600); - this.controls.positionAdjustment.set_upper(duration); - this.controls.durationFormatted = Misc.getFormattedTime(duration); + this.controls.positionAdjustment.set_upper(durationFloor); + this.controls.durationFormatted = Misc.getFormattedTime(durationFloor); this.controls.updateElapsedLabel(); + + if(settings.get_boolean('resume-enabled')) { + const resumeDatabase = JSON.parse(settings.get_string('resume-database')); + const title = player.playlistWidget.getActiveFilename(); + + debug(`searching database for resume info: ${title}`); + + const resumeInfo = resumeDatabase.find(info => { + return (info.title === title && info.duration === durationSeconds); + }); + + if(resumeInfo) { + debug('found resume info: ' + JSON.stringify(resumeInfo)); + new Dialogs.ResumeDialog(this.root, resumeInfo); + + const shrunkDatabase = resumeDatabase.filter(info => { + return !(info.title === title && info.duration === durationSeconds); + }); + settings.set_string('resume-database', JSON.stringify(shrunkDatabase)); + } + else + debug('resume info not found'); + } } _onPlayerPositionUpdated(player, position)