text:
<?php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    public function _initConfig()
    {
        Zend_Registry::set('config', $this->getOptions());
    }

    public function _initAutoloaderNamespaces()
    {
        require_once APPLICATION_PATH .
            '/../library/Doctrine/Common/ClassLoader.php';

        require_once APPLICATION_PATH .
            '/../library/Symfony/Component/Di/sfServiceContainerAutoloader.php';

        sfServiceContainerAutoloader::register();
        $autoloader = \Zend_Loader_Autoloader::getInstance();

        $fmmAutoloader = new \Doctrine\Common\ClassLoader('Bisna');
        $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Bisna');

        $fmmAutoloader = new \Doctrine\Common\ClassLoader('App');
        $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'App');

        $fmmAutoloader = new \Doctrine\Common\ClassLoader('Boilerplate');
        $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Boilerplate');

        $fmmAutoloader = new \Doctrine\Common\ClassLoader('Doctrine\DBAL\Migrations');
        $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Doctrine\DBAL\Migrations');
    }

    public function _initModuleLayout()
    {
        $front = Zend_Controller_Front::getInstance();

        $front->registerPlugin(
            new Boilerplate_Controller_Plugin_ModuleLayout()
        );

        $front->setParam('prefixDefaultModule', true);
        $eh = new Zend_Controller_Plugin_ErrorHandler();
        $front = Zend_Controller_Front::getInstance()->registerPlugin($eh);
    }

    public function _initServices()
    {
        $sc = new sfServiceContainerBuilder();
        $loader = new sfServiceContainerLoaderFileXml($sc);
        $loader->load(APPLICATION_PATH . "/configs/services.xml");
        Zend_Registry::set('sc', $sc);
    }

    public function _initLocale()
    {
        $config = $this->getOptions();

        try{
            $locale = new Zend_Locale(Zend_Locale::BROWSER);
        } catch (Zend_Locale_Exception $e) {
            $locale = new Zend_Locale($config['resources']['locale']['default']);
        }

        Zend_Registry::set('Zend_Locale', $locale);

        $translator = new Zend_Translate(
            array(
                'adapter' => 'Csv',
                'content' => APPLICATION_PATH . '/../data/lang/',
                'scan' => Zend_Translate::LOCALE_DIRECTORY,
                'delimiter' => ',',
                'disableNotices' => true,
            )
        );

        if (!$translator->isAvailable($locale->getLanguage()))
            $translator->setLocale($config['resources']['locale']['default']);

        Zend_Registry::set('Zend_Translate', $translator);
        Zend_Form::setDefaultTranslator($translator);
    }

    public function _initElasticSearch()
    {
        $es = new Elastica_Client();
        Zend_Registry::set('es', $es);
    }

}

repo_name: michael-romer/zf-boilerplate

repo_language: PHP

mime_type: text/html


text:
[production]
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts/"

; --------------------------
; PHP Specific Configuration
; --------------------------
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
phpSettings.soap.wsdl_cache_enabled = 1
includePaths.library = APPLICATION_PATH "/../library"
includePaths.resource = APPLICATION_PATH "/../library/Bisna/Application/Resource"

; ----------------------------------------
; Zend Framework Application Configuration
; ----------------------------------------
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
autoloadernamespaces[] = "App_"
autoloadernamespaces[] = "Boilerplate_"
autoloadernamespaces[] = "Elastica_"
autoloadernamespaces[] = "EasyBib_"
pluginPaths.Boilerplate_Resource = "Boilerplate/Resource"
pluginPaths.Bisna_Application_Resource = "Bisna/Application/Resource"
appnamespace = "Application"
resources.Depinjector = ""
resources.locale.default = "en"

; ------------------------------
; Front Controller Configuration
; ------------------------------

resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"
resources.frontController.params.displayExceptions = 0
resources.frontController.defaultModule = "site"
resources.frontController.actionhelperpaths.Boilerplate_Controller_Helper = "Boilerplate/Controller/Helper"

; ------------------------------------------------------------------------------
; Doctrine Cache Configuration
; ------------------------------------------------------------------------------

; Points to default cache instance to be used. Optional is only one cache is defined
resources.doctrine.cache.defaultCacheInstance = default

; Cache Instance configuration for "default" cache
;resources.doctrine.cache.instances.default.id = default
resources.doctrine.cache.instances.default.adapterClass = "Doctrine\Common\Cache\MemcacheCache"
resources.doctrine.cache.instances.default.namespace    = "App_"
resources.doctrine.cache.instances.default.options.servers.0.host = localhost
resources.doctrine.cache.instances.default.options.servers.0.port = 11211
;resources.doctrine.cache.instances.default.options.servers.0.persistent    = true
;resources.doctrine.cache.instances.default.options.servers.0.weight        = 1
;resources.doctrine.cache.instances.default.options.servers.0.timeout       = 1
;resources.doctrine.cache.instances.default.options.servers.0.retryInterval = 15
;resources.doctrine.cache.instances.default.options.servers.0.status        = true

; ------------------------------------------------------------------------------
; Doctrine DBAL Configuration
; ------------------------------------------------------------------------------

; Points to default connection to be used. Optional if only one connection is defined
resources.doctrine.dbal.defaultConnection = default

; DBAL Connection configuration for "default" connection
;resources.doctrine.dbal.connections.default.id = default
;resources.doctrine.dbal.connections.default.eventManagerClass  = "Doctrine\Common\EventManager"
;resources.doctrine.dbal.connections.default.eventSubscribers[] = "DoctrineExtensions\Sluggable\SluggableSubscriber"
;resources.doctrine.dbal.connections.default.configurationClass = "Doctrine\DBAL\Configuration"
;resources.doctrine.dbal.connections.default.sqlLoggerClass     = "Doctrine\DBAL\Logging\EchoSQLLogger"

; Database configuration
;resources.doctrine.dbal.connections.default.parameters.wrapperClass = ""
resources.doctrine.dbal.connections.default.parameters.driver   = "pdo_mysql"
resources.doctrine.dbal.connections.default.parameters.dbname   = "app"
resources.doctrine.dbal.connections.default.parameters.host = "localhost"
resources.doctrine.dbal.connections.default.parameters.port = 3306
resources.doctrine.dbal.connections.default.parameters.user = "root"
resources.doctrine.dbal.connections.default.parameters.password = ""
;resources.doctrine.dbal.connections.default.parameters.driverOptions.ATTR_USE_BUFFERED_QUERIES = true

; ------------------------------------------------------------------------------
; Doctrine ORM Configuration
; ------------------------------------------------------------------------------

; Points to default EntityManager to be used. Optional if only one EntityManager is defined
resources.doctrine.orm.defaultEntityManager = default

; EntityManager configuration for "default" manager
;resources.doctrine.orm.entityManagers.default.id = default
;resources.doctrine.orm.entityManager.default.entityManagerClass   = "Doctrine\ORM\EntityManager"
;resources.doctrine.orm.entityManager.default.configurationClass   = "Doctrine\ORM\Configuration"
resources.doctrine.orm.entityManager.default.entityNamespaces.app = "App\Entity"
resources.doctrine.orm.entityManagers.default.connection     = default
resources.doctrine.orm.entityManagers.default.proxy.autoGenerateClasses = false
resources.doctrine.orm.entityManagers.default.proxy.namespace           = "App\Entity\Proxy"
resources.doctrine.orm.entityManagers.default.proxy.dir                 = APPLICATION_PATH "/../library/App/Entity/Proxy"
;resources.doctrine.orm.entityManagers.default.metadataCache = default
;resources.doctrine.orm.entityManagers.default.queryCache    = default
;resources.doctrine.orm.entityManagers.default.resultCache   = default
;resources.doctrine.orm.entityManagers.default.DQLFunctions.numeric.PI = "DoctrineExtensions\ORM\Query\Functions\Numeric\PiFunction"
resources.doctrine.orm.entityManagers.default.metadataDrivers.annotationRegistry.annotationFiles[]     = APPLICATION_PATH "/../library/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php"
resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.adapterClass          = "Doctrine\ORM\Mapping\Driver\AnnotationDriver"
resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.mappingNamespace      = "App\Entity"
resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.mappingDirs[]         = APPLICATION_PATH "/../library/App/Entity"
resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.annotationReaderClass = "Doctrine\Common\Annotations\AnnotationReader"
resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.annotationReaderCache = default
;resources.doctrine.orm.entityManagers.default.metadataDrivers.drivers.0.annotationReaderNamespaces.App = "App\Entity"

; ------------------------------------------------------------------------------
; Custom Routes
; ------------------------------------------------------------------------------

resources.router.routes.artists.route = "/homepage"
resources.router.routes.artists.defaults.controller = index
resources.router.routes.artists.defaults.action = index
resources.router.routes.artists.defaults.module = site

[staging : production]

[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
phpSettings.soap.wsdl_cache_enabled = 0

[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
phpSettings.soap.wsdl_cache_enabled = 0


text:
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
  <services>
    <service id="randomQuote" class="App\Service\RandomQuote" shared="true">
      <argument type="service" id="randomizer" />
    </service>
    <service id="randomizer" class="App\Service\Randomizer" shared="true">
    </service>
  </services>
</container>


text:
<?php

class Site_ErrorController extends Zend_Controller_Action
{

    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:

                // 404 error -- controller or action not found
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Page not found';
                break;
            default:
                // application error
                if (APPLICATION_ENV == 'development')
                    throw $errors->exception;

                $this->getResponse()->setHttpResponseCode(500);
                $this->view->message = 'Application error';
                break;
        }

        // Log exception, if logger available
        if ($log = $this->getLog()) {
            $log->crit($this->view->message, $errors->exception);
        }

        // conditionally display exceptions
        if ($this->getInvokeArg('displayExceptions') == true) {
            $this->view->exception = $errors->exception;
        }

        $this->view->request   = $errors->request;
    }

    public function getLog()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        if (!$bootstrap->hasPluginResource('Log')) {
            return false;
        }
        $log = $bootstrap->getResource('Log');
        return $log;
    }

}


