2017-06-11 02:02:39 +08:00
|
|
|
// Aseprite
|
2018-02-21 21:39:30 +08:00
|
|
|
// Copyright (C) 2017-2018 David Capello
|
2017-06-11 02:02:39 +08:00
|
|
|
//
|
|
|
|
// This program is distributed under the terms of
|
|
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "app/extensions.h"
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
#include "app/ini_file.h"
|
2017-06-15 03:34:09 +08:00
|
|
|
#include "app/load_matrix.h"
|
2017-06-12 23:38:53 +08:00
|
|
|
#include "app/pref/preferences.h"
|
2017-06-11 02:02:39 +08:00
|
|
|
#include "app/resource_finder.h"
|
2017-06-12 23:38:53 +08:00
|
|
|
#include "base/exception.h"
|
|
|
|
#include "base/file_handle.h"
|
2017-06-11 02:02:39 +08:00
|
|
|
#include "base/fs.h"
|
2017-06-15 06:50:44 +08:00
|
|
|
#include "base/fstream_path.h"
|
2017-06-15 03:34:09 +08:00
|
|
|
#include "render/dithering_matrix.h"
|
2017-06-11 02:02:39 +08:00
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
#include "archive.h"
|
|
|
|
#include "archive_entry.h"
|
2017-06-15 06:50:44 +08:00
|
|
|
#include "json11.hpp"
|
2017-06-11 02:02:39 +08:00
|
|
|
|
2017-06-15 06:50:44 +08:00
|
|
|
#include <fstream>
|
2017-06-12 23:38:53 +08:00
|
|
|
#include <queue>
|
2017-06-14 04:06:45 +08:00
|
|
|
#include <sstream>
|
2017-06-15 06:50:44 +08:00
|
|
|
#include <string>
|
2017-06-12 23:38:53 +08:00
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
namespace app {
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
namespace {
|
|
|
|
|
2017-06-13 02:30:18 +08:00
|
|
|
const char* kPackageJson = "package.json";
|
2017-06-23 19:29:21 +08:00
|
|
|
const char* kInfoJson = "__info.json";
|
2017-06-13 22:51:49 +08:00
|
|
|
const char* kAsepriteDefaultThemeExtensionName = "aseprite-theme";
|
2017-06-13 02:30:18 +08:00
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
class ReadArchive {
|
|
|
|
public:
|
|
|
|
ReadArchive(const std::string& filename)
|
|
|
|
: m_arch(nullptr), m_open(false) {
|
|
|
|
m_arch = archive_read_new();
|
|
|
|
archive_read_support_format_zip(m_arch);
|
|
|
|
|
|
|
|
m_file = base::open_file(filename, "rb");
|
|
|
|
if (!m_file)
|
|
|
|
throw base::Exception("Error loading file %s",
|
|
|
|
filename.c_str());
|
|
|
|
|
|
|
|
int err;
|
|
|
|
if ((err = archive_read_open_FILE(m_arch, m_file.get())))
|
|
|
|
throw base::Exception("Error uncompressing extension\n%s (%d)",
|
|
|
|
archive_error_string(m_arch), err);
|
|
|
|
|
|
|
|
m_open = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
~ReadArchive() {
|
|
|
|
if (m_arch) {
|
|
|
|
if (m_open)
|
|
|
|
archive_read_close(m_arch);
|
|
|
|
archive_read_free(m_arch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
archive_entry* readEntry() {
|
|
|
|
archive_entry* entry;
|
|
|
|
int err = archive_read_next_header(m_arch, &entry);
|
|
|
|
|
|
|
|
if (err == ARCHIVE_EOF)
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
if (err != ARCHIVE_OK)
|
|
|
|
throw base::Exception("Error uncompressing extension\n%s",
|
|
|
|
archive_error_string(m_arch));
|
|
|
|
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
int copyDataTo(archive* out) {
|
|
|
|
const void* buf;
|
|
|
|
size_t size;
|
|
|
|
int64_t offset;
|
|
|
|
for (;;) {
|
|
|
|
int err = archive_read_data_block(m_arch, &buf, &size, &offset);
|
|
|
|
if (err == ARCHIVE_EOF)
|
|
|
|
break;
|
|
|
|
if (err != ARCHIVE_OK)
|
|
|
|
return err;
|
|
|
|
|
|
|
|
err = archive_write_data_block(out, buf, size, offset);
|
|
|
|
if (err != ARCHIVE_OK) {
|
|
|
|
throw base::Exception("Error writing data blocks\n%s (%d)",
|
|
|
|
archive_error_string(out), err);
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ARCHIVE_OK;
|
|
|
|
}
|
|
|
|
|
2017-06-14 04:06:45 +08:00
|
|
|
int copyDataTo(std::ostream& dst) {
|
|
|
|
const void* buf;
|
|
|
|
size_t size;
|
|
|
|
int64_t offset;
|
|
|
|
for (;;) {
|
|
|
|
int err = archive_read_data_block(m_arch, &buf, &size, &offset);
|
|
|
|
if (err == ARCHIVE_EOF)
|
|
|
|
break;
|
|
|
|
if (err != ARCHIVE_OK)
|
|
|
|
return err;
|
|
|
|
dst.write((const char*)buf, size);
|
|
|
|
}
|
|
|
|
return ARCHIVE_OK;
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
private:
|
|
|
|
base::FileHandle m_file;
|
|
|
|
archive* m_arch;
|
|
|
|
bool m_open;
|
|
|
|
};
|
|
|
|
|
|
|
|
class WriteArchive {
|
|
|
|
public:
|
2017-06-14 04:06:45 +08:00
|
|
|
WriteArchive()
|
2017-06-12 23:38:53 +08:00
|
|
|
: m_arch(nullptr)
|
2017-06-14 04:06:45 +08:00
|
|
|
, m_open(false) {
|
2017-06-12 23:38:53 +08:00
|
|
|
m_arch = archive_write_disk_new();
|
|
|
|
m_open = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
~WriteArchive() {
|
|
|
|
if (m_arch) {
|
|
|
|
if (m_open)
|
|
|
|
archive_write_close(m_arch);
|
|
|
|
archive_write_free(m_arch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void writeEntry(ReadArchive& in, archive_entry* entry) {
|
|
|
|
int err = archive_write_header(m_arch, entry);
|
|
|
|
if (err != ARCHIVE_OK)
|
|
|
|
throw base::Exception("Error writing file into disk\n%s (%d)",
|
|
|
|
archive_error_string(m_arch), err);
|
|
|
|
|
|
|
|
in.copyDataTo(m_arch);
|
|
|
|
err = archive_write_finish_entry(m_arch);
|
|
|
|
if (err != ARCHIVE_OK)
|
|
|
|
throw base::Exception("Error saving the last part of a file entry in disk\n%s (%d)",
|
|
|
|
archive_error_string(m_arch), err);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
archive* m_arch;
|
|
|
|
bool m_open;
|
|
|
|
};
|
|
|
|
|
2017-06-23 19:29:21 +08:00
|
|
|
void read_json_file(const std::string& path, json11::Json& json)
|
|
|
|
{
|
|
|
|
std::string jsonText, line;
|
|
|
|
std::ifstream in(FSTREAM_PATH(path), std::ifstream::binary);
|
|
|
|
while (std::getline(in, line)) {
|
|
|
|
jsonText += line;
|
|
|
|
jsonText.push_back('\n');
|
|
|
|
}
|
|
|
|
std::string err;
|
|
|
|
json = json11::Json::parse(jsonText, err);
|
|
|
|
if (!err.empty())
|
|
|
|
throw base::Exception("Error parsing JSON file: %s\n",
|
|
|
|
err.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
void write_json_file(const std::string& path, const json11::Json& json)
|
|
|
|
{
|
|
|
|
std::string text;
|
|
|
|
json.dump(text);
|
|
|
|
std::ofstream out(FSTREAM_PATH(path), std::ifstream::binary);
|
|
|
|
out.write(text.c_str(), text.size());
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
} // anonymous namespace
|
|
|
|
|
2017-06-15 03:34:09 +08:00
|
|
|
const render::DitheringMatrix& Extension::DitheringMatrixInfo::matrix() const
|
|
|
|
{
|
|
|
|
if (!m_matrix) {
|
|
|
|
m_matrix = new render::DitheringMatrix;
|
|
|
|
load_dithering_matrix_from_sprite(m_path, *m_matrix);
|
|
|
|
}
|
|
|
|
return *m_matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Extension::DitheringMatrixInfo::destroyMatrix()
|
|
|
|
{
|
|
|
|
if (m_matrix)
|
|
|
|
delete m_matrix;
|
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
Extension::Extension(const std::string& path,
|
|
|
|
const std::string& name,
|
2017-06-23 18:53:31 +08:00
|
|
|
const std::string& version,
|
2017-06-11 02:02:39 +08:00
|
|
|
const std::string& displayName,
|
|
|
|
const bool isEnabled,
|
|
|
|
const bool isBuiltinExtension)
|
|
|
|
: m_path(path)
|
|
|
|
, m_name(name)
|
2017-06-23 18:53:31 +08:00
|
|
|
, m_version(version)
|
2017-06-11 02:02:39 +08:00
|
|
|
, m_displayName(displayName)
|
|
|
|
, m_isEnabled(isEnabled)
|
|
|
|
, m_isInstalled(true)
|
|
|
|
, m_isBuiltinExtension(isBuiltinExtension)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2017-06-15 03:34:09 +08:00
|
|
|
Extension::~Extension()
|
|
|
|
{
|
|
|
|
// Delete all matrices
|
|
|
|
for (auto& it : m_ditheringMatrices)
|
|
|
|
it.second.destroyMatrix();
|
|
|
|
}
|
|
|
|
|
2018-03-20 06:37:39 +08:00
|
|
|
void Extension::addLanguage(const std::string& id, const std::string& path)
|
|
|
|
{
|
|
|
|
m_languages[id] = path;
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
void Extension::addTheme(const std::string& id, const std::string& path)
|
|
|
|
{
|
|
|
|
m_themes[id] = path;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Extension::addPalette(const std::string& id, const std::string& path)
|
|
|
|
{
|
|
|
|
m_palettes[id] = path;
|
|
|
|
}
|
|
|
|
|
2017-06-15 03:34:09 +08:00
|
|
|
void Extension::addDitheringMatrix(const std::string& id,
|
|
|
|
const std::string& path,
|
|
|
|
const std::string& name)
|
|
|
|
{
|
|
|
|
DitheringMatrixInfo info(path, name);
|
|
|
|
m_ditheringMatrices[id] = info;
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
bool Extension::canBeDisabled() const
|
|
|
|
{
|
2017-06-13 22:51:49 +08:00
|
|
|
return (m_isEnabled &&
|
2017-06-24 02:19:00 +08:00
|
|
|
//!isCurrentTheme() &&
|
|
|
|
!isDefaultTheme()); // Default theme cannot be disabled or uninstalled
|
2017-06-12 23:38:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Extension::canBeUninstalled() const
|
|
|
|
{
|
2017-06-13 22:51:49 +08:00
|
|
|
return (!m_isBuiltinExtension &&
|
2017-06-24 02:19:00 +08:00
|
|
|
// We can uninstall the current theme (e.g. to upgrade it)
|
|
|
|
//!isCurrentTheme() &&
|
2017-06-13 22:51:49 +08:00
|
|
|
!isDefaultTheme());
|
2017-06-12 23:38:53 +08:00
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
void Extension::enable(const bool state)
|
|
|
|
{
|
|
|
|
// Do nothing
|
|
|
|
if (m_isEnabled == state)
|
|
|
|
return;
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
set_config_bool("extensions", m_name.c_str(), state);
|
|
|
|
flush_config_file();
|
2017-06-11 02:02:39 +08:00
|
|
|
|
|
|
|
m_isEnabled = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Extension::uninstall()
|
|
|
|
{
|
|
|
|
if (!m_isInstalled)
|
|
|
|
return;
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
ASSERT(canBeUninstalled());
|
|
|
|
if (!canBeUninstalled())
|
|
|
|
return;
|
|
|
|
|
|
|
|
TRACE("EXT: Uninstall extension '%s' from '%s'...\n",
|
|
|
|
m_name.c_str(), m_path.c_str());
|
|
|
|
|
|
|
|
// Remove all files inside the extension path
|
|
|
|
uninstallFiles(m_path);
|
|
|
|
ASSERT(!base::is_directory(m_path));
|
2017-06-11 02:02:39 +08:00
|
|
|
|
|
|
|
m_isEnabled = false;
|
|
|
|
m_isInstalled = false;
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
void Extension::uninstallFiles(const std::string& path)
|
|
|
|
{
|
2017-06-23 19:29:21 +08:00
|
|
|
#if 1 // Read the list of files to be uninstalled from __info.json file
|
|
|
|
|
|
|
|
std::string infoFn = base::join_path(path, kInfoJson);
|
|
|
|
if (!base::is_file(infoFn))
|
|
|
|
throw base::Exception("Cannot remove extension, '%s' file doesn't exist",
|
|
|
|
infoFn.c_str());
|
|
|
|
|
|
|
|
json11::Json json;
|
|
|
|
read_json_file(infoFn, json);
|
|
|
|
|
2018-02-21 21:39:30 +08:00
|
|
|
base::paths installedDirs;
|
2017-06-23 19:29:21 +08:00
|
|
|
|
|
|
|
for (const auto& value : json["installedFiles"].array_items()) {
|
|
|
|
std::string fn = base::join_path(path, value.string_value());
|
|
|
|
if (base::is_file(fn)) {
|
|
|
|
TRACE("EXT: Deleting file '%s'\n", fn.c_str());
|
|
|
|
base::delete_file(fn);
|
|
|
|
}
|
|
|
|
else if (base::is_directory(fn)) {
|
|
|
|
installedDirs.push_back(fn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::sort(installedDirs.begin(),
|
|
|
|
installedDirs.end(),
|
|
|
|
[](const std::string& a,
|
|
|
|
const std::string& b) {
|
|
|
|
return b.size() < a.size();
|
|
|
|
});
|
|
|
|
|
|
|
|
for (const auto& dir : installedDirs) {
|
|
|
|
TRACE("EXT: Deleting directory '%s'\n", dir.c_str());
|
|
|
|
base::remove_directory(dir);
|
|
|
|
}
|
|
|
|
|
2018-11-28 21:31:02 +08:00
|
|
|
// Delete __info.json file if it does exist (e.g. maybe the
|
|
|
|
// "installedFiles" list included the __info.json so the file was
|
|
|
|
// already deleted, this can happen if the .json file was modified
|
|
|
|
// by hand/the user)
|
|
|
|
if (base::is_file(infoFn)) {
|
|
|
|
TRACE("EXT: Deleting file '%s'\n", infoFn.c_str());
|
|
|
|
base::delete_file(infoFn);
|
|
|
|
}
|
2017-06-23 19:29:21 +08:00
|
|
|
|
|
|
|
TRACE("EXT: Deleting extension directory '%s'\n", path.c_str());
|
|
|
|
base::remove_directory(path);
|
|
|
|
|
|
|
|
#else // The following code delete the whole "path",
|
|
|
|
// we prefer the __info.json approach.
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
for (auto& item : base::list_files(path)) {
|
|
|
|
std::string fn = base::join_path(path, item);
|
|
|
|
if (base::is_file(fn)) {
|
|
|
|
TRACE("EXT: Deleting file '%s'\n", fn.c_str());
|
|
|
|
base::delete_file(fn);
|
|
|
|
}
|
|
|
|
else if (base::is_directory(fn)) {
|
|
|
|
uninstallFiles(fn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TRACE("EXT: Deleting directory '%s'\n", path.c_str());
|
|
|
|
base::remove_directory(path);
|
2017-06-23 19:29:21 +08:00
|
|
|
|
|
|
|
#endif
|
2017-06-12 23:38:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Extension::isCurrentTheme() const
|
|
|
|
{
|
2017-06-13 22:51:49 +08:00
|
|
|
auto it = m_themes.find(Preferences::instance().theme.selected());
|
2017-06-12 23:38:53 +08:00
|
|
|
return (it != m_themes.end());
|
|
|
|
}
|
|
|
|
|
2017-06-13 22:51:49 +08:00
|
|
|
bool Extension::isDefaultTheme() const
|
|
|
|
{
|
|
|
|
return (name() == kAsepriteDefaultThemeExtensionName);
|
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
Extensions::Extensions()
|
|
|
|
{
|
2017-06-12 23:38:53 +08:00
|
|
|
// Create and get the user extensions directory
|
|
|
|
{
|
|
|
|
ResourceFinder rf2;
|
2017-06-14 22:43:36 +08:00
|
|
|
rf2.includeUserDir("extensions/.");
|
2017-06-12 23:38:53 +08:00
|
|
|
m_userExtensionsPath = rf2.getFirstOrCreateDefault();
|
|
|
|
m_userExtensionsPath = base::normalize_path(m_userExtensionsPath);
|
2017-06-24 02:09:41 +08:00
|
|
|
if (!m_userExtensionsPath.empty() &&
|
|
|
|
m_userExtensionsPath.back() == '.') {
|
|
|
|
m_userExtensionsPath = base::get_file_path(m_userExtensionsPath);
|
|
|
|
}
|
2017-06-12 23:38:53 +08:00
|
|
|
LOG("EXT: User extensions path '%s'\n", m_userExtensionsPath.c_str());
|
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
ResourceFinder rf;
|
2017-06-14 22:43:36 +08:00
|
|
|
rf.includeUserDir("extensions");
|
2017-06-11 02:02:39 +08:00
|
|
|
rf.includeDataDir("extensions");
|
|
|
|
|
|
|
|
// Load extensions from data/ directory on all possible locations
|
|
|
|
// (installed folder and user folder)
|
|
|
|
while (rf.next()) {
|
|
|
|
auto extensionsDir = rf.filename();
|
|
|
|
|
|
|
|
if (base::is_directory(extensionsDir)) {
|
|
|
|
for (auto fn : base::list_files(extensionsDir)) {
|
2017-06-12 23:38:53 +08:00
|
|
|
const auto dir = base::join_path(extensionsDir, fn);
|
2017-06-11 02:02:39 +08:00
|
|
|
if (!base::is_directory(dir))
|
|
|
|
continue;
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
const bool isBuiltinExtension =
|
|
|
|
(m_userExtensionsPath != base::get_file_path(dir));
|
|
|
|
|
2017-06-13 02:30:18 +08:00
|
|
|
auto fullFn = base::join_path(dir, kPackageJson);
|
2017-06-12 23:38:53 +08:00
|
|
|
fullFn = base::normalize_path(fullFn);
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
|
|
|
|
if (!base::is_file(fullFn)) {
|
|
|
|
LOG("EXT: File '%s' not found\n", fullFn.c_str());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2017-06-12 23:38:53 +08:00
|
|
|
loadExtension(dir, fullFn, isBuiltinExtension);
|
2017-06-11 02:02:39 +08:00
|
|
|
}
|
|
|
|
catch (const std::exception& ex) {
|
|
|
|
LOG("EXT: Error loading JSON file: %s\n",
|
|
|
|
ex.what());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Extensions::~Extensions()
|
|
|
|
{
|
|
|
|
for (auto ext : m_extensions)
|
|
|
|
delete ext;
|
|
|
|
}
|
|
|
|
|
2018-03-20 06:37:39 +08:00
|
|
|
std::string Extensions::languagePath(const std::string& langId)
|
|
|
|
{
|
|
|
|
for (auto ext : m_extensions) {
|
|
|
|
if (!ext->isEnabled()) // Ignore disabled extensions
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto it = ext->languages().find(langId);
|
|
|
|
if (it != ext->languages().end())
|
|
|
|
return it->second;
|
|
|
|
}
|
|
|
|
return std::string();
|
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
std::string Extensions::themePath(const std::string& themeId)
|
|
|
|
{
|
2017-06-12 23:38:53 +08:00
|
|
|
for (auto ext : m_extensions) {
|
2017-06-13 23:03:16 +08:00
|
|
|
if (!ext->isEnabled()) // Ignore disabled extensions
|
2017-06-13 05:08:02 +08:00
|
|
|
continue;
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
auto it = ext->themes().find(themeId);
|
|
|
|
if (it != ext->themes().end())
|
|
|
|
return it->second;
|
|
|
|
}
|
2017-06-11 02:02:39 +08:00
|
|
|
return std::string();
|
|
|
|
}
|
|
|
|
|
2017-06-13 23:03:16 +08:00
|
|
|
std::string Extensions::palettePath(const std::string& palId)
|
|
|
|
{
|
|
|
|
for (auto ext : m_extensions) {
|
|
|
|
if (!ext->isEnabled()) // Ignore disabled extensions
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto it = ext->palettes().find(palId);
|
|
|
|
if (it != ext->palettes().end())
|
|
|
|
return it->second;
|
|
|
|
}
|
|
|
|
return std::string();
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
ExtensionItems Extensions::palettes() const
|
|
|
|
{
|
|
|
|
ExtensionItems palettes;
|
2017-06-13 05:08:02 +08:00
|
|
|
for (auto ext : m_extensions) {
|
|
|
|
if (!ext->isEnabled()) // Ignore disabled themes
|
|
|
|
continue;
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
for (auto item : ext->palettes())
|
|
|
|
palettes[item.first] = item.second;
|
2017-06-13 05:08:02 +08:00
|
|
|
}
|
2017-06-12 23:38:53 +08:00
|
|
|
return palettes;
|
|
|
|
}
|
|
|
|
|
2017-06-15 03:34:09 +08:00
|
|
|
const render::DitheringMatrix* Extensions::ditheringMatrix(const std::string& matrixId)
|
|
|
|
{
|
|
|
|
for (auto ext : m_extensions) {
|
|
|
|
if (!ext->isEnabled()) // Ignore disabled themes
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto it = ext->m_ditheringMatrices.find(matrixId);
|
|
|
|
if (it != ext->m_ditheringMatrices.end())
|
|
|
|
return &it->second.matrix();
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<Extension::DitheringMatrixInfo> Extensions::ditheringMatrices()
|
|
|
|
{
|
|
|
|
std::vector<Extension::DitheringMatrixInfo> result;
|
|
|
|
for (auto ext : m_extensions) {
|
|
|
|
if (!ext->isEnabled()) // Ignore disabled themes
|
|
|
|
continue;
|
|
|
|
|
|
|
|
for (auto it : ext->m_ditheringMatrices)
|
|
|
|
result.push_back(it.second);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-06-13 05:08:02 +08:00
|
|
|
void Extensions::enableExtension(Extension* extension, const bool state)
|
|
|
|
{
|
|
|
|
extension->enable(state);
|
|
|
|
generateExtensionSignals(extension);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Extensions::uninstallExtension(Extension* extension)
|
|
|
|
{
|
|
|
|
extension->uninstall();
|
|
|
|
generateExtensionSignals(extension);
|
2017-06-23 18:53:31 +08:00
|
|
|
|
|
|
|
auto it = std::find(m_extensions.begin(),
|
|
|
|
m_extensions.end(), extension);
|
|
|
|
ASSERT(it != m_extensions.end());
|
|
|
|
if (it != m_extensions.end())
|
|
|
|
m_extensions.erase(it);
|
|
|
|
|
|
|
|
delete extension;
|
2017-06-13 05:08:02 +08:00
|
|
|
}
|
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
ExtensionInfo Extensions::getCompressedExtensionInfo(const std::string& zipFn)
|
2017-06-11 03:44:19 +08:00
|
|
|
{
|
2017-06-23 18:53:31 +08:00
|
|
|
ExtensionInfo info;
|
|
|
|
info.dstPath =
|
2017-06-12 23:38:53 +08:00
|
|
|
base::join_path(m_userExtensionsPath,
|
|
|
|
base::get_file_title(zipFn));
|
|
|
|
|
2017-06-14 04:06:45 +08:00
|
|
|
// First of all we read the package.json file inside the .zip to
|
|
|
|
// know 1) the extension name, 2) that the .json file can be parsed
|
|
|
|
// correctly, 3) the final destination directory.
|
2017-06-23 18:53:31 +08:00
|
|
|
ReadArchive in(zipFn);
|
|
|
|
archive_entry* entry;
|
|
|
|
while ((entry = in.readEntry()) != nullptr) {
|
|
|
|
const std::string entryFn = archive_entry_pathname(entry);
|
|
|
|
if (base::get_file_name(entryFn) != kPackageJson)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
info.commonPath = base::get_file_path(entryFn);
|
|
|
|
if (!info.commonPath.empty() &&
|
|
|
|
entryFn.size() > info.commonPath.size()) {
|
|
|
|
info.commonPath.push_back(entryFn[info.commonPath.size()]);
|
2017-06-14 04:06:45 +08:00
|
|
|
}
|
2017-06-23 18:53:31 +08:00
|
|
|
|
|
|
|
std::stringstream out;
|
|
|
|
in.copyDataTo(out);
|
|
|
|
|
|
|
|
std::string err;
|
|
|
|
auto json = json11::Json::parse(out.str(), err);
|
|
|
|
if (err.empty()) {
|
|
|
|
info.name = json["name"].string_value();
|
|
|
|
info.version = json["version"].string_value();
|
|
|
|
info.dstPath = base::join_path(m_userExtensionsPath, info.name);
|
|
|
|
}
|
|
|
|
break;
|
2017-06-14 04:06:45 +08:00
|
|
|
}
|
2017-06-23 18:53:31 +08:00
|
|
|
return info;
|
|
|
|
}
|
2017-06-14 04:06:45 +08:00
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
Extension* Extensions::installCompressedExtension(const std::string& zipFn,
|
|
|
|
const ExtensionInfo& info)
|
|
|
|
{
|
2018-02-21 21:39:30 +08:00
|
|
|
base::paths installedFiles;
|
2017-06-23 19:29:21 +08:00
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
// Uncompress zipFn in info.dstPath
|
2017-06-14 04:06:45 +08:00
|
|
|
{
|
|
|
|
ReadArchive in(zipFn);
|
|
|
|
WriteArchive out;
|
|
|
|
|
|
|
|
archive_entry* entry;
|
|
|
|
while ((entry = in.readEntry()) != nullptr) {
|
|
|
|
// Fix the entry filename to write the file in the disk
|
|
|
|
std::string fn = archive_entry_pathname(entry);
|
|
|
|
|
|
|
|
LOG("EXT: Original filename in zip <%s>...\n", fn.c_str());
|
|
|
|
|
2018-11-28 21:31:02 +08:00
|
|
|
// Do not install __info.json file if it's inside the .zip as
|
|
|
|
// some users are distirbuting extensions with the __info.json
|
|
|
|
// file inside.
|
|
|
|
if (base::string_to_lower(base::get_file_name(fn)) == kInfoJson) {
|
|
|
|
LOG("EXT: Ignoring <%s>...\n", fn.c_str());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
if (!info.commonPath.empty()) {
|
2017-06-14 04:06:45 +08:00
|
|
|
// Check mismatch with package.json common path
|
2017-06-23 18:53:31 +08:00
|
|
|
if (fn.compare(0, info.commonPath.size(), info.commonPath) != 0)
|
2017-06-14 04:06:45 +08:00
|
|
|
continue;
|
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
fn.erase(0, info.commonPath.size());
|
2017-06-14 04:06:45 +08:00
|
|
|
if (fn.empty())
|
|
|
|
continue;
|
|
|
|
}
|
2017-06-12 23:38:53 +08:00
|
|
|
|
2017-06-23 19:29:21 +08:00
|
|
|
installedFiles.push_back(fn);
|
|
|
|
|
2017-06-23 18:53:31 +08:00
|
|
|
const std::string fullFn = base::join_path(info.dstPath, fn);
|
2018-03-23 00:24:32 +08:00
|
|
|
#if _WIN32
|
|
|
|
archive_entry_copy_pathname_w(entry, base::from_utf8(fullFn).c_str());
|
|
|
|
#else
|
2017-06-14 04:06:45 +08:00
|
|
|
archive_entry_set_pathname(entry, fullFn.c_str());
|
2018-03-23 00:24:32 +08:00
|
|
|
#endif
|
2017-06-14 04:06:45 +08:00
|
|
|
|
|
|
|
LOG("EXT: Uncompressing file <%s> to <%s>\n",
|
|
|
|
fn.c_str(), fullFn.c_str());
|
|
|
|
|
|
|
|
out.writeEntry(in, entry);
|
|
|
|
}
|
|
|
|
}
|
2017-06-12 23:38:53 +08:00
|
|
|
|
2017-06-23 19:29:21 +08:00
|
|
|
// Save the list of installed files in "__info.json" file
|
|
|
|
{
|
|
|
|
json11::Json::object obj;
|
2017-06-23 21:29:24 +08:00
|
|
|
obj["installedFiles"] = json11::Json(installedFiles);
|
2017-06-23 19:29:21 +08:00
|
|
|
json11::Json json(obj);
|
|
|
|
|
|
|
|
const std::string fullFn = base::join_path(info.dstPath, kInfoJson);
|
|
|
|
LOG("EXT: Saving list of installed files in <%s>\n", fullFn.c_str());
|
|
|
|
write_json_file(fullFn, json);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the extension
|
2017-06-12 23:38:53 +08:00
|
|
|
Extension* extension = loadExtension(
|
2017-06-23 18:53:31 +08:00
|
|
|
info.dstPath,
|
|
|
|
base::join_path(info.dstPath, kPackageJson),
|
2017-06-12 23:38:53 +08:00
|
|
|
false);
|
|
|
|
if (!extension)
|
|
|
|
throw base::Exception("Error adding the new extension");
|
|
|
|
|
2017-06-13 05:08:02 +08:00
|
|
|
// Generate signals
|
2017-06-12 23:38:53 +08:00
|
|
|
NewExtension(extension);
|
2017-06-13 05:08:02 +08:00
|
|
|
generateExtensionSignals(extension);
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
return extension;
|
2017-06-11 03:44:19 +08:00
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
Extension* Extensions::loadExtension(const std::string& path,
|
|
|
|
const std::string& fullPackageFilename,
|
|
|
|
const bool isBuiltinExtension)
|
|
|
|
{
|
2017-06-15 06:50:44 +08:00
|
|
|
json11::Json json;
|
2017-06-23 19:29:21 +08:00
|
|
|
read_json_file(fullPackageFilename, json);
|
2017-06-15 06:50:44 +08:00
|
|
|
auto name = json["name"].string_value();
|
2017-06-23 18:53:31 +08:00
|
|
|
auto version = json["version"].string_value();
|
2017-06-15 06:50:44 +08:00
|
|
|
auto displayName = json["displayName"].string_value();
|
2017-06-11 02:02:39 +08:00
|
|
|
|
|
|
|
LOG("EXT: Extension '%s' loaded\n", name.c_str());
|
|
|
|
|
2018-08-09 04:27:26 +08:00
|
|
|
std::unique_ptr<Extension> extension(
|
2017-06-11 02:02:39 +08:00
|
|
|
new Extension(path,
|
|
|
|
name,
|
2017-06-23 18:53:31 +08:00
|
|
|
version,
|
2017-06-11 02:02:39 +08:00
|
|
|
displayName,
|
2017-06-12 23:38:53 +08:00
|
|
|
// Extensions are enabled by default
|
|
|
|
get_config_bool("extensions", name.c_str(), true),
|
2017-06-11 02:02:39 +08:00
|
|
|
isBuiltinExtension));
|
|
|
|
|
|
|
|
auto contributes = json["contributes"];
|
|
|
|
if (contributes.is_object()) {
|
2018-03-20 06:37:39 +08:00
|
|
|
// Languages
|
|
|
|
auto languages = contributes["languages"];
|
|
|
|
if (languages.is_array()) {
|
|
|
|
for (const auto& lang : languages.array_items()) {
|
|
|
|
std::string langId = lang["id"].string_value();
|
|
|
|
std::string langPath = lang["path"].string_value();
|
|
|
|
|
|
|
|
// The path must be always relative to the extension
|
|
|
|
langPath = base::join_path(path, langPath);
|
|
|
|
|
|
|
|
LOG("EXT: New language '%s' in '%s'\n",
|
|
|
|
langId.c_str(),
|
|
|
|
langPath.c_str());
|
|
|
|
|
|
|
|
extension->addLanguage(langId, langPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-11 03:44:19 +08:00
|
|
|
// Themes
|
2017-06-11 02:02:39 +08:00
|
|
|
auto themes = contributes["themes"];
|
|
|
|
if (themes.is_array()) {
|
2017-06-15 06:50:44 +08:00
|
|
|
for (const auto& theme : themes.array_items()) {
|
|
|
|
std::string themeId = theme["id"].string_value();
|
|
|
|
std::string themePath = theme["path"].string_value();
|
2017-06-11 02:02:39 +08:00
|
|
|
|
2017-06-11 03:44:19 +08:00
|
|
|
// The path must be always relative to the extension
|
|
|
|
themePath = base::join_path(path, themePath);
|
|
|
|
|
|
|
|
LOG("EXT: New theme '%s' in '%s'\n",
|
|
|
|
themeId.c_str(),
|
|
|
|
themePath.c_str());
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
extension->addTheme(themeId, themePath);
|
2017-06-11 02:02:39 +08:00
|
|
|
}
|
|
|
|
}
|
2017-06-11 03:44:19 +08:00
|
|
|
|
|
|
|
// Palettes
|
|
|
|
auto palettes = contributes["palettes"];
|
|
|
|
if (palettes.is_array()) {
|
2017-06-15 06:50:44 +08:00
|
|
|
for (const auto& palette : palettes.array_items()) {
|
|
|
|
std::string palId = palette["id"].string_value();
|
|
|
|
std::string palPath = palette["path"].string_value();
|
2017-06-11 03:44:19 +08:00
|
|
|
|
|
|
|
// The path must be always relative to the extension
|
|
|
|
palPath = base::join_path(path, palPath);
|
|
|
|
|
|
|
|
LOG("EXT: New palette '%s' in '%s'\n",
|
|
|
|
palId.c_str(),
|
|
|
|
palPath.c_str());
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
extension->addPalette(palId, palPath);
|
2017-06-11 03:44:19 +08:00
|
|
|
}
|
|
|
|
}
|
2017-06-15 03:34:09 +08:00
|
|
|
|
|
|
|
// Dithering matrices
|
|
|
|
auto ditheringMatrices = contributes["ditheringMatrices"];
|
|
|
|
if (ditheringMatrices.is_array()) {
|
2017-06-15 06:50:44 +08:00
|
|
|
for (const auto& ditheringMatrix : ditheringMatrices.array_items()) {
|
|
|
|
std::string matId = ditheringMatrix["id"].string_value();
|
|
|
|
std::string matPath = ditheringMatrix["path"].string_value();
|
|
|
|
std::string matName = ditheringMatrix["name"].string_value();
|
|
|
|
if (matName.empty())
|
|
|
|
matName = matId;
|
2017-06-15 03:34:09 +08:00
|
|
|
|
|
|
|
// The path must be always relative to the extension
|
|
|
|
matPath = base::join_path(path, matPath);
|
|
|
|
|
|
|
|
LOG("EXT: New dithering matrix '%s' in '%s'\n",
|
|
|
|
matId.c_str(),
|
|
|
|
matPath.c_str());
|
|
|
|
|
|
|
|
extension->addDitheringMatrix(matId, matPath, matName);
|
|
|
|
}
|
|
|
|
}
|
2017-06-11 02:02:39 +08:00
|
|
|
}
|
|
|
|
|
2017-06-12 23:38:53 +08:00
|
|
|
if (extension)
|
|
|
|
m_extensions.push_back(extension.get());
|
2017-06-11 02:02:39 +08:00
|
|
|
return extension.release();
|
|
|
|
}
|
|
|
|
|
2017-06-13 05:08:02 +08:00
|
|
|
void Extensions::generateExtensionSignals(Extension* extension)
|
|
|
|
{
|
2018-03-20 06:37:39 +08:00
|
|
|
if (extension->hasLanguages()) LanguagesChange(extension);
|
2017-06-15 03:34:09 +08:00
|
|
|
if (extension->hasThemes()) ThemesChange(extension);
|
|
|
|
if (extension->hasPalettes()) PalettesChange(extension);
|
|
|
|
if (extension->hasDitheringMatrices()) DitheringMatricesChange(extension);
|
2017-06-13 05:08:02 +08:00
|
|
|
}
|
|
|
|
|
2017-06-11 02:02:39 +08:00
|
|
|
} // namespace app
|