Skip to content

Commit

Permalink
Add lib-theme
Browse files Browse the repository at this point in the history
- Add a new library responsible for opening theme packages. This library
  is still very much a work in progress as of now.

- Add jsoncpp and libzip as dependencies.

- Add a new option to load theme packages in Preferences > Theme. On
  success, this shows a dialog box explaining everythinhg succeeded.

Signed-off-by: Avery King <[email protected]>
Avery King committed Nov 23, 2023
1 parent a4e851d commit 818a794
Showing 17 changed files with 957 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -561,6 +561,8 @@ elseif( CMAKE_SYSTEM_NAME MATCHES "Windows" )
file( TO_NATIVE_PATH "${pkgdir}/tools/python.exe" PYTHON )
endif()

find_package(jsoncpp REQUIRED)
find_package(libzip REQUIRED)
find_package(ZLIB REQUIRED)
find_package(EXPAT REQUIRED)
find_package(mp3lame REQUIRED)
1 change: 1 addition & 0 deletions libraries/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ set( LIBRARIES
lib-registries
lib-strings
lib-track
lib-theme
lib-utility
lib-xml
lib-audio-devices
22 changes: 22 additions & 0 deletions libraries/lib-theme/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
set(SOURCES
ThemePackage.cpp
ThemePackage.h
ThemeResources.cpp
ThemeResources.h
ThemeResourceList.cpp
ThemeResourceList.h

# Exceptions
exceptions/ArchiveError.cpp
exceptions/ArchiveError.h
exceptions/IncompatibleTheme.cpp
exceptions/IncompatibleTheme.h
)

set(LIBRARIES
PRIVATE
jsoncpp
libzip::zip
)

tenacity_library(lib-theme "${SOURCES}" "${LIBRARIES}" "" "")
51 changes: 51 additions & 0 deletions libraries/lib-theme/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Tenacity Theme Package Documentation

Tenacity theme packages come as ZIP files. Their structure looks something
like this:

```
TenacityDarkTheme.zip:
|
|--- theme.json
|--- colors.json
|--- bitmaps
|--- bmpPause.png
|--- bmpPlay.png
|--- ...etc
```

## Package Precedence

When an archive is first opened, Tenacity first reads `theme.json`. If this
file is not found, this file is invalid, or contains invalid values of required
properties, Tenacity considers the entire theme package invalid.

After reading `theme.json`, Tenacity reads `colors.json` next and loads any
color resources. Tenacity will always continue parsing the rest of the package
even if `colors.json` is invalid or missing, although it will use the system's
colors, which might not look good with the icon set.

Finally, after

## `theme.json`

At the root of each package is a `theme.json` file. This file contains basic
information about the theme package, such as the name, minimum required app
version, and other information.

### Properties

- `name` (string): The name of the theme package. Required.
- `minAppVersion` (int array): The minimum required app version (e.g.,
`[1, 4, 0]`). Optional, but strongly recommended. If not specified, a value
of `[1, 4, 0]` is specified as the deafult.
- `themeType` (string): Either "dark" for dark themes, "light" for light
themes, or "neutral" for neither.

## `colors.json`

This is where colors are specified.

### Properties

TODO.
240 changes: 240 additions & 0 deletions libraries/lib-theme/ThemePackage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**********************************************************************
Tenacity
ThemePackage.cpp
Avery King
License: GPL v2 or later
**********************************************************************/

#include "ThemePackage.h"

#include <stdexcept>

#include <jsoncpp/json/value.h>
#include <jsoncpp/json/reader.h>

#include "exceptions/ArchiveError.h"
#include "exceptions/IncompatibleTheme.h"

using namespace ThemeExceptions;

#define THROW_NOT_IMPLEMENTED throw std::runtime_error("Not implemented")

ThemePackage::ThemePackage() : mPackageArchive{nullptr}
{
}

ThemePackage::~ThemePackage()
{
ClosePackage();
}

void ThemePackage::OpenPackage(const std::string& path)
{
// Read the archive
int error = 0;
mPackageArchive = zip_open(path.c_str(), ZIP_RDONLY, &error);
if (!mPackageArchive)
{
switch (error)
{
case ZIP_ER_INCONS:
case ZIP_ER_READ:
case ZIP_ER_SEEK:
// TODO: better error handling
throw ArchiveError(ArchiveError::Type::OperationalError);
break;
case ZIP_ER_NOZIP:
case ZIP_ER_INVAL:
throw ArchiveError(ArchiveError::Type::InvalidArchive);
break;
case ZIP_ER_MEMORY:
throw std::bad_alloc();
break;
}
}

error = 0;

// Extract the JSON data from the archive
std::unique_ptr<char> jsonData;
zip_stat_t jsonInfo;

error = zip_stat(mPackageArchive, "theme.json", ZIP_STAT_SIZE, &jsonInfo);
if (error != 0)
{
// TODO: Better error handling
throw ArchiveError(ArchiveError::Type::OperationalError);
}

// Read the entire theme.json into memory
jsonData.reset(new char[jsonInfo.size]);
zip_file_t* themeFile = zip_fopen(mPackageArchive, "theme.json", 0);
zip_int64_t bytesRead = zip_fread(themeFile, jsonData.get(), jsonInfo.size);
if (bytesRead != jsonInfo.size)
{
zip_fclose(themeFile);
// TODO: Better error handling
throw ArchiveError(ArchiveError::Type::OperationalError);
}

std::string jsonString = jsonData.get();
mJsonStream = std::istringstream(jsonString);
}

/** @brief Parses a version string.
*
* This function works
*
* @param versionString The version string to parse
* @return Returns a std::vector<int> containing the version string values.
*
*/
std::vector<int> ParseVersionString(const std::string& versionString)
{
std::vector<int> version;
std::string tempString;
int tempVersion;
std::size_t previousPeriod = 0;
std::size_t period;

// This is a very simple version string parsing algorithm that merely
// creates substrings and converts them to an integer.
do
{
period = versionString.find('.', previousPeriod);
tempString = versionString.substr(previousPeriod, period - previousPeriod);

try
{
tempVersion = std::stoi(tempString);
} catch(...)
{
tempVersion = 0;
}

version.push_back(tempVersion);
previousPeriod = period + 1;
} while (period != std::string::npos);

return version;
}

void ThemePackage::ParsePackage()
{
if (mJsonStream.str().empty())
{
return;
}

Json::Value packageRoot;
{
Json::CharReaderBuilder builder;
std::string parserErrors;
bool ok = Json::parseFromStream(builder, mJsonStream, &packageRoot, &parserErrors);
if (!ok)
{
throw ArchiveError(ArchiveError::Type::OperationalError);
}
}

// Check if the theme package is a multi-theme package. If so, parse those separately
const Json::Value subthemes = packageRoot["subthemes"];
if (subthemes)
{
// TODO: handles subthemes
// throw std::runtime_error("Not implemented yet!");
return;
}

const Json::Value themeName = packageRoot["name"];
Json::Value minAppVersionString = packageRoot.get("minAppVersion", "0.0.0");
std::vector<int> minAppVersion = ParseVersionString(minAppVersionString.asString());
int minVersionMajor = TENACITY_VERSION;
int minVersionRelease = TENACITY_RELEASE;
int minVersionRevision = TENACITY_REVISION;

try
{
minVersionMajor = minAppVersion.at(0);
minVersionRelease = minAppVersion.at(1);
if (minAppVersion.size() >= 3) minVersionRevision = minAppVersion[2];
} catch (...)
{
// Something happened when parsing the version number. Assume '0.0.0' by default
minVersionMajor = minVersionRelease = minVersionRevision = 0;
}

// Handle minimum version compatibility
if (minVersionMajor < TENACITY_VERSION || minVersionRelease < TENACITY_RELEASE)
{
// TODO: Better exception handling
throw IncompatibleTheme(minVersionMajor, minVersionRelease, minVersionRevision);
}

// FIXME: Should the revision number really matter between theme packages?
// I don't think it should, but I'm leaving this in until we decide on that
// behavior...
// else if (minVersionRevision < TENACITY_REVISION)
// {
// // TODO: Better exception handling
// throw std::runtime_error("Incompatible theme");
// }

// Handle theme name
if (themeName.asString().empty())
{
// TODO: Better exception handling
throw std::runtime_error("Theme package does not have a name!");
}

// TODO: handle other properities.
}

void ThemePackage::ClosePackage()
{
if (mPackageArchive)
{
zip_close(mPackageArchive);
}
}

ThemePackage::ResourceMap& ThemePackage::GetResourceMap()
{
return mThemeResources;
}

void ThemePackage::LoadAllResources()
{
THROW_NOT_IMPLEMENTED;
}

void ThemePackage::LoadResource(const std::string name)
{
THROW_NOT_IMPLEMENTED;
}

void ThemePackage::LoadResources(const ThemePackage::ResourceList& names)
{
THROW_NOT_IMPLEMENTED;
}

std::any& ThemePackage::GetResource(std::string name)
{
return mThemeResources.at(name);
}

std::string ThemePackage::GetName() const
{
return mPackageName;
}

bool ThemePackage::IsMultiThemePackage() const
{
// FIXME: Unimplemented.
return false;
}
Loading

0 comments on commit 818a794

Please sign in to comment.