text:
<?php

class Site_IndexController extends Zend_Controller_Action
{

    /**
     * @var Doctrine\ORM\EntityManager
     */
    protected $_em = null;

    /**
     * @var \sfServiceContainer
     */
    protected $_sc = null;

    /**
     * @var \App\Service\RandomQuote
     * @InjectService RandomQuote
     */
    protected $_randomQuote = null;

    public function init()
    {
        $this->_em = Zend_Registry::get('em');
    }

    public function searchAction()
    {
        if ($query = $this->getRequest()->getParam('query')) {
            $resultSet = array();

            try {
                $client = Zend_Registry::get('es');
                $index = $client->getIndex('quotes');
                $type = $index->getType('quote');
                $resultSet = $type->search($query);
            } catch (Exception $e) {
                $this->_redirect('/');
            }

            $data = array();

            foreach ($resultSet as $result) {
                $hit = $result->getHit();
                $quote = new \App\Entity\Quote();
                $quote->setAuthor($hit['_source']['author']);
                $quote->setWording($hit['_source']['wording']);
                $data[] = $quote;
            }

            $this->view->search = true;
            $this->view->data = $data;
            $this->_helper->viewRenderer('index');
        }
        else
            $this->_redirect('/');

    }

    public function addRandomAction()
    {
        $tempQuote = $this->_randomQuote->getQuote();
        $newQuote = new \App\Entity\Quote();
        $newQuote->setWording($tempQuote[0]);
        $newQuote->setAuthor($tempQuote[1]);

        try
        {
            $this->_em->persist($newQuote);
            $this->_em->flush();
            $this->indexQuote($newQuote);
        }
        catch (Exception $e)
        {
            $this->_redirect('/');
        }

        $this->_redirect('/');
    }

    public function addCustomAction()
    {
        if ($this->_request->isPost()) {
            $addQuoteForm = new \App\Form\AddQuote();

            if ($addQuoteForm->isValid($this->_request->getPost())) {

                $values = $addQuoteForm->getValues();
                $newQuote = new \App\Entity\Quote();
                $newQuote->setWording($values['quote']);
                $newQuote->setAuthor($values['name']);

                try {
                    $this->_em->persist($newQuote);
                    $this->_em->flush();
                    $this->indexQuote($newQuote);
                }
                catch (Exception $e) {
                    $this->_redirect('/');
                }
            } else {
                $addQuoteForm->buildBootstrapErrorDecorators();
            }

            $data = $this->_em->getRepository("\App\Entity\Quote")
                    ->findThemAll();

            $this->view->data = $data;
            $this->_helper->viewRenderer('index');
        } else {
            $this->_redirect('/');
        }
    }

    public function indexAction()
    {
        $addQuoteForm = new \App\Form\AddQuote();
        $this->view->form = $addQuoteForm;
        $this->checkSearchindex();

        try {
            $data = $this->_em->getRepository("\App\Entity\Quote")
                    ->findThemAll();
            $this->view->data = $data;
        }
        catch (Exception $e) {
            $this->view->databaseError = true;
        }
    }

    private function checkSearchindex()
    {
        try {
            $client = Zend_Registry::get('es');
            $index = $client->getIndex('quotes');
            $type = $index->getType('quote');
            $resultSet = $type->search('*');
            //var_dump($resultSet);exit;
        } catch (Exception $e) {
            $this->view->searchindexError = true;
        }
    }

    private function indexQuote(\App\Entity\Quote $quote)
    {
       $client = Zend_Registry::get('es');
       $index = $client->getIndex('quotes');
       //$index->create(array(), true);
       $type = $index->getType('quote');

       $doc = new Elastica_Document(
           $quote->getId(), array('id' => $quote->getId(),
           'wording' => $quote->getWording(),
           'author' => $quote->getAuthor())
       );
       // var_dump($doc);exit;
       $type->addDocument($doc);
       $index->refresh();
    }

    public function headerAction()
    {
        $container = new Zend_Navigation(
            array(
                array(
                    'action'     => 'index',
                    'controller' => 'index',
                    'module'     => 'site',
                    'label'      => 'Home'
                ),
                array(
                    'uri'        => 'http://zf-boilerplate.com/documentation/',
                    'label'      => 'Documentation'
                )
            )
        );

        $this->view->navigation($container);
    }

    public function footerAction()
    {
        $cache = Zend_Registry::get('cache');

        if ($cache->contains('timestamp')) {
            $timestamp = $cache->fetch('timestamp');
            $this->view->cachedTimestamp = true;
        } else {
            $timestamp = time();
            $cache->save('timestamp', $timestamp);
        }

        $this->view->timestamp = $timestamp;
    }
}


text:
An error occurred, handled by Site Error Controller

<?php echo $this->message ?>

<?php if (isset($this->exception)): ?>

Exception information:

    Message: <?php echo $this->exception->getMessage() ?>

Stack trace:

<?php echo $this->exception->getTraceAsString() ?>

Request Parameters:

<?php echo var_export($this->request->getParams(), true) ?>

<?php endif ?>


text:
© 2011 Michael Romer

<?php echo $this->translate("Today is"); ?> <?php echo new Zend_Date($this->timestamp); ?> <?php if ($this->cachedTimestamp) { ?>(<?php echo $this->translate("Timestamp served from Memcached");?>)<?php } ?>


text:
<?php if ($this->databaseError): ?>

        ×

Sample Database: If you want see some persistence action, you will first need to SSH into your VM and run the database and fixtures setup script.

<?php endif; ?>

<?php if ($this->searchindexError): ?>

        ×

Fulltext Search: If you want query the fulltext search index, you will first need to SSH into your VM and run the searchindex and fixtures setup script.

<?php endif; ?>

<?php echo $this->translate("Hello from ZF-Boilerplate!");?>

<?php echo $this->translate("Well done, ZF-Boilerplate is up and running. You may want to modify or remove this example page.");?>

<?php echo $this->translate("Learn more");?> »

<?php if (!$this->databaseError): ?>

                <?php if (!$this->search): ?>
                    <?php echo $this->translate("Random Quotes"); ?>
                    <?php echo $this->translate("Add another"); ?>
                <?php else: ?>
                    <?php echo $this->translate("Quotes found"); ?>
                    <?php echo $this->translate("Back"); ?>
                <?php endif; ?>

            <?php foreach($this->data as $quote): ?>

<?php echo $quote; ?>

                    <?php echo $quote->getAuthor(); ?>

            <?php endforeach; ?>

        <?php echo $this->form; ?>

<?php endif; ?>


text:
            ZF-Boilerplate

            <?php echo $this->navigation()->menu(); ?>

            <form action="<?php echo $this->baseUrl('/index/search')?>" class="pull-right">
                <input class="input" name="query" type="text" placeholder="Cherralea Morgen">
                <button class="btn" type="submit"><?php echo $this->translate("Find Quote"); ?></button>
            </form>


text:
<!doctype html>
<!-- paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ -->
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
<!--[if IE 7]>    <html class="no-js ie7 oldie" lang="en"> <![endif]-->
<!--[if IE 8]>    <html class="no-js ie8 oldie" lang="en"> <![endif]-->
<!-- Consider adding an manifest.appcache: h5bp.com/d/Offline -->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
    <head>
        <meta charset="utf-8">

        <!-- Use the .htaccess and remove these lines to avoid edge case issues.
             More info: h5bp.com/b/378 -->
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <title>ZF-Boilerplate</title>
        <meta name="description" content="">
        <meta name="author" content="">

        <!-- Mobile viewport optimized: j.mp/bplateviewport -->
        <meta name="viewport" content="width=device-width,initial-scale=1">

        <!-- Place favicon.ico and apple-touch-icon.png in the root directory: mathiasbynens.be/notes/touch-icons -->

        <!-- CSS: implied media=all -->
        <!-- CSS concatenated and minified via ant build script-->
        <link rel="stylesheet" href="<?php echo $this->baseUrl('/css/style.css')?>">
        <link rel="stylesheet" href="<?php echo $this->baseUrl('/css/bootstrap.css')?>">
        <!-- end CSS-->

        <!-- More ideas for your <head> here: h5bp.com/d/head-Tips -->

        <!-- All JavaScript at the bottom, except for Modernizr / Respond.
             Modernizr enables HTML5 elements & feature detects; Respond is a polyfill for min/max-width CSS3 Media Queries
             For optimal performance, use a custom Modernizr build: www.modernizr.com/download/ -->
        <script src="<?php echo $this->baseUrl('js/libs/modernizr-2.0.6.min.js')?>"></script>
    </head>

    <body>
        <?php echo $this->action('header', 'index', 'site'); ?>

        <div class="container" style="margin-top:70px;">
            <?php echo $this->layout()->content; ?>
        </div>

        <footer>
            <?php echo $this->action('footer', 'index', 'site'); ?>
        </footer>

        <!-- JavaScript at the bottom for fast page loading -->

        <!-- Grab Google CDN's jQuery, with a protocol relative URL; fall back to local if offline -->
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
        <script>window.jQuery || document.write('<script src="js/libs/jquery-1.6.2.min.js"><\/script>')</script>

        <!-- scripts concatenated and minified via ant build script-->
        <script defer src="<?php echo $this->baseUrl('js/plugins.js')?>"></script>
        <script defer src="<?php echo $this->baseUrl('js/script.js')?>"></script>
        <!-- end scripts-->

        <!-- Change UA-XXXXX-X to be your site's ID -->
        <script>
            window._gaq = [['_setAccount','UAXXXXXXXX1'],['_trackPageview'],['_trackPageLoadTime']];
            Modernizr.load({
                load: ('https:' == location.protocol ? '//ssl' : '//www') + '.google-analytics.com/ga.js'
            });
        </script>

        <!-- Prompt IE 6 users to install Chrome Frame. Remove this if you want to support IE 6.
             chromium.org/developers/how-tos/chrome-frame-getting-started -->
        <!--[if lt IE 7 ]>
            <script src="//ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js"></script>
            <script>window.attachEvent('onload',function(){CFInstall.check({mode:'overlay'})})</script>
        <![endif]-->
    </body>
