diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf80a9ae..7ef365e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,8 @@ jobs: - uses: actions/checkout@v1 - name: Install dependencies run: composer install --prefer-dist --no-progress --no-suggest + - name: Validate Twig templates + run: php artisan twig:lint -v frontend: name: JavaScript Check runs-on: ubuntu-latest diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 32a7107f..7f5e1ca7 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -21,6 +21,18 @@ use Illuminate\Support\Facades\Redis; class AdminController extends Controller { + public function index() + { + return view('admin.index', [ + 'sum' => [ + 'users' => User::count(), + 'players' => Player::count(), + 'textures' => Texture::count(), + 'storage' => Texture::select('size')->sum('size'), + ], + ]); + } + public function chartData() { $today = Carbon::today()->timestamp; @@ -105,7 +117,7 @@ class AdminController extends Controller return redirect('/admin'); } - public function customize(Request $request) + public function customize(Request $request, \App\Services\Webpack $webpack) { $homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) { $form->text('home_pic_url')->hint(); @@ -142,7 +154,14 @@ class AdminController extends Controller option(['color_scheme' => $color]); } - return view('admin.customize', ['forms' => compact('homepage', 'customJsCss')]); + return view('admin.customize', [ + 'colors' => ['blue', 'yellow', 'green', 'purple', 'red', 'black'], + 'skins_css' => $webpack->url('skins/_all-skins.min.css'), + 'forms' => [ + 'homepage' => $homepage, + 'custom_js_css' => $customJsCss, + ], + ]); } public function score() diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index f4aaa527..71976160 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -41,21 +41,26 @@ class UserController extends Controller $user = Auth::user(); [$from, $to] = explode(',', option('sign_score')); - $scoreIntro = nl2br(trans('user.score-intro.introduction', [ + $scoreIntro = trans('user.score-intro.introduction', [ 'initial_score' => option('user_initial_score'), 'score-from' => $from, 'score-to' => $to, 'return-score' => option('return_score') ? trans('user.score-intro.will-return-score') : trans('user.score-intro.no-return-score'), - ])); + ]); return view('user.index')->with([ 'statistics' => [ 'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), 'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')), ], - 'scoreIntro' => $scoreIntro, + 'score_intro' => $scoreIntro, + 'rates' => [ + 'storage' => option('score_per_storage'), + 'player' => option('score_per_player'), + 'closet' => option('score_per_closet_item'), + ], 'announcement' => app('parsedown')->text(option_localized('announcement')), 'extra' => ['unverified' => option('require_verification') && ! $user->verified], ]); diff --git a/app/Http/View/Composers/FootComposer.php b/app/Http/View/Composers/FootComposer.php new file mode 100644 index 00000000..78e9c3d3 --- /dev/null +++ b/app/Http/View/Composers/FootComposer.php @@ -0,0 +1,70 @@ +request = $request; + $this->webpack = $webpack; + $this->javascript = $javascript; + $this->dispatcher = $dispatcher; + } + + public function compose(View $view) + { + $this->injectJavaScript($view); + $this->addExtra($view); + } + + public function injectJavaScript(View $view) + { + $scripts = []; + + $locale = app()->getLocale(); + $scripts[] = $this->javascript->generate($locale); + if ($pluginI18n = $this->javascript->plugin($locale)) { + $scripts[] = $pluginI18n; + } + if ($this->request->is('admin*') && auth()->user()->permission >= User::SUPER_ADMIN) { + $scripts[] = $this->webpack->url('check-updates.js'); + } + $scripts[] = $this->webpack->url('index.js'); + + $view->with([ + 'scripts' => $scripts, + 'inline_js' => option('custom_js'), + ]); + } + + public function addExtra(View $view) + { + $content = []; + $this->dispatcher->dispatch(new \App\Events\RenderingFooter($content)); + $view->with('extra_foot', $content); + } +} diff --git a/app/Http/View/Composers/HeadComposer.php b/app/Http/View/Composers/HeadComposer.php new file mode 100644 index 00000000..28f09c15 --- /dev/null +++ b/app/Http/View/Composers/HeadComposer.php @@ -0,0 +1,79 @@ +webpack = $webpack; + $this->dispatcher = $dispatcher; + } + + public function compose(View $view) + { + $this->addFavicon($view); + $this->applyThemeColor($view); + $this->seo($view); + $this->injectStyles($view); + $this->addExtra($view); + } + + public function addFavicon(View $view) + { + $url = option('favicon_url', config('options.favicon_url')); + $url = Str::startsWith($url, 'http') ? $url : url($url); + $view->with('favicon', $url); + } + + public function applyThemeColor(View $view) + { + $colors = [ + 'blue' => '#3c8dbc', + 'yellow' => '#f39c12', + 'green' => '#00a65a', + 'purple' => '#605ca8', + 'red' => '#dd4b39', + 'black' => '#ffffff', + ]; + preg_match('/skin-(\w+)?(?:-light)?/', option('color_scheme'), $matches); + $view->with('theme_color', Arr::get($colors, $matches[1])); + } + + public function seo(View $view) + { + $view->with('seo', [ + 'keywords' => option('meta_keywords'), + 'description' => option('meta_description'), + 'extra' => option('meta_extras'), + ]); + } + + public function injectStyles(View $view) + { + $view->with('styles', [ + $this->webpack->url('style.css'), + $this->webpack->url('skins/'.option('color_scheme').'.min.css'), + ]); + $view->with('inline_css', option('custom_css')); + } + + public function addExtra(View $view) + { + $content = []; + $this->dispatcher->dispatch(new \App\Events\RenderingHeader($content)); + $view->with('extra_head', $content); + } +} diff --git a/app/Http/View/Composers/LanguagesMenuComposer.php b/app/Http/View/Composers/LanguagesMenuComposer.php new file mode 100644 index 00000000..d0dc26b0 --- /dev/null +++ b/app/Http/View/Composers/LanguagesMenuComposer.php @@ -0,0 +1,40 @@ +request = $request; + } + + public function compose(View $view) + { + $query = $this->request->query(); + $url = $this->request->url(); + + $langs = collect(config('locales')) + ->reject(function ($locale) { + return Arr::has($locale, 'alias'); + }) + ->map(function ($locale, $id) use ($query, $url) { + $query = array_merge($query, ['lang' => $id]); + $locale['url'] = $url.'?'.http_build_query($query); + + return $locale; + }); + + $view->with([ + 'current' => config('locales.'.app()->getLocale().'.short_name'), + 'langs' => $langs, + ]); + } +} diff --git a/app/Http/View/Composers/SideMenuComposer.php b/app/Http/View/Composers/SideMenuComposer.php new file mode 100644 index 00000000..b6a1cb1a --- /dev/null +++ b/app/Http/View/Composers/SideMenuComposer.php @@ -0,0 +1,101 @@ +request = $request; + } + + public function compose(View $view) + { + $type = $view->gatherData()['type']; + + $menu = config('menu'); + switch ($type) { + case 'user': + event(new Events\ConfigureUserMenu($menu)); + break; + case 'explore': + event(new Events\ConfigureExploreMenu($menu)); + break; + case 'admin': + event(new Events\ConfigureAdminMenu($menu)); + $menu['admin'] = $this->collectPluginConfigs($menu['admin']); + break; + } + + $view->with('items', array_map(function ($item) { + return $this->transform($item); + }, $menu[$type])); + } + + public function transform(array $item): array + { + $isActive = $this->request->is(Arr::get($item, 'link')); + foreach (Arr::get($item, 'children', []) as $k => $v) { + if ($this->request->is(Arr::get($v, 'link'))) { + $isActive = true; + break; + } + } + + $classes = []; + if ($isActive) { + $classes[] = 'active menu-open'; + } + + if (Arr::has($item, 'children')) { + $classes[] = 'treeview'; + $item['children'] = array_map(function ($item) { + return $this->transform($item); + }, $item['children']); + } + + $item['classes'] = $classes; + + return $item; + } + + public function collectPluginConfigs(array &$menu) + { + $menu = array_map(function ($item) { + if (Arr::get($item, 'id') === 'plugin-configs') { + $pluginConfigs = resolve(PluginManager::class) + ->getEnabledPlugins() + ->filter(function ($plugin) { + return $plugin->hasConfigView(); + }) + ->map(function ($plugin) { + return [ + 'title' => trans($plugin->title), + 'link' => 'admin/plugins/config/'.$plugin->name, + 'icon' => 'fa-circle', + ]; + }); + + // Don't display this menu item when no plugin config is available + if ($pluginConfigs->isNotEmpty()) { + $item['children'] = array_merge($item['children'], $pluginConfigs->values()->all()); + + return $item; + } + } else { + return $item; + } + }, $menu); + + return array_filter($menu); // Remove empty items + } +} diff --git a/app/Http/View/Composers/UserMenuComposer.php b/app/Http/View/Composers/UserMenuComposer.php new file mode 100644 index 00000000..d59063ca --- /dev/null +++ b/app/Http/View/Composers/UserMenuComposer.php @@ -0,0 +1,35 @@ +request = $request; + } + + public function compose(View $view) + { + $user = auth()->user(); + $view->with('user', $user); + + if ($this->request->is('skinlib*') || $this->request->is('/')) { + $view->with( + 'tiny_avatar', + url('avatar/25/'.base64_encode($user->email).'.png?tid='.$user->avatar) + ); + } + + $view->with( + 'avatar', + url('avatar/128/'.base64_encode($user->email).'.png?tid='.$user->avatar) + ); + } +} diff --git a/app/Http/View/Composers/UserPanelComposer.php b/app/Http/View/Composers/UserPanelComposer.php new file mode 100644 index 00000000..6ba1efc3 --- /dev/null +++ b/app/Http/View/Composers/UserPanelComposer.php @@ -0,0 +1,41 @@ +dispatcher = $dispatcher; + } + + public function compose(View $view) + { + $user = auth()->user(); + $roles = [ + User::BANNED => 'banned', + User::NORMAL => 'normal', + User::ADMIN => 'admin', + User::SUPER_ADMIN => 'super-admin', + ]; + $role = $roles[$user->permission]; + $avatar = url('avatar/45/'.base64_encode($user->email).'.png?tid='.$user->avatar); + + $badges = []; + $this->dispatcher->dispatch(new \App\Events\RenderingBadges($badges)); + + $view->with([ + 'user' => $user, + 'role' => trans("admin.users.status.$role"), + 'avatar' => $avatar, + 'badges' => $badges, + ]); + } +} diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php new file mode 100644 index 00000000..4e09009b --- /dev/null +++ b/app/Providers/ViewServiceProvider.php @@ -0,0 +1,54 @@ +with([ + 'site_name' => option_localized('site_name'), + 'color_scheme' => option('color_scheme'), + ]); + }); + + View::composer('shared.head', Composers\HeadComposer::class); + + View::composer('shared.notifications', function ($view) { + $notifications = auth()->user()->unreadNotifications; + $view->with([ + 'notifications' => $notifications, + 'amount' => count($notifications), + ]); + }); + + View::composer('shared.languages', Composers\LanguagesMenuComposer::class); + + View::composer('shared.user-menu', Composers\UserMenuComposer::class); + + View::composer('shared.side-menu', Composers\SideMenuComposer::class); + + View::composer('shared.user-panel', Composers\UserPanelComposer::class); + + View::composer('shared.footer', function ($view) { + $customCopyright = get_string_replaced( + option_localized('copyright_text'), + [ + '{site_name}' => option_localized('site_name'), + '{site_url}' => option('site_url'), + ] + ); + $view->with([ + 'copyright' => option_localized('copyright_prefer', 0), + 'custom_copyright' => $customCopyright, + ]); + }); + + View::composer('shared.foot', Composers\FootComposer::class); + } +} diff --git a/app/helpers.php b/app/helpers.php index 153bc7bd..e93e7c57 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -77,102 +77,6 @@ if (! function_exists('bs_header_extra')) { } } -if (! function_exists('bs_menu')) { - function bs_menu(string $type): string - { - $menu = config('menu'); - - switch ($type) { - case 'user': - event(new App\Events\ConfigureUserMenu($menu)); - break; - case 'explore': - event(new App\Events\ConfigureExploreMenu($menu)); - break; - case 'admin': - event(new App\Events\ConfigureAdminMenu($menu)); - break; - } - - if (! isset($menu[$type])) { - throw new InvalidArgumentException; - } - - $menu[$type] = array_map(function ($item) { - if (Arr::get($item, 'id') === 'plugin-configs') { - $pluginConfigs = app('plugins')->getEnabledPlugins() - ->filter(function ($plugin) { - return $plugin->hasConfigView(); - }) - ->map(function ($plugin) { - return [ - 'title' => trans($plugin->title), - 'link' => 'admin/plugins/config/'.$plugin->name, - 'icon' => 'fa-circle', - ]; - }); - - // Don't display this menu item when no plugin config is available - if ($pluginConfigs->isNotEmpty()) { - $item['children'] = array_merge($item['children'], $pluginConfigs->values()->all()); - - return $item; - } - } else { - return $item; - } - }, $menu[$type]); - - return bs_menu_render($menu[$type]); - } - - function bs_menu_render(array $data): string - { - $content = ''; - - foreach ($data as $key => $value) { - $active = app('request')->is(@$value['link']); - - // also set parent as active if any child is active - foreach ((array) @$value['children'] as $childKey => $childValue) { - if (app('request')->is(@$childValue['link'])) { - $active = true; - } - } - - $classes = []; - $active ? ($classes[] = 'active menu-open') : null; - isset($value['children']) ? ($classes[] = 'treeview') : null; - - $attr = count($classes) ? sprintf(' class="%s"', implode(' ', $classes)) : ''; - - $content .= "
'.implode('
', $parts).'
'; - // Remove empty paragraphs - return str_replace('', '', $result); - } -} - if (! function_exists('png')) { function png($resource) { diff --git a/composer.json b/composer.json index 9bd58eaf..ad56831d 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,9 @@ "composer/ca-bundle": "^1.2", "facade/ignition": "^1.4", "spatie/laravel-translation-loader": "^2.4", - "symfony/yaml": "^4.3" + "symfony/yaml": "^4.3", + "twig/twig": "^2.11", + "rcrowe/twigbridge": "^0.11.1" }, "require-dev": { "fzaninotto/faker": "~1.8", diff --git a/composer.lock b/composer.lock index 9a9ecf50..6191f436 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e0bda645aa2fc04fc9dc1d6e0d711c50", + "content-hash": "5ac658b15e8a3bf5b2f92d73193abf8a", "packages": [ { "name": "composer/ca-bundle", @@ -2577,6 +2577,78 @@ ], "time": "2018-07-19T23:38:55+00:00" }, + { + "name": "rcrowe/twigbridge", + "version": "v0.11.1", + "source": { + "type": "git", + "url": "https://github.com/rcrowe/TwigBridge.git", + "reference": "2054abdbd8dcde573cd86d8d3856873e2388d47b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/2054abdbd8dcde573cd86d8d3856873e2388d47b", + "reference": "2054abdbd8dcde573cd86d8d3856873e2388d47b", + "shasum": "" + }, + "require": { + "illuminate/support": "5.5.*|5.6.*|5.7.*|5.8.x|6.0.*", + "illuminate/view": "5.5.*|5.6.*|5.7.*|5.8.x|6.0.*", + "php": ">=7.1", + "twig/twig": "~2.0" + }, + "require-dev": { + "laravel/framework": "5.5.*", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~6.0", + "satooshi/php-coveralls": "~0.6", + "squizlabs/php_codesniffer": "~1.5" + }, + "suggest": { + "laravelcollective/html": "For bringing back html/form in Laravel 5.x", + "twig/extensions": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + }, + "laravel": { + "providers": [ + "TwigBridge\\ServiceProvider" + ], + "aliases": { + "Twig": "TwigBridge\\Facade\\Twig" + } + } + }, + "autoload": { + "psr-4": { + "TwigBridge\\": "src", + "TwigBridge\\Tests\\": "tests" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Crowe", + "email": "hello@vivalacrowe.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Adds the power of Twig to Laravel", + "keywords": [ + "laravel", + "twig" + ], + "time": "2019-09-05T11:59:53+00:00" + }, { "name": "scrivo/highlight.php", "version": "v9.15.10.0", @@ -4356,6 +4428,73 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "time": "2017-11-27T11:13:29+00:00" }, + { + "name": "twig/twig", + "version": "v2.11.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "699ed2342557c88789a15402de5eb834dedd6792" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/699ed2342557c88789a15402de5eb834dedd6792", + "reference": "699ed2342557c88789a15402de5eb834dedd6792", + "shasum": "" + }, + "require": { + "php": "^7.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.11-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "https://twig.symfony.com/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "time": "2019-06-18T15:37:11+00:00" + }, { "name": "tymon/jwt-auth", "version": "dev-develop", diff --git a/config/app.php b/config/app.php index 432e8353..f0e753a6 100644 --- a/config/app.php +++ b/config/app.php @@ -188,7 +188,7 @@ return [ App\Providers\PluginServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\ValidatorExtendServiceProvider::class, - + App\Providers\ViewServiceProvider::class, ], /* diff --git a/config/twigbridge.php b/config/twigbridge.php new file mode 100644 index 00000000..c3cc931c --- /dev/null +++ b/config/twigbridge.php @@ -0,0 +1,214 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Configuration options for Twig. + */ +return [ + + 'twig' => [ + /* + |-------------------------------------------------------------------------- + | Extension + |-------------------------------------------------------------------------- + | + | File extension for Twig view files. + | + */ + 'extension' => 'twig', + + /* + |-------------------------------------------------------------------------- + | Accepts all Twig environment configuration options + |-------------------------------------------------------------------------- + | + | http://twig.sensiolabs.org/doc/api.html#environment-options + | + */ + 'environment' => [ + + // When set to true, the generated templates have a __toString() method + // that you can use to display the generated nodes. + // default: false + 'debug' => env('APP_DEBUG', false), + + // The charset used by the templates. + // default: utf-8 + 'charset' => 'utf-8', + + // The base template class to use for generated templates. + // default: TwigBridge\Twig\Template + 'base_template_class' => 'TwigBridge\Twig\Template', + + // An absolute path where to store the compiled templates, or false to disable caching. If null + // then the cache file path is used. + // default: cache file storage path + 'cache' => null, + + // When developing with Twig, it's useful to recompile the template + // whenever the source code changes. If you don't provide a value + // for the auto_reload option, it will be determined automatically based on the debug value. + 'auto_reload' => true, + + // If set to false, Twig will silently ignore invalid variables + // (variables and or attributes/methods that do not exist) and + // replace them with a null value. When set to true, Twig throws an exception instead. + // default: false + 'strict_variables' => false, + + // If set to true, auto-escaping will be enabled by default for all templates. + // default: 'html' + 'autoescape' => 'html', + + // A flag that indicates which optimizations to apply + // (default to -1 -- all optimizations are enabled; set it to 0 to disable) + 'optimizations' => -1, + ], + + /* + |-------------------------------------------------------------------------- + | Global variables + |-------------------------------------------------------------------------- + | + | These will always be passed in and can be accessed as Twig variables. + | NOTE: these will be overwritten if you pass data into the view with the same key. + | + */ + 'globals' => [], + ], + + 'extensions' => [ + + /* + |-------------------------------------------------------------------------- + | Extensions + |-------------------------------------------------------------------------- + | + | Enabled extensions. + | + | `Twig\Extension\DebugExtension` is enabled automatically if twig.debug is TRUE. + | + */ + 'enabled' => [ + 'Twig\Extension\StringLoaderExtension', + + 'TwigBridge\Extension\Loader\Facades', + 'TwigBridge\Extension\Loader\Filters', + 'TwigBridge\Extension\Loader\Functions', + + 'TwigBridge\Extension\Laravel\Auth', + 'TwigBridge\Extension\Laravel\Config', + 'TwigBridge\Extension\Laravel\Dump', + // 'TwigBridge\Extension\Laravel\Input', + 'TwigBridge\Extension\Laravel\Session', + // 'TwigBridge\Extension\Laravel\Str', + 'TwigBridge\Extension\Laravel\Translator', + 'TwigBridge\Extension\Laravel\Url', + // 'TwigBridge\Extension\Laravel\Model', + // 'TwigBridge\Extension\Laravel\Gate', + + // 'TwigBridge\Extension\Laravel\Form', + // 'TwigBridge\Extension\Laravel\Html', + // 'TwigBridge\Extension\Laravel\Legacy\Facades', + ], + + /* + |-------------------------------------------------------------------------- + | Facades + |-------------------------------------------------------------------------- + | + | Available facades. Access like `{{ Config.get('foo.bar') }}`. + | + | Each facade can take an optional array of options. To mark the whole facade + | as safe you can set the option `'is_safe' => true`. Setting the facade as + | safe means that any HTML returned will not be escaped. + | + | It is advisable to not set the whole facade as safe and instead mark the + | each appropriate method as safe for security reasons. You can do that with + | the following syntax: + | + |
+ | 'Form' => [
+ | 'is_safe' => [
+ | 'open'
+ | ]
+ | ]
+ |
+ |
+ | The values of the `is_safe` array must match the called method on the facade
+ | in order to be marked as safe.
+ |
+ */
+ 'facades' => [],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Functions
+ |--------------------------------------------------------------------------
+ |
+ | Available functions. Access like `{{ secure_url(...) }}`.
+ |
+ | Each function can take an optional array of options. These options are
+ | passed directly to `Twig\TwigFunction`.
+ |
+ | So for example, to mark a function as safe you can do the following:
+ |
+ |
+ | 'link_to' => [
+ | 'is_safe' => ['html']
+ | ]
+ |
+ |
+ | The options array also takes a `callback` that allows you to name the
+ | function differently in your Twig templates than what it's actually called.
+ |
+ |
+ | 'link' => [
+ | 'callback' => 'link_to'
+ | ]
+ |
+ |
+ */
+ 'functions' => [],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Filters
+ |--------------------------------------------------------------------------
+ |
+ | Available filters. Access like `{{ variable|filter }}`.
+ |
+ | Each filter can take an optional array of options. These options are
+ | passed directly to `Twig\TwigFilter`.
+ |
+ | So for example, to mark a filter as safe you can do the following:
+ |
+ |
+ | 'studly_case' => [
+ | 'is_safe' => ['html']
+ | ]
+ |
+ |
+ | The options array also takes a `callback` that allows you to name the
+ | filter differently in your Twig templates than what is actually called.
+ |
+ |
+ | 'snake' => [
+ | 'callback' => 'snake_case'
+ | ]
+ |
+ |
+ */
+ 'filters' => [
+ 'get' => 'data_get',
+ ],
+ ],
+];
diff --git a/resources/assets/src/styles/common.styl b/resources/assets/src/styles/common.styl
index d211e743..b820fe63 100644
--- a/resources/assets/src/styles/common.styl
+++ b/resources/assets/src/styles/common.styl
@@ -57,6 +57,9 @@ body, h1, h2, h3, h4, h5, h6
.main-header .navbar .user-menu .user-image
border-radius 10%
+.navbar-nav > .user-menu > .dropdown-menu > li.user-header
+ height unset
+
#language-menu li
a
color #777
diff --git a/resources/lang/en/admin.yml b/resources/lang/en/admin.yml
index 3d365e23..755be9b8 100644
--- a/resources/lang/en/admin.yml
+++ b/resources/lang/en/admin.yml
@@ -159,15 +159,16 @@ update:
latest: "Latest Version:"
current: "Current Version:"
- check-github: Check GitHub Releases
+ check-github: Check GitHub Releases
button: Update Now
cautions:
title: Cautions
+ link: check out this.
text: |
Please choose update source according to your host's network environment.
Low-speed connection between update source and your host will cause long-time loading at checking and downloading page.
- To change the default update source, please refer to this article.
+ To change the default update source,
download:
downloading: Downloading update package...
diff --git a/resources/lang/zh_CN/admin.yml b/resources/lang/zh_CN/admin.yml
index 5bcb3bd6..a173bd60 100644
--- a/resources/lang/zh_CN/admin.yml
+++ b/resources/lang/zh_CN/admin.yml
@@ -164,15 +164,16 @@ update:
latest: 最新版本:
current: 当前版本:
- check-github: 查看 GitHub Releases
+ check-github: 查看 GitHub Releases
button: 马上升级
cautions:
title: 注意事项
+ link: 点击了解详情
text: |
请根据你的主机所在位置(国内/国外)选择更新源。
如错选至相对于你的主机速度较慢的源,可能会造成检查与下载更新页面长时间无响应。
- 如何更换更新源?点击了解详情。
+ 如何更换更新源?
download:
downloading: 正在下载更新包
diff --git a/resources/views/admin/base.twig b/resources/views/admin/base.twig
new file mode 100644
index 00000000..3e745839
--- /dev/null
+++ b/resources/views/admin/base.twig
@@ -0,0 +1,25 @@
+
+
+
+ {{ include('shared.head') }}
+ @lang('admin.index.total-users')
-@lang('admin.index.total-players')
-@lang('admin.index.total-textures')
-@lang('admin.index.disk-usage')
-{{ trans('admin.index.total-users') }}
+{{ trans('admin.index.total-players') }}
+{{ trans('admin.index.total-textures') }}
+{{ trans('admin.index.disk-usage') }}
+| @lang("admin.status.$category.name") | -|
|---|---|
| @lang("admin.status.$category.$key") | -{{ $value }} | -
| @lang('admin.status.plugins', ['amount' => $plugins->count()]) | -|
| {{ $plugin['title'] }} | -{{ $plugin['version'] }} | -
| {{ trans("admin.status.#{category}.name") }} | +|
|---|---|
| {{ trans("admin.status.#{category}.#{key}") }} | +{{ value }} | +
| + {{ trans('admin.status.plugins', {amount: plugins|length}) }} + | +|
| {{ plugin.title }} | +{{ plugin.version }} | +
| @lang('admin.update.info.versions.latest') | -- v{{ $info['latest'] }} - | -
| @lang('admin.update.info.versions.current') | -- v{{ $info['current'] }} - | -
| @lang('admin.update.info.versions.current') | -- v{{ $info['current'] }} - | -
| {{ trans('admin.update.info.versions.latest') }} | ++ v{{ info.latest }} + | +
| {{ trans('admin.update.info.versions.current') }} | ++ v{{ info.current }} + | +
| + {{ trans('admin.update.info.versions.current') }} | ++ v{{ info.current }} + | +
{{ text }}
+ {% endfor %} + + {{ trans('admin.update.cautions.link') }} + +{{ user.nickname ?? user.email }}
+ {{ role }} + {% if badges|length == 1 %} + {{ badges[0][0] }} + {% endif %} +