diff --git a/wallpapers/image/image.cpp b/wallpapers/image/image.cpp
index d4812434..9c189918 100644
--- a/wallpapers/image/image.cpp
+++ b/wallpapers/image/image.cpp
@@ -1,866 +1,840 @@
/***************************************************************************
* Copyright 2007 Paolo Capriotti
*
* Copyright 2007 Aaron Seigo *
* Copyright 2008 Petri Damsten *
* Copyright 2008 Alexis Ménard *
* Copyright 2014 Sebastian Kügler *
* Copyright 2015 Kai Uwe Broulik *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
***************************************************************************/
#include "image.h"
#include
#include // FLT_MAX
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "backgroundlistmodel.h"
#include
Image::Image(QObject *parent)
: QObject(parent),
m_ready(false),
m_delay(10),
m_dirWatch(new KDirWatch(this)),
m_mode(SingleImage),
m_currentSlide(-1),
m_model(0),
- m_dialog(0),
- m_width(0),
- m_height(0)
+ m_dialog(0)
{
m_wallpaperPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images"));
connect(&m_timer, &QTimer::timeout, this, &Image::nextSlide);
connect(m_dirWatch, &KDirWatch::created, this, &Image::pathCreated);
connect(m_dirWatch, &KDirWatch::dirty, this, &Image::pathDirty);
connect(m_dirWatch, &KDirWatch::deleted, this, &Image::pathDeleted);
m_dirWatch->startScan();
- connect(this, &Image::sizeChanged, this, &Image::setTargetSize);
-
useSingleImageDefaults();
setSingleImage();
}
Image::~Image()
{
delete m_dialog;
}
void Image::classBegin()
{
}
void Image::componentComplete()
{
// don't bother loading single image until all properties have settled
// otherwise we would load a too small image (initial view size) just
// to load the proper one afterwards etc etc
m_ready = true;
if (m_mode == SingleImage) {
setSingleImage();
}
}
QString Image::photosPath() const
{
return QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
}
QUrl Image::wallpaperPath() const
{
return QUrl::fromLocalFile(m_wallpaperPath);
}
void Image::addUrl(const QString &url)
{
addUrl(QUrl(url), true);
}
void Image::addUrls(const QStringList &urls)
{
bool first = true;
for (const QString &url: urls) {
// set the first drop as the current paper, just add the rest to the roll
addUrl(QUrl(url), first);
first = false;
}
}
Image::RenderingMode Image::renderingMode() const
{
return m_mode;
}
void Image::setRenderingMode(RenderingMode mode)
{
if (mode == m_mode) {
return;
}
m_mode = mode;
if (m_mode == SlideShow) {
if (m_slidePaths.isEmpty()) {
m_slidePaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("share/wallpapers"), QStandardPaths::LocateDirectory);
}
QTimer::singleShot(200, this, &Image::startSlideshow);
updateDirWatch(m_slidePaths);
updateDirWatch(m_slidePaths);
} else {
// we need to reset the prefered image
setSingleImage();
}
}
float distance(const QSize& size, const QSize& desired)
{
// compute difference of areas
float desiredAspectRatio = ( desired.height() > 0 ) ? desired.width() / (float)desired.height() : 0;
float candidateAspectRatio = ( size.height() > 0 ) ? size.width() / (float)size.height() : FLT_MAX;
float delta = size.width() - desired.width();
delta = (delta >= 0.0 ? delta : -delta*2 ); // Penalize for scaling up
return qAbs(candidateAspectRatio - desiredAspectRatio)*25000 + delta;
}
QSize resSize(const QString &str)
{
int index = str.indexOf('x');
if (index != -1) {
return QSize(str.leftRef(index).toInt(),
str.midRef(index + 1).toInt());
}
return QSize();
}
QString Image::findPreferedImage(const QStringList &images)
{
if (images.empty()) {
return QString();
}
//float targetAspectRatio = (m_targetSize.height() > 0 ) ? m_targetSize.width() / (float)m_targetSize.height() : 0;
//qDebug() << "wanted" << m_targetSize << "options" << images << "aspect ratio" << targetAspectRatio;
float best = FLT_MAX;
QString bestImage;
foreach (const QString &entry, images) {
QSize candidate = resSize(QFileInfo(entry).baseName());
if (candidate == QSize()) {
continue;
}
//float candidateAspectRatio = (candidate.height() > 0 ) ? candidate.width() / (float)candidate.height() : FLT_MAX;
float dist = distance(candidate, m_targetSize);
//qDebug() << "candidate" << candidate << "distance" << dist << "aspect ratio" << candidateAspectRatio;
if (bestImage.isEmpty() || dist < best) {
bestImage = entry;
best = dist;
//qDebug() << "best" << bestImage;
}
}
//qDebug() << "best image" << bestImage;
return bestImage;
}
void Image::findPreferedImageInPackage(KPackage::Package &package)
{
if (!package.isValid() || !package.filePath("preferred").isEmpty()) {
return;
}
QString preferred = findPreferedImage( package.entryList("images") );
package.removeDefinition("preferred");
package.addFileDefinition("preferred", "images/" + preferred, i18n("Recommended wallpaper file"));
}
QSize Image::targetSize() const
{
return m_targetSize;
}
void Image::setTargetSize(const QSize &size)
{
bool sizeChanged = m_targetSize != size;
m_targetSize = size;
if (m_mode == SingleImage) {
if (sizeChanged) {
// If screen size was changed, we may want to select a new preferred image
// which has correct aspect ratio for the new screen size.
m_wallpaperPackage.removeDefinition("preferred");
}
setSingleImage();
}
-}
-int Image::height() const
-{
- return m_height;
-}
-
-void Image::setHeight(int h)
-{
- if (m_height != h) {
- m_height = h;
- emit sizeChanged(QSize(m_width, m_height));
- }
-}
-
-int Image::width() const
-{
- return m_width;
-}
-
-void Image::setWidth(int w)
-{
- if (m_width != w) {
- m_width = w;
- emit sizeChanged(QSize(m_width, m_height));
+ if (sizeChanged) {
+ emit targetSizeChanged();
}
}
KPackage::Package *Image::package()
{
return &m_wallpaperPackage;
}
void Image::useSingleImageDefaults()
{
Plasma::Theme theme;
m_wallpaper = theme.wallpaperPath();
int index = m_wallpaper.indexOf(QString::fromLatin1("/contents/images/"));
if (index > -1) { // We have file from package -> get path to package
m_wallpaper = m_wallpaper.left(index);
}
}
QAbstractItemModel* Image::wallpaperModel()
{
if (!m_model) {
KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")),
QStringLiteral("Wallpapers"));
m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers);
m_model = new BackgroundListModel(this, this);
m_model->reload(m_usersWallpapers);
}
return m_model;
}
int Image::slideTimer() const
{
return m_delay;
}
void Image::setSlideTimer(int time)
{
if (time == m_delay) {
return;
}
m_delay = time;
if (m_mode == SlideShow) {
updateDirWatch(m_slidePaths);
startSlideshow();
}
emit slideTimerChanged();
}
QStringList Image::usersWallpapers() const
{
return m_usersWallpapers;
}
void Image::setUsersWallpapers(const QStringList &usersWallpapers)
{
if (usersWallpapers == m_usersWallpapers) {
return;
}
m_usersWallpapers = usersWallpapers;
emit usersWallpapersChanged();
}
QStringList Image::slidePaths() const
{
return m_slidePaths;
}
void Image::setSlidePaths(const QStringList &slidePaths)
{
if (slidePaths == m_slidePaths) {
return;
}
m_slidePaths = slidePaths;
m_slidePaths.removeAll(QString());
if (m_slidePaths.isEmpty()) {
m_slidePaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("share/wallpapers"), QStandardPaths::LocateDirectory);
}
if (m_mode == SlideShow) {
updateDirWatch(m_slidePaths);
startSlideshow();
}
emit slidePathsChanged();
}
void Image::showAddSlidePathsDialog()
{
QFileDialog *dialog = new QFileDialog(0, i18n("Directory with the wallpaper to show slides from"), QLatin1String(""));
dialog->setAttribute(Qt::WA_DeleteOnClose, true );
dialog->setOptions(QFileDialog::ShowDirsOnly);
dialog->setAcceptMode(QFileDialog::AcceptOpen);
connect(dialog, &QDialog::accepted, this, &Image::addDirFromSelectionDialog);
dialog->show();
}
void Image::addSlidePath(const QString &path)
{
if (!path.isEmpty() && !m_slidePaths.contains(path)) {
m_slidePaths.append(path);
if (m_mode == SlideShow) {
updateDirWatch(m_slidePaths);
}
emit slidePathsChanged();
startSlideshow();
}
}
void Image::removeSlidePath(const QString &path)
{
if (m_slidePaths.contains(path)) {
m_slidePaths.removeAll(path);
if (m_mode == SlideShow) {
updateDirWatch(m_slidePaths);
}
emit slidePathsChanged();
startSlideshow();
}
}
void Image::pathDirty(const QString& path)
{
updateDirWatch(QStringList(path));
}
void Image::updateDirWatch(const QStringList &newDirs)
{
Q_FOREACH(const QString &oldDir, m_dirs) {
if(!newDirs.contains(oldDir)) {
m_dirWatch->removeDir(oldDir);
}
}
Q_FOREACH(const QString &newDir, newDirs) {
if(!m_dirWatch->contains(newDir)) {
m_dirWatch->addDir(newDir, KDirWatch::WatchSubDirs | KDirWatch::WatchFiles);
}
}
m_dirs = newDirs;
}
void Image::addDirFromSelectionDialog()
{
QFileDialog *dialog = qobject_cast(sender());
if (dialog) {
addSlidePath(dialog->directoryUrl().toLocalFile());
}
}
void Image::syncWallpaperPackage()
{
m_wallpaperPackage.setPath(m_wallpaper);
findPreferedImageInPackage(m_wallpaperPackage);
m_wallpaperPath = m_wallpaperPackage.filePath("preferred");
}
void Image::setSingleImage()
{
if (!m_ready) {
return;
}
// supposedly QSize::isEmpty() is true if "either width or height are >= 0"
if (!m_targetSize.width() || !m_targetSize.height()) {
return;
}
const QString oldPath = m_wallpaperPath;
if (m_wallpaper.isEmpty()) {
useSingleImageDefaults();
}
QString img;
if (QDir::isAbsolutePath(m_wallpaper)) {
syncWallpaperPackage();
if (QFile::exists(m_wallpaperPath)) {
img = m_wallpaperPath;
}
} else {
//if it's not an absolute path, check if it's just a wallpaper name
QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("wallpapers/") + QString(m_wallpaper + QString::fromLatin1("/metadata.json")));
if (path.isEmpty())
path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("wallpapers/") + QString(m_wallpaper + QString::fromLatin1("/metadata.desktop")));
if (!path.isEmpty()) {
QDir dir(path);
dir.cdUp();
syncWallpaperPackage();
img = m_wallpaperPath;
}
}
if (img.isEmpty()) {
// ok, so the package we have failed to work out; let's try the default
useSingleImageDefaults();
syncWallpaperPackage();
}
if (m_wallpaperPath != oldPath) {
Q_EMIT wallpaperPathChanged();
}
}
void Image::addUrls(const QList &urls)
{
bool first = true;
Q_FOREACH (const QUrl &url, urls) {
// set the first drop as the current paper, just add the rest to the roll
addUrl(url, first);
first = false;
}
}
void Image::addUrl(const QUrl &url, bool setAsCurrent)
{
QString path;
if (url.isLocalFile()) {
path = url.toLocalFile();
} else if (url.scheme().isEmpty()) {
if (QDir::isAbsolutePath(url.path())) {
path = url.path();
} else {
path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
QLatin1String("wallpapers/") + url.path(),
QStandardPaths::LocateDirectory);
}
if (path.isEmpty()) {
return;
}
} else {
QString wallpaperPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("wallpapers/") + url.path();
if (!wallpaperPath.isEmpty()) {
KIO::FileCopyJob *job = KIO::file_copy(url, QUrl(wallpaperPath), -1, KIO::HideProgressInfo);
if (setAsCurrent) {
connect(job, &KJob::result, this, &Image::setWallpaperRetrieved);
} else {
connect(job, &KJob::result, this, &Image::addWallpaperRetrieved);
}
}
return;
}
if (setAsCurrent) {
setWallpaper(path);
} else {
if (m_mode != SingleImage) {
// it's a slide show, add it to the slide show
m_slideshowBackgrounds.append(path);
m_unseenSlideshowBackgrounds.append(path);
}
// always add it to the user papers, though
addUsersWallpaper(path);
}
}
void Image::setWallpaperRetrieved(KJob *job)
{
KIO::FileCopyJob *copyJob = qobject_cast(job);
if (copyJob && !copyJob->error()) {
setWallpaper(copyJob->destUrl().toLocalFile());
}
}
void Image::addWallpaperRetrieved(KJob *job)
{
KIO::FileCopyJob *copyJob = qobject_cast(job);
if (copyJob && !copyJob->error()) {
addUrl(copyJob->destUrl(), false);
}
}
void Image::setWallpaper(const QString &path)
{
if (m_mode == SingleImage) {
m_wallpaper = path;
setSingleImage();
} else {
m_slideshowBackgrounds.append(path);
m_unseenSlideshowBackgrounds.clear();
m_currentSlide = m_slideshowBackgrounds.size() - 2;
nextSlide();
}
//addUsersWallpaper(path);
}
void Image::startSlideshow()
{
if(m_findToken.isEmpty()) {
// populate background list
m_timer.stop();
m_slideshowBackgrounds.clear();
m_unseenSlideshowBackgrounds.clear();
BackgroundFinder *finder = new BackgroundFinder(this, m_dirs);
m_findToken = finder->token();
connect(finder, &BackgroundFinder::backgroundsFound, this, &Image::backgroundsFound);
finder->start();
//TODO: what would be cool: paint on the wallpaper itself a busy widget and perhaps some text
//about loading wallpaper slideshow while the thread runs
} else {
m_scanDirty = true;
}
}
void Image::backgroundsFound(const QStringList &paths, const QString &token)
{
if (token != m_findToken) {
return;
}
m_findToken.clear();
if(m_scanDirty) {
m_scanDirty = false;
startSlideshow();
return;
}
m_slideshowBackgrounds = paths;
m_unseenSlideshowBackgrounds.clear();
// start slideshow
if (m_slideshowBackgrounds.isEmpty()) {
// no image has been found, which is quite weird... try again later (this is useful for events which
// are not detected by KDirWatch, like a NFS directory being mounted)
QTimer::singleShot(1000, this, &Image::startSlideshow);
} else {
m_currentSlide = -1;
nextSlide();
m_timer.start(m_delay * 1000);
}
}
void Image::getNewWallpaper()
{
if (!m_newStuffDialog) {
m_newStuffDialog = new KNS3::DownloadDialog( QString::fromLatin1("wallpaper.knsrc") );
KNS3::DownloadDialog *strong = m_newStuffDialog.data();
strong->setTitle(i18n("Download Wallpapers"));
connect(m_newStuffDialog.data(), &QDialog::accepted, this, &Image::newStuffFinished);
}
m_newStuffDialog.data()->show();
}
void Image::newStuffFinished()
{
if (m_model && (!m_newStuffDialog || m_newStuffDialog.data()->changedEntries().size() > 0)) {
m_model->reload(m_usersWallpapers);
}
}
void Image::showFileDialog()
{
if (!m_dialog) {
QUrl baseUrl;
if(m_wallpaper.indexOf(QDir::homePath()) > -1){
baseUrl = QUrl(m_wallpaper);
}
/*
m_dialog = new KFileDialog(baseUrl, QString::fromLatin1("*.png *.jpeg *.jpg *.xcf *.svg *.svgz *.bmp"), 0);
m_dialog->setOperationMode(KFileDialog::Opening);
m_dialog->setInlinePreviewShown(true);
m_dialog->setModal(false);
connect(m_dialog, SIGNAL(okClicked()), this, SLOT(wallpaperBrowseCompleted()));
connect(m_dialog, SIGNAL(destroyed(QObject*)), this, SLOT(fileDialogFinished()));
*/
QString path;
const QStringList &locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
if (!locations.isEmpty()) {
path = locations.at(0);
} else {
// HomeLocation is guaranteed not to be empty.
path = QStandardPaths::standardLocations(QStandardPaths::HomeLocation).at(0);
}
QMimeDatabase db;
QStringList imageGlobPatterns;
foreach(const QByteArray &mimeType, QImageReader::supportedMimeTypes()) {
QMimeType mime(db.mimeTypeForName(mimeType));
imageGlobPatterns << mime.globPatterns();
}
m_dialog = new QFileDialog(0, i18n("Open Image"),
path,
i18n("Image Files") + " ("+imageGlobPatterns.join(' ') + ')');
//i18n people, this isn't a "word puzzle". there is a specific string format for QFileDialog::setNameFilters
m_dialog->setFileMode(QFileDialog::ExistingFile);
connect(m_dialog, &QDialog::accepted, this, &Image::wallpaperBrowseCompleted);
}
m_dialog->show();
m_dialog->raise();
m_dialog->activateWindow();
}
void Image::fileDialogFinished()
{
m_dialog = 0;
}
void Image::wallpaperBrowseCompleted()
{
Q_ASSERT(m_model);
if (m_dialog && m_dialog->selectedFiles().count() > 0) {
addUsersWallpaper(m_dialog->selectedFiles().first());
emit customWallpaperPicked();
}
}
void Image::addUsersWallpaper(const QString &file)
{
QString f = file;
f.replace(QLatin1String("file:/"), QLatin1String(""));
const QFileInfo info(f); // FIXME
//the full file path, so it isn't broken when dealing with symlinks
const QString wallpaper = info.canonicalFilePath();
if (wallpaper.isEmpty()) {
return;
}
if (m_model) {
if (m_model->contains(wallpaper)) {
return;
}
// add background to the model
m_model->addBackground(wallpaper);
}
// save it
KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")),
QStringLiteral("Wallpapers"));
m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers);
if (!m_usersWallpapers.contains(wallpaper)) {
m_usersWallpapers.prepend(wallpaper);
cfg.writeEntry("usersWallpapers", m_usersWallpapers);
cfg.sync();
emit usersWallpapersChanged();
}
}
void Image::nextSlide()
{
if (m_slideshowBackgrounds.isEmpty()) {
return;
}
QString previousPath;
if (m_currentSlide > -1 && m_currentSlide < m_unseenSlideshowBackgrounds.size()) {
previousPath = m_unseenSlideshowBackgrounds.takeAt(m_currentSlide);
}
if (m_unseenSlideshowBackgrounds.isEmpty()) {
m_unseenSlideshowBackgrounds = m_slideshowBackgrounds;
// We're filling the queue again, make sure we can't pick up again
// the last one picked from the previous set
if (!previousPath.isEmpty()) {
m_unseenSlideshowBackgrounds.removeAll(previousPath);
// prevent empty list
if (m_unseenSlideshowBackgrounds.isEmpty()) {
m_unseenSlideshowBackgrounds = m_slideshowBackgrounds;
}
}
}
m_currentSlide = KRandom::random() % m_unseenSlideshowBackgrounds.size();
const QString currentPath = m_unseenSlideshowBackgrounds.at(m_currentSlide);
m_wallpaperPackage.setPath(currentPath);
findPreferedImageInPackage(m_wallpaperPackage);
m_timer.stop();
m_timer.start(m_delay * 1000);
QString current = m_wallpaperPackage.filePath("preferred");
if (current.isEmpty()) {
m_wallpaperPath = currentPath;
} else {
m_wallpaperPath = current;
}
Q_EMIT wallpaperPathChanged();
}
void Image::openSlide()
{
if (!m_wallpaperPackage.isValid()) {
return;
}
// open in image viewer
QUrl filepath(m_wallpaperPackage.filePath("preferred"));
qDebug() << "opening file " << filepath.path();
new KRun(filepath, NULL);
}
void Image::pathCreated(const QString &path)
{
if(!m_slideshowBackgrounds.contains(path)) {
QFileInfo fileInfo(path);
if(fileInfo.isFile() && BackgroundFinder::isAcceptableSuffix(fileInfo.suffix())) {
m_slideshowBackgrounds.append(path);
m_unseenSlideshowBackgrounds.append(path);
if(m_slideshowBackgrounds.count() == 1) {
nextSlide();
}
}
}
}
void Image::pathDeleted(const QString &path)
{
if(m_slideshowBackgrounds.removeAll(path)) {
m_unseenSlideshowBackgrounds.removeAll(path);
if(path == m_img) {
nextSlide();
}
}
}
//FIXME: we have to save the configuration also when the dialog cancel button is clicked.
void Image::removeWallpaper(QString name)
{
QString localWallpapers = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/wallpapers/";
QUrl nameUrl(name);
//Package plugin name
if (!name.contains('/')) {
KPackage::Package p = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images"));
KJob *j = p.uninstall(name, localWallpapers);
connect(j, &KJob::finished, [=] () {
m_model->reload(m_usersWallpapers);
});
//absolute path in the home
} else if (nameUrl.path().startsWith(localWallpapers)) {
QFile f(nameUrl.path());
if (f.exists()) {
f.remove();
}
m_model->reload(m_usersWallpapers);
} else {
// save it
KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")),
QStringLiteral("Wallpapers"));
m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers);
int wallpaperIndex = -1;
//passed as a path or as a file:// url?
if (nameUrl.isValid()) {
wallpaperIndex = m_usersWallpapers.indexOf(nameUrl.path());
} else {
wallpaperIndex = m_usersWallpapers.indexOf(name);
}
if (wallpaperIndex >= 0){
m_usersWallpapers.removeAt(wallpaperIndex);
m_model->reload(m_usersWallpapers);
cfg.writeEntry("usersWallpapers", m_usersWallpapers);
cfg.sync();
emit usersWallpapersChanged();
Q_EMIT settingsChanged(true);
}
}
}
void Image::commitDeletion()
{
//This is invokable from qml, so at any moment
//we can't be sure the model exists
if (!m_model) {
return;
}
for (const QString &wallpaperCandidate : m_model->wallpapersAwaitingDeletion()) {
removeWallpaper(wallpaperCandidate);
}
}
diff --git a/wallpapers/image/image.h b/wallpapers/image/image.h
index 0f50959d..aa738f93 100644
--- a/wallpapers/image/image.h
+++ b/wallpapers/image/image.h
@@ -1,196 +1,187 @@
/***************************************************************************
* Copyright 2007 Paolo Capriotti *
* Copyright 2008 by Petri Damsten *
* Copyright 2014 Sebastian Kügler *
* Copyright 2015 Kai Uwe Broulik *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
***************************************************************************/
#ifndef IMAGE_HEADER
#define IMAGE_HEADER
#include
#include
#include
#include
#include
#include
#include
#include
#include
class QPropertyAnimation;
class QFileDialog;
class KDirWatch;
class KJob;
namespace KNS3 {
class DownloadDialog;
}
class BackgroundListModel;
class Image : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(RenderingMode renderingMode READ renderingMode WRITE setRenderingMode NOTIFY renderingModeChanged)
Q_PROPERTY(QUrl wallpaperPath READ wallpaperPath NOTIFY wallpaperPathChanged)
Q_PROPERTY(QAbstractItemModel *wallpaperModel READ wallpaperModel CONSTANT)
Q_PROPERTY(int slideTimer READ slideTimer WRITE setSlideTimer NOTIFY slideTimerChanged)
Q_PROPERTY(QStringList usersWallpapers READ usersWallpapers WRITE setUsersWallpapers NOTIFY usersWallpapersChanged)
Q_PROPERTY(QStringList slidePaths READ slidePaths WRITE setSlidePaths NOTIFY slidePathsChanged)
- Q_PROPERTY(int width MEMBER m_width READ width WRITE setWidth NOTIFY sizeChanged)
- Q_PROPERTY(int height MEMBER m_height READ height WRITE setHeight NOTIFY sizeChanged)
+ Q_PROPERTY(QSize targetSize READ targetSize WRITE setTargetSize NOTIFY targetSizeChanged)
Q_PROPERTY(QString photosPath READ photosPath CONSTANT)
public:
enum RenderingMode {
SingleImage,
SlideShow
};
Q_ENUM(RenderingMode)
Image(QObject* parent = 0);
~Image() override;
QUrl wallpaperPath() const;
//this is for QML use
Q_INVOKABLE void addUrl(const QString &url);
Q_INVOKABLE void addUrls(const QStringList &urls);
Q_INVOKABLE void addSlidePath(const QString &path);
Q_INVOKABLE void removeSlidePath(const QString &path);
Q_INVOKABLE void getNewWallpaper();
Q_INVOKABLE void showFileDialog();
Q_INVOKABLE void addUsersWallpaper(const QString &file);
Q_INVOKABLE void commitDeletion();
RenderingMode renderingMode() const;
void setRenderingMode(RenderingMode mode);
QSize targetSize() const;
void setTargetSize(const QSize &size);
- int width() const;
- int height() const;
- void setWidth(int w);
- void setHeight(int h);
-
KPackage::Package *package();
QAbstractItemModel* wallpaperModel();
int slideTimer() const;
void setSlideTimer(int time);
QStringList usersWallpapers() const;
void setUsersWallpapers(const QStringList &usersWallpapers);
QStringList slidePaths() const;
void setSlidePaths(const QStringList &slidePaths);
void findPreferedImageInPackage(KPackage::Package &package);
QString findPreferedImage(const QStringList &images);
void classBegin() override;
void componentComplete() override;
QString photosPath() const;
public Q_SLOTS:
void nextSlide();
void removeWallpaper(QString name);
Q_SIGNALS:
void settingsChanged(bool);
void wallpaperPathChanged();
void renderingModeChanged();
+ void targetSizeChanged();
void slideTimerChanged();
void usersWallpapersChanged();
void slidePathsChanged();
void resizeMethodChanged();
- void sizeChanged(QSize s);
void customWallpaperPicked();
protected Q_SLOTS:
void showAddSlidePathsDialog();
void wallpaperBrowseCompleted();
/**
* Open the current slide in the default image application
*/
void openSlide();
void startSlideshow();
void fileDialogFinished();
void addUrl(const QUrl &url, bool setAsCurrent);
void addUrls(const QList &urls);
void setWallpaper(const QString &path);
void setWallpaperRetrieved(KJob *job);
void addWallpaperRetrieved(KJob *job);
void newStuffFinished();
void updateDirWatch(const QStringList &newDirs);
void addDirFromSelectionDialog();
void pathCreated(const QString &path);
void pathDeleted(const QString &path);
void pathDirty(const QString &path);
void backgroundsFound(const QStringList &paths, const QString &token);
protected:
void syncWallpaperPackage();
void setSingleImage();
void useSingleImageDefaults();
private:
bool m_ready;
int m_delay;
QStringList m_dirs;
QString m_wallpaper;
QString m_wallpaperPath;
QStringList m_usersWallpapers;
KDirWatch *m_dirWatch;
bool m_scanDirty;
QSize m_targetSize;
RenderingMode m_mode;
KPackage::Package m_wallpaperPackage;
QStringList m_slideshowBackgrounds;
QStringList m_unseenSlideshowBackgrounds;
QStringList m_slidePaths;
QTimer m_timer;
int m_currentSlide;
BackgroundListModel *m_model;
QFileDialog *m_dialog;
- QSize m_size;
- int m_width;
- int m_height;
QString m_img;
QDateTime m_previousModified;
QPointer m_newStuffDialog;
QString m_findToken;
};
#endif
diff --git a/wallpapers/image/imagepackage/contents/config/main.xml b/wallpapers/image/imagepackage/contents/config/main.xml
index f920a0d7..c097b30a 100644
--- a/wallpapers/image/imagepackage/contents/config/main.xml
+++ b/wallpapers/image/imagepackage/contents/config/main.xml
@@ -1,39 +1,31 @@
#000000
0
-
-
- 0
-
-
-
- 0
-
10
diff --git a/wallpapers/image/imagepackage/contents/ui/config.qml b/wallpapers/image/imagepackage/contents/ui/config.qml
index bc2f6bbb..29fc1dca 100644
--- a/wallpapers/image/imagepackage/contents/ui/config.qml
+++ b/wallpapers/image/imagepackage/contents/ui/config.qml
@@ -1,360 +1,359 @@
/*
* Copyright 2013 Marco Martin
* Copyright 2014 Kai Uwe Broulik
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA.
*/
import QtQuick 2.5
import QtQuick.Controls 1.0 as QtControls
import QtQuick.Dialogs 1.1 as QtDialogs
import QtQuick.Layouts 1.0
//We need units from it
import org.kde.plasma.core 2.0 as Plasmacore
import org.kde.plasma.wallpapers.image 2.0 as Wallpaper
import org.kde.kquickcontrolsaddons 2.0
ColumnLayout {
id: root
property alias cfg_Color: colorDialog.color
property string cfg_Image
property int cfg_FillMode
property var cfg_SlidePaths: ""
property int cfg_SlideInterval: 0
signal restoreIndex(int count)
function saveConfig() {
root.restoreIndex(imageWallpaper.wallpaperModel.count)
imageWallpaper.commitDeletion();
}
SystemPalette {
id: syspal
}
Wallpaper.Image {
id: imageWallpaper
- width: wallpaper.configuration.width
- height: wallpaper.configuration.height
+ targetSize: Qt.size(plasmoid.width, plasmoid.height)
onSlidePathsChanged: cfg_SlidePaths = slidePaths
}
onCfg_SlidePathsChanged: {
imageWallpaper.slidePaths = cfg_SlidePaths
}
property int hoursIntervalValue: Math.floor(cfg_SlideInterval / 3600)
property int minutesIntervalValue: Math.floor(cfg_SlideInterval % 3600) / 60
property int secondsIntervalValue: cfg_SlideInterval % 3600 % 60
//Rectangle { color: "orange"; x: formAlignment; width: formAlignment; height: 20 }
TextMetrics {
id: textMetrics
text: "00"
}
Row {
//x: formAlignment - positionLabel.paintedWidth
spacing: units.largeSpacing / 2
QtControls.Label {
id: positionLabel
width: formAlignment - units.largeSpacing
anchors {
verticalCenter: resizeComboBox.verticalCenter
}
text: i18nd("plasma_applet_org.kde.image", "Positioning:")
horizontalAlignment: Text.AlignRight
}
QtControls.ComboBox {
id: resizeComboBox
property int textLength: 24
width: theme.mSize(theme.defaultFont).width * textLength
model: [
{
'label': i18nd("plasma_applet_org.kde.image", "Scaled and Cropped"),
'fillMode': Image.PreserveAspectCrop
},
{
'label': i18nd("plasma_applet_org.kde.image","Scaled"),
'fillMode': Image.Stretch
},
{
'label': i18nd("plasma_applet_org.kde.image","Scaled, Keep Proportions"),
'fillMode': Image.PreserveAspectFit
},
{
'label': i18nd("plasma_applet_org.kde.image", "Centered"),
'fillMode': Image.Pad
},
{
'label': i18nd("plasma_applet_org.kde.image","Tiled"),
'fillMode': Image.Tile
}
]
textRole: "label"
onCurrentIndexChanged: cfg_FillMode = model[currentIndex]["fillMode"]
Component.onCompleted: setMethod();
function setMethod() {
for (var i = 0; i < model.length; i++) {
if (model[i]["fillMode"] == wallpaper.configuration.FillMode) {
resizeComboBox.currentIndex = i;
var tl = model[i]["label"].length;
//resizeComboBox.textLength = Math.max(resizeComboBox.textLength, tl+5);
}
}
}
}
}
QtDialogs.ColorDialog {
id: colorDialog
modality: Qt.WindowModal
showAlphaChannel: false
title: i18nd("plasma_applet_org.kde.image", "Select Background Color")
}
Row {
id: colorRow
spacing: units.largeSpacing / 2
QtControls.Label {
width: formAlignment - units.largeSpacing
anchors.verticalCenter: colorButton.verticalCenter
horizontalAlignment: Text.AlignRight
text: i18nd("plasma_applet_org.kde.image", "Background Color:")
}
QtControls.Button {
id: colorButton
width: units.gridUnit * 3
text: " " // needed to it gets a proper height...
onClicked: colorDialog.open()
Rectangle {
id: colorRect
anchors.centerIn: parent
width: parent.width - 2 * units.smallSpacing
height: theme.mSize(theme.defaultFont).height
color: colorDialog.color
}
}
}
Component {
id: foldersComponent
ColumnLayout {
anchors.fill: parent
Connections {
target: root
onHoursIntervalValueChanged: hoursInterval.value = root.hoursIntervalValue
onMinutesIntervalValueChanged: minutesInterval.value = root.minutesIntervalValue
onSecondsIntervalValueChanged: secondsInterval.value = root.secondsIntervalValue
}
//FIXME: there should be only one spinbox: QtControls spinboxes are still too limited for it tough
RowLayout {
Layout.fillWidth: true
spacing: units.largeSpacing / 2
QtControls.Label {
Layout.minimumWidth: formAlignment - units.largeSpacing
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
text: i18nd("plasma_applet_org.kde.image","Change every:")
}
QtControls.SpinBox {
id: hoursInterval
anchors.verticalCenter: parent.verticalCenter
Layout.minimumWidth: textMetrics.width + units.gridUnit
width: units.gridUnit * 3
decimals: 0
value: root.hoursIntervalValue
minimumValue: 0
maximumValue: 24
onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value
}
QtControls.Label {
anchors.verticalCenter: parent.verticalCenter
text: i18nd("plasma_applet_org.kde.image","Hours")
}
Item {
Layout.preferredWidth: units.gridUnit
}
QtControls.SpinBox {
id: minutesInterval
anchors.verticalCenter: parent.verticalCenter
Layout.minimumWidth: textMetrics.width + units.gridUnit
width: units.gridUnit * 3
decimals: 0
value: root.minutesIntervalValue
minimumValue: 0
maximumValue: 60
onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value
}
QtControls.Label {
anchors.verticalCenter: parent.verticalCenter
text: i18nd("plasma_applet_org.kde.image","Minutes")
}
Item {
Layout.preferredWidth: units.gridUnit
}
QtControls.SpinBox {
id: secondsInterval
anchors.verticalCenter: parent.verticalCenter
Layout.minimumWidth: textMetrics.width + units.gridUnit
width: units.gridUnit * 3
decimals: 0
value: root.secondsIntervalValue
minimumValue: root.hoursIntervalValue === 0 && root.minutesIntervalValue === 0 ? 1 : 0
maximumValue: 60
onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value
}
QtControls.Label {
anchors.verticalCenter: parent.verticalCenter
text: i18nd("plasma_applet_org.kde.image","Seconds")
}
}
QtControls.ScrollView {
Layout.fillHeight: true;
Layout.fillWidth: true
frameVisible: true
ListView {
id: slidePathsView
anchors.margins: 4
model: imageWallpaper.slidePaths
delegate: QtControls.Label {
text: modelData
width: slidePathsView.width
height: Math.max(paintedHeight, removeButton.height);
QtControls.ToolButton {
id: removeButton
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
iconName: "list-remove"
onClicked: imageWallpaper.removeSlidePath(modelData);
}
}
}
}
}
}
Component {
id: thumbnailsComponent
QtControls.ScrollView {
anchors.fill: parent
frameVisible: true
highlightOnFocus: true;
Component.onCompleted: {
//replace the current binding on the scrollbar that makes it visible when content doesn't fit
//otherwise we adjust gridSize when we hide the vertical scrollbar and
//due to layouting that can make everything adjust which changes the contentWidth/height which
//changes our scrollbars and we continue being stuck in a loop
//looks better to not have everything resize anyway.
//BUG: 336301
__verticalScrollBar.visible = true
}
GridView {
id: wallpapersGrid
model: imageWallpaper.wallpaperModel
currentIndex: -1
focus: true
cellWidth: Math.floor(wallpapersGrid.width / Math.max(Math.floor(wallpapersGrid.width / (units.gridUnit*12)), 3))
- cellHeight: cellWidth / (imageWallpaper.width / imageWallpaper.height)
+ cellHeight: Math.round(cellWidth / (imageWallpaper.targetSize.width / imageWallpaper.targetSize.height))
anchors.margins: 4
boundsBehavior: Flickable.StopAtBounds
delegate: WallpaperDelegate {
color: cfg_Color
}
onCountChanged: {
wallpapersGrid.currentIndex = imageWallpaper.wallpaperModel.indexOf(cfg_Image);
wallpapersGrid.positionViewAtIndex(wallpapersGrid.currentIndex, GridView.Visible)
}
Connections {
target: root
onRestoreIndex: {
wallpapersGrid.currentIndex = wallpapersGrid.currentIndex - count
}
}
Keys.onPressed: {
if (count < 1) {
return;
}
if (event.key == Qt.Key_Home) {
currentIndex = 0;
} else if (event.key == Qt.Key_End) {
currentIndex = count - 1;
}
}
Keys.onLeftPressed: moveCurrentIndexLeft()
Keys.onRightPressed: moveCurrentIndexRight()
Keys.onUpPressed: moveCurrentIndexUp()
Keys.onDownPressed: moveCurrentIndexDown()
Connections {
target: imageWallpaper
onCustomWallpaperPicked: {
wallpapersGrid.currentIndex = 0
}
}
}
}
}
Loader {
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: (configDialog.currentWallpaper == "org.kde.image") ? thumbnailsComponent : foldersComponent
}
RowLayout {
id: buttonsRow
anchors {
right: parent.right
}
QtControls.Button {
visible: (configDialog.currentWallpaper == "org.kde.slideshow")
iconName: "list-add"
text: i18nd("plasma_applet_org.kde.image","Add Folder")
onClicked: imageWallpaper.showAddSlidePathsDialog()
}
QtControls.Button {
visible: (configDialog.currentWallpaper == "org.kde.image")
iconName: "document-open-folder"
text: i18nd("plasma_applet_org.kde.image","Open...")
onClicked: imageWallpaper.showFileDialog();
}
QtControls.Button {
iconName: "get-hot-new-stuff"
text: i18nd("plasma_applet_org.kde.image","Get New Wallpapers...")
onClicked: imageWallpaper.getNewWallpaper();
}
}
}
diff --git a/wallpapers/image/imagepackage/contents/ui/main.qml b/wallpapers/image/imagepackage/contents/ui/main.qml
index a19d754f..d5384e42 100644
--- a/wallpapers/image/imagepackage/contents/ui/main.qml
+++ b/wallpapers/image/imagepackage/contents/ui/main.qml
@@ -1,239 +1,226 @@
/*
* Copyright 2013 Marco Martin
* Copyright 2014 Sebastian Kügler
* Copyright 2014 Kai Uwe Broulik
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA.
*/
import QtQuick 2.5
import org.kde.plasma.wallpapers.image 2.0 as Wallpaper
import org.kde.plasma.core 2.0 as PlasmaCore
Item {
id: root
readonly property string configuredImage: wallpaper.configuration.Image
readonly property string modelImage: imageWallpaper.wallpaperPath
property Item currentImage: imageB
property Item otherImage: imageA
readonly property int fillMode: wallpaper.configuration.FillMode
property bool ready: false
//public API, the C++ part will look for those
function setUrl(url) {
wallpaper.configuration.Image = url
imageWallpaper.addUsersWallpaper(url);
}
function action_next() {
imageWallpaper.nextSlide();
}
function action_open() {
Qt.openUrlExternally(currentImage.source)
}
//private
function fadeWallpaper() {
if (!ready && width > 0 && height > 0) { // shell startup, setup immediately
currentImage.sourceSize = Qt.size(root.width, root.height)
currentImage.source = modelImage
ready = true
return
}
fadeAnim.running = false
swapImages()
currentImage.source = modelImage
currentImage.sourceSize = Qt.size(root.width, root.height)
// Prevent source size change when image has just been setup anyway
sourceSizeTimer.stop()
currentImage.opacity = 0
otherImage.z = 0
currentImage.z = 1
// only cross-fade if the new image could be smaller than the old one
fadeOtherAnimator.enabled = Qt.binding(function() {
return currentImage.paintedWidth < otherImage.paintedWidth || currentImage.paintedHeight < otherImage.paintedHeight
})
// Alleviate stuttering by waiting with the fade animation until the image is loaded (or failed to)
fadeAnim.running = Qt.binding(function() {
return currentImage.status !== Image.Loading && otherImage.status !== Image.Loading
})
}
function fadeFillMode() {
fadeAnim.running = false
swapImages()
currentImage.sourceSize = otherImage.sourceSize
sourceSizeTimer.stop()
currentImage.source = modelImage
currentImage.opacity = 0
otherImage.z = 0
currentImage.fillMode = fillMode
currentImage.z = 1
// only cross-fade if the new image could be smaller than the old one
fadeOtherAnimator.enabled = Qt.binding(function() {
return currentImage.paintedWidth < otherImage.paintedWidth || currentImage.paintedHeight < otherImage.paintedHeight
})
fadeAnim.running = Qt.binding(function() {
return currentImage.status !== Image.Loading && otherImage.status !== Image.Loading
})
}
function fadeSourceSize() {
if (currentImage.sourceSize === Qt.size(root.width, root.height)) {
return
}
fadeAnim.running = false
swapImages()
currentImage.sourceSize = Qt.size(root.width, root.height)
currentImage.opacity = 0
currentImage.source = otherImage.source
otherImage.z = 0
currentImage.z = 1
fadeOtherAnimator.enabled = false // the image size didn't change, avoid cross-dissolve
fadeAnim.running = Qt.binding(function() {
return currentImage.status !== Image.Loading && otherImage.status !== Image.Loading
})
}
function startFadeSourceTimer() {
if (width > 0 && height > 0 && (imageA.status !== Image.Null || imageB.status !== Image.Null)) {
sourceSizeTimer.restart()
}
}
function swapImages() {
if (currentImage == imageA) {
currentImage = imageB
otherImage = imageA
} else {
currentImage = imageA
otherImage = imageB
}
}
- Binding {
- target: wallpaper.configuration
- property: "width"
- value: root.width
- }
- Binding {
- target: wallpaper.configuration
- property: "height"
- value: root.height
- }
-
onWidthChanged: startFadeSourceTimer()
onHeightChanged: startFadeSourceTimer()
Timer {
id: sourceSizeTimer
interval: 1000 // always delay reloading the image even when animations are turned off
onTriggered: fadeSourceSize()
}
Component.onCompleted: {
if (wallpaper.pluginName == "org.kde.slideshow") {
wallpaper.setAction("open", i18nd("plasma_applet_org.kde.image", "Open Wallpaper Image"), "document-open");
wallpaper.setAction("next", i18nd("plasma_applet_org.kde.image","Next Wallpaper Image"),"user-desktop");
}
}
Wallpaper.Image {
id: imageWallpaper
//the oneliner of difference between image and slideshow wallpapers
renderingMode: (wallpaper.pluginName == "org.kde.image") ? Wallpaper.Image.SingleImage : Wallpaper.Image.SlideShow
-// targetSize: "1920x1080"
- width: root.width
- height: root.height
+ targetSize: Qt.size(root.width, root.height)
slidePaths: wallpaper.configuration.SlidePaths
slideTimer: wallpaper.configuration.SlideInterval
}
onFillModeChanged: {
fadeFillMode();
}
onConfiguredImageChanged: {
imageWallpaper.addUrl(configuredImage)
}
onModelImageChanged: {
fadeWallpaper();
}
SequentialAnimation {
id: fadeAnim
running: false
ParallelAnimation {
OpacityAnimator {
target: currentImage
from: 0
to: 1
duration: units.longDuration
}
OpacityAnimator {
id: fadeOtherAnimator
property bool enabled: true
target: otherImage
from: 1
// cannot disable an animation individually, so we just fade from 1 to 1
to: enabled ? 0 : 1
duration: units.longDuration
}
}
ScriptAction {
script: {
otherImage.fillMode = fillMode;
otherImage.source = "";
}
}
}
Rectangle {
id: backgroundColor
anchors.fill: parent
visible: ready && (currentImage.status === Image.Ready || otherImage.status === Image.Ready)
color: wallpaper.configuration.Color
Behavior on color {
ColorAnimation { duration: units.longDuration }
}
}
Image {
id: imageA
anchors.fill: parent
asynchronous: true
cache: false
fillMode: wallpaper.configuration.FillMode
autoTransform: true //new API in Qt 5.5, do not backport into Plasma 5.4.
}
Image {
id: imageB
anchors.fill: parent
asynchronous: true
cache: false
fillMode: wallpaper.configuration.FillMode
autoTransform: true //new API in Qt 5.5, do not backport into Plasma 5.4.
}
}
diff --git a/wallpapers/image/slideshowpackage/contents/config/main.xml b/wallpapers/image/slideshowpackage/contents/config/main.xml
index f920a0d7..c097b30a 100644
--- a/wallpapers/image/slideshowpackage/contents/config/main.xml
+++ b/wallpapers/image/slideshowpackage/contents/config/main.xml
@@ -1,39 +1,31 @@
#000000
0
-
-
- 0
-
-
-
- 0
-
10