</html>


text:
<?php

class My_ErrorController extends Zend_Controller_Action
{

    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        echo "<pre>";
            var_dump($errors->exception->getMessage());
            var_dump($errors->exception->getTraceAsString());
        echo "</pre>";

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:

                // 404 error -- controller or action not found
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Page not found';
                break;
            default:
                // application error
                $this->getResponse()->setHttpResponseCode(500);
                $this->view->message = 'Application error';
                break;
        }

        // Log exception, if logger available
        if ($log = $this->getLog()) {
            $log->crit($this->view->message, $errors->exception);
        }

        // conditionally display exceptions
        if ($this->getInvokeArg('displayExceptions') == true) {
            $this->view->exception = $errors->exception;
        }

        $this->view->request   = $errors->request;
    }

    public function getLog()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        if (!$bootstrap->hasPluginResource('Log')) {
            return false;
        }
        $log = $bootstrap->getResource('Log');
        return $log;
    }

}


text:
<?php

class My_IndexController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
       // action body
    }

    public function headerAction()
    {
        // action body
    }

    public function footerAction()
    {
        // action body
    }

}


text:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Zend Framework Default Application</title>

An error occurred, handled by My Error Controller

<?php echo $this->message ?>

  <?php if (isset($this->exception)): ?>

Exception information:

      Message: <?php echo $this->exception->getMessage() ?>

Stack trace:

<?php echo $this->exception->getTraceAsString() ?>

Request Parameters:

<?php echo var_export($this->request->getParams(), true) ?>

  <?php endif ?>


text:
<!doctype html>

        <meta charset="utf-8">

        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <title></title>
        <meta name="description" content="">
        <meta name="author" content="">

        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <link rel="shortcut icon" href="<?php echo $this->baseUrl('/favicon.ico')?>">
        <link rel="apple-touch-icon" href="<?php echo $this->baseUrl('/apple-touch-icon.png')?>">

        <link rel="stylesheet" href="<?php echo $this->baseUrl('css/style.css?v=2')?>">

                <?php echo $this->action('header', 'index', 'my');  ?>

                <?php echo $this->layout()->content; ?>

                <?php echo $this->action('footer', 'index', 'my');  ?>


text:
<?php

class Admin_ErrorController extends Zend_Controller_Action
{

    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        echo "<pre>";
            var_dump($errors->exception->getMessage());
            var_dump($errors->exception->getTraceAsString());
        echo "</pre>";

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:

                // 404 error -- controller or action not found
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Page not found';
                break;
            default:
                // application error
                $this->getResponse()->setHttpResponseCode(500);
                $this->view->message = 'Application error';
                break;
        }

        // Log exception, if logger available
        if ($log = $this->getLog()) {
            $log->crit($this->view->message, $errors->exception);
        }

        // conditionally display exceptions
        if ($this->getInvokeArg('displayExceptions') == true) {
            $this->view->exception = $errors->exception;
        }

        $this->view->request   = $errors->request;
    }

    public function getLog()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        if (!$bootstrap->hasPluginResource('Log')) {
            return false;
        }
        $log = $bootstrap->getResource('Log');
        return $log;
    }

}


text:
<?php

class Admin_IndexController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
    }

    public function headerAction()
    {
        // action body
    }

    public function footerAction()
    {
        // action body
    }

}


text:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Zend Framework Default Application</title>

An error occurred, handled by Admin Error Controller

<?php echo $this->message ?>

  <?php if (isset($this->exception)): ?>

Exception information:

      Message: <?php echo $this->exception->getMessage() ?>

Stack trace:

<?php echo $this->exception->getTraceAsString() ?>

Request Parameters:

<?php echo var_export($this->request->getParams(), true) ?>

  <?php endif ?>


text:
<!doctype html>

        <meta charset="utf-8">

        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <title></title>
        <meta name="description" content="">
        <meta name="author" content="">

        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <link rel="shortcut icon" href="<?php echo $this->baseUrl('/favicon.ico')?>">
        <link rel="apple-touch-icon" href="<?php echo $this->baseUrl('/apple-touch-icon.png')?>">

        <link rel="stylesheet" href="<?php echo $this->baseUrl('css/style.css?v=2')?>">

                <?php echo $this->action('header', 'index', 'admin');  ?>

                <?php echo $this->layout()->content; ?>

                <?php echo $this->action('footer', 'index', 'admin');  ?>


text:
# Project
group=com.mrcrayfish

# Common
minecraft_version=1.20.1
common_runs_enabled=false
common_client_run_name=Common Client
common_server_run_name=Common Server

# Forge
forge_version=47.0.3
//forge_ats_enabled=true

# Fabric
fabric_version=0.83.1+1.20.1
fabric_loader_version=0.14.21

# Mod options
mod_version=1.7.1
mod_name=Catalogue
mod_author=MrCrayfish
mod_id=catalogue
mod_desc=A new and improved mod list with a modern design
mod_homepage=https://mrcrayfish.com/mods?id=catalogue
mod_source=https://github.com/MrCrayfish/Catalogue
mod_issues=https://github.com/MrCrayfish/Catalogue/issues
mod_license=GNU General Public License v3

# Gradle
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false

repo_name: MrCrayfish/Catalogue

repo_language: Java

mime_type: text/x-java


text:
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


text:
#!/usr/bin/env bash

#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MSYS* | MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`

    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=`expr $i + 1`
    done
    case $i in
        0) set -- ;;
        1) set -- "$args0" ;;
        2) set -- "$args0" "$args1" ;;
        3) set -- "$args0" "$args1" "$args2" ;;
        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

ARGV=("$@")
eval set -- $DEFAULT_JVM_OPTS

IFS=$'
' read -rd '' -a JAVA_OPTS_ARR <<< "$(echo $JAVA_OPTS | xargs -n1)"
IFS=$'
' read -rd '' -a GRADLE_OPTS_ARR <<< "$(echo $GRADLE_OPTS | xargs -n1)"

exec "$JAVACMD" "$@" "${JAVA_OPTS_ARR[@]}" "${GRADLE_OPTS_ARR[@]}" "-Dorg.gradle.appname=$APP_BASE_NAME" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "${ARGV[@]}"


