Inertial chicken
This commit is contained in:
parent
0927bd8d6e
commit
2c9f8932c0
26 changed files with 2987 additions and 0 deletions
18
Audiobooks.desktop
Normal file
18
Audiobooks.desktop
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
|
||||
Name=Audiobook Player
|
||||
Comment=Listen to audiobooks
|
||||
GenericName=Audiobook Player
|
||||
|
||||
Exec=Audiobooks %U
|
||||
TryExec=Audiobooks
|
||||
|
||||
Icon=Audiobooks
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Audio;Player;
|
||||
MimeType=audio/mpeg;audio/mp4;audio/x-m4b;
|
||||
|
||||
StartupNotify=true
|
||||
|
||||
BIN
Audiobooks.png
Normal file
BIN
Audiobooks.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
63
Audiobooks.pro
Normal file
63
Audiobooks.pro
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
QT += core quick widgets dbus sql
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
# any feature of Qt which as been marked deprecated (the exact warnings
|
||||
# depend on your compiler). Please consult the documentation of the
|
||||
# deprecated API in order to know how to port your code away from it.
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
|
||||
# You can also make your code fail to compile if you use deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
SOURCES += \
|
||||
src/file.cpp \
|
||||
src/librarydb.cpp \
|
||||
src/main.cpp \
|
||||
src/mpris.cpp \
|
||||
src/mprisadaptor.cpp \
|
||||
src/mprisplayer.cpp \
|
||||
src/screen.cpp \
|
||||
src/thumbnailprovider.cpp \
|
||||
src/mpvaudio.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/file.h \
|
||||
src/librarydb.h \
|
||||
src/mpris.h \
|
||||
src/mprisadaptor.h \
|
||||
src/mprisplayer.h \
|
||||
src/screen.h \
|
||||
src/thumbnailprovider.h \
|
||||
src/mpvaudio.h
|
||||
|
||||
CONFIG += c++20 link_pkgconfig
|
||||
|
||||
PKGCONFIG += mpv
|
||||
#PKGCONFIG += KF6KIOCore KF6KIOGui
|
||||
LIBS += -lKF6KIOCore -lKF6KIOGui -lKF6CoreAddons
|
||||
INCLUDEPATH += /usr/include/KF6/KIOGui /usr/include/KF6/KIO /usr/include/KF6/KIOCore /usr/include/KF6/KCoreAddons
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
# Additional import path used to resolve QML modules just for Qt Quick Designer
|
||||
QML_DESIGNER_IMPORT_PATH =
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
DISTFILES = \
|
||||
$$files(qml/*, true) \
|
||||
qml/AProgressBar.qml \
|
||||
qml/PageNowPlaying.qml
|
||||
|
||||
resources.prefix = /
|
||||
resources.base = $$PWD
|
||||
resources.files = \
|
||||
$$files(qml/*, true)
|
||||
|
||||
RESOURCES += resources
|
||||
23
qml/AProgressBar.qml
Normal file
23
qml/AProgressBar.qml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Rectangle {
|
||||
id: progressBar
|
||||
color: "transparent"
|
||||
border.color: Kirigami.Theme.alternateBackgroundColor
|
||||
border.width: 1
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
implicitHeight: Kirigami.Units.largeSpacing
|
||||
height: Kirigami.Units.largeSpacing
|
||||
|
||||
property real value
|
||||
|
||||
Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
x: Kirigami.Units.smallSpacing * 0.5
|
||||
y: Kirigami.Units.smallSpacing * 0.5
|
||||
width: (parent.width - Kirigami.Units.smallSpacing) * parent.value
|
||||
height: parent.height - Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.focusColor
|
||||
}
|
||||
}
|
||||
576
qml/Manager.qml
Normal file
576
qml/Manager.qml
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
import QtQuick
|
||||
import QtCore
|
||||
|
||||
Item {
|
||||
id: manager
|
||||
|
||||
property var m_model
|
||||
property var m_files
|
||||
|
||||
property string m_home
|
||||
property string m_path
|
||||
|
||||
property QtObject m_db
|
||||
|
||||
property variant m_toParse: []
|
||||
property variant m_library: []
|
||||
|
||||
property bool playing: false
|
||||
property bool paused: false
|
||||
property bool loaded: false
|
||||
property real position: 0
|
||||
|
||||
property variant m_currentBook: false
|
||||
property variant m_currentChapter: { "index":0, "chapterPosition":0, "chapterLength":0, "chapterProgress":0, "title":"Not Playing", "chapterStart":-1, "chapterEnd":-1 }
|
||||
|
||||
signal contentsChanged(variant contents)
|
||||
signal libraryEntryUpdated(int idx, variant entry);
|
||||
|
||||
// Navigation:
|
||||
function goHome() {
|
||||
setPath( m_home );
|
||||
}
|
||||
|
||||
function goUp() {
|
||||
let parts = m_path.split(/\//g);
|
||||
parts.pop();
|
||||
let npath = parts.join('/');
|
||||
if( "file://" === npath )
|
||||
npath = "file:///";
|
||||
setPath(npath);
|
||||
}
|
||||
|
||||
function setPath(path) {
|
||||
m_path = path;
|
||||
m_model = File.readDir(m_path);
|
||||
|
||||
/** Can't use filter/map here cuz race on idx set, so... **/
|
||||
let files = [];
|
||||
for( let a=0, b=0; a < m_model.length; a++ )
|
||||
{
|
||||
let obj = m_model[a];
|
||||
if( obj.type === "dir" )
|
||||
continue;
|
||||
|
||||
obj["source"] = m_path + "/" + obj["name"];
|
||||
obj["index"] = a;
|
||||
obj["fileindex"] = b++;
|
||||
files.push(obj);
|
||||
}
|
||||
m_files = files;
|
||||
|
||||
for( let f of files )
|
||||
dbCheck(f);
|
||||
|
||||
dbParsePending();
|
||||
}
|
||||
|
||||
// Utility:
|
||||
function formatSeconds(secs) {
|
||||
let s = 0+secs;
|
||||
|
||||
if(s < 0 || s === undefined)
|
||||
return "Unknown";
|
||||
const hours = Math.floor(s / 3600);
|
||||
s -= hours * 3600;
|
||||
const minutes = Math.floor(s / 60);
|
||||
s -= minutes * 60;
|
||||
const seconds = Math.floor(s);
|
||||
|
||||
if( hours < 1 )
|
||||
return minutes + ":" + (seconds < 10 ? "0" + seconds : seconds);
|
||||
return hours + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
|
||||
}
|
||||
|
||||
function play() {
|
||||
if( !m_currentBook ) {
|
||||
console.log("Manager.play(): Cannot play. No current book.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !MPV.loaded )
|
||||
return console.log("Manager.play(): Cannot play. MPV isn't loaded/ready.");
|
||||
|
||||
/*
|
||||
if( !MPV.loaded )
|
||||
return bookPlay(m_currentBook["id"]);
|
||||
*/
|
||||
console.log(`play()`);
|
||||
return MPV.play();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if( MPV.loaded && MPV.playing )
|
||||
return MPV.pause();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function playPause() {
|
||||
if( !MPV.loaded )
|
||||
return play();
|
||||
|
||||
if( MPV.paused )
|
||||
return play();
|
||||
|
||||
return pause();
|
||||
}
|
||||
|
||||
function setPosition(pos) {
|
||||
console.log(`setPosition(${pos})`);
|
||||
MPV.position = pos;
|
||||
}
|
||||
|
||||
// Library functions:
|
||||
function bookPlay(bookid, position) {
|
||||
console.log(`Manager.bookPlay(${bookid}, ${position})`);
|
||||
if( m_currentBook && m_currentBook["id"] === bookid)
|
||||
{
|
||||
console.log(`On current book, taking shortcut...`);
|
||||
if( position )
|
||||
setPosition(position);
|
||||
|
||||
return play();
|
||||
}
|
||||
|
||||
for( let book of manager.m_library )
|
||||
{
|
||||
if( book["id"] === bookid )
|
||||
{
|
||||
MPV.paused = false;
|
||||
MPV.stop();
|
||||
|
||||
let thenPlay = function() {
|
||||
if( !position )
|
||||
position = book["position"];
|
||||
|
||||
let onLoad = function() {
|
||||
if( !MPV.loaded )
|
||||
return;
|
||||
|
||||
if( position )
|
||||
MPV.position = position;
|
||||
|
||||
MPV.loadedChanged.disconnect(onLoad);
|
||||
MPV.play();
|
||||
};
|
||||
MPV.loadedChanged.connect(onLoad);
|
||||
MPV.load(book["path"]);
|
||||
}
|
||||
|
||||
m_currentBook = book;
|
||||
if( !m_currentBook["artUrl"] )
|
||||
{
|
||||
// Get album art, spit it into a cache file:
|
||||
const cachePath = StandardPaths.writableLocation(StandardPaths.CacheLocation);
|
||||
const newUrl = `${cachePath}/${m_currentBook["id"]}.png`;
|
||||
const cachedPath = "image://thumbnails/" + encodeURIComponent(m_currentBook["path"]);
|
||||
let thumber = thumbnail.createObject(manager, {"source":cachedPath, "destination":newUrl});
|
||||
thumber.done.connect(function(success) {
|
||||
console.log("Thumber done.");
|
||||
if( success )
|
||||
m_currentBook["artUrl"] = newUrl;
|
||||
|
||||
thenPlay();
|
||||
Qt.callLater( function() {
|
||||
thumber.destroy();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
thenPlay();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`Couldn't find book with ID ${bookid}!`);
|
||||
}
|
||||
|
||||
Component {
|
||||
id: thumbnail
|
||||
Image {
|
||||
id: image
|
||||
width: 256
|
||||
height: 256
|
||||
fillMode: Image.PreserveAspectFit
|
||||
visible: false
|
||||
|
||||
required property string destination
|
||||
signal done(bool success)
|
||||
onStatusChanged: {
|
||||
if( status === Image.Ready )
|
||||
{
|
||||
image.grabToImage(function(result) {
|
||||
result.saveToFile(destination);
|
||||
console.log(`image captured to ${destination}!`);
|
||||
image.done(true);
|
||||
});
|
||||
}
|
||||
else if( status === Image.Error )
|
||||
{
|
||||
console.log("image.grab failed.");
|
||||
image.done(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function playChapter(chapidx) {
|
||||
if( !m_currentBook )
|
||||
return;
|
||||
|
||||
let ch = m_currentBook.chapters[chapidx];
|
||||
setPosition(ch["start"]);
|
||||
play();
|
||||
}
|
||||
|
||||
function chapterSeek(rel) {
|
||||
if( !m_currentBook || !m_currentChapter )
|
||||
return;
|
||||
|
||||
if( rel < 0 || rel > 1 )
|
||||
return;
|
||||
|
||||
const newPos = m_currentChapter["chapterStart"] + (m_currentChapter["chapterLength"] * rel);
|
||||
setPosition(newPos);
|
||||
}
|
||||
|
||||
// Database:
|
||||
Timer {
|
||||
id: previewTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
property bool doMetadata: false
|
||||
property bool doChapters: false
|
||||
|
||||
onTriggered: handlePreviewLoaded();
|
||||
|
||||
function handlePreviewLoaded() {
|
||||
MPVPreview.stop();
|
||||
|
||||
if( doMetadata ) {
|
||||
const path = LibraryDb.escape("file://" + MPVPreview.source);
|
||||
const title = LibraryDb.escape( MPVPreview.metadata["title"]);
|
||||
const author = LibraryDb.escape(MPVPreview.metadata["artist"]);
|
||||
const desc = LibraryDb.escape(MPVPreview.metadata["comment"]);
|
||||
const length = LibraryDb.escape(MPVPreview.duration);
|
||||
LibraryDb.exec(`UPDATE library SET parsed=1, title='${title}', author='${author}', description='${desc}', length='${length}' WHERE path='${path}'`);
|
||||
}
|
||||
|
||||
if( doChapters ) {
|
||||
const path = LibraryDb.escape("file://" + MPVPreview.source);
|
||||
const ents = LibraryDb.query(`SELECT id FROM library WHERE path='${path}'`);
|
||||
const lid = ents[0]["id"];
|
||||
if( !lid )
|
||||
{
|
||||
console.log(`Couldn't find ID for ${path} in the database.`);
|
||||
return;
|
||||
}
|
||||
|
||||
LibraryDb.exec(`DELETE FROM chapters WHERE bookid=${lid}`);
|
||||
for( let chap of MPVPreview.chapters )
|
||||
{
|
||||
const title = LibraryDb.escape(chap["title"]);
|
||||
LibraryDb.exec(`INSERT INTO chapters (bookid, start, title)VALUES(${lid}, ${chap["time"]}, '${title}')`);
|
||||
}
|
||||
}
|
||||
|
||||
MPVPreview.
|
||||
doMetadata = doChapters = false;
|
||||
dbParsePending();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: positionTimer
|
||||
interval: 30000
|
||||
repeat: true
|
||||
running: MPV.playing && !MPV.paused && MPV.loaded
|
||||
triggeredOnStart: true
|
||||
onTriggered: manager.savePosition();
|
||||
}
|
||||
|
||||
function savePosition() {
|
||||
if( !m_currentBook )
|
||||
return;
|
||||
|
||||
const id = m_currentBook["id"];
|
||||
const position = MPV.position;
|
||||
LibraryDb.exec(`UPDATE library SET position=${position} WHERE id=${id}`)
|
||||
const idx = m_currentBook["index"];
|
||||
m_currentBook["position"] = position;
|
||||
m_library[idx] = m_currentBook; // Update "position" record for LibraryPage.
|
||||
libraryEntryUpdated(idx, m_library[idx]);
|
||||
console.log("New position saved!");
|
||||
}
|
||||
|
||||
function initMpris() {
|
||||
MPRIS.play.connect( function() {
|
||||
console.log("play received via MPRIS2");
|
||||
MPV.play();
|
||||
} );
|
||||
MPRIS.pause.connect( function() {
|
||||
console.log("pause received via MPRIS2");
|
||||
MPV.pause();
|
||||
} );
|
||||
MPRIS.playPause.connect( function() {
|
||||
console.log("playPause received via MPRIS2");
|
||||
MPV.togglePause();
|
||||
} );
|
||||
MPRIS.stop.connect( function() {
|
||||
console.log("stop received via MPRIS2");
|
||||
MPV.stop();
|
||||
} );
|
||||
MPRIS.next.connect( function() {
|
||||
console.log("next received via MPRIS2");
|
||||
const newIdx = manager.m_currentChapter["index"] + 1;
|
||||
manager.playChapter( newIdx );
|
||||
} );
|
||||
MPRIS.previous.connect( function() {
|
||||
console.log("previous received via MPRIS2");
|
||||
const newIdx = manager.m_currentChapter["index"] - 1;
|
||||
manager.playChapter( newIdx );
|
||||
} );
|
||||
MPRIS.seek.connect( function(offset) {
|
||||
console.log(`seek ${offset}s received via MPRIS2`);
|
||||
if( offset > 0 ) {
|
||||
if( MPV.position + offset < manager.m_currentChapter["chapterEnd"] )
|
||||
MPV.position += offset;
|
||||
else
|
||||
MPV.position = manager.m_currentChapter["chapterEnd"];
|
||||
} else {
|
||||
if( MPV.position - offset > manager.m_currentChapter["chapterStart"] )
|
||||
MPV.position += offset;
|
||||
else
|
||||
MPV.position = manager.m_currentChapter["chapterStart"];
|
||||
}
|
||||
} );
|
||||
MPRIS.setPosition.connect( function(position) {
|
||||
console.log(`setPosition ${position}s received via MPRIS2`);
|
||||
} );
|
||||
MPRIS.setRate.connect( function(rate) {
|
||||
console.log(`setRate ${rate}s received via MPRIS2`);
|
||||
} );
|
||||
MPRIS.setVolume.connect( function(volume) {
|
||||
console.log(`setVolume ${volume}s received via MPRIS2`);
|
||||
} );
|
||||
}
|
||||
|
||||
function mprisUpdateMetadata()
|
||||
{
|
||||
let obj = { "id":m_currentBook["id"],
|
||||
"chapter":m_currentChapter["index"],
|
||||
"chapterCount":m_currentBook["chapters"].length,
|
||||
"chapterTitle":m_currentChapter["title"],
|
||||
"artist":m_currentBook["author"],
|
||||
"title":m_currentBook["title"],
|
||||
"duration":m_currentChapter["chapterLength"] };
|
||||
|
||||
if( m_currentBook["artUrl"] )
|
||||
obj["artUrl"] = m_currentBook["artUrl"];
|
||||
|
||||
MPRIS.updateMetadata(obj);
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
onTriggered: mprisUpdatePosition();
|
||||
running: MPV.playing && !MPV.paused
|
||||
}
|
||||
|
||||
function mprisUpdatePosition()
|
||||
{
|
||||
MPRIS.updatePosition(m_currentChapter["chapterPosition"]);
|
||||
}
|
||||
|
||||
function initMpv() {
|
||||
MPV.loadedChanged.connect( function() {
|
||||
manager.loaded = MPV.loaded;
|
||||
});
|
||||
|
||||
MPV.playingChanged.connect( function() {
|
||||
manager.playing = MPV.playing;
|
||||
|
||||
Qt.callLater( function() {
|
||||
ScreenOps.setInhibited(MPV.playing && !MPV.paused, MPV.mediaTitle);
|
||||
} );
|
||||
|
||||
if( MPV.paused )
|
||||
MPRIS.updateStatus("Paused");
|
||||
else if( MPV.playing )
|
||||
MPRIS.updateStatus("Playing");
|
||||
else
|
||||
MPRIS.updateStatus("Stopped");
|
||||
});
|
||||
|
||||
MPV.pausedChanged.connect( function() {
|
||||
manager.paused = MPV.paused;
|
||||
|
||||
if( MPV.paused )
|
||||
MPRIS.updateStatus("Paused");
|
||||
else if( MPV.playing )
|
||||
MPRIS.updateStatus("Playing");
|
||||
else
|
||||
MPRIS.updateStatus("Stopped");
|
||||
});
|
||||
|
||||
MPV.positionChanged.connect( function() {
|
||||
if( !manager.m_currentBook )
|
||||
return;
|
||||
|
||||
const realPos = MPV.position;
|
||||
manager.position = realPos;
|
||||
|
||||
if( !manager.m_currentChapter
|
||||
|| realPos <= manager.m_currentChapter["chapterStart"]
|
||||
|| realPos > manager.m_currentChapter["chapterEnd"] )
|
||||
{
|
||||
// Find which chapter we're in:
|
||||
let chapIndex = 0;
|
||||
let start = 0;
|
||||
let chapterLength = 0;
|
||||
let title = "";
|
||||
for( let a=0; a < manager.m_currentBook.chapters.length; a++ )
|
||||
{
|
||||
let ch = manager.m_currentBook.chapters[a];
|
||||
if( ch["start"] > realPos ) {
|
||||
if( a === manager.m_currentBook["chapters"].length - 1 ) {
|
||||
chapIndex = a;
|
||||
start = ch["start"];
|
||||
chapterLength = manager.m_currentBook["length"] - start;
|
||||
title = ch["title"];
|
||||
} else if( a > 0 ) {
|
||||
chapIndex = a-1;
|
||||
let prev = manager.m_currentBook.chapters[chapIndex];
|
||||
start = prev["start"];
|
||||
chapterLength = ch["start"] - prev["start"];
|
||||
title = prev["title"];
|
||||
} else
|
||||
chapterLength = ch["start"];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
let chapterPosition = realPos - start;
|
||||
let chapterProgress = chapterPosition / chapterLength;
|
||||
console.log(`Chapter changed, new progress: ${chapterProgress}`);
|
||||
manager.m_currentChapter = {
|
||||
"index":chapIndex,
|
||||
"chapterStart":start,
|
||||
"chapterEnd":start+chapterLength,
|
||||
"chapterPosition":chapterPosition,
|
||||
"chapterLength":chapterLength,
|
||||
"chapterProgress":chapterProgress,
|
||||
"title":title
|
||||
};
|
||||
mprisUpdateMetadata();
|
||||
mprisUpdatePosition();
|
||||
savePosition();
|
||||
} else {
|
||||
let chapterPosition = realPos - manager.m_currentChapter["chapterStart"];
|
||||
let chapterProgress = chapterPosition / manager.m_currentChapter["chapterLength"];
|
||||
//console.log(`Chapter progress: ${chapterProgress}`);
|
||||
manager.m_currentChapter = {
|
||||
"index":manager.m_currentChapter["index"],
|
||||
"chapterStart":manager.m_currentChapter["chapterStart"],
|
||||
"chapterEnd":manager.m_currentChapter["chapterEnd"],
|
||||
"chapterPosition":chapterPosition,
|
||||
"chapterLength":manager.m_currentChapter["chapterLength"],
|
||||
"chapterProgress":chapterProgress,
|
||||
"title":manager.m_currentChapter["title"]
|
||||
};
|
||||
}
|
||||
} );
|
||||
|
||||
MPVPreview.metadataChanged.connect( function() {
|
||||
if( Object.keys(MPVPreview.metadata).length === 0 )
|
||||
return;
|
||||
|
||||
previewTimer.doMetadata = true;
|
||||
} );
|
||||
|
||||
MPVPreview.chaptersChanged.connect( function() {
|
||||
if( MPVPreview.chapters.length < 1 )
|
||||
return;
|
||||
|
||||
previewTimer.doChapters = true;
|
||||
});
|
||||
|
||||
MPVPreview.loadedChanged.connect( function() {
|
||||
if( !MPVPreview.loaded )
|
||||
return;
|
||||
|
||||
Qt.callLater( function() {
|
||||
previewTimer.handlePreviewLoaded();
|
||||
} );
|
||||
});
|
||||
}
|
||||
|
||||
function dbInit() {
|
||||
if( !LibraryDb.open() )
|
||||
return console.error("Failed to open database!");
|
||||
|
||||
dbUpdate();
|
||||
}
|
||||
|
||||
function dbParsePending() {
|
||||
if( m_toParse.length < 1 ) {
|
||||
dbLoadLibrary();
|
||||
console.log(`All details read!`);
|
||||
return;
|
||||
}
|
||||
|
||||
let ent = m_toParse.pop();
|
||||
const plainpath = ent["fullpath"].substring(7);
|
||||
console.log(`Reading details for ${ent["fullpath"]}...`);
|
||||
|
||||
//previewTimer.start();
|
||||
MPVPreview.volume = 0;
|
||||
MPVPreview.load( plainpath );
|
||||
}
|
||||
|
||||
function dbUpdate() {
|
||||
LibraryDb.exec("CREATE TABLE IF NOT EXISTS library (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT, modified DATETIME, size BIGINT, parsed TINYINT(1), title TEXT, author TEXT, description TEXT, length DECIMAL(8,2), position DECIMAL(8,2))");
|
||||
LibraryDb.exec("CREATE TABLE IF NOT EXISTS chapters (id INTEGER PRIMARY KEY AUTOINCREMENT, bookid INT NOT NULL, start DECIMAL(8,2), title TEXT)");
|
||||
}
|
||||
|
||||
function dbNewEntry(ent) {
|
||||
const path = LibraryDb.escape(ent["fullpath"]);
|
||||
const modified = LibraryDb.escape(ent["modified"]);
|
||||
const size = LibraryDb.escape(ent["size"]);
|
||||
|
||||
LibraryDb.exec(`INSERT INTO library (path, modified, size, parsed)VALUES('${path}', '${modified}', '${size}', 0)`);
|
||||
dbQueueParse(ent);
|
||||
}
|
||||
|
||||
function dbQueueParse(ent)
|
||||
{
|
||||
m_toParse.push( ent );
|
||||
}
|
||||
|
||||
function dbCheck(ent) {
|
||||
const qstr = `SELECT id, path, modified, size, parsed, title, author, description, length, position FROM library WHERE path='${LibraryDb.escape(ent["fullpath"])}'`;
|
||||
let rows = LibraryDb.query(qstr);
|
||||
|
||||
if( 0 === rows.length )
|
||||
return dbNewEntry(ent);
|
||||
|
||||
if( 1 !== rows[0]["parsed"] )
|
||||
return dbQueueParse(ent);
|
||||
}
|
||||
|
||||
function dbLoadLibrary() {
|
||||
let books = LibraryDb.query("SELECT id, path, modified, size, title, author, description, length, position FROM library");
|
||||
for( let a=0; a < books.length; a++ ) {
|
||||
let b = books[a];
|
||||
// A little cleanup:
|
||||
b["index"] = a;
|
||||
b["description"] = b["description"].replace(/\\"/g, '"').replace(/\\n/g, '');
|
||||
b["chapters"] = LibraryDb.query(`SELECT id, start, title FROM chapters WHERE bookid=${b['id']}`);
|
||||
}
|
||||
|
||||
manager.m_library = books;
|
||||
//console.log(JSON.stringify(books,null,4));
|
||||
}
|
||||
}
|
||||
130
qml/MiniPlayerOverlay.qml
Normal file
130
qml/MiniPlayerOverlay.qml
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Item {
|
||||
id: playerBar
|
||||
|
||||
readonly property real miniPlayerHeight: playerLayout.implicitHeight + (Kirigami.Units.largeSpacing * 2)
|
||||
|
||||
height: visible ? playerBar.opacity * miniPlayerHeight : 0
|
||||
visible: opacity > 0
|
||||
opacity: playerBar.wantShown ? 1 : 0
|
||||
//Behavior on opacity { PropertyAnimation { duration: 200 } }
|
||||
|
||||
property bool wantShown: false
|
||||
signal cardClicked()
|
||||
|
||||
Kirigami.AbstractCard {
|
||||
id: playerCard
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: playerBar.width - (Kirigami.Units.largeSpacing * 2)
|
||||
height: playerLayout.implicitHeight + Kirigami.Units.largeSpacing // FIXME: This is needed for ProgressBar to show... broken stinky logic here.
|
||||
|
||||
showClickFeedback: true
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: {
|
||||
playerBar.cardClicked();
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: playerLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
columns: 5
|
||||
columnSpacing: Kirigami.Units.mediumSpacing
|
||||
rowSpacing: Kirigami.Units.smallSpacing
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
Layout.rowSpan: 3
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: manager.m_currentBook ? "image://thumbnails/" + encodeURIComponent(manager.m_currentBook["path"]) : ""
|
||||
smooth: true
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 4
|
||||
elide: Text.ElideRight
|
||||
level: 2
|
||||
text: manager.m_currentBook ? manager.m_currentBook["title"] : ""
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: Kirigami.Theme.textColor
|
||||
text: manager.m_currentBook ? manager.m_currentBook["author"] : ""
|
||||
font.pixelSize: Kirigami.Theme.smallFont.pixelSize
|
||||
}
|
||||
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "media-seek-backward"
|
||||
Layout.rowSpan: 2
|
||||
enabled: manager.m_currentChapter["index"] > 0
|
||||
onClicked: {
|
||||
const newIdx = manager.m_currentChapter["index"] - 1;
|
||||
manager.playChapter( newIdx );
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: manager.playing ? "media-playback-pause" : "media-playback-start"
|
||||
Layout.rowSpan: 2
|
||||
onClicked: manager.playPause();
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "media-seek-forward"
|
||||
Layout.rowSpan: 2
|
||||
enabled: manager.m_currentBook ? manager.m_currentChapter["index"] < (manager.m_currentBook["chapters"].length - 1) : false
|
||||
onClicked: {
|
||||
const newIdx = manager.m_currentChapter["index"] + 1;
|
||||
manager.playChapter( newIdx );
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Kirigami.Theme.smallFont.pixelSize
|
||||
color: Kirigami.Theme.textColor
|
||||
text: manager.m_currentChapter["title"]
|
||||
}
|
||||
|
||||
/*
|
||||
// --- NOTE ---
|
||||
// DO NOT USE ProgressBar!
|
||||
// It hammers the CPU at IDLE as of Qt 6.10.1 which pins a mobile CPU core to 100%!
|
||||
// ------------
|
||||
QQC2.ProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 5
|
||||
|
||||
from: 0
|
||||
to: 1
|
||||
value: manager.m_currentChapter["chapterProgress"]
|
||||
//visible: manager.m_currentChapter["chapterProgress"] > 0 && manager.m_currentChapter["chapterProgress"] < 1
|
||||
|
||||
onValueChanged: console.log(`Value changed: ${value}`);
|
||||
}
|
||||
*/
|
||||
AProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 5
|
||||
|
||||
value: manager.m_currentChapter["chapterProgress"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
214
qml/PageLibrary.qml
Normal file
214
qml/PageLibrary.qml
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
id: libraryPageItem
|
||||
title: qsTr("Library")
|
||||
focus: false
|
||||
|
||||
property bool searchMode: false
|
||||
property alias bookListModel: bookListView.model
|
||||
//property var bookListModel
|
||||
|
||||
signal cardClicked(variant cardInfo)
|
||||
|
||||
header: Kirigami.SearchField {
|
||||
visible: opacity > 0
|
||||
height: libraryPageItem.searchMode ? implicitHeight : 0
|
||||
opacity: height < implicitHeight ? (height / implicitHeight) : 1
|
||||
Behavior on height { PropertyAnimation { duration: 150 } }
|
||||
|
||||
onAccepted: libraryPageItem.searchTextChanged(text)
|
||||
focus: true
|
||||
Keys.onEscapePressed: libraryPageItem.searchMode = false
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "search"
|
||||
onTriggered: {
|
||||
libraryPageItem.searchMode = !libraryPageItem.searchMode;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
//Kirigami.CardsListView
|
||||
ListView {
|
||||
id: bookListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
delegate: Kirigami.AbstractCard { // QQC2.Pane {
|
||||
id: card
|
||||
required property variant modelData
|
||||
required property int index
|
||||
readonly property bool isPlaying: (manager.m_currentBook && modelData["path"] === manager.m_currentBook["path"]) ? true : false
|
||||
|
||||
height: cardInner.implicitHeight + Kirigami.Units.smallSpacing*2
|
||||
clip: true
|
||||
showClickFeedback: true
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: {
|
||||
libraryPageItem.cardClicked(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: cardInner
|
||||
width: parent.width - Kirigami.Units.largeSpacing
|
||||
anchors.centerIn: parent
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 4
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
|
||||
source: "image://thumbnails/" + encodeURIComponent(modelData.path)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.title
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: modelData.author
|
||||
}
|
||||
QQC2.Label {
|
||||
id: textPosition
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: card.isPlaying && manager.position >= 0 ?
|
||||
qsTr("%1 / %2").arg(manager.formatSeconds(manager.position)).arg(manager.formatSeconds(modelData.length))
|
||||
: progressBar.timeposition > 0 ?
|
||||
qsTr("%1 / %2").arg(manager.formatSeconds(progressBar.timeposition)).arg(manager.formatSeconds(modelData.length)) : manager.formatSeconds(modelData.length)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
QQC2.ToolButton {
|
||||
icon.name: card.isPlaying && manager.playing ? "media-playback-pause" : "media-playback-start"
|
||||
onClicked: {
|
||||
if( !card.isPlaying )
|
||||
return manager.bookPlay(modelData.id);
|
||||
|
||||
manager.playPause();
|
||||
}
|
||||
}
|
||||
/*
|
||||
QQC2.ToolButton {
|
||||
icon.name: "overflow-menu"
|
||||
onClicked: {
|
||||
cardSheet.open();
|
||||
}
|
||||
|
||||
Kirigami.OverlayDrawer {
|
||||
id: cardSheet
|
||||
edge: Qt.BottomEdge
|
||||
modal: true
|
||||
handleVisible: true
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
//padding: Kirigami.Units.largeSpacing
|
||||
|
||||
Repeater {
|
||||
model: cardSheet.actions
|
||||
delegate: Kirigami.NavigationTabButton {
|
||||
display: Kirigami.NavigationTabButton.TextBesideIcon
|
||||
text: modelData.text
|
||||
icon.name: modelData.icon.name
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
cardSheet.close();
|
||||
modelData.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<Kirigami.Action> actions: [
|
||||
Kirigami.Action { text: "Play"; icon.name: "media-playback-start"; onTriggered: {
|
||||
manager.bookPlay(modelData.id);
|
||||
} },
|
||||
Kirigami.Action { text: "Share"; icon.name: "document-share"; onTriggered: console.log("share") },
|
||||
Kirigami.Action { text: "Delete"; icon.name: "edit-delete"; onTriggered: console.log("delete") }
|
||||
]
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
/*
|
||||
// --- NOTE ---
|
||||
// DO NOT USE ProgressBar!
|
||||
// It hammers the CPU at IDLE as of Qt 6.10.1 which pins a mobile CPU core to 100%!
|
||||
// ------------
|
||||
QQC2.ProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
|
||||
property real timeposition: modelData.position
|
||||
readonly property real progress: timeposition ? (timeposition / modelData.length) : 0
|
||||
|
||||
from: 0
|
||||
to: 1
|
||||
value: progress
|
||||
visible: progress > 0 && progress < 1
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: manager
|
||||
function onLibraryEntryUpdated(idx, entry) {
|
||||
console.log("HUP.");
|
||||
if( index !== idx )
|
||||
return;
|
||||
|
||||
progressBar.timeposition = entry.position;
|
||||
}
|
||||
}
|
||||
*/
|
||||
AProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 5
|
||||
|
||||
property real timeposition: modelData.position
|
||||
readonly property real progress: timeposition ? (timeposition / modelData.length) : 0
|
||||
value: progress
|
||||
visible: progress > 0 && progress < 1
|
||||
|
||||
Connections {
|
||||
target: manager
|
||||
function onLibraryEntryUpdated(idx, entry) {
|
||||
if( index !== idx )
|
||||
return;
|
||||
|
||||
progressBar.timeposition = entry.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // ListView
|
||||
} // ColumnLayout
|
||||
} // Page
|
||||
299
qml/PageNowPlaying.qml
Normal file
299
qml/PageNowPlaying.qml
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
id: nowPlayingPageItem
|
||||
title: isPlaying ? qsTr("Now Playing") : qsTr("Book Details")
|
||||
|
||||
property variant m_book
|
||||
signal closePage()
|
||||
|
||||
readonly property bool isPlaying: (m_book && manager.m_currentBook && m_book["path"] === manager.m_currentBook["path"]) ? true : false
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "go-previous"
|
||||
onTriggered: {
|
||||
closePage();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Flickable {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
/*
|
||||
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
contentWidth: -1 // disable horizontal scroll
|
||||
*/
|
||||
contentWidth: parent.width
|
||||
contentHeight: column.height
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
width: scrollView.width
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: column.width * 0.75
|
||||
//Layout.preferredWidth: height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: nowPlayingPageItem.m_book ? "image://thumbnails/" + encodeURIComponent(nowPlayingPageItem.m_book["path"]) : ""
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: nowPlayingPageItem.m_book ? nowPlayingPageItem.m_book["title"] : ""
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
level: 3
|
||||
text: nowPlayingPageItem.m_book ? qsTr("By <b>%1</b>").arg(nowPlayingPageItem.m_book["author"]) : ""
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: labelBookTime
|
||||
font: Kirigami.Theme.smallFont
|
||||
color: Kirigami.Theme.textColor
|
||||
text: nowPlayingPageItem.m_book ?
|
||||
nowPlayingPageItem.m_book["position"] > 0 ? qsTr("%1 / %2").arg(manager.formatSeconds(nowPlayingPageItem.m_book["position"])).arg(manager.formatSeconds(nowPlayingPageItem.m_book["length"]))
|
||||
: qsTr("%1").arg(manager.formatSeconds(nowPlayingPageItem.m_book["length"]))
|
||||
: ""
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Connections {
|
||||
target: MPV
|
||||
function onPositionChanged() {
|
||||
labelBookTime.text = qsTr("%1 / %2").arg(manager.formatSeconds(manager.position)).arg(manager.formatSeconds(nowPlayingPageItem.m_book["length"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
weight: Kirigami.Separator.Weight.Light
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
font: Kirigami.Theme.defaultFont
|
||||
color: Kirigami.Theme.textColor
|
||||
text: nowPlayingPageItem.m_book ? nowPlayingPageItem.m_book["description"] : ""
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
weight: Kirigami.Separator.Weight.Light
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: nowPlayingPageItem.isPlaying
|
||||
Layout.preferredWidth: parent.width * 0.75
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "media-skip-backward"
|
||||
enabled: manager.m_currentChapter["index"] > 0
|
||||
onClicked: {
|
||||
const newIdx = manager.m_currentChapter["index"] - 1;
|
||||
manager.playChapter( newIdx );
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("Prev")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "go-previous-skip"
|
||||
onClicked: {
|
||||
if( manager.position + 30 > manager.m_currentChapter["chapterStart"] )
|
||||
manager.setPosition(manager.position - 30);
|
||||
else
|
||||
manager.setPosition(manager.m_currentChapter["chapterStart"]);
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("30s")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "go-previous"
|
||||
onClicked: {
|
||||
if( manager.position + 5 > manager.m_currentChapter["chapterStart"] )
|
||||
manager.setPosition(manager.position - 5);
|
||||
else
|
||||
manager.setPosition(manager.m_currentChapter["chapterStart"]);
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("5s")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
Layout.fillWidth: true
|
||||
icon.name: manager.playing ? "media-playback-pause" : "media-playback-start"
|
||||
onClicked: manager.playPause();
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: manager.playing ? qsTr("Pause") : qsTr("Play")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "go-next"
|
||||
onClicked: {
|
||||
if( manager.position + 5 < manager.m_currentChapter["chapterEnd"] )
|
||||
manager.setPosition(manager.position + 5);
|
||||
else
|
||||
manager.setPosition(manager.m_currentChapter["chapterEnd"]);
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("5s")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "go-next-skip"
|
||||
onClicked: {
|
||||
if( manager.position + 30 < manager.m_currentChapter["chapterEnd"] )
|
||||
manager.setPosition(manager.position + 30);
|
||||
else
|
||||
manager.setPosition(manager.m_currentChapter["chapterEnd"]);
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("30s")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "media-skip-forward"
|
||||
enabled: manager.m_currentBook ? manager.m_currentChapter["index"] < (manager.m_currentBook["chapters"].length - 1) : false
|
||||
onClicked: {
|
||||
const newIdx = manager.m_currentChapter["index"] + 1;
|
||||
manager.playChapter( newIdx );
|
||||
}
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
text: qsTr("Next")
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Slider {
|
||||
id: chapterScrubber
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
value: manager.m_currentChapter["chapterProgress"]
|
||||
onMoved: {
|
||||
manager.chapterSeek(value);
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Kirigami.Theme.smallFont.pixelSize
|
||||
color: Kirigami.Theme.textColor
|
||||
text: qsTr("%1 - %2 / %3").arg(manager.m_currentChapter["title"]).arg(manager.formatSeconds(manager.m_currentChapter["chapterPosition"])).arg(manager.formatSeconds(manager.m_currentChapter["chapterLength"]))
|
||||
}
|
||||
} // ColumnLayout (player controls)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: parent.width * 0.75
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Button {
|
||||
visible: !nowPlayingPageItem.isPlaying
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
Layout.fillWidth: true
|
||||
text: atStart ? qsTr("Start Book") : qsTr("Resume")
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: manager.bookPlay(nowPlayingPageItem.m_book["id"]);
|
||||
readonly property bool atStart: nowPlayingPageItem.m_book["position"] < 1
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
display: QQC2.AbstractButton.TextUnderIcon
|
||||
visible: nowPlayingPageItem.isPlaying ?
|
||||
manager.m_currentBook["position"] > (manager.m_currentBook["length"] - 10)
|
||||
: nowPlayingPageItem.m_book["position"] > (nowPlayingPageItem.m_book["length"] - 10)
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Start Over")
|
||||
icon.name: "edit-reset"
|
||||
onClicked: manager.bookPlay(nowPlayingPageItem.m_book["id"], 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
//visible: nowPlayingPageItem.isPlaying
|
||||
weight: Kirigami.Separator.Weight.Light
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
level: 3
|
||||
text: qsTr("Chapters")
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
*/
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: listviewChapters
|
||||
//Layout.fillWidth: true
|
||||
//Layout.preferredHeight: contentHeight > scrollView.height ? scrollView.height : contentHeight
|
||||
|
||||
model: nowPlayingPageItem.m_book ? nowPlayingPageItem.m_book["chapters"] : []
|
||||
Kirigami.AbstractCard {
|
||||
//height: innerLayout.height
|
||||
//width: listviewChapters.width
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight + innerLayout.implicitHeight
|
||||
|
||||
required property variant modelData
|
||||
required property int index
|
||||
|
||||
clip: true
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: nowPlayingPageItem.isPlaying && index === manager.m_currentChapter["index"]
|
||||
color: Kirigami.Theme.activeBackgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: innerLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: modelData["title"]
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
if( nowPlayingPageItem.isPlaying )
|
||||
return manager.playChapter( index );
|
||||
|
||||
manager.bookPlay(nowPlayingPageItem.m_book["id"], modelData["start"]+0.0001);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Repeater (chapters)
|
||||
} // ColumnLayout (chapters)
|
||||
} // ColumnLayout (biggun)
|
||||
} // Flickable
|
||||
}
|
||||
181
qml/main.qml
Normal file
181
qml/main.qml
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import QtCore
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
width: 400
|
||||
height: 720
|
||||
visible: true
|
||||
minimumWidth: Kirigami.Units.gridUnit * 20
|
||||
minimumHeight: Kirigami.Units.gridUnit * 30
|
||||
title: "Audiobook Player"
|
||||
|
||||
property bool showingNowPlaying: true
|
||||
|
||||
Manager {
|
||||
id: manager
|
||||
|
||||
Component.onCompleted: {
|
||||
manager.initMpris();
|
||||
manager.initMpv();
|
||||
manager.dbInit();
|
||||
|
||||
let locs = StandardPaths.standardLocations(StandardPaths.DocumentsLocation);
|
||||
manager.m_home = locs[0] + "/Audiobooks";
|
||||
manager.goHome();
|
||||
|
||||
// TODO: This is a dumb hack. Our "initialPage" is Library.
|
||||
root.showingNowPlaying = false;
|
||||
}
|
||||
|
||||
onContentsChanged: function(newlist) {
|
||||
console.log(`New contents: ${JSON.stringify(newlist,null,4)}`);
|
||||
}
|
||||
}
|
||||
|
||||
property variant pagesModel: [
|
||||
{"code":"nowplaying", "name":"Now Playing", "icon":"contact-new", "page":nowPlayingPage, "hideMiniPlayer":true },
|
||||
{"code":"library", "name":"Library", "icon":"user-home", "page":libraryPage }
|
||||
/*
|
||||
{"name":"Recently Played", "icon":"document-open-recent"},
|
||||
{"name":"Authors", "icon":"contact-new"},
|
||||
{"name":"Favorites", "icon":"media-tape"},
|
||||
{"name":"Series", "icon":"multimedia-player"}
|
||||
*/
|
||||
]
|
||||
|
||||
property string currentPageCode
|
||||
function showPageByCode(code, properties) {
|
||||
for( let page of pagesModel )
|
||||
{
|
||||
if( page["code"] !== code )
|
||||
continue;
|
||||
|
||||
currentPageCode = code;
|
||||
|
||||
pageStack.clear(QQC2.StackView.ReplaceTransition);
|
||||
pageStack.push(page.page, properties);
|
||||
menuDrawer.close();
|
||||
|
||||
root.showingNowPlaying = page.hideMiniPlayer ? true : false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`showPageByCode couldn't find a page with code "${code}"`);
|
||||
}
|
||||
|
||||
globalDrawer: Kirigami.GlobalDrawer {
|
||||
id: menuDrawer
|
||||
title: "Menu"
|
||||
modal: true
|
||||
width: Kirigami.Units.gridUnit * 14
|
||||
|
||||
edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
//padding: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.AbstractApplicationHeader {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.largeSpacing
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: "Audiobooks"
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
contentWidth: -1 // disable horizontal scroll
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
width: scrollView.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.pagesModel
|
||||
delegate: QQC2.Button { //Kirigami.NavigationTabButton {
|
||||
display: Kirigami.NavigationTabButton.TextBesideIcon
|
||||
text: modelData["name"]
|
||||
icon.name: modelData["icon"]
|
||||
|
||||
flat: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
if( modelData["code"] === "nowplaying" )
|
||||
root.showPageByCode(modelData["code"], {"m_book":manager.m_currentBook});
|
||||
else
|
||||
root.showPageByCode(modelData["code"]);
|
||||
}
|
||||
enabled: modelData["code"] !== "nowplaying" || (manager.playing || manager.paused)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator { Layout.fillWidth: true }
|
||||
|
||||
Kirigami.NavigationTabButton {
|
||||
display: Kirigami.NavigationTabButton.TextBesideIcon
|
||||
text: "Settings"
|
||||
icon.name: "preferences-other"
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
// stub
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageStack.initialPage: libraryPage
|
||||
pageStack.spacing: 0
|
||||
|
||||
Component {
|
||||
id: nowPlayingPage
|
||||
PageNowPlaying {
|
||||
visible: isCurrentPage
|
||||
onClosePage: {
|
||||
root.showPageByCode("library");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: libraryPage
|
||||
PageLibrary {
|
||||
visible: isCurrentPage
|
||||
bookListModel: manager.m_library
|
||||
onCardClicked: function(bookInfo) {
|
||||
root.showPageByCode("nowplaying", {"m_book":bookInfo});
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
contextDrawer: Kirigami.ContextDrawer {
|
||||
id: ctxDrawer
|
||||
title: "Page Actions"
|
||||
}
|
||||
*/
|
||||
footer: MiniPlayerOverlay {
|
||||
id: playerBar
|
||||
wantShown: (manager.playing || manager.paused) && !root.showingNowPlaying
|
||||
onCardClicked: {
|
||||
root.showPageByCode("nowplaying", {"m_book":manager.m_currentBook});
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/file.cpp
Normal file
80
src/file.cpp
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#include <QQmlEngine>
|
||||
|
||||
#include "file.h"
|
||||
|
||||
File::File(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QVariantList File::readDir(const QString &path)
|
||||
{
|
||||
QString filepath = QUrl(path).toLocalFile();
|
||||
QDir d(filepath);
|
||||
QStringList nameFilters;
|
||||
nameFilters << "*.mp3" << "*.opus" << "*.flac" << "*.m4a" << "*.m4b" << "*.aac";
|
||||
QFileInfoList ents = d.entryInfoList(nameFilters, QDir::Drives | QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::DirsFirst | QDir::Name);
|
||||
|
||||
QVariantList results;
|
||||
for( const QFileInfo &ent : std::as_const(ents) ) {
|
||||
QString filename = ent.fileName();
|
||||
|
||||
QVariantMap vent;
|
||||
vent["type"] = ent.isDir() ? "dir" : "file";
|
||||
vent["name"] = filename;
|
||||
vent["fullpath"] = "file://" + ent.absoluteFilePath();
|
||||
vent["dirpath"] = "file://" + ent.absolutePath();
|
||||
vent["size"] = ent.size();
|
||||
vent["modified"] = ent.lastModified();
|
||||
vent["mime"] = ent.isDir() ? "dir" : "file";
|
||||
|
||||
results.append(vent);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
QByteArray File::readFile(const QString &path)
|
||||
{
|
||||
QString filepath = QUrl(path).toLocalFile();
|
||||
QFile f(filepath);
|
||||
if( !f.open(QIODevice::ReadOnly) )
|
||||
{
|
||||
qDebug() << "Failed to read file: " << filepath;
|
||||
//qmlEngine(this)->throwError(tr("Failed to read file!"));
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QByteArray r = f.readAll();
|
||||
f.close();
|
||||
return r;
|
||||
}
|
||||
|
||||
bool File::writeFile(const QString &path, const QByteArray &data)
|
||||
{
|
||||
QString filepath = QUrl(path).toLocalFile();
|
||||
QFile f(filepath);
|
||||
if( !f.open(QIODevice::WriteOnly) )
|
||||
{
|
||||
qDebug() << "Failed to open file: " << filepath;
|
||||
//qmlEngine(this)->throwError(tr("Failed to read file!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
qint64 ops = f.write(data);
|
||||
if( data.length() != ops )
|
||||
return false;
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString File::toBase64(const QByteArray &data)
|
||||
{
|
||||
return QString::fromLatin1( data.toBase64() );
|
||||
}
|
||||
|
||||
QByteArray File::fromBase64(const QString &data)
|
||||
{
|
||||
return QByteArray::fromBase64(data.toLatin1());
|
||||
}
|
||||
28
src/file.h
Normal file
28
src/file.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef FILE_H
|
||||
#define FILE_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class File : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit File(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QVariantList readDir(const QString &path);
|
||||
Q_INVOKABLE QByteArray readFile(const QString &path);
|
||||
|
||||
Q_INVOKABLE bool writeFile(const QString &path, const QByteArray &data);
|
||||
|
||||
Q_INVOKABLE QString toBase64(const QByteArray &data);
|
||||
Q_INVOKABLE QByteArray fromBase64(const QString &data);
|
||||
|
||||
signals:
|
||||
|
||||
};
|
||||
|
||||
#endif // FILE_H
|
||||
103
src/librarydb.cpp
Normal file
103
src/librarydb.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "librarydb.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
|
||||
LibraryDb::LibraryDb(QObject *parent) : QObject(parent) {}
|
||||
|
||||
LibraryDb::~LibraryDb() {
|
||||
close();
|
||||
}
|
||||
|
||||
QString LibraryDb::makeDbPath() {
|
||||
QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
if( !base.isEmpty() )
|
||||
base = QDir(base).filePath("library.sqlite3");
|
||||
return base;
|
||||
}
|
||||
|
||||
void LibraryDb::ensureDirExists(const QString &dirPath) {
|
||||
if (!dirPath.isEmpty() && !QDir(dirPath).exists())
|
||||
QDir().mkpath(dirPath);
|
||||
}
|
||||
|
||||
bool LibraryDb::open() {
|
||||
if (m_db.isOpen())
|
||||
return true;
|
||||
|
||||
m_dbPath = makeDbPath();
|
||||
if (m_dbPath.isEmpty())
|
||||
return false;
|
||||
|
||||
ensureDirExists(QFileInfo(m_dbPath).absolutePath());
|
||||
|
||||
m_connName = QStringLiteral("librarydb_%1_%2")
|
||||
.arg(quintptr(QThread::currentThreadId()))
|
||||
.arg(QDateTime::currentMSecsSinceEpoch());
|
||||
|
||||
m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connName);
|
||||
m_db.setDatabaseName(m_dbPath);
|
||||
|
||||
return m_db.open();
|
||||
}
|
||||
|
||||
void LibraryDb::close() {
|
||||
if (m_connName.isEmpty())
|
||||
return;
|
||||
|
||||
if (m_db.isOpen())
|
||||
m_db.close();
|
||||
|
||||
m_db = QSqlDatabase();
|
||||
QSqlDatabase::removeDatabase(m_connName);
|
||||
m_connName.clear();
|
||||
}
|
||||
|
||||
int LibraryDb::exec(const QString &sql) {
|
||||
if (!m_db.isOpen() && !open())
|
||||
return -1;
|
||||
|
||||
QSqlQuery q(m_db);
|
||||
if (!q.exec(sql))
|
||||
return -1;
|
||||
|
||||
return q.numRowsAffected();
|
||||
}
|
||||
|
||||
QVariantList LibraryDb::query(const QString &sql) {
|
||||
QVariantList rows;
|
||||
|
||||
if (!m_db.isOpen() && !open())
|
||||
return rows;
|
||||
|
||||
QSqlQuery q(m_db);
|
||||
if (!q.exec(sql))
|
||||
return rows;
|
||||
|
||||
const QSqlRecord rec = q.record();
|
||||
const int cols = rec.count();
|
||||
|
||||
while (q.next()) {
|
||||
QVariantMap row;
|
||||
for (int i = 0; i < cols; ++i)
|
||||
row.insert(rec.fieldName(i), q.value(i));
|
||||
rows.push_back(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
QString LibraryDb::escape(const QString &s) {
|
||||
if( !m_db.isOpen() )
|
||||
return QString();
|
||||
|
||||
QString out = s;
|
||||
out.replace('\'', "''");
|
||||
return out;
|
||||
}
|
||||
33
src/librarydb.h
Normal file
33
src/librarydb.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSqlDatabase>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
class LibraryDb : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LibraryDb(QObject *parent = nullptr);
|
||||
~LibraryDb() override;
|
||||
|
||||
Q_INVOKABLE bool open();
|
||||
Q_INVOKABLE void close();
|
||||
|
||||
Q_INVOKABLE bool isOpen() const { return m_db.isOpen(); }
|
||||
Q_INVOKABLE QString dbPath() const { return m_dbPath; }
|
||||
|
||||
Q_INVOKABLE int exec(const QString &sql); // rows affected, -1 on failure
|
||||
Q_INVOKABLE QVariantList query(const QString &sql);
|
||||
|
||||
Q_INVOKABLE QString escape(const QString &s);
|
||||
|
||||
private:
|
||||
static QString makeDbPath();
|
||||
static void ensureDirExists(const QString &dirPath);
|
||||
|
||||
QSqlDatabase m_db;
|
||||
QString m_connName;
|
||||
QString m_dbPath;
|
||||
};
|
||||
64
src/main.cpp
Normal file
64
src/main.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "file.h"
|
||||
#include "mpvaudio.h"
|
||||
#include "screen.h"
|
||||
#include "thumbnailprovider.h"
|
||||
#include "librarydb.h"
|
||||
#include "mpris.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication app(argc, argv);
|
||||
app.setApplicationDisplayName("Audiobooks");
|
||||
app.setApplicationName("Audiobooks");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("ONeill Codesmithing");
|
||||
app.setOrganizationDomain("oneill.app");
|
||||
app.setDesktopFileName("Audiobooks.desktop");
|
||||
|
||||
setlocale(LC_NUMERIC, "C");
|
||||
|
||||
// set up for db:
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir().mkpath(dir);
|
||||
// QString dbPath = dir + "/library.sqlite3";
|
||||
|
||||
// set up for audiobooks:
|
||||
QStringList docLocations = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
|
||||
dir = docLocations[0] + QDir::separator() + "Audiobooks";
|
||||
QDir().mkpath(dir);
|
||||
|
||||
// qml time:
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
engine.addImageProvider("thumbnails", new ThumbnailProvider);
|
||||
|
||||
engine.rootContext()->setContextProperty("File", new File());
|
||||
engine.rootContext()->setContextProperty("MPV", new MpvAudio());
|
||||
engine.rootContext()->setContextProperty("MPVPreview", new MpvAudio());
|
||||
engine.rootContext()->setContextProperty("ScreenOps", new Screen());
|
||||
engine.rootContext()->setContextProperty("LibraryDb", new LibraryDb());
|
||||
engine.rootContext()->setContextProperty("MPRIS", new Mpris());
|
||||
|
||||
QObject::connect(
|
||||
&engine,
|
||||
&QQmlApplicationEngine::objectCreationFailed,
|
||||
&app,
|
||||
[]() { QCoreApplication::exit(-1); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
|
||||
QUrl path = QUrl(QStringLiteral("qrc:/qml/main.qml"));
|
||||
if( argc > 1 )
|
||||
path = QUrl(QString::fromLocal8Bit(argv[1]));
|
||||
|
||||
engine.load(path);
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
103
src/mpris.cpp
Normal file
103
src/mpris.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "mpris.h"
|
||||
#include "mprisadaptor.h"
|
||||
#include "mprisplayer.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDebug>
|
||||
|
||||
MprisContainer::MprisContainer(Mpris *mpris)
|
||||
: QObject{}
|
||||
{
|
||||
m_adaptor = new MprisAdaptor(this);
|
||||
m_player = new MprisPlayer(this, mpris);
|
||||
|
||||
auto bus = QDBusConnection::sessionBus();
|
||||
|
||||
bus.registerService("org.mpris.MediaPlayer2.audiobookplayer");
|
||||
|
||||
auto success = bus.registerObject(
|
||||
"/org/mpris/MediaPlayer2",
|
||||
this, QDBusConnection::ExportAdaptors
|
||||
/*
|
||||
QDBusConnection::ExportAllSlots |
|
||||
QDBusConnection::ExportAllProperties |
|
||||
QDBusConnection::ExportAllSignals |
|
||||
QDBusConnection::ExportAdaptors
|
||||
*/
|
||||
);
|
||||
|
||||
qDebug() << "Registration of MPRIS object:" << success;
|
||||
}
|
||||
|
||||
Mpris::Mpris(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
m_root = new MprisContainer(this);
|
||||
}
|
||||
|
||||
Mpris::~Mpris() {
|
||||
m_root->deleteLater();
|
||||
m_root = nullptr;
|
||||
}
|
||||
|
||||
void Mpris::updateMetadata(const QVariantMap &metadata)
|
||||
{
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
m_root->m_player->updateMetadata(metadata);
|
||||
propertiesChanged();
|
||||
}
|
||||
|
||||
void Mpris::updateStatus(const QString &status)
|
||||
{
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
m_root->m_player->updateStatus(status);
|
||||
propertiesChanged();
|
||||
}
|
||||
|
||||
void Mpris::updateRate(double rate)
|
||||
{
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
return m_root->m_player->updateRate(rate);
|
||||
}
|
||||
|
||||
void Mpris::updateVolume(double volume)
|
||||
{
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
return m_root->m_player->updateVolume(volume);
|
||||
}
|
||||
|
||||
void Mpris::updatePosition(double position)
|
||||
{
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
m_root->m_player->updatePosition(position);
|
||||
propertiesChanged();
|
||||
}
|
||||
|
||||
void Mpris::propertiesChanged() {
|
||||
if( !m_root || !m_root->m_player )
|
||||
return;
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
|
||||
QVariantMap changed;
|
||||
changed["Metadata"] = m_root->m_player->metadata();
|
||||
changed["PlaybackStatus"] = m_root->m_player->playbackStatus();
|
||||
|
||||
auto message = QDBusMessage::createSignal(
|
||||
"/org/mpris/MediaPlayer2",
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged"
|
||||
);
|
||||
message.setArguments({
|
||||
"org.mpris.MediaPlayer2.Player",
|
||||
changed,
|
||||
QStringList{}
|
||||
});
|
||||
|
||||
bus.send(message);
|
||||
}
|
||||
63
src/mpris.h
Normal file
63
src/mpris.h
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#ifndef MPRIS_H
|
||||
#define MPRIS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Mpris;
|
||||
class MprisAdaptor;
|
||||
class MprisPlayer;
|
||||
class MprisContainer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MprisContainer(Mpris *parent = nullptr);
|
||||
|
||||
MprisAdaptor *m_adaptor;
|
||||
MprisPlayer *m_player;
|
||||
};
|
||||
|
||||
class Mpris : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
MprisContainer *m_root;
|
||||
|
||||
public:
|
||||
explicit Mpris(QObject *parent = nullptr);
|
||||
~Mpris();
|
||||
|
||||
void wantPlay() { emit play(); }
|
||||
void wantPause() { emit pause(); }
|
||||
void wantPlayPause() { emit playPause(); }
|
||||
void wantStop() { emit stop(); }
|
||||
void wantNext() { emit next(); }
|
||||
void wantPrevious() { emit previous(); }
|
||||
void wantSeek(double v) { emit seek(v); }
|
||||
void wantSetPosition(double v) { emit setPosition(v); }
|
||||
void wantSetRate(double v) { emit setRate(v); }
|
||||
void wantSetVolume(double v) { emit setVolume(v); }
|
||||
|
||||
public slots:
|
||||
void updateMetadata(const QVariantMap &metadata);
|
||||
void updateStatus(const QString &status);
|
||||
void updateRate(double rate);
|
||||
void updateVolume(double volume);
|
||||
void updatePosition(double position);
|
||||
|
||||
void propertiesChanged();
|
||||
|
||||
signals:
|
||||
void play();
|
||||
void pause();
|
||||
void playPause();
|
||||
void stop();
|
||||
void next();
|
||||
void previous();
|
||||
void seek(double offset);
|
||||
void setPosition(double position);
|
||||
void setRate(double rate);
|
||||
void setVolume(double volume);
|
||||
};
|
||||
|
||||
#endif // MPRIS_H
|
||||
5
src/mprisadaptor.cpp
Normal file
5
src/mprisadaptor.cpp
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#include "mprisadaptor.h"
|
||||
|
||||
MprisAdaptor::MprisAdaptor(QObject *parent)
|
||||
: QDBusAbstractAdaptor{parent}
|
||||
{}
|
||||
38
src/mprisadaptor.h
Normal file
38
src/mprisadaptor.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef MPRISADAPTOR_H
|
||||
#define MPRISADAPTOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#include <QDBusAbstractAdaptor>
|
||||
|
||||
class MprisAdaptor : public QDBusAbstractAdaptor
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
|
||||
|
||||
Q_PROPERTY(bool CanQuit READ canQuit CONSTANT)
|
||||
Q_PROPERTY(bool CanRaise READ canRaise CONSTANT)
|
||||
Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT)
|
||||
Q_PROPERTY(QString Identity READ identity CONSTANT)
|
||||
Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT)
|
||||
Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT)
|
||||
Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT)
|
||||
|
||||
public:
|
||||
explicit MprisAdaptor(QObject *parent = nullptr);
|
||||
|
||||
bool canQuit() const { return true; }
|
||||
bool canRaise() const { return true; }
|
||||
bool hasTrackList() const { return false; }
|
||||
QString identity() const { return QStringLiteral("Audiobook Player"); }
|
||||
QString desktopEntry() const { return QStringLiteral("Audiobooks"); }
|
||||
QStringList supportedUriSchemes() const { return { "file" }; }
|
||||
QStringList supportedMimeTypes() const { return { "audio/mpeg", "audio/mp4", "audio/x-m4b" }; }
|
||||
|
||||
public slots:
|
||||
void Quit() {} // TODO
|
||||
void Raise() {} // TODO
|
||||
};
|
||||
|
||||
|
||||
#endif // MPRISADAPTOR_H
|
||||
134
src/mprisplayer.cpp
Normal file
134
src/mprisplayer.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "mprisplayer.h"
|
||||
#include "mpris.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
|
||||
MprisPlayer::MprisPlayer(QObject *parent, Mpris *mpris)
|
||||
: QDBusAbstractAdaptor{parent},
|
||||
m_mpris{mpris}
|
||||
{
|
||||
}
|
||||
|
||||
QString MprisPlayer::playbackStatus() const
|
||||
{
|
||||
return m_status;
|
||||
}
|
||||
|
||||
void MprisPlayer::updateMetadata(const QVariantMap &metadata)
|
||||
{
|
||||
m_bookid = metadata["id"].toInt();
|
||||
m_chapter = metadata["chapter"].toInt();
|
||||
m_chapterCount = metadata["chapterCount"].toInt();
|
||||
m_trackId = QDBusObjectPath(QString("/org/mpris/MediaPlayer2/track/%1/%2").arg(m_bookid).arg(m_chapter));
|
||||
m_metadata = {
|
||||
{ "mpris:trackid", QVariant::fromValue(m_trackId) },
|
||||
{ "xesam:title", metadata["chapterTitle"].toString() },
|
||||
{ "xesam:artist", QStringList{ metadata["artist"].toString() } },
|
||||
{ "xesam:album", metadata["title"].toString() },
|
||||
{ "mpris:length", qlonglong(metadata["duration"].toUInt() * 1000000) },
|
||||
{ "xesam:genre", QStringList{ "Audiobook" } }
|
||||
};
|
||||
if( metadata.contains("artUrl") )
|
||||
m_metadata["mpris:artUrl"] = QUrl(metadata.value("artUrl").toString()).toString(QUrl::FullyEncoded);
|
||||
//m_metadata["xesam:artUrl"] = metadata.value("artUrl").toString();
|
||||
|
||||
qDebug() << m_metadata;
|
||||
|
||||
emit metadataChanged();
|
||||
}
|
||||
|
||||
void MprisPlayer::updateStatus(const QString &status)
|
||||
{
|
||||
if( m_status == status )
|
||||
return;
|
||||
|
||||
if( status == "Playing" || status == "Paused" || status == "Stopped" )
|
||||
{
|
||||
m_status = status;
|
||||
emit playbackStatusChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MprisPlayer::updateRate(double rate)
|
||||
{
|
||||
if( m_rate == rate )
|
||||
return;
|
||||
|
||||
m_rate = rate;
|
||||
emit rateChanged();
|
||||
}
|
||||
|
||||
void MprisPlayer::updateVolume(double volume)
|
||||
{
|
||||
if( m_volume == volume )
|
||||
return;
|
||||
|
||||
m_volume = volume;
|
||||
emit volumeChanged();
|
||||
}
|
||||
|
||||
void MprisPlayer::updatePosition(double position)
|
||||
{
|
||||
qlonglong newpos = 1000000 * position;
|
||||
if( newpos == m_positionUs )
|
||||
return;
|
||||
m_positionUs = newpos;
|
||||
emit positionChanged();
|
||||
|
||||
emit Seeked(m_positionUs);
|
||||
}
|
||||
|
||||
void MprisPlayer::Play()
|
||||
{
|
||||
m_mpris->wantPlay();
|
||||
}
|
||||
|
||||
void MprisPlayer::Pause()
|
||||
{
|
||||
m_mpris->wantPause();
|
||||
}
|
||||
|
||||
void MprisPlayer::PlayPause()
|
||||
{
|
||||
m_mpris->wantPlayPause();
|
||||
}
|
||||
|
||||
void MprisPlayer::Stop()
|
||||
{
|
||||
m_mpris->wantStop();
|
||||
}
|
||||
|
||||
void MprisPlayer::Next()
|
||||
{
|
||||
m_mpris->wantNext();
|
||||
}
|
||||
|
||||
void MprisPlayer::Previous()
|
||||
{
|
||||
m_mpris->wantPrevious();
|
||||
}
|
||||
|
||||
void MprisPlayer::Seek(qlonglong offsetUs)
|
||||
{
|
||||
m_mpris->wantSeek(offsetUs * 0.000001);
|
||||
}
|
||||
|
||||
void MprisPlayer::SetPosition(const QDBusObjectPath &p, qlonglong posUs)
|
||||
{
|
||||
if( p != m_trackId )
|
||||
return;
|
||||
|
||||
m_mpris->wantSetPosition(posUs * 0.000001);
|
||||
}
|
||||
|
||||
void MprisPlayer::setRate(double r)
|
||||
{
|
||||
m_mpris->wantSetRate(r);
|
||||
}
|
||||
|
||||
void MprisPlayer::setVolume(double v)
|
||||
{
|
||||
m_mpris->wantSetVolume(v);
|
||||
}
|
||||
93
src/mprisplayer.h
Normal file
93
src/mprisplayer.h
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#ifndef MPRISPLAYER_H
|
||||
#define MPRISPLAYER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusAbstractAdaptor>
|
||||
|
||||
class Mpris;
|
||||
class MprisPlayer : public QDBusAbstractAdaptor
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
|
||||
|
||||
Q_PROPERTY(QString PlaybackStatus READ playbackStatus NOTIFY playbackStatusChanged)
|
||||
Q_PROPERTY(QString LoopStatus READ loopStatus CONSTANT)
|
||||
Q_PROPERTY(double Rate READ rate WRITE setRate NOTIFY rateChanged)
|
||||
Q_PROPERTY(bool Shuffle READ shuffle CONSTANT)
|
||||
Q_PROPERTY(QVariantMap Metadata READ metadata NOTIFY metadataChanged)
|
||||
Q_PROPERTY(double Volume READ volume WRITE setVolume NOTIFY volumeChanged)
|
||||
Q_PROPERTY(qlonglong Position READ position NOTIFY positionChanged)
|
||||
|
||||
Q_PROPERTY(bool CanGoNext READ canGoNext NOTIFY canGoNextChanged)
|
||||
Q_PROPERTY(bool CanGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged)
|
||||
Q_PROPERTY(bool CanPlay READ canPlay CONSTANT)
|
||||
Q_PROPERTY(bool CanPause READ canPause CONSTANT)
|
||||
Q_PROPERTY(bool CanSeek READ canSeek CONSTANT)
|
||||
Q_PROPERTY(bool CanControl READ canControl CONSTANT)
|
||||
|
||||
public:
|
||||
explicit MprisPlayer(QObject *parent, Mpris *mpris);
|
||||
|
||||
QString playbackStatus() const;
|
||||
QString loopStatus() const { return QStringLiteral("None"); }
|
||||
double rate() const { return m_rate; }
|
||||
bool shuffle() const { return false; }
|
||||
QVariantMap metadata() const { return m_metadata; }
|
||||
double volume() const { return m_volume; }
|
||||
qlonglong position() const { return m_positionUs; }
|
||||
|
||||
bool canGoNext() const { return true; }
|
||||
bool canGoPrevious() const { return true; }
|
||||
bool canPlay() const { return true; }
|
||||
bool canPause() const { return true; }
|
||||
bool canSeek() const { return true; }
|
||||
bool canControl() const { return true; }
|
||||
|
||||
// For Mpris class to fiddle with:
|
||||
void updateMetadata(const QVariantMap &metadata);
|
||||
void updateStatus(const QString &status);
|
||||
void updateRate(double rate);
|
||||
void updateVolume(double volume);
|
||||
void updatePosition(double position);
|
||||
|
||||
public slots:
|
||||
void Play();
|
||||
void Pause();
|
||||
void PlayPause();
|
||||
void Stop();
|
||||
void Next();
|
||||
void Previous();
|
||||
void Seek(qlonglong offsetUs);
|
||||
void SetPosition(const QDBusObjectPath &, qlonglong posUs);
|
||||
|
||||
void setRate(double r);
|
||||
void setVolume(double v);
|
||||
|
||||
signals:
|
||||
void Seeked(qlonglong Position);
|
||||
|
||||
void playbackStatusChanged();
|
||||
void metadataChanged();
|
||||
void rateChanged();
|
||||
void volumeChanged();
|
||||
void positionChanged();
|
||||
void canGoNextChanged();
|
||||
void canGoPreviousChanged();
|
||||
|
||||
private:
|
||||
Mpris *m_mpris;
|
||||
QString m_status = "Stopped";
|
||||
double m_rate = 1.0;
|
||||
double m_volume = 1.0;
|
||||
qlonglong m_positionUs = 0;
|
||||
QVariantMap m_metadata;
|
||||
int m_bookid;
|
||||
int m_chapter;
|
||||
int m_chapterCount;
|
||||
|
||||
QDBusObjectPath m_trackId;
|
||||
};
|
||||
|
||||
#endif // MPRISPLAYER_H
|
||||
407
src/mpvaudio.cpp
Normal file
407
src/mpvaudio.cpp
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
#include "mpvaudio.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
static constexpr int PROP_TIME_POS = 1;
|
||||
static constexpr int PROP_DURATION = 2;
|
||||
static constexpr int PROP_PAUSE = 3;
|
||||
static constexpr int PROP_SPEED = 4;
|
||||
static constexpr int PROP_VOLUME = 5;
|
||||
static constexpr int PROP_MEDIA_TITLE = 6;
|
||||
static constexpr int PROP_CORE_IDLE = 7;
|
||||
//static constexpr int PROP_MEDIA_ARTIST = 8;
|
||||
|
||||
static constexpr int PROP_METADATA = 20; // "metadata" (map)
|
||||
static constexpr int PROP_CHAPTER_LIST = 21; // "chapter-list" (array)
|
||||
static constexpr int PROP_CHAPTER = 22; // "chapter" (int)
|
||||
static constexpr int PROP_CHAPTER_META = 23; // "chapter-metadata" (map)
|
||||
|
||||
MpvAudio::MpvAudio(QObject *parent) : QObject(parent) {
|
||||
initMpv();
|
||||
}
|
||||
|
||||
MpvAudio::~MpvAudio() {
|
||||
if (m_notifier) {
|
||||
m_notifier->setEnabled(false);
|
||||
delete m_notifier;
|
||||
m_notifier = nullptr;
|
||||
}
|
||||
if (m_mpv) {
|
||||
mpv_set_wakeup_callback(m_mpv, nullptr, nullptr);
|
||||
mpv_terminate_destroy(m_mpv);
|
||||
m_mpv = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MpvAudio::initMpv() {
|
||||
m_mpv = mpv_create();
|
||||
if (!m_mpv) {
|
||||
emit errorOccurred(QStringLiteral("mpv_create() failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Audio-only
|
||||
mpv_set_option_string(m_mpv, "video", "no");
|
||||
mpv_set_option_string(m_mpv, "vid", "no");
|
||||
mpv_set_option_string(m_mpv, "vo", "null");
|
||||
|
||||
// Audiobook niceties
|
||||
mpv_set_option_string(m_mpv, "audio-pitch-correction", "yes");
|
||||
mpv_set_option_string(m_mpv, "gapless-audio", "yes");
|
||||
mpv_set_option_string(m_mpv, "keep-open", "yes");
|
||||
|
||||
// Reduce log spam
|
||||
mpv_set_option_string(m_mpv, "terminal", "yes");
|
||||
mpv_set_option_string(m_mpv, "msg-level", "all=warn");
|
||||
|
||||
if (mpv_initialize(m_mpv) < 0) {
|
||||
emit errorOccurred(QStringLiteral("mpv_initialize() failed"));
|
||||
mpv_terminate_destroy(m_mpv);
|
||||
m_mpv = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
mpv_set_wakeup_callback(m_mpv, &MpvAudio::mpvWakeup, this);
|
||||
/*
|
||||
int fd = mpv_get_wakeup_pipe(m_mpv);
|
||||
if (fd >= 0) {
|
||||
m_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
|
||||
connect(m_notifier, &QSocketNotifier::activated, this, &MpvAudio::drainMpvEvents);
|
||||
}
|
||||
*/
|
||||
observeProperties();
|
||||
}
|
||||
|
||||
void MpvAudio::observeProperties() {
|
||||
if (!m_mpv) return;
|
||||
|
||||
mpv_observe_property(m_mpv, PROP_TIME_POS, "time-pos", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(m_mpv, PROP_DURATION, "duration", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(m_mpv, PROP_PAUSE, "pause", MPV_FORMAT_FLAG);
|
||||
mpv_observe_property(m_mpv, PROP_SPEED, "speed", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(m_mpv, PROP_VOLUME, "volume", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(m_mpv, PROP_MEDIA_TITLE, "media-title", MPV_FORMAT_STRING);
|
||||
//mpv_observe_property(m_mpv, PROP_MEDIA_ARTIST,"media-artist", MPV_FORMAT_STRING);
|
||||
mpv_observe_property(m_mpv, PROP_CORE_IDLE, "core-idle", MPV_FORMAT_FLAG);
|
||||
|
||||
mpv_observe_property(m_mpv, PROP_METADATA, "metadata", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(m_mpv, PROP_CHAPTER_LIST, "chapter-list", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(m_mpv, PROP_CHAPTER, "chapter", MPV_FORMAT_INT64);
|
||||
mpv_observe_property(m_mpv, PROP_CHAPTER_META, "chapter-metadata", MPV_FORMAT_NODE);
|
||||
}
|
||||
|
||||
void MpvAudio::mpvWakeup(void *ctx) {
|
||||
auto *self = static_cast<MpvAudio*>(ctx);
|
||||
if (!self) return;
|
||||
QMetaObject::invokeMethod(self, "drainMpvEvents", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void MpvAudio::drainMpvEvents() {
|
||||
if (!m_mpv) return;
|
||||
|
||||
while (true) {
|
||||
mpv_event *ev = mpv_wait_event(m_mpv, 0);
|
||||
if (!ev || ev->event_id == MPV_EVENT_NONE) break;
|
||||
handleEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool fuzzySame(double a, double b) {
|
||||
return qFuzzyCompare(a + 1.0, b + 1.0);
|
||||
}
|
||||
|
||||
static inline bool notableTimeGap(double a, double b) {
|
||||
if( qAbs( a-b ) < 0.25 )
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariantMap MpvAudio::pullNodeMap(const char *prop) {
|
||||
QVariantMap out;
|
||||
if (!m_mpv) return out;
|
||||
|
||||
mpv_node n{};
|
||||
if (mpv_get_property(m_mpv, prop, MPV_FORMAT_NODE, &n) >= 0) {
|
||||
out = nodeToMap(n);
|
||||
mpv_free_node_contents(&n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QVariantList MpvAudio::pullNodeList(const char *prop) {
|
||||
QVariantList out;
|
||||
if (!m_mpv) return out;
|
||||
|
||||
mpv_node n{};
|
||||
if (mpv_get_property(m_mpv, prop, MPV_FORMAT_NODE, &n) >= 0) {
|
||||
out = nodeToList(n);
|
||||
mpv_free_node_contents(&n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void MpvAudio::handleEvent(mpv_event *ev) {
|
||||
switch( ev->event_id ) {
|
||||
case MPV_EVENT_START_FILE: {
|
||||
if (m_loaded) { m_loaded = false; emit loadedChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
case MPV_EVENT_FILE_LOADED: {
|
||||
// Force refresh: mpv_observe_property is fine, but this guarantees immediate population.
|
||||
// (Implement pullNodeProperty below.)
|
||||
m_metadata = pullNodeMap("metadata");
|
||||
emit metadataChanged();
|
||||
|
||||
m_chapters = pullNodeList("chapter-list");
|
||||
emit chaptersChanged();
|
||||
|
||||
m_chapterMetadata = pullNodeMap("chapter-metadata");
|
||||
emit chapterMetadataChanged();
|
||||
|
||||
if (!m_loaded) { m_loaded = true; emit loadedChanged(); }
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev->event_id != MPV_EVENT_PROPERTY_CHANGE) return;
|
||||
|
||||
auto *p = static_cast<mpv_event_property*>(ev->data);
|
||||
if (!p) return;
|
||||
|
||||
switch (ev->reply_userdata) {
|
||||
case PROP_TIME_POS: {
|
||||
if (!p->data) break;
|
||||
double v = *static_cast<double*>(p->data);
|
||||
if (!notableTimeGap(m_position, v)) { m_position = v; emit positionChanged(); }
|
||||
break;
|
||||
}
|
||||
case PROP_DURATION: {
|
||||
if (!p->data) break;
|
||||
double v = *static_cast<double*>(p->data);
|
||||
if (!fuzzySame(m_duration, v)) { m_duration = v; emit durationChanged(); }
|
||||
break;
|
||||
}
|
||||
case PROP_PAUSE: {
|
||||
if (!p->data) break;
|
||||
bool v = (*static_cast<int*>(p->data)) != 0;
|
||||
if (m_paused != v) { m_paused = v; emit pausedChanged(); }
|
||||
break;
|
||||
}
|
||||
case PROP_SPEED: {
|
||||
if (!p->data) break;
|
||||
double v = *static_cast<double*>(p->data);
|
||||
if (!fuzzySame(m_speed, v)) { m_speed = v; emit speedChanged(); }
|
||||
break;
|
||||
}
|
||||
case PROP_VOLUME: {
|
||||
if (!p->data) break;
|
||||
double v = *static_cast<double*>(p->data);
|
||||
if (!fuzzySame(m_volume, v)) { m_volume = v; emit volumeChanged(); }
|
||||
break;
|
||||
}
|
||||
case PROP_MEDIA_TITLE: {
|
||||
const char *s = p->data ? *static_cast<const char**>(p->data) : nullptr;
|
||||
QString v = s ? QString::fromUtf8(s) : QString();
|
||||
if (m_mediaTitle != v) { m_mediaTitle = v; emit mediaTitleChanged(); }
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case PROP_MEDIA_ARTIST: {
|
||||
const char *s = p->data ? *static_cast<const char**>(p->data) : nullptr;
|
||||
QString v = s ? QString::fromUtf8(s) : QString();
|
||||
if (m_mediaArtist != v) { m_mediaArtist = v; emit mediaArtistChanged(); }
|
||||
break;
|
||||
}
|
||||
*/
|
||||
case PROP_CORE_IDLE: {
|
||||
if (!p->data) break;
|
||||
bool idle = (*static_cast<int*>(p->data)) != 0;
|
||||
bool newPlaying = !idle && !m_paused;
|
||||
if (m_playing != newPlaying) { m_playing = newPlaying; emit playingChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
case PROP_METADATA: {
|
||||
if (!p->data) { // cleared
|
||||
if (!m_metadata.isEmpty()) { m_metadata.clear(); emit metadataChanged(); }
|
||||
break;
|
||||
}
|
||||
const mpv_node &n = *static_cast<mpv_node*>(p->data);
|
||||
QVariantMap v = nodeToMap(n);
|
||||
if (m_metadata != v) { m_metadata = std::move(v); emit metadataChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
case PROP_CHAPTER_LIST: {
|
||||
if (!p->data) {
|
||||
if (!m_chapters.isEmpty()) { m_chapters.clear(); emit chaptersChanged(); }
|
||||
break;
|
||||
}
|
||||
const mpv_node &n = *static_cast<mpv_node*>(p->data);
|
||||
QVariantList v = nodeToList(n);
|
||||
if (m_chapters != v) { m_chapters = std::move(v); emit chaptersChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
case PROP_CHAPTER: {
|
||||
if (!p->data) break;
|
||||
qint64 v = *static_cast<qint64*>(p->data);
|
||||
int vi = (v < 0) ? -1 : int(v);
|
||||
if (m_currentChapter != vi) { m_currentChapter = vi; emit currentChapterChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
case PROP_CHAPTER_META: {
|
||||
if (!p->data) {
|
||||
if (!m_chapterMetadata.isEmpty()) { m_chapterMetadata.clear(); emit chapterMetadataChanged(); }
|
||||
break;
|
||||
}
|
||||
const mpv_node &n = *static_cast<mpv_node*>(p->data);
|
||||
QVariantMap v = nodeToMap(n);
|
||||
if (m_chapterMetadata != v) { m_chapterMetadata = std::move(v); emit chapterMetadataChanged(); }
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MpvAudio::setPropBool(const char *name, bool v) {
|
||||
if (!m_mpv) return;
|
||||
int flag = v ? 1 : 0;
|
||||
if (mpv_set_property(m_mpv, name, MPV_FORMAT_FLAG, &flag) < 0)
|
||||
emit errorOccurred(QStringLiteral("Failed to set %1").arg(QString::fromLatin1(name)));
|
||||
}
|
||||
|
||||
void MpvAudio::setPropDouble(const char *name, double v) {
|
||||
if (!m_mpv) return;
|
||||
if (mpv_set_property(m_mpv, name, MPV_FORMAT_DOUBLE, &v) < 0)
|
||||
emit errorOccurred(QStringLiteral("Failed to set %1").arg(QString::fromLatin1(name)));
|
||||
}
|
||||
|
||||
void MpvAudio::setPropInt64(const char *name, qint64 v) {
|
||||
if (!m_mpv) return;
|
||||
if (mpv_set_property(m_mpv, name, MPV_FORMAT_INT64, &v) < 0)
|
||||
emit errorOccurred(QStringLiteral("Failed to set %1").arg(QString::fromLatin1(name)));
|
||||
}
|
||||
|
||||
// === node conversion ===
|
||||
|
||||
QVariant MpvAudio::nodeToVariant(const mpv_node &n) {
|
||||
switch (n.format) {
|
||||
case MPV_FORMAT_STRING: return n.u.string ? QString::fromUtf8(n.u.string) : QVariant();
|
||||
case MPV_FORMAT_FLAG: return bool(n.u.flag);
|
||||
case MPV_FORMAT_INT64: return qint64(n.u.int64);
|
||||
case MPV_FORMAT_DOUBLE: return double(n.u.double_);
|
||||
case MPV_FORMAT_NODE_MAP: return nodeToMap(n);
|
||||
case MPV_FORMAT_NODE_ARRAY:return nodeToList(n);
|
||||
case MPV_FORMAT_NONE:
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap MpvAudio::nodeToMap(const mpv_node &n) {
|
||||
QVariantMap out;
|
||||
if (n.format != MPV_FORMAT_NODE_MAP) return out;
|
||||
|
||||
const mpv_node_list *lst = n.u.list;
|
||||
if (!lst) return out;
|
||||
|
||||
for (int i = 0; i < lst->num; ++i) {
|
||||
const char *k = lst->keys ? lst->keys[i] : nullptr;
|
||||
if (!k) continue;
|
||||
out.insert(QString::fromUtf8(k), nodeToVariant(lst->values[i]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QVariantList MpvAudio::nodeToList(const mpv_node &n) {
|
||||
QVariantList out;
|
||||
if (n.format != MPV_FORMAT_NODE_ARRAY) return out;
|
||||
|
||||
const mpv_node_list *lst = n.u.list;
|
||||
if (!lst) return out;
|
||||
|
||||
out.reserve(lst->num);
|
||||
for (int i = 0; i < lst->num; ++i)
|
||||
out.push_back(nodeToVariant(lst->values[i]));
|
||||
return out;
|
||||
}
|
||||
|
||||
/*** Controls ***/
|
||||
|
||||
void MpvAudio::load(const QUrl &url) {
|
||||
if (!m_mpv) return;
|
||||
|
||||
const QString s = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||
if (s.isEmpty()) { emit errorOccurred(QStringLiteral("Empty URL")); return; }
|
||||
|
||||
QByteArray u8 = s.toUtf8();
|
||||
const char *cmd[] = { "loadfile", u8.constData(), "replace", nullptr };
|
||||
int r = mpv_command(m_mpv, cmd);
|
||||
if (r < 0) emit errorOccurred(QStringLiteral("loadfile failed: %1").arg(QString::fromUtf8(mpv_error_string(r))));
|
||||
|
||||
//setPaused(false);
|
||||
m_source = url;
|
||||
emit sourceChanged();
|
||||
}
|
||||
|
||||
void MpvAudio::play() { setPaused(false); }
|
||||
void MpvAudio::pause() { setPaused(true); }
|
||||
void MpvAudio::togglePause() { setPaused(!m_paused); }
|
||||
|
||||
void MpvAudio::stop() {
|
||||
if (!m_mpv) return;
|
||||
const char *cmd[] = { "stop", nullptr };
|
||||
mpv_command(m_mpv, cmd);
|
||||
}
|
||||
|
||||
void MpvAudio::seek(double seconds, bool absolute) {
|
||||
if (!m_mpv) return;
|
||||
QByteArray t = QByteArray::number(seconds, 'f', 3);
|
||||
const char *mode = absolute ? "absolute" : "relative";
|
||||
const char *cmd[] = { "seek", t.constData(), mode, nullptr };
|
||||
int r = mpv_command(m_mpv, cmd);
|
||||
if (r < 0) emit errorOccurred(QStringLiteral("seek failed: %1").arg(QString::fromUtf8(mpv_error_string(r))));
|
||||
}
|
||||
|
||||
void MpvAudio::seekBy(double deltaSeconds) { seek(deltaSeconds, false); }
|
||||
|
||||
void MpvAudio::setLoop(bool enabled) {
|
||||
if( !m_mpv ) return;
|
||||
mpv_set_option_string(m_mpv, "loop-file", enabled ? "inf" : "no");
|
||||
}
|
||||
|
||||
void MpvAudio::setPaused(bool v) {
|
||||
if (m_paused == v) return;
|
||||
setPropBool("pause", v);
|
||||
}
|
||||
|
||||
void MpvAudio::setPosition(double seconds) { seek(seconds, true); }
|
||||
|
||||
void MpvAudio::setSpeed(double v) {
|
||||
if (v < 0.25) v = 0.25;
|
||||
if (v > 4.0) v = 4.0;
|
||||
setPropDouble("speed", v);
|
||||
}
|
||||
|
||||
void MpvAudio::setVolume(double v) {
|
||||
if (v < 0.0) v = 0.0;
|
||||
if (v > 130.0) v = 130.0;
|
||||
setPropDouble("volume", v);
|
||||
}
|
||||
|
||||
void MpvAudio::setCurrentChapter(int idx) {
|
||||
if (idx < -1) idx = -1;
|
||||
setPropInt64("chapter", idx);
|
||||
}
|
||||
|
||||
void MpvAudio::goToChapter(int index) {
|
||||
setCurrentChapter(index);
|
||||
}
|
||||
152
src/mpvaudio.h
Normal file
152
src/mpvaudio.h
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#ifndef MPVAUDIO_H
|
||||
#define MPVAUDIO_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QVariantList>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
class MpvAudio : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QUrl source READ source WRITE load NOTIFY sourceChanged)
|
||||
Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
|
||||
Q_PROPERTY(bool playing READ playing NOTIFY playingChanged)
|
||||
Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged)
|
||||
|
||||
Q_PROPERTY(double position READ position WRITE setPosition NOTIFY positionChanged)
|
||||
Q_PROPERTY(double duration READ duration NOTIFY durationChanged)
|
||||
|
||||
Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged)
|
||||
Q_PROPERTY(double volume READ volume WRITE setVolume NOTIFY volumeChanged)
|
||||
|
||||
Q_PROPERTY(QString mediaTitle READ mediaTitle NOTIFY mediaTitleChanged)
|
||||
Q_PROPERTY(QString mediaArtist READ mediaArtist NOTIFY mediaArtistChanged)
|
||||
|
||||
Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged)
|
||||
Q_PROPERTY(QVariantList chapters READ chapters NOTIFY chaptersChanged)
|
||||
Q_PROPERTY(int currentChapter READ currentChapter WRITE setCurrentChapter NOTIFY currentChapterChanged)
|
||||
Q_PROPERTY(int chapterCount READ chapterCount NOTIFY chaptersChanged)
|
||||
Q_PROPERTY(QVariantMap chapterMetadata READ chapterMetadata NOTIFY chapterMetadataChanged)
|
||||
|
||||
public:
|
||||
explicit MpvAudio(QObject *parent = nullptr);
|
||||
~MpvAudio() override;
|
||||
|
||||
Q_INVOKABLE void load(const QUrl &url);
|
||||
Q_INVOKABLE void play();
|
||||
Q_INVOKABLE void pause();
|
||||
Q_INVOKABLE void togglePause();
|
||||
Q_INVOKABLE void stop();
|
||||
Q_INVOKABLE void seek(double seconds, bool absolute = true);
|
||||
Q_INVOKABLE void seekBy(double deltaSeconds);
|
||||
Q_INVOKABLE void setLoop(bool enabled);
|
||||
|
||||
Q_INVOKABLE void goToChapter(int index);
|
||||
|
||||
QUrl source() const { return m_source; }
|
||||
|
||||
bool loaded() const { return m_loaded; }
|
||||
|
||||
bool playing() const { return m_playing; }
|
||||
|
||||
bool paused() const { return m_paused; }
|
||||
void setPaused(bool v);
|
||||
|
||||
double position() const { return m_position; }
|
||||
void setPosition(double seconds);
|
||||
|
||||
double duration() const { return m_duration; }
|
||||
|
||||
double speed() const { return m_speed; }
|
||||
void setSpeed(double v);
|
||||
|
||||
double volume() const { return m_volume; }
|
||||
void setVolume(double v);
|
||||
|
||||
QString mediaTitle() const { return m_mediaTitle; }
|
||||
QString mediaArtist() const { return m_mediaArtist; }
|
||||
|
||||
QVariantMap metadata() const { return m_metadata; }
|
||||
QVariantList chapters() const { return m_chapters; }
|
||||
|
||||
int currentChapter() const { return m_currentChapter; }
|
||||
void setCurrentChapter(int idx);
|
||||
|
||||
int chapterCount() const { return m_chapters.size(); }
|
||||
|
||||
QVariantMap chapterMetadata() const { return m_chapterMetadata; }
|
||||
|
||||
signals:
|
||||
void sourceChanged();
|
||||
void loadedChanged();
|
||||
|
||||
void playingChanged();
|
||||
void pausedChanged();
|
||||
|
||||
void positionChanged();
|
||||
void durationChanged();
|
||||
|
||||
void speedChanged();
|
||||
void volumeChanged();
|
||||
|
||||
void mediaTitleChanged();
|
||||
void mediaArtistChanged();
|
||||
|
||||
void metadataChanged();
|
||||
void chaptersChanged();
|
||||
void currentChapterChanged();
|
||||
void chapterMetadataChanged();
|
||||
|
||||
void errorOccurred(QString message);
|
||||
|
||||
private slots:
|
||||
void drainMpvEvents();
|
||||
|
||||
private:
|
||||
static void mpvWakeup(void *ctx);
|
||||
|
||||
void initMpv();
|
||||
void observeProperties();
|
||||
void handleEvent(mpv_event *ev);
|
||||
|
||||
void setPropBool(const char *name, bool v);
|
||||
void setPropDouble(const char *name, double v);
|
||||
void setPropInt64(const char *name, qint64 v);
|
||||
|
||||
static QVariant nodeToVariant(const mpv_node &n);
|
||||
static QVariantMap nodeToMap(const mpv_node &n);
|
||||
static QVariantList nodeToList(const mpv_node &n);
|
||||
|
||||
QVariantMap pullNodeMap(const char *prop);
|
||||
QVariantList pullNodeList(const char *prop);
|
||||
|
||||
mpv_handle *m_mpv = nullptr;
|
||||
QSocketNotifier *m_notifier = nullptr;
|
||||
|
||||
QUrl m_source;
|
||||
bool m_loaded = false;
|
||||
|
||||
bool m_playing = false;
|
||||
bool m_paused = true;
|
||||
|
||||
double m_position = 0.0;
|
||||
double m_duration = 0.0;
|
||||
|
||||
double m_speed = 1.0;
|
||||
double m_volume = 100.0;
|
||||
|
||||
QString m_mediaTitle;
|
||||
QString m_mediaArtist;
|
||||
|
||||
QVariantMap m_metadata;
|
||||
QVariantList m_chapters;
|
||||
int m_currentChapter = -1;
|
||||
QVariantMap m_chapterMetadata;
|
||||
};
|
||||
|
||||
#endif // MPVAUDIO_H
|
||||
68
src/screen.cpp
Normal file
68
src/screen.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#include "screen.h"
|
||||
|
||||
Screen::Screen(QObject *parent)
|
||||
: QObject{parent}
|
||||
{}
|
||||
|
||||
void Screen::setInhibited(bool on, const QString &reason)
|
||||
{
|
||||
if(on == m_inhibited)
|
||||
return;
|
||||
|
||||
if(on)
|
||||
enableInhibit(reason);
|
||||
else
|
||||
disableInhibit();
|
||||
}
|
||||
|
||||
void Screen::enableInhibit(const QString &reason)
|
||||
{
|
||||
// Try org.freedesktop.ScreenSaver first
|
||||
if (tryInhibit("org.freedesktop.ScreenSaver",
|
||||
"/ScreenSaver",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
reason))
|
||||
return;
|
||||
|
||||
// Fallback: KDE power management
|
||||
tryInhibit("org.freedesktop.PowerManagement",
|
||||
"/org/freedesktop/PowerManagement/Inhibit",
|
||||
"org.freedesktop.PowerManagement.Inhibit",
|
||||
reason);
|
||||
}
|
||||
|
||||
bool Screen::tryInhibit(const QString &service,
|
||||
const QString &path,
|
||||
const QString &ifaceName,
|
||||
const QString &reason)
|
||||
{
|
||||
QDBusInterface iface(service, path, ifaceName, QDBusConnection::sessionBus());
|
||||
if (!iface.isValid())
|
||||
return false;
|
||||
|
||||
QDBusReply<uint> reply = iface.call("Inhibit",
|
||||
qApp->applicationName(),
|
||||
reason);
|
||||
if (!reply.isValid())
|
||||
return false;
|
||||
|
||||
m_cookie = reply.value();
|
||||
m_service = service;
|
||||
m_path = path;
|
||||
m_iface = ifaceName;
|
||||
m_inhibited = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::disableInhibit()
|
||||
{
|
||||
if (!m_inhibited || m_cookie == 0)
|
||||
return;
|
||||
|
||||
QDBusInterface iface(m_service, m_path, m_iface, QDBusConnection::sessionBus());
|
||||
if (iface.isValid())
|
||||
iface.call("UnInhibit", m_cookie);
|
||||
|
||||
m_cookie = 0;
|
||||
m_inhibited = false;
|
||||
}
|
||||
36
src/screen.h
Normal file
36
src/screen.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef SCREEN_H
|
||||
#define SCREEN_H
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QObject>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
|
||||
class Screen : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Screen(QObject *parent = nullptr);
|
||||
|
||||
|
||||
Q_INVOKABLE void setInhibited(bool on, const QString &reason={});
|
||||
|
||||
private:
|
||||
bool m_inhibited = false;
|
||||
uint m_cookie = 0;
|
||||
QString m_service;
|
||||
QString m_path;
|
||||
QString m_iface;
|
||||
|
||||
void enableInhibit(const QString &reason);
|
||||
|
||||
bool tryInhibit(const QString &service,
|
||||
const QString &path,
|
||||
const QString &ifaceName,
|
||||
const QString &reason);
|
||||
|
||||
void disableInhibit();
|
||||
};
|
||||
|
||||
#endif // SCREEN_H
|
||||
48
src/thumbnailprovider.cpp
Normal file
48
src/thumbnailprovider.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#include "thumbnailprovider.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QQuickAsyncImageProvider>
|
||||
|
||||
#include <KIO/PreviewJob>
|
||||
#include <KFileItem>
|
||||
|
||||
AsyncThumbnailResponse::AsyncThumbnailResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
m_id = id;
|
||||
m_size = requestedSize;
|
||||
fetch();
|
||||
}
|
||||
|
||||
QQuickTextureFactory *AsyncThumbnailResponse::textureFactory() const
|
||||
{
|
||||
return QQuickTextureFactory::textureFactoryForImage(m_image);
|
||||
}
|
||||
|
||||
void AsyncThumbnailResponse::fetch() {
|
||||
const QUrl url = QUrl::fromPercentEncoding(m_id.toUtf8());
|
||||
const QSize thumbSize = m_size.isValid() ? m_size : QSize(256, 256);
|
||||
|
||||
KFileItem item(url, KFileItem::NormalMimeTypeDetermination);
|
||||
QStringList plugins = KIO::PreviewJob::availablePlugins();
|
||||
auto job = KIO::filePreview({ item }, thumbSize, &plugins);
|
||||
|
||||
QObject::connect(job, &KIO::PreviewJob::gotPreview, this,
|
||||
[this](const KFileItem &, const QPixmap &pixmap) {
|
||||
m_image = pixmap.toImage();
|
||||
//qDebug() << "Done:" << m_image.size();
|
||||
emit finished();
|
||||
});
|
||||
|
||||
QObject::connect(job, &KIO::PreviewJob::failed, this,
|
||||
[this](const KFileItem &item) {
|
||||
qDebug() << QStringLiteral("Failed to load thumbnail for %1").arg(item.url().toString());
|
||||
//m_error = QStringLiteral("Failed to load thumbnail for %1").arg(item.url().toString());
|
||||
emit finished();
|
||||
});
|
||||
}
|
||||
|
||||
QQuickImageResponse *ThumbnailProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
AsyncThumbnailResponse *res = new AsyncThumbnailResponse(id, requestedSize);
|
||||
return res;
|
||||
}
|
||||
28
src/thumbnailprovider.h
Normal file
28
src/thumbnailprovider.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
#include <QQuickAsyncImageProvider>
|
||||
#include <QQuickTextureFactory>
|
||||
#include <QString>
|
||||
|
||||
class AsyncThumbnailResponse : public QQuickImageResponse {
|
||||
public:
|
||||
AsyncThumbnailResponse(const QString &id, const QSize &requestedSize);
|
||||
|
||||
QQuickTextureFactory *textureFactory() const override;
|
||||
|
||||
void fetch();
|
||||
|
||||
QString m_id;
|
||||
QImage m_image;
|
||||
QSize m_size;
|
||||
|
||||
QMimeDatabase m_mimedb;
|
||||
};
|
||||
|
||||
|
||||
class ThumbnailProvider : public QQuickAsyncImageProvider {
|
||||
public:
|
||||
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
|
||||
};
|
||||
Loading…
Reference in a new issue