mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Media: add per-game screenshots manager and gallery
This commit is contained in:
parent
27f5a31561
commit
3ec2badcd7
8 changed files with 483 additions and 0 deletions
|
|
@ -8,6 +8,8 @@ add_executable(alakarte
|
|||
gamesortfiltermodel.cpp
|
||||
game.cpp
|
||||
gamepadmanager.cpp
|
||||
mediamanager.cpp
|
||||
screenshotmodel.cpp
|
||||
platformimporter.cpp
|
||||
steamimporter.cpp
|
||||
lutrisimporter.cpp
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ App::App(QObject *parent)
|
|||
, m_gameModel(new GameModel(this))
|
||||
, m_launcher(new GameLauncher(this))
|
||||
, m_steamGridDB(new SteamGridDB(this))
|
||||
, m_mediaManager(new MediaManager(this))
|
||||
, m_config(new Config(this))
|
||||
{
|
||||
loadLibrary();
|
||||
|
|
@ -172,6 +173,11 @@ SteamGridDB *App::steamGridDB() const
|
|||
return m_steamGridDB;
|
||||
}
|
||||
|
||||
MediaManager *App::mediaManager() const
|
||||
{
|
||||
return m_mediaManager;
|
||||
}
|
||||
|
||||
Config *App::config() const
|
||||
{
|
||||
return m_config;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "config.h"
|
||||
#include "gamelauncher.h"
|
||||
#include "gamemodel.h"
|
||||
#include "mediamanager.h"
|
||||
#include "steamgriddb.h"
|
||||
|
||||
class App : public QObject
|
||||
|
|
@ -22,6 +23,7 @@ class App : public QObject
|
|||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||
Q_PROPERTY(bool importing READ importing NOTIFY importingChanged)
|
||||
Q_PROPERTY(QString importStatus READ importStatus NOTIFY importStatusChanged)
|
||||
|
|
@ -33,6 +35,7 @@ public:
|
|||
GameModel *gameModel() const;
|
||||
GameLauncher *launcher() const;
|
||||
SteamGridDB *steamGridDB() const;
|
||||
MediaManager *mediaManager() const;
|
||||
Config *config() const;
|
||||
|
||||
bool importing() const;
|
||||
|
|
@ -72,6 +75,7 @@ private:
|
|||
GameModel *m_gameModel;
|
||||
GameLauncher *m_launcher;
|
||||
SteamGridDB *m_steamGridDB;
|
||||
MediaManager *m_mediaManager;
|
||||
Config *m_config;
|
||||
|
||||
bool m_importing = false;
|
||||
|
|
|
|||
145
src/mediamanager.cpp
Normal file
145
src/mediamanager.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#include "mediamanager.h"
|
||||
|
||||
#include "game.h"
|
||||
#include "screenshotmodel.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
|
||||
MediaManager::MediaManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString MediaManager::screenshotsBasePath() const
|
||||
{
|
||||
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/media/screenshots");
|
||||
}
|
||||
|
||||
QString MediaManager::gameId(Game *game) const
|
||||
{
|
||||
return game ? game->id() : QString();
|
||||
}
|
||||
|
||||
QString MediaManager::ensureDirectory(const QString &path) const
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
QDir parent;
|
||||
parent.mkpath(path);
|
||||
}
|
||||
|
||||
return QDir(path).absolutePath();
|
||||
}
|
||||
|
||||
QString MediaManager::gameScreenshotsFolderPath(Game *game) const
|
||||
{
|
||||
const QString id = gameId(game);
|
||||
if (id.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return screenshotsBasePath() + QLatin1Char('/') + id;
|
||||
}
|
||||
|
||||
QString MediaManager::screenshotsFolderPath(Game *game) const
|
||||
{
|
||||
const QString path = gameScreenshotsFolderPath(game);
|
||||
return ensureDirectory(path);
|
||||
}
|
||||
|
||||
QUrl MediaManager::screenshotsFolderUrl(Game *game) const
|
||||
{
|
||||
const QString path = screenshotsFolderPath(game);
|
||||
if (path.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
|
||||
QObject *MediaManager::screenshotsModel(Game *game)
|
||||
{
|
||||
const QString id = gameId(game);
|
||||
if (id.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (m_models.contains(id) && m_models.value(id)) {
|
||||
return m_models.value(id);
|
||||
}
|
||||
|
||||
auto *model = new ScreenshotModel(this);
|
||||
model->setDirectoryPath(screenshotsFolderPath(game));
|
||||
m_models.insert(id, model);
|
||||
return model;
|
||||
}
|
||||
|
||||
QString MediaManager::uniqueDestinationPath(const QString &folderPath, const QString &fileName) const
|
||||
{
|
||||
const QString baseName = QFileInfo(fileName).completeBaseName();
|
||||
const QString suffix = QFileInfo(fileName).suffix();
|
||||
|
||||
const QString cleanBase = baseName.isEmpty() ? QStringLiteral("screenshot") : baseName;
|
||||
const QString cleanSuffix = suffix.isEmpty() ? QStringLiteral("png") : suffix;
|
||||
|
||||
QString candidate = folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('.') + cleanSuffix;
|
||||
if (!QFileInfo::exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
for (int i = 1; i < 10000; ++i) {
|
||||
candidate = folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + cleanSuffix;
|
||||
if (!QFileInfo::exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('_') + QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1Char('.') + cleanSuffix;
|
||||
}
|
||||
|
||||
bool MediaManager::importScreenshot(Game *game, const QUrl &sourceUrl)
|
||||
{
|
||||
if (!game) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sourceUrl.isLocalFile()) {
|
||||
Q_EMIT importError(game, tr("Only local files are supported"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString srcPath = sourceUrl.toLocalFile();
|
||||
QFileInfo srcInfo(srcPath);
|
||||
if (!srcInfo.exists() || !srcInfo.isFile()) {
|
||||
Q_EMIT importError(game, tr("Source file does not exist"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString folder = screenshotsFolderPath(game);
|
||||
if (folder.isEmpty()) {
|
||||
Q_EMIT importError(game, tr("Failed to create screenshots folder"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString destPath = uniqueDestinationPath(folder, srcInfo.fileName());
|
||||
|
||||
if (!QFile::copy(srcPath, destPath)) {
|
||||
Q_EMIT importError(game, tr("Failed to copy file"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString id = gameId(game);
|
||||
if (m_models.contains(id) && m_models.value(id)) {
|
||||
m_models.value(id)->refresh();
|
||||
}
|
||||
|
||||
Q_EMIT screenshotsImported(game, 1);
|
||||
return true;
|
||||
}
|
||||
41
src/mediamanager.h
Normal file
41
src/mediamanager.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
|
||||
class Game;
|
||||
class ScreenshotModel;
|
||||
|
||||
class MediaManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QString screenshotsBasePath READ screenshotsBasePath CONSTANT)
|
||||
|
||||
public:
|
||||
explicit MediaManager(QObject *parent = nullptr);
|
||||
|
||||
QString screenshotsBasePath() const;
|
||||
|
||||
Q_INVOKABLE QString screenshotsFolderPath(Game *game) const;
|
||||
Q_INVOKABLE QUrl screenshotsFolderUrl(Game *game) const;
|
||||
|
||||
Q_INVOKABLE QObject *screenshotsModel(Game *game);
|
||||
|
||||
Q_INVOKABLE bool importScreenshot(Game *game, const QUrl &sourceUrl);
|
||||
|
||||
Q_SIGNALS:
|
||||
void screenshotsImported(Game *game, int count);
|
||||
void importError(Game *game, const QString &error);
|
||||
|
||||
private:
|
||||
QString gameId(Game *game) const;
|
||||
QString gameScreenshotsFolderPath(Game *game) const;
|
||||
QString ensureDirectory(const QString &path) const;
|
||||
QString uniqueDestinationPath(const QString &folderPath, const QString &fileName) const;
|
||||
|
||||
QHash<QString, QPointer<ScreenshotModel>> m_models;
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
|
@ -13,6 +14,8 @@ Kirigami.OverlaySheet {
|
|||
|
||||
property var game
|
||||
|
||||
readonly property var screenshotsModel: game ? App.mediaManager.screenshotsModel(game) : null
|
||||
|
||||
readonly property bool isMobile: {
|
||||
let w = applicationWindow()
|
||||
if (w && w.isMobile !== undefined) return w.isMobile
|
||||
|
|
@ -422,6 +425,94 @@ Kirigami.OverlaySheet {
|
|||
Kirigami.FormData.label: i18n("Install path:")
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 2
|
||||
text: i18n("Screenshots")
|
||||
visible: !!game
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !!game
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("Open Folder")
|
||||
icon.name: "folder-open"
|
||||
enabled: !!game
|
||||
onClicked: if (game) {
|
||||
Qt.openUrlExternally(App.mediaManager.screenshotsFolderUrl(game))
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("Import Screenshot")
|
||||
icon.name: "document-open"
|
||||
enabled: !!game
|
||||
onClicked: screenshotImportDialog.open()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: !!game && (!detailsSheet.screenshotsModel || detailsSheet.screenshotsModel.count === 0)
|
||||
text: i18n("No screenshots yet")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: screenshotsGrid
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 12
|
||||
visible: detailsSheet.screenshotsModel && detailsSheet.screenshotsModel.count > 0
|
||||
clip: true
|
||||
|
||||
cellWidth: Kirigami.Units.gridUnit * 6
|
||||
cellHeight: Kirigami.Units.gridUnit * 4
|
||||
model: detailsSheet.screenshotsModel
|
||||
|
||||
delegate: Item {
|
||||
width: screenshotsGrid.cellWidth
|
||||
height: screenshotsGrid.cellHeight
|
||||
|
||||
Kirigami.ShadowedRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: url
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: App.config.highQualityImages
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Qt.openUrlExternally(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: screenshotImportDialog
|
||||
title: i18n("Import Screenshot")
|
||||
fileMode: FileDialog.OpenFile
|
||||
nameFilters: [i18n("Image files (*.png *.jpg *.jpeg *.webp)"), i18n("All files (*)")]
|
||||
onAccepted: {
|
||||
if (detailsSheet.game) {
|
||||
App.mediaManager.importScreenshot(detailsSheet.game, selectedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatformColor(platform) {
|
||||
|
|
|
|||
135
src/screenshotmodel.cpp
Normal file
135
src/screenshotmodel.cpp
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#include "screenshotmodel.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QtConcurrent>
|
||||
|
||||
ScreenshotModel::ScreenshotModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(&m_watcher, &QFutureWatcher<QVector<ScreenshotEntry>>::finished, this, [this]() {
|
||||
const int token = m_watcher.property("refreshToken").toInt();
|
||||
if (token != m_refreshToken) {
|
||||
return;
|
||||
}
|
||||
setEntries(m_watcher.result());
|
||||
});
|
||||
}
|
||||
|
||||
ScreenshotModel::~ScreenshotModel()
|
||||
{
|
||||
m_refreshToken++;
|
||||
m_watcher.cancel();
|
||||
m_watcher.waitForFinished();
|
||||
}
|
||||
|
||||
QString ScreenshotModel::directoryPath() const
|
||||
{
|
||||
return m_directoryPath;
|
||||
}
|
||||
|
||||
void ScreenshotModel::setDirectoryPath(const QString &path)
|
||||
{
|
||||
if (m_directoryPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_directoryPath = path;
|
||||
Q_EMIT directoryPathChanged();
|
||||
refresh();
|
||||
}
|
||||
|
||||
int ScreenshotModel::count() const
|
||||
{
|
||||
return rowCount();
|
||||
}
|
||||
|
||||
int ScreenshotModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return m_entries.size();
|
||||
}
|
||||
|
||||
QVariant ScreenshotModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ScreenshotEntry &e = m_entries.at(index.row());
|
||||
switch (role) {
|
||||
case UrlRole:
|
||||
return QUrl::fromLocalFile(e.filePath);
|
||||
case FileNameRole:
|
||||
return e.fileName;
|
||||
case CreatedRole:
|
||||
return e.created;
|
||||
case SizeRole:
|
||||
return e.size;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ScreenshotModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[UrlRole] = "url";
|
||||
roles[FileNameRole] = "fileName";
|
||||
roles[CreatedRole] = "created";
|
||||
roles[SizeRole] = "size";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void ScreenshotModel::refresh()
|
||||
{
|
||||
const QString path = m_directoryPath;
|
||||
const int token = ++m_refreshToken;
|
||||
|
||||
m_watcher.setProperty("refreshToken", token);
|
||||
|
||||
auto future = QtConcurrent::run([path]() -> QVector<ScreenshotEntry> {
|
||||
QVector<ScreenshotEntry> result;
|
||||
if (path.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QStringList nameFilters = {
|
||||
QStringLiteral("*.png"),
|
||||
QStringLiteral("*.jpg"),
|
||||
QStringLiteral("*.jpeg"),
|
||||
QStringLiteral("*.webp"),
|
||||
};
|
||||
|
||||
const QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files | QDir::Readable, QDir::Time | QDir::Reversed);
|
||||
result.reserve(files.size());
|
||||
|
||||
for (const QFileInfo &fi : files) {
|
||||
ScreenshotEntry e;
|
||||
e.filePath = fi.absoluteFilePath();
|
||||
e.fileName = fi.fileName();
|
||||
e.created = fi.lastModified();
|
||||
e.size = fi.size();
|
||||
result.push_back(std::move(e));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
m_watcher.setFuture(future);
|
||||
}
|
||||
|
||||
void ScreenshotModel::setEntries(QVector<ScreenshotEntry> entries)
|
||||
{
|
||||
beginResetModel();
|
||||
m_entries = std::move(entries);
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
59
src/screenshotmodel.h
Normal file
59
src/screenshotmodel.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QFutureWatcher>
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
|
||||
class ScreenshotModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QString directoryPath READ directoryPath WRITE setDirectoryPath NOTIFY directoryPathChanged)
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
UrlRole = Qt::UserRole + 1,
|
||||
FileNameRole,
|
||||
CreatedRole,
|
||||
SizeRole,
|
||||
};
|
||||
|
||||
explicit ScreenshotModel(QObject *parent = nullptr);
|
||||
~ScreenshotModel() override;
|
||||
|
||||
QString directoryPath() const;
|
||||
void setDirectoryPath(const QString &path);
|
||||
|
||||
int count() const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
Q_SIGNALS:
|
||||
void directoryPathChanged();
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
struct ScreenshotEntry {
|
||||
QString filePath;
|
||||
QString fileName;
|
||||
QDateTime created;
|
||||
qint64 size = 0;
|
||||
};
|
||||
|
||||
void setEntries(QVector<ScreenshotEntry> entries);
|
||||
|
||||
QString m_directoryPath;
|
||||
QVector<ScreenshotEntry> m_entries;
|
||||
|
||||
QFutureWatcher<QVector<ScreenshotEntry>> m_watcher;
|
||||
int m_refreshToken = 0;
|
||||
};
|
||||
Loading…
Reference in a new issue