text:
![Catalogue Banner](https://i.imgur.com/iIW4kpp.png)

![Minecraft](https://img.shields.io/static/v1?label=&message=1.17%20|%201.16&color=2d2d2d&labelColor=dddddd&style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjCGJ1kDAAACoElEQVQ4T22SeU8aURTF/ULGtNRWWVQY9lXABWldIDPIMgVbNgEVtaa0damiqGBdipXaJcY2ofEf4ycbTt97pVAabzK5b27u+Z377kwXgK77QthRy7OfXbeJM+ttqKSXN8sdwbT/A0L7elmsYqrPHZmROLPh5YkV4oEBwaKuHj+yyJptLDoAhbq3O1V1XCVObY3FL24mfn5oRPrcwSCRfQOyNWcjVjZdCbtcdwcgXrXUspdOKbDN/XE9tiBJMhXHT60gUIT2dMhcDLMc3NVKQklz0QIkf5qlyEcO6Qs7yPhMJB4amDMFimQSmqNlE8SKAZFzDfxHfVILIIZ10sJ3OwIbcqSuiOjchkzNCboHev9o2YhgiUP8mxnLN24I6/3ghYdtQG5iUMpFBuCP9iKwLsfiLyeCp2rMnZgwX3NArGoxW1Ridl+BzLEVKa8KSxOqNmDdz0kFnxaLHhWEgAyZigWhHXL+pEDy2ozsDxv8vAzTnh7w5kcghqCaFmCT10of4iPIT2mRdPUh4HoCcVwBH/8Ac2kzUkEV5r3EfVSOvbAJa5NDyI0r2oDtWb1EClh+OoC3Pg7v/Bw7p939yI4rsRW2Y3lKh01eh7WpIRyKZqzyjjYgPdIvlaMWRqYuG7wWryYHsRM0sFolZiPvQ3jheIwSmSBPdkByG/B6Wi3RYiVmRX7GiAPiUCRisii8D+jZNKvPBrHCW1GY0bAz6WkDCtOaSyKQFsi4K5NqNiZtehN2Y5uAShETqolhBqJXpfdPuPsuWwAaRdHSkxdc11mPqkGnyY4pyKbpl1GyJ0Pel7yqBoFcF3zqno5f+d8ohYy9Sx7lzQpxo1eirluCDgt++00p6uxttrG4F/A39sJGZWZMfrcp6O6+5kaVzXJHAOj6DeSs8qw5o8oxAAAAAElFTkSuQmCC) [![Curseforge](http://cf.way2muchnoise.eu/full_catalogue_downloads.svg?badge_style=for_the_badge)](https://www.curseforge.com/minecraft/mc-mods/catalogue)

# Catalogue

Catalogue is a simple mod that replaces Forge's mod list menu with an updated and modernised version. Catalogue creates a more rich experience for players, making it easier to search installed mods, check for updates, submit bugs and see relevant mod information all in a brand new and modernised layout. All mods that work with Forge's mod list will automatically work with Catalogue and requires no extra work from developers.

## Developers

This mod has support to change the item icon in the mod list incase you want to change it to an item in your mod. All you need to do is add `itemIcon` property into your `mods.toml` under `[[mods]]` and set the value to `"minecraft:apple"` or another item. You do not need to make your mod depend on this mod, however on CurseForge I appreciate if you mark this mod as an optional dependency.

## Screenshots

![Screenshot 1](https://i.imgur.com/pb4H0pQ.png)
![Screenshot 2](https://i.imgur.com/hFVqGJX.png)


text:
<component name="DependencyValidationManager">
  <scope name="Forge sources" pattern="!ext[Gradle: loom_mappings_*:*:*]:*/&&!ext[Gradle: net.fabricmc:*:*]:*/&&!ext[Gradle: net.minecraft:joined*:*]:*/&&!ext[Gradle: net.minecraft:mappings_official:zip:*]:*/&&!ext[Gradle: net.minecraft:minecraft-project-*:*]:*/" />
</component>


text:
name: Build and Sign Mod
on:
  push:
    paths:
      - 'release_build'
jobs:
  build:
    runs-on: ubuntu-latest
    environment: Build
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'adopt'
    - name: Validate Gradle Wrapper
      uses: gradle/wrapper-validation-action@v1
    - name: Read modid from gradle.properties
      id: mod_id
      uses: Reedyuk/read-properties@v1.0.1
      with:
        path: './gradle.properties'
        property: 'mod_id'
    - name: Read mod version from gradle.properties
      id: mod_version
      uses: Reedyuk/read-properties@v1.0.1
      with:
        path: './gradle.properties'
        property: 'mod_version'
    - name: Read minecraft version from gradle.properties
      id: mc_version
      uses: Reedyuk/read-properties@v1.0.1
      with:
        path: './gradle.properties'
        property: 'minecraft_version'
    - name: Cache
      uses: actions/cache@v3
      with:
        path: ~/.gradle/caches
        key: gradle-minecraft-${{ steps.mc_version.outputs.value }}
    - name: Load Keystore
      id: load_keystore
      env:
        JKS: ${{ secrets.MRCRAYFISH_JKS }}
      if: ${{ env.JKS != '' }}
      uses: timheuer/base64-to-file@v1.2
      with:
        fileName: 'keystore.jks'
        encodedString: ${{ env.JKS }}
    - name: Set Keystore Variables
      if: ${{ steps.load_keystore.outputs.filePath != '' }}
      run: |
        echo "KEYSTORE=${{ steps.load_keystore.outputs.filePath }}" >> $GITHUB_ENV
        echo "KEYSTORE_ALIAS=${{ secrets.MRCRAYFISH_JKS_ALIAS }}" >> $GITHUB_ENV
        echo "KEYSTORE_PASS=${{ secrets.MRCRAYFISH_JKS_PASSPHRASE }}" >> $GITHUB_ENV
    - name: Make gradlew executable
      run: chmod +x ./gradlew
    - name: Build and Publish
      env:
        GPR_USER: "MrCrayfish"
        GPR_KEY: ${{ secrets.GPR_TOKEN }}
        SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
        SIGNING_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
      run: ./gradlew build publish
    - name: Stop Gradle
      run: ./gradlew --stop
    - name: Construct artifact files
      id: artifacts
      run: |
        echo "forge=Forge/build/libs/${{ steps.mod_id.outputs.value }}-forge-${{ steps.mc_version.outputs.value }}-${{ steps.mod_version.outputs.value }}.jar" >> "$GITHUB_OUTPUT"
        echo "fabric=Fabric/build/libs/${{ steps.mod_id.outputs.value }}-fabric-${{ steps.mc_version.outputs.value }}-${{ steps.mod_version.outputs.value }}.jar" >> "$GITHUB_OUTPUT"
    - name: Import GPG
      env:
        SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
        SIGNING_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
      if: ${{ env.SIGNING_KEY != '' && env.SIGNING_PASSPHRASE != '' }}
      uses: crazy-max/ghaction-import-gpg@v5.3.0
      with:
        gpg_private_key: ${{ env.SIGNING_KEY }}
        passphrase: ${{ env.SIGNING_PASSPHRASE }}
    - name: Sign JAR
      env:
        SIGNING_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
      if: ${{ env.SIGNING_PASSPHRASE != '' }}
      run: |
        gpg --batch --yes --passphrase "${{ env.SIGNING_PASSPHRASE }}" --armor --detach-sign ${{ steps.artifacts.outputs.forge }}
        gpg --batch --yes --passphrase "${{ env.SIGNING_PASSPHRASE }}" --armor --detach-sign ${{ steps.artifacts.outputs.fabric }}
    - name: Upload Forge Artifacts
      uses: actions/upload-artifact@v2
      with:
        name: '${{ steps.mod_id.outputs.value }}-forge-${{ steps.mc_version.outputs.value }}-${{ steps.mod_version.outputs.value }}'
        path: |
          ${{ steps.artifacts.outputs.forge }}
          ${{ steps.artifacts.outputs.forge }}.asc
    - name: Upload Fabric Artifacts
      uses: actions/upload-artifact@v2
      with:
        name: '${{ steps.mod_id.outputs.value }}-fabric-${{ steps.mc_version.outputs.value }}-${{ steps.mod_version.outputs.value }}'
        path: |
          ${{ steps.artifacts.outputs.fabric }}
          ${{ steps.artifacts.outputs.fabric }}.asc


text:
modLoader="javafml"
loaderVersion="[39,)"
license="${mod_license}"
issueTrackerURL="${mod_issues}"

[[mods]]
modId="${mod_id}"
version="${mod_version}"
displayName="${mod_name}"
updateJSONURL="https://mrcrayfish.com/modupdatejson?id=${mod_id}"
displayURL="${mod_homepage}"
logoFile="${mod_id}_banner.png"
logoBlur=false
authors="${mod_author}"
description="${mod_desc}"

[[dependencies."${mod_id}"]]
    modId="forge"
    mandatory=true
    versionRange="[${forge_version},)"
    ordering="NONE"
    side="BOTH"

[[dependencies."${mod_id}"]]
    modId="minecraft"
    mandatory=true
    versionRange="[${minecraft_version},)"
    ordering="NONE"
    side="BOTH"

[modproperties."${mod_id}"]
    catalogueImageIcon="${mod_id}_icon.png"
    catalogueBackground="${mod_id}_background.png"
    configuredBackground="minecraft:textures/block/stone.png"


text:
package com.mrcrayfish.catalogue;

import net.minecraftforge.fml.IExtensionPoint;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.network.NetworkConstants;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Author: MrCrayfish
 */
@Mod(Constants.MOD_ID)
public class Catalogue
{
    public Catalogue()
    {
        ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true));
    }
}


text:
package com.mrcrayfish.catalogue.client;

import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraftforge.client.ConfigScreenHandler;
import net.minecraftforge.fml.VersionChecker;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.forgespi.language.IModInfo;

import javax.annotation.Nullable;

/**
 * Author: MrCrayfish
 */
public class ForgeModData implements IModData
{
    private final IModInfo info;

    public ForgeModData(IModInfo info)
    {
        this.info = info;
    }

    @Override
    public Type getType()
    {
        return Type.DEFAULT;
    }

    @Override
    public String getModId()
    {
        return this.info.getModId();
    }

    @Override
    public String getDisplayName()
    {
        return this.info.getDisplayName();
    }

    @Override
    public String getVersion()
    {
        return this.info.getVersion().toString();
    }

    @Override
    public String getDescription()
    {
        return this.info.getDescription();
    }

    @Override
    @Nullable
    public String getItemIcon()
    {
        String itemIcon = (String) this.info.getModProperties().get("catalogueItemIcon");
        if(itemIcon == null)
        {
            // Fallback to old method for backwards compatibility on Forge
            itemIcon = (String) ((ModInfo) this.info).getConfigElement("itemIcon").orElse(null);
        }
        return itemIcon;
    }

    @Nullable
    @Override
    public String getImageIcon()
    {
        return this.info.getModProperties().get("catalogueImageIcon") instanceof String s ? s : null;
    }

    @Override
    public String getLicense()
    {
        return this.info.getOwningFile().getLicense();
    }

    @Override
    public String getCredits()
    {
        return this.getConfigString("credits");
    }

    @Nullable
    @Override
    public String getAuthors()
    {
        return this.getConfigString("authors");
    }

    @Nullable
    @Override
    public String getHomepage()
    {
        return this.getConfigString("displayURL");
    }

    @Nullable
    @Override
    public String getIssueTracker()
    {
        return this.getConfigString("issueTrackerURL");
    }

    @Nullable
    @Override
    public String getBanner()
    {
        return this.info.getLogoFile().orElse(null);
    }

    @Nullable
    @Override
    public String getBackground()
    {
        return this.info.getModProperties().get("catalogueBackground") instanceof String s ? s : null;
    }

    @Override
    public Update getUpdate()
    {
        VersionChecker.CheckResult result = VersionChecker.getResult(this.info);
        if(result.status().shouldDraw())
        {
            return new Update(result.status().isAnimated(), result.url(), result.status().getSheetOffset());
        }
        return null;
    }

    @Override
    public boolean hasConfig()
    {
        return ConfigScreenHandler.getScreenFactoryFor(this.info).isPresent();
    }

    @Override
    public boolean isLogoSmooth()
    {
        return this.info.getLogoBlur();
    }

    @Override
    public void openConfigScreen(Screen parent)
    {
        ConfigScreenHandler.getScreenFactoryFor(this.info).map(f -> f.apply(Minecraft.getInstance(), parent)).ifPresent(newScreen -> Minecraft.getInstance().setScreen(newScreen));
    }

    @Nullable
    private String getConfigString(String key)
    {
        return ((ModInfo) this.info).getConfigElement(key).map(Object::toString).orElse(null);
    }
}


text:
package com.mrcrayfish.catalogue.client;

import com.mrcrayfish.catalogue.Constants;
import com.mrcrayfish.catalogue.client.screen.CatalogueModListScreen;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ScreenEvent;
import net.minecraftforge.client.gui.ModListScreen;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

/**
 * Author: MrCrayfish
 */
@Mod.EventBusSubscriber(modid = Constants.MOD_ID, value = Dist.CLIENT)
public class ClientHandler
{
    @SubscribeEvent
    public static void onOpenScreen(ScreenEvent.Opening event)
    {
        if(event.getScreen() instanceof ModListScreen)
        {
            event.setNewScreen(new CatalogueModListScreen(event.getCurrentScreen()));
        }
    }
}


text:
package com.mrcrayfish.catalogue.platform;

import com.mrcrayfish.catalogue.platform.services.IComponentHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;

/**
 * Author: MrCrayfish
 */
public class ForgeComponentHelper implements IComponentHelper
{
    @Override
    public MutableComponent createTitle()
    {
        return Component.translatable("fml.menu.mods.title");
    }

    @Override
    public MutableComponent createVersion(String version)
    {
        return Component.translatable("fml.menu.mods.info.version", version);
    }

    @Override
    public MutableComponent createFormatted(String formatKey, String value)
    {
        return Component.translatable(formatKey, value);
    }

    @Override
    public Component createFilterUpdates()
    {
        return Component.translatable("fml.menu.mods.filter_updates");
    }

    @Override
    public String getCreditsKey()
    {
        return "catalogue.gui.credits";
    }
}


text:
package com.mrcrayfish.catalogue.platform;

import com.mojang.blaze3d.platform.NativeImage;
import com.mrcrayfish.catalogue.client.ForgeModData;
import com.mrcrayfish.catalogue.client.IModData;
import com.mrcrayfish.catalogue.client.screen.CatalogueModListScreen;
import com.mrcrayfish.catalogue.platform.services.IPlatformHelper;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.server.packs.resources.IoSupplier;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.resource.PathPackResources;
import net.minecraftforge.resource.ResourcePackLoader;
import org.apache.commons.lang3.tuple.Pair;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Author: MrCrayfish
 */
public class ForgePlatformHelper implements IPlatformHelper
{
    @Override
    public boolean isForge()
    {
        return true;
    }

    @Override
    public List<IModData> getAllModData()
    {
        return ModList.get().getMods().stream().map(ForgeModData::new).collect(Collectors.toList());
    }

    @Override
    public File getModDirectory()
    {
        return FMLPaths.MODSDIR.get().toFile();
    }

    @Override
    public void loadNativeImage(String modId, String resource, Consumer<NativeImage> consumer)
    {
        PathPackResources resourcePack = ResourcePackLoader.getPackFor(modId).orElse(ResourcePackLoader.getPackFor("forge").orElseThrow(() -> new RuntimeException("Can't find forge, WHAT!")));
        IoSupplier<InputStream> supplier = resourcePack.getRootResource(resource);
        if(supplier != null)
        {
            try(InputStream is = supplier.get(); NativeImage image = NativeImage.read(is))
            {
                consumer.accept(image);
            }
            catch(IOException ignored) {}
        }
    }
}


text:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


text:
package com.mrcrayfish.catalogue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Constants
{
    public static final String MOD_ID = "catalogue";
    public static final String MOD_NAME = "Catalogue";
    public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME);
}


text:
package com.mrcrayfish.catalogue.client;

import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.screens.Screen;

import javax.annotation.Nullable;

/**
 * Author: MrCrayfish
 */
public interface IModData
{
    Type getType();

    String getModId();

    String getDisplayName();

    String getVersion();

    String getDescription();

    @Nullable
    String getItemIcon();

    @Nullable
    String getImageIcon();

    String getLicense();

    @Nullable
    String getCredits();

    @Nullable
    String getAuthors();

    @Nullable
    String getHomepage();

    @Nullable
    String getIssueTracker();

    @Nullable
    String getBanner();

    @Nullable
    String getBackground();

    Update getUpdate();

    boolean hasConfig();

    boolean isLogoSmooth();

    void openConfigScreen(Screen parent);

    record Update(boolean animated, String url, int texOffset) {}

    enum Type
    {
        DEFAULT(ChatFormatting.RESET),
        LIBRARY(ChatFormatting.DARK_GRAY),
        GENERATED(ChatFormatting.AQUA);

        private final ChatFormatting style;

        Type(ChatFormatting style)
        {
            this.style = style;
        }

        public ChatFormatting getStyle()
        {
            return this.style;
        }
    }
}


text:
package com.mrcrayfish.catalogue.client;

import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;

/**
 * Author: MrCrayfish
 */
public class ClientHelper
{
    /**
     * Creates a scissor test using minecraft screen coordinates instead of pixel coordinates.
     * @param screenX
     * @param screenY
     * @param boxWidth
     * @param boxHeight
     */
    public static void scissor(int screenX, int screenY, int boxWidth, int boxHeight)
    {
        Minecraft mc = Minecraft.getInstance();
        int scale = (int) mc.getWindow().getGuiScale();
        int x = screenX * scale;
        int y = mc.getWindow().getHeight() - screenY * scale - boxHeight * scale;
        int width = Math.max(0, boxWidth * scale);
        int height = Math.max(0, boxHeight * scale);
        RenderSystem.enableScissor(x, y, width, height); //TODO this might be weird now
    }

    public static boolean isMouseWithin(int x, int y, int width, int height, int mouseX, int mouseY)
    {
        return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height;
    }
}


text:
package com.mrcrayfish.catalogue.client.screen;

import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexFormat;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mrcrayfish.catalogue.Constants;
import com.mrcrayfish.catalogue.client.ClientHelper;
import com.mrcrayfish.catalogue.client.IModData;
import com.mrcrayfish.catalogue.client.screen.widget.CatalogueCheckBoxButton;
import com.mrcrayfish.catalogue.client.screen.widget.CatalogueIconButton;
import com.mrcrayfish.catalogue.platform.ClientServices;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractSelectionList;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Checkbox;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.ObjectSelectionList;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.client.sounds.SoundManager;
import net.minecraft.commands.arguments.item.ItemParser;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.apache.commons.lang3.tuple.Pair;
import org.joml.Matrix4f;
import org.lwjgl.glfw.GLFW;

import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Author: MrCrayfish
 */
public class CatalogueModListScreen extends Screen
{
    private static final Comparator<ModListEntry> SORT = Comparator.comparing(o -> o.getData().getDisplayName());
    private static final ResourceLocation MISSING_BANNER = new ResourceLocation(Constants.MOD_ID, "textures/gui/missing_banner.png");
    private static final ResourceLocation VERSION_CHECK_ICONS = new ResourceLocation("forge", "textures/gui/version_check_icons.png");
    private static final Map<String, Pair<ResourceLocation, Dimension>> BANNER_CACHE = new HashMap<>();
    private static final Map<String, Pair<ResourceLocation, Dimension>> IMAGE_ICON_CACHE = new HashMap<>();
    private static final Map<String, ItemStack> ITEM_ICON_CACHE = new HashMap<>();
    private static final Map<String, IModData> CACHED_MODS = new HashMap<>();
    private static ResourceLocation cachedBackground;
    private static boolean loaded = false;

    private final Screen parentScreen;
    private EditBox searchTextField;
    private ModList modList;
    private IModData selectedModData;
    private Button modFolderButton;
    private Button configButton;
    private Button websiteButton;
    private Button issueButton;
    private Checkbox updatesButton;
    private StringList descriptionList;
    private int tooltipYOffset;
    private List<? extends FormattedCharSequence> activeTooltip;

    public CatalogueModListScreen(Screen parent)
    {
        super(CommonComponents.EMPTY);
        this.parentScreen = parent;
        if(!loaded)
        {
            ClientServices.PLATFORM.getAllModData().forEach(data -> CACHED_MODS.put(data.getModId(), data));
            loaded = true;
        }
    }

    @Override
    public void onClose()
    {
        this.minecraft.setScreen(this.parentScreen);
    }

    @Override
    protected void init()
    {
        super.init();
        this.searchTextField = new EditBox(this.font, 11, 25, 148, 20, CommonComponents.EMPTY);
        this.searchTextField.setResponder(s -> {
            this.updateSearchField(s);
            this.modList.filterAndUpdateList(s);
            this.updateSelectedModList();
        });
        this.addWidget(this.searchTextField);
        this.modList = new ModList();
        this.modList.setLeftPos(10);
        this.modList.setRenderTopAndBottom(false);
        this.addWidget(this.modList);
        this.addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, btn -> {
            this.minecraft.setScreen(null);
        }).pos(10, this.modList.getBottom() + 8).size(127, 20).build());
        this.modFolderButton = this.addRenderableWidget(new CatalogueIconButton(140, this.modList.getBottom() + 8, 0, 0, onPress -> {
            Util.getPlatform().openFile(ClientServices.PLATFORM.getModDirectory());
        }));
        int padding = 10;
        int contentLeft = this.modList.getRight() + 12 + padding;
        int contentWidth = this.width - contentLeft - padding;
        int buttonWidth = (contentWidth - padding) / 3;
        this.configButton = this.addRenderableWidget(new CatalogueIconButton(contentLeft, 105, 10, 0, buttonWidth, Component.translatable("catalogue.gui.config"), onPress ->
        {
            if(this.selectedModData != null)
            {
                this.selectedModData.openConfigScreen(this);
            }
        }));
        this.configButton.visible = false;
        this.websiteButton = this.addRenderableWidget(new CatalogueIconButton(contentLeft + buttonWidth + 5, 105, 20, 0, buttonWidth, Component.literal("Website"), onPress -> {
            this.openLink(this.selectedModData.getHomepage());
        }));
        this.websiteButton.visible = false;
        this.issueButton = this.addRenderableWidget(new CatalogueIconButton(contentLeft + buttonWidth + buttonWidth + 10, 105, 30, 0, buttonWidth, Component.literal("Submit Bug"), onPress -> {
            this.openLink(this.selectedModData.getIssueTracker());
        }));
        this.issueButton.visible = false;
        this.descriptionList = new StringList(contentWidth, this.height - 135 - 55, contentLeft, 130);
        this.descriptionList.setRenderTopAndBottom(false);
        this.descriptionList.setRenderBackground(false);
        this.addWidget(this.descriptionList);

        this.updatesButton = this.addRenderableWidget(new CatalogueCheckBoxButton(this.modList.getRight() - 14, 7, button -> {
            this.modList.filterAndUpdateList(this.searchTextField.getValue());
            this.updateSelectedModList();
        }));

        this.modList.filterAndUpdateList(this.searchTextField.getValue());

        // Resizing window causes all widgets to be recreated, therefore need to update selected info
        if(this.selectedModData != null)
        {
            this.setSelectedModData(this.selectedModData);
            this.updateSelectedModList();
            ModListEntry entry = this.modList.getEntryFromInfo(this.selectedModData);
            if(entry != null)
            {
                this.modList.centerScrollOn(entry);
            }
        }
        this.updateSearchField(this.searchTextField.getValue());
    }

    private void openLink(@Nullable String url)
    {
        if(url != null)
        {
            Style style = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url));
            this.handleComponentClicked(style);
        }
    }

    @Override
    public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks)
    {
        this.activeTooltip = null;
        this.renderBackground(graphics);
        this.drawModList(graphics, mouseX, mouseY, partialTicks);
        this.drawModInfo(graphics, mouseX, mouseY, partialTicks);
        super.render(graphics, mouseX, mouseY, partialTicks);

        Optional<IModData> optional = Optional.ofNullable(CACHED_MODS.get(Constants.MOD_ID));
        optional.ifPresent(this::loadAndCacheLogo);
        Pair<ResourceLocation, Dimension> pair = BANNER_CACHE.get(Constants.MOD_ID);
        if(pair != null && pair.getLeft() != null)
        {
            ResourceLocation textureId = pair.getLeft();
            Dimension size = pair.getRight();
            RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
            graphics.blit(textureId, 10, 9, 10, 10, 0.0F, 0.0F, size.width, size.height, size.width, size.height);
        }

        if(ClientHelper.isMouseWithin(10, 9, 10, 10, mouseX, mouseY))
        {
            this.setActiveTooltip(Component.translatable("catalogue.gui.info"));
            this.tooltipYOffset = 10;
        }

        if(this.modFolderButton.isMouseOver(mouseX, mouseY))
        {
            this.setActiveTooltip(Component.translatable("catalogue.gui.open_mods_folder"));
        }

        if(this.activeTooltip != null)
        {
            graphics.renderTooltip(this.font, this.activeTooltip, mouseX, mouseY + this.tooltipYOffset);
            this.tooltipYOffset = 0;
        }
    }

    private void updateSelectedModList()
    {
        ModListEntry selectedEntry = this.modList.getEntryFromInfo(this.selectedModData);
        if(selectedEntry != null)
        {
            this.modList.setSelected(selectedEntry);
        }
    }

    private void updateSearchField(String value)
    {
        if(value.isEmpty())
        {
            this.searchTextField.setSuggestion(Component.translatable("catalogue.gui.search").append(Component.literal("...")).getString());
        }
        else
        {
            Optional<IModData> optional = CACHED_MODS.values().stream().filter(data -> {
                return data.getDisplayName().toLowerCase(Locale.ENGLISH).startsWith(value.toLowerCase(Locale.ENGLISH));
            }).min(Comparator.comparing(IModData::getDisplayName));
            if(optional.isPresent())
            {
                int length = value.length();
                String displayName = optional.get().getDisplayName();
                this.searchTextField.setSuggestion(displayName.substring(length));
            }
            else
            {
                this.searchTextField.setSuggestion("");
            }
        }
    }

    /**
     * Draws everything considered left of the screen; title, search bar and mod list.
     *
     * @param graphics     the current GuiGraphics instance
     * @param mouseX       the current mouse x position
     * @param mouseY       the current mouse y position
     * @param partialTicks the partial ticks
     */
    private void drawModList(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks)
    {
        if(ClientServices.PLATFORM.isForge())
        {
            RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
            graphics.blit(VERSION_CHECK_ICONS, this.modList.getRight() - 24, 10, 24, 0, 8, 8, 64, 16);
        }

        this.modList.render(graphics, mouseX, mouseY, partialTicks);
        graphics.drawString(this.font, ClientServices.COMPONENT.createTitle().withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.WHITE), 70, 10, 0xFFFFFF);
        this.searchTextField.render(graphics, mouseX, mouseY, partialTicks);

        if(ClientHelper.isMouseWithin(this.modList.getRight() - 14, 7, 14, 14, mouseX, mouseY))
        {
            this.setActiveTooltip(ClientServices.COMPONENT.createFilterUpdates());
            this.tooltipYOffset = 10;
        }
    }

    /**
     * Draws everything considered right of the screen; logo, mod title, description and more.
     *
     * @param graphics     the current GuiGraphics instance
     * @param mouseX       the current mouse x position
     * @param mouseY       the current mouse y position
     * @param partialTicks the partial ticks
     */
    private void drawModInfo(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks)
    {
        int listRight = this.modList.getRight();
        graphics.vLine(listRight + 11, -1, this.height, 0xFF707070);
        graphics.fill(listRight + 12, 0, this.width, this.height, 0x66000000);
        this.descriptionList.render(graphics, mouseX, mouseY, partialTicks);

        int contentLeft = listRight + 12 + 10;
        int contentWidth = this.width - contentLeft - 10;

        if(this.selectedModData != null)
        {
            this.drawBackground(graphics, this.width - contentLeft + 10, listRight + 12, 0);

            // Draw mod logo
            this.drawBanner(graphics, contentWidth, contentLeft, 10, this.width - (listRight + 12 + 10) - 10, 50);

            // Draw mod name
            PoseStack poseStack = graphics.pose();
            poseStack.pushPose();
            poseStack.translate(contentLeft, 70, 0);
            poseStack.scale(2.0F, 2.0F, 2.0F);
            graphics.drawString(this.font, this.selectedModData.getDisplayName(), 0, 0, 0xFFFFFF);
            poseStack.popPose();

            // Draw version
            Component modId = Component.literal("Mod ID: " + this.selectedModData.getModId()).withStyle(ChatFormatting.DARK_GRAY);
            int modIdWidth = this.font.width(modId);
            graphics.drawString(this.font, modId, contentLeft + contentWidth - modIdWidth, 92, 0xFFFFFF);

            // Draw version
            this.drawStringWithLabel(graphics, "catalogue.gui.version", this.selectedModData.getVersion().toString(), contentLeft, 92, contentWidth, mouseX, mouseY, ChatFormatting.GRAY, ChatFormatting.WHITE);

            // Draws an icon if there is an update for the mod
            IModData.Update update = this.selectedModData.getUpdate();
            if(ClientServices.PLATFORM.isForge() && update != null && update.url() != null && !update.url().isBlank())
            {
                Component version = ClientServices.COMPONENT.createVersion(this.selectedModData.getVersion());
                int versionWidth = this.font.width(version);
                RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
                int vOffset = update.animated() && (System.currentTimeMillis() / 800 & 1) == 1 ? 8 : 0;
                graphics.blit(VERSION_CHECK_ICONS, contentLeft + versionWidth + 5, 92, update.texOffset() * 8, vOffset, 8, 8, 64, 16);
                if(ClientHelper.isMouseWithin(contentLeft + versionWidth + 5, 92, 8, 8, mouseX, mouseY))
                {
                    Component message = ClientServices.COMPONENT.createFormatted("catalogue.gui.update_available", update.url());
                    this.setActiveTooltip(message);
                }
            }

            int labelOffset = this.height - 20;

            // Draw license
            String license = this.selectedModData.getLicense();
            this.drawStringWithLabel(graphics, "catalogue.gui.licenses", license, contentLeft, labelOffset, contentWidth, mouseX, mouseY, ChatFormatting.GRAY, ChatFormatting.WHITE);
            labelOffset -= 15;

            // Draw credits
            String credits = this.selectedModData.getCredits();
            if(credits != null)
            {
                this.drawStringWithLabel(graphics, ClientServices.COMPONENT.getCreditsKey(), credits, contentLeft, labelOffset, contentWidth, mouseX, mouseY, ChatFormatting.GRAY, ChatFormatting.WHITE);
                labelOffset -= 15;
            }

            // Draw authors
            String authors = this.selectedModData.getAuthors();
            if(authors != null)
            {
                this.drawStringWithLabel(graphics, "catalogue.gui.authors", authors, contentLeft, labelOffset, contentWidth, mouseX, mouseY, ChatFormatting.GRAY, ChatFormatting.WHITE);
            }
        }
        else
        {
            Component message = Component.translatable("catalogue.gui.no_selection").withStyle(ChatFormatting.GRAY);
            graphics.drawCenteredString(this.font, message, contentLeft + contentWidth / 2, this.height / 2 - 5, 0xFFFFFF);
        }
    }

    /**
     * Draws a string and prepends a label. If the formed string and label is longer than the
     * specified max width, it will automatically be trimmed and allows the user to hover the
     * string with their mouse to read the full contents.
     *
     * @param graphics    the current matrix stack
     * @param format      a string to prepend to the content
     * @param text        the string to render
     * @param x           the x position
     * @param y           the y position
     * @param maxWidth    the maximum width the string can render
     * @param mouseX      the current mouse x position
     * @param mouseY      the current mouse u position
     */
    private void drawStringWithLabel(GuiGraphics graphics, String format, String text, int x, int y, int maxWidth, int mouseX, int mouseY, ChatFormatting labelColor, ChatFormatting contentColor)
    {
        Component formatted = ClientServices.COMPONENT.createFormatted(format, text);
        String rawString = formatted.getString();
        String label = rawString.substring(0, rawString.indexOf(":") + 1);
        String content = rawString.substring(rawString.indexOf(":") + 1);
        if(this.font.width(formatted) > maxWidth)
        {
            content = this.font.plainSubstrByWidth(content, maxWidth - this.font.width(label) - 7) + "...";
            MutableComponent credits = Component.literal(label).withStyle(labelColor);
            credits.append(Component.literal(content).withStyle(contentColor));
            graphics.drawString(this.font, credits, x, y, 0xFFFFFF);
            if(ClientHelper.isMouseWithin(x, y, maxWidth, 9, mouseX, mouseY)) // Sets the active tool tip if string is too long so users can still read it
            {
                this.setActiveTooltip(Component.literal(text));
            }
        }
        else
        {
            graphics.drawString(this.font, Component.literal(label).withStyle(labelColor).append(Component.literal(content).withStyle(contentColor)), x, y, 0xFFFFFF);
        }
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button)
    {
        if(ClientHelper.isMouseWithin(10, 9, 10, 10, (int) mouseX, (int) mouseY) && button == GLFW.GLFW_MOUSE_BUTTON_1)
        {
            this.openLink("https://www.curseforge.com/minecraft/mc-mods/catalogue");
            return true;
        }
        if(ClientServices.PLATFORM.isForge() && this.selectedModData != null)
        {
            int contentLeft = this.modList.getRight() + 12 + 10;
            Component version = ClientServices.COMPONENT.createVersion(this.selectedModData.getVersion());
            int versionWidth = this.font.width(version);
            if(ClientHelper.isMouseWithin(contentLeft + versionWidth + 5, 92, 8, 8, (int) mouseX, (int) mouseY))
            {
                IModData.Update update = this.selectedModData.getUpdate();
                if(update != null && update.url() != null && !update.url().isBlank())
                {
                    Style style = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, update.url()));
                    this.handleComponentClicked(style);
                }
            }
        }
        return super.mouseClicked(mouseX, mouseY, button);
    }

    private void setActiveTooltip(Component content)
    {
        this.activeTooltip = this.font.split(content, Math.min(200, this.width));
        this.tooltipYOffset = 0;
    }

    private void setSelectedModData(IModData data)
    {
        this.selectedModData = data;
        this.loadAndCacheLogo(data);
        this.loadAndCacheBackground(data);
        this.configButton.visible = true;
        this.websiteButton.visible = true;
        this.issueButton.visible = true;
        this.configButton.active = data.hasConfig();
        this.websiteButton.active = data.getHomepage() != null;
        this.issueButton.active = data.getIssueTracker() != null;
        int contentLeft = this.modList.getRight() + 12 + 10;
        int contentWidth = this.width - contentLeft - 10;
        int labelCount = this.getLabelCount(data);
        this.descriptionList.updateSize(contentWidth, this.height - 135 - 10 - labelCount * 15, 130, this.height - 10 - labelCount * 15);
        this.descriptionList.setLeftPos(contentLeft);
        this.descriptionList.setTextFromInfo(data);
        this.descriptionList.setScrollAmount(0);
    }

    private int getLabelCount(IModData selectedModData)
    {
        int count = 1; //1 by default since license property will always exist
        if(selectedModData.getCredits() != null && !selectedModData.getCredits().isBlank()) count++;
        if(selectedModData.getAuthors() != null && !selectedModData.getAuthors().isBlank()) count++;
        return count;
    }

    private void drawBackground(GuiGraphics graphics, int contentWidth, int x, int y)
    {
        if(this.selectedModData == null)
            return;

        if(cachedBackground == null)
            return;

        RenderSystem.setShader(GameRenderer::getPositionColorTexShader);
        RenderSystem.setShaderTexture(0, cachedBackground);
        RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
        RenderSystem.enableBlend();
        Matrix4f matrix = graphics.pose().last().pose();
        BufferBuilder builder = Tesselator.getInstance().getBuilder();
        builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR_TEX);
        builder.vertex(matrix, x, y, 0).color(1.0F, 1.0F, 1.0F, 1.0F).uv(0, 0).endVertex();
        builder.vertex(matrix, x, y + 128, 0).color(0.0F, 0.0F, 0.0F, 0.0F).uv(0, 1).endVertex();
        builder.vertex(matrix, x + contentWidth, y + 128, 0).color(0.0F, 0.0F, 0.0F, 0.0F).uv(1, 1).endVertex();
        builder.vertex(matrix, x + contentWidth, y, 0).color(1.0F, 1.0F, 1.0F, 1.0F).uv(1, 0).endVertex();
        BufferUploader.drawWithShader(builder.end());
        RenderSystem.disableBlend();
    }

    private void drawBanner(GuiGraphics graphics, int contentWidth, int x, int y, int maxWidth, int maxHeight)
    {
        if(this.selectedModData != null)
        {
            ResourceLocation logoResource = MISSING_BANNER;
            Dimension size = new Dimension(600, 120);

            if(BANNER_CACHE.containsKey(this.selectedModData.getModId()))
            {
                Pair<ResourceLocation, Dimension> logoInfo = BANNER_CACHE.get(this.selectedModData.getModId());
                if(logoInfo.getLeft() != null)
                {
                    logoResource = logoInfo.getLeft();
                    size = logoInfo.getRight();
                }
            }

            RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
            RenderSystem.enableBlend();

            int width = size.width;
            int height = size.height;
            if(size.width > maxWidth)
            {
                width = maxWidth;
                height = (width * size.height) / size.width;
            }
            if(height > maxHeight)
            {
                height = maxHeight;
                width = (height * size.width) / size.height;
            }

            x += (contentWidth - width) / 2;
            y += (maxHeight - height) / 2;

            graphics.blit(logoResource, x, y, width, height, 0.0F, 0.0F, size.width, size.height, size.width, size.height);

            RenderSystem.disableBlend();
        }
    }

    private void loadAndCacheLogo(IModData data)
    {
        if(BANNER_CACHE.containsKey(data.getModId()))
            return;

        // Fills an empty logo as logo may not be present
        BANNER_CACHE.put(data.getModId(), Pair.of(null, new Dimension(0, 0)));

        // Attempts to load the real logo
        String banner = data.getBanner();
        if(banner != null && !banner.isEmpty())
        {
            if(ClientServices.PLATFORM.isForge() && (banner.contains("/") || banner.contains("\\")))
            {
                Constants.LOG.warn("Skipped loading logo file from {}. The file name '{}' contained illegal characters '/' or '\\'", data.getDisplayName(), banner);
                return;
            }

            ClientServices.PLATFORM.loadNativeImage(data.getModId(), banner, image ->
            {
                if(image.getWidth() == image.getHeight())
                {
                    TextureManager textureManager = this.minecraft.getTextureManager();
                    BANNER_CACHE.put(data.getModId(), Pair.of(textureManager.register("modlogo", this.createLogoTexture(image, data.isLogoSmooth())), new Dimension(image.getWidth(), image.getHeight())));
                }
            });
        }
    }

    private void loadAndCacheIcon(IModData data)
    {
        if(IMAGE_ICON_CACHE.containsKey(data.getModId()))
            return;

        // Fills an empty icon as icon may not be present
        IMAGE_ICON_CACHE.put(data.getModId(), Pair.of(null, new Dimension(0, 0)));

        // Attempts to load the real icon
        String imageIcon = data.getImageIcon();
        if(imageIcon != null && !imageIcon.isEmpty())
        {
            if(ClientServices.PLATFORM.isForge() && (imageIcon.contains("/") || imageIcon.contains("\\")))
            {
                Constants.LOG.warn("Skipped loading Catalogue icon file from {}. The file name '{}' contained illegal characters '/' or '\\'", data.getDisplayName(), imageIcon);
                return;
            }

            ClientServices.PLATFORM.loadNativeImage(data.getModId(), imageIcon, image -> {
                TextureManager textureManager = this.minecraft.getTextureManager();
                IMAGE_ICON_CACHE.put(data.getModId(), Pair.of(textureManager.register("catalogueicon", this.createLogoTexture(image, false)), new Dimension(image.getWidth(), image.getHeight())));
            });
            return;
        }

        // Attempts to use the logo file if it's a square
        String logoFile = data.getBanner();
        if(logoFile != null && !logoFile.isEmpty())
        {
            if(ClientServices.PLATFORM.isForge() && (logoFile.contains("/") || logoFile.contains("\\")))
            {
                Constants.LOG.warn("Skipped loading logo file from {}. The file name '{}' contained illegal characters '/' or '\\'", data.getDisplayName(), imageIcon);
                return;
            }

            ClientServices.PLATFORM.loadNativeImage(data.getModId(), imageIcon, image ->
            {
                if(image.getWidth() == image.getHeight())
                {
                    TextureManager textureManager = this.minecraft.getTextureManager();
                    String modId = data.getModId();

                    /* The first selected mod will have it's logo cached before the icon, so we
                     * can just use the logo instead of loading the image again. */
                    if(BANNER_CACHE.containsKey(modId))
                    {
                        if(BANNER_CACHE.get(modId).getLeft() != null)
                        {
                            IMAGE_ICON_CACHE.put(modId, BANNER_CACHE.get(modId));
                            return;
                        }
                    }

                    /* Since the icon will be same as the logo, we can cache into both icon and logo cache */
                    DynamicTexture texture = this.createLogoTexture(image, data.isLogoSmooth());
                    Dimension size = new Dimension(image.getWidth(), image.getHeight());
                    ResourceLocation textureId = textureManager.register("catalogueicon", texture);
                    IMAGE_ICON_CACHE.put(modId, Pair.of(textureId, size));
                    BANNER_CACHE.put(modId, Pair.of(textureId, size));
                }
            });
        }
    }

    private void loadAndCacheBackground(IModData data)
    {
        // Deletes the last cached background since they are large images
        if(cachedBackground != null)
        {
            TextureManager textureManager = this.minecraft.getTextureManager();
            textureManager.release(cachedBackground);
        }
        cachedBackground = null;

        String background = data.getBackground();
        if(background != null && !background.isEmpty())
        {
            if(ClientServices.PLATFORM.isForge() && (background.contains("/") || background.contains("\\")))
            {
                Constants.LOG.warn("Skipped loading Catalogue background file from {}. The file name '{}' contained illegal characters '/' or '\\'", data.getDisplayName(), background);
                return;
            }

            ClientServices.PLATFORM.loadNativeImage(data.getModId(), background, image ->
            {
                if(image.getWidth() != 512 || image.getHeight() != 256)
                    return;
                TextureManager textureManager = this.minecraft.getTextureManager();
                cachedBackground = textureManager.register("cataloguebackground", this.createLogoTexture(image, false));
            });
        }
    }

    private DynamicTexture createLogoTexture(NativeImage image, boolean smooth)
    {
        return new DynamicTexture(image)
        {
            @Override
            public void upload()
            {
                this.bind();
                image.upload(0, 0, 0, 0, 0, image.getWidth(), image.getHeight(), smooth, false, false, false);
            }
        };
    }

    private class ModList extends ObjectSelectionList<ModListEntry>
    {
        public ModList()
        {
            super(CatalogueModListScreen.this.minecraft, 150, CatalogueModListScreen.this.height, 46, CatalogueModListScreen.this.height - 35, 26);
        }

        @Override
        protected int getScrollbarPosition()
        {
            return this.x0 + this.width - 6;
        }

        @Override
        public int getRowLeft()
        {
            return this.x0;
        }

        @Override
        public int getRowWidth()
        {
            return this.width;
        }

        public void filterAndUpdateList(String text)
        {
            Predicate<IModData> filter = ClientServices.PLATFORM.isForge() ?
                    data -> !updatesButton.selected() || data.getUpdate() != null :
                    data -> data.getType() == IModData.Type.DEFAULT || data.getModId().equals("minecraft") || data.getModId().equals("fabric-api") || updatesButton.selected();
            List<ModListEntry> entries = CACHED_MODS.values().stream()
                .filter(info -> info.getDisplayName().toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH)))
                .filter(filter)
                .map(info -> new ModListEntry(info, this))
                .sorted(SORT)
                .collect(Collectors.toList());
            this.replaceEntries(entries);
            this.setScrollAmount(0);
        }

        @Nullable
        public ModListEntry getEntryFromInfo(IModData data)
        {
            return this.children().stream().filter(entry -> entry.data == data).findFirst().orElse(null);
        }

        @Override
        public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks)
        {
            ClientHelper.scissor(this.getRowLeft(), this.getTop(), this.getWidth(), this.getBottom() - this.getTop());
            super.render(graphics, mouseX, mouseY, partialTicks);
            RenderSystem.disableScissor();
        }

        @Override
        public boolean keyPressed(int key, int scanCode, int modifiers)
        {
            if(key == GLFW.GLFW_KEY_ENTER && this.getSelected() != null)
            {
                CatalogueModListScreen.this.setSelectedModData(this.getSelected().data);
                SoundManager handler = Minecraft.getInstance().getSoundManager();
                handler.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
                return true;
            }
            return super.keyPressed(key, scanCode, modifiers);
        }

        @Override
        public void updateNarration(NarrationElementOutput p_169152_)
        {

        }

        @Override
        public void centerScrollOn(ModListEntry entry)
        {
            super.centerScrollOn(entry);
        }

        public int getLeft()
        {
            return this.x0;
        }

        public int getRight()
        {
            return this.x1;
        }

        public int getTop()
        {
            return this.y0;
        }

        public int getBottom()
        {
            return this.y1;
        }

        public int getWidth()
        {
            return this.width;
        }
    }

    private class ModListEntry extends ObjectSelectionList.Entry<ModListEntry>
    {
        private final IModData data;
        private final ModList list;

        public ModListEntry(IModData data, ModList list)
        {
            this.data = data;
            this.list = list;
        }

        @Override
        public void render(GuiGraphics graphics, int index, int top, int left, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float partialTicks)
        {
            // Draws mod name and version
            graphics.drawString(CatalogueModListScreen.this.font, this.getFormattedModName(), left + 24, top + 2, 0xFFFFFF);
            graphics.drawString(CatalogueModListScreen.this.font, Component.literal(this.data.getVersion().toString()).withStyle(ChatFormatting.GRAY), left + 24, top + 12, 0xFFFFFF);

            CatalogueModListScreen.this.loadAndCacheIcon(this.data);

            // Draw icon
            if(IMAGE_ICON_CACHE.containsKey(this.data.getModId()) && IMAGE_ICON_CACHE.get(this.data.getModId()).getLeft() != null)
            {
                ResourceLocation logoResource = TextureManager.INTENTIONAL_MISSING_TEXTURE;
                Dimension size = new Dimension(16, 16);

                Pair<ResourceLocation, Dimension> logoInfo = IMAGE_ICON_CACHE.get(this.data.getModId());
                if(logoInfo != null && logoInfo.getLeft() != null)
                {
                    logoResource = logoInfo.getLeft();
                    size = logoInfo.getRight();
                }

                RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
                RenderSystem.enableBlend();
                graphics.blit(logoResource, left + 4, top + 2, 16, 16, 0.0F, 0.0F, size.width, size.height, size.width, size.height);
                RenderSystem.disableBlend();
            }
            else
            {
                // Some items from mods utilise the player or world instance to render. This means
                // rendering the item from the main menu may result in a crash since mods don't check
                // for null pointers. Switches the icon to a grass block if an exception occurs.
                try
                {
                    graphics.renderItem(this.getItemIcon(), left + 4, top + 2);
                }
                catch(Exception e)
                {
                    ITEM_ICON_CACHE.put(this.data.getModId(), new ItemStack(Items.GRASS_BLOCK));
                }
            }

            // Draws an icon if there is an update for the mod
            IModData.Update update = this.data.getUpdate();
            if(ClientServices.PLATFORM.isForge() && update != null)
            {
                RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
                int vOffset = update.animated() && (System.currentTimeMillis() / 800 & 1) == 1 ? 8 : 0;
                graphics.blit(VERSION_CHECK_ICONS, left + rowWidth - 8 - 10, top + 6, update.texOffset() * 8, vOffset, 8, 8, 64, 16);
            }
        }

        private ItemStack getItemIcon()
        {
            if(ITEM_ICON_CACHE.containsKey(this.data.getModId()))
            {
                return ITEM_ICON_CACHE.get(this.data.getModId());
            }

            // Put grass as default item icon
            ITEM_ICON_CACHE.put(this.data.getModId(), new ItemStack(Items.GRASS_BLOCK));

            // Special case for Forge to set item icon to anvil
            if(this.data.getModId().equals("forge"))
            {
                ItemStack item = new ItemStack(Items.ANVIL);
                ITEM_ICON_CACHE.put("forge", item);
                return item;
            }

            String itemIcon = this.data.getItemIcon();
            if(itemIcon != null && !itemIcon.isEmpty())
            {
                try
                {
                    ItemParser.ItemResult result = ItemParser.parseForItem(BuiltInRegistries.ITEM.asLookup(), new StringReader(itemIcon));
                    ItemStack item = new ItemStack(result.item().value(), 1);
                    item.setTag(result.nbt());
                    ITEM_ICON_CACHE.put(this.data.getModId(), item);
                    return item;
                }
                catch (CommandSyntaxException ignored) {}
            }

            // If the mod doesn't specify an item to use, Catalogue will attempt to get an item from the mod
            Optional<ItemStack> optional = BuiltInRegistries.ITEM.stream().filter(item -> item.builtInRegistryHolder().key().location().getNamespace().equals(this.data.getModId())).map(ItemStack::new).findFirst();
            if(optional.isPresent())
            {
                ItemStack item = optional.get();
                if(item.getItem() != Items.AIR)
                {
                    ITEM_ICON_CACHE.put(this.data.getModId(), item);
                    return item;
                }
            }

            return new ItemStack(Items.GRASS_BLOCK);
        }

        private Component getFormattedModName()
        {
            String name = this.data.getDisplayName();
            int width = this.list.getRowWidth() - (this.list.getMaxScroll() > 0 ? 30 : 24);
            if(CatalogueModListScreen.this.font.width(name) > width)
            {
                name = CatalogueModListScreen.this.font.plainSubstrByWidth(name, width - 10) + "...";
            }
            MutableComponent title = Component.literal(name);
            if(this.data.getModId().equals("forge") || this.data.getModId().equals("minecraft"))
            {
                title.withStyle(ChatFormatting.DARK_GRAY);
            }
            return title;
        }

        @Override
        public boolean mouseClicked(double mouseX, double mouseY, int button)
        {
            CatalogueModListScreen.this.setSelectedModData(this.data);
            this.list.setSelected(this);
            return false;
        }

        public IModData getData()
        {
            return this.data;
        }

        @Override
        public Component getNarration()
        {
            return Component.literal(this.data.getDisplayName());
        }
    }

    private class StringList extends AbstractSelectionList<StringEntry>
    {
        public StringList(int width, int height, int left, int top)
        {
            super(CatalogueModListScreen.this.minecraft, width, CatalogueModListScreen.this.height, top, top + height, 10);
            this.setLeftPos(left);
        }

        public void setTextFromInfo(IModData data)
        {
            this.clearEntries();
            CatalogueModListScreen.this.font.getSplitter().splitLines(data.getDescription().trim(), this.getRowWidth(), Style.EMPTY).forEach(text -> {
                this.addEntry(new StringEntry(text.getString().replace("\n", "").replace("\r", "").trim()));
            });
        }

        @Override
        public void setSelected(@Nullable StringEntry entry) {}

        @Override
        protected int getScrollbarPosition()
        {
            return this.x0 + this.width - 7;
        }

        @Override
        public int getRowLeft()
        {
            return this.x0;
        }

        @Override
        public int getRowWidth()
        {
            return this.width - 10;
        }

        @Override
        public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks)
        {
            ClientHelper.scissor(this.x0, this.y0, this.width, this.y1 - this.y0);
            super.render(graphics, mouseX, mouseY, partialTicks);
            RenderSystem.disableScissor();
        }

        @Override
        public void updateNarration(NarrationElementOutput output) {}
    }

    private class StringEntry extends ObjectSelectionList.Entry<StringEntry>
    {
        private final String line;

        public StringEntry(String line)
        {
            this.line = line;
        }

        @Override
        public void render(GuiGraphics graphics, int index, int top, int left, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float partialTicks)
        {
            graphics.drawString(CatalogueModListScreen.this.font, this.line, left, top, 0xFFFFFF);
        }

        @Override
        public Component getNarration()
        {
            return Component.literal(this.line);
        }
    }

    private record Dimension(int width, int height) {